Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8d9115b
Added hybrid search command
htemelski-redis Oct 29, 2025
78b26ac
fixed lint, fixed some tests
htemelski-redis Oct 29, 2025
13e725a
lint fix
htemelski-redis Oct 29, 2025
43c5b39
Merge branch 'master' into hybrid-search
htemelski-redis Oct 29, 2025
f1e8761
Merge branch 'master' into hybrid-search
ndyakov Nov 3, 2025
87e40f3
Merge branch 'master' into hybrid-search
ndyakov Nov 3, 2025
61bf9f0
Add support for XReadGroup CLAIM argument (#3578)
ofekshenawa Nov 3, 2025
c2639c7
feat(acl): add acl support and test (#3576)
destinyoooo Nov 4, 2025
d404288
feat(cmd): Add support for MSetEX command (#3580)
ofekshenawa Nov 5, 2025
4b6b715
fix(sentinel): handle empty address (#3577)
manisharma Nov 5, 2025
d8ee7f2
feat: support for latency command (#3584)
destinyoooo Nov 5, 2025
3dfa6a9
feat: Add support for certain slowlog commands (#3585)
destinyoooo Nov 5, 2025
f5281ae
feat(cmd): Add CAS/CAD commands (#3583)
ndyakov Nov 7, 2025
51de3df
updated ft hybrid, marked as experimental
htemelski-redis Nov 10, 2025
bbddaf9
updated fthybrid and its tests
htemelski-redis Nov 10, 2025
1bd2012
removed debugging prints
htemelski-redis Nov 10, 2025
f77909f
Merge branch 'master' into hybrid-search
htemelski-redis Nov 10, 2025
0bc0888
fixed lint, addressed comment
htemelski-redis Nov 10, 2025
4a57b61
fixed issues
htemelski-redis Nov 10, 2025
5d05e3b
fixed lint
htemelski-redis Nov 10, 2025
faf27d0
Ensure that the args are prefixed only if theres no prefix already
htemelski-redis Nov 11, 2025
e780ef0
Merge branch 'master' into hybrid-search
htemelski-redis Nov 11, 2025
1ae8313
Removed automatic args prefixing
htemelski-redis Nov 11, 2025
fff0000
Merge branch 'master' into hybrid-search
ndyakov Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 328 additions & 0 deletions search_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ type SearchCmdable interface {
FTDropIndexWithArgs(ctx context.Context, index string, options *FTDropIndexOptions) *StatusCmd
FTExplain(ctx context.Context, index string, query string) *StringCmd
FTExplainWithArgs(ctx context.Context, index string, query string, options *FTExplainOptions) *StringCmd
FTHybrid(ctx context.Context, index string, searchExpr string, vectorField string, vectorData Vector) *FTHybridCmd
FTHybridWithArgs(ctx context.Context, index string, options *FTHybridOptions) *FTHybridCmd
FTInfo(ctx context.Context, index string) *FTInfoCmd
FTSpellCheck(ctx context.Context, index string, query string) *FTSpellCheckCmd
FTSpellCheckWithArgs(ctx context.Context, index string, query string, options *FTSpellCheckOptions) *FTSpellCheckCmd
Expand Down Expand Up @@ -344,6 +346,85 @@ type FTSearchOptions struct {
DialectVersion int
}

// FTHybridCombineMethod represents the fusion method for combining search and vector results
type FTHybridCombineMethod string

const (
FTHybridCombineRRF FTHybridCombineMethod = "RRF"
FTHybridCombineLinear FTHybridCombineMethod = "LINEAR"
FTHybridCombineFunction FTHybridCombineMethod = "FUNCTION"
)

// FTHybridSearchExpression represents a search expression in hybrid search
type FTHybridSearchExpression struct {
Query string
Scorer string
ScorerParams []interface{}
YieldScoreAs string
}

// FTHybridVectorExpression represents a vector expression in hybrid search
type FTHybridVectorExpression struct {
VectorField string
VectorData Vector
Method string // KNN or RANGE
MethodParams []interface{}
Filter string
YieldScoreAs string
}

// FTHybridCombineOptions represents options for result fusion
type FTHybridCombineOptions struct {
Method FTHybridCombineMethod
Count int
Window int // For RRF
Constant float64 // For RRF
Alpha float64 // For LINEAR
Beta float64 // For LINEAR
YieldScoreAs string
}

// FTHybridGroupBy represents GROUP BY functionality
type FTHybridGroupBy struct {
Count int
Fields []string
ReduceFunc string
ReduceCount int
ReduceParams []interface{}
}

// FTHybridApply represents APPLY functionality
type FTHybridApply struct {
Expression string
AsField string
}

// FTHybridWithCursor represents cursor configuration for hybrid search
type FTHybridWithCursor struct {
Count int // Number of results to return per cursor read
MaxIdle int // Maximum idle time in milliseconds before cursor is automatically deleted
}

// FTHybridOptions hold options that can be passed to the FT.HYBRID command
type FTHybridOptions struct {
CountExpressions int // Number of search/vector expressions
SearchExpressions []FTHybridSearchExpression // Multiple search expressions
VectorExpressions []FTHybridVectorExpression // Multiple vector expressions
Combine *FTHybridCombineOptions // Fusion step options
Load []string // Projected fields
GroupBy *FTHybridGroupBy // Aggregation grouping
Apply []FTHybridApply // Field transformations
SortBy []FTSearchSortBy // Reuse from FTSearch
Filter string // Post-filter expression
LimitOffset int // Result limiting
Limit int
Params map[string]interface{} // Parameter substitution
ExplainScore bool // Include score explanations
Timeout int // Runtime timeout
WithCursor bool // Enable cursor support for large result sets
WithCursorOptions *FTHybridWithCursor // Cursor configuration options
}

type FTSynDumpResult struct {
Term string
Synonyms []string
Expand Down Expand Up @@ -1819,6 +1900,66 @@ func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) {
return nil
}

// FTHybridResult represents the result of a hybrid search operation
type FTHybridResult = FTSearchResult

type FTHybridCmd struct {
baseCmd
val FTHybridResult
options *FTHybridOptions
}

func newFTHybridCmd(ctx context.Context, options *FTHybridOptions, args ...interface{}) *FTHybridCmd {
return &FTHybridCmd{
baseCmd: baseCmd{
ctx: ctx,
args: args,
},
options: options,
}
}

func (cmd *FTHybridCmd) String() string {
return cmdString(cmd, cmd.val)
}

func (cmd *FTHybridCmd) SetVal(val FTHybridResult) {
cmd.val = val
}

func (cmd *FTHybridCmd) Result() (FTHybridResult, error) {
return cmd.val, cmd.err
}

func (cmd *FTHybridCmd) Val() FTHybridResult {
return cmd.val
}

func (cmd *FTHybridCmd) RawVal() interface{} {
return cmd.rawVal
}

func (cmd *FTHybridCmd) RawResult() (interface{}, error) {
return cmd.rawVal, cmd.err
}

func (cmd *FTHybridCmd) readReply(rd *proto.Reader) (err error) {
data, err := rd.ReadSlice()
if err != nil {
return err
}
// Parse hybrid search results similarly to FT.SEARCH
// We can reuse the FTSearch parser since the result format should be similar
searchResult, err := parseFTSearch(data, false, true, false, false)
if err != nil {
return err
}

// FTSearchResult and FTHybridResult are aliases
cmd.val = searchResult
return nil
}

// FTSearch - Executes a search query on an index.
// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
// For more information, please refer to the Redis documentation about [FT.SEARCH].
Expand Down Expand Up @@ -2191,3 +2332,190 @@ func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *Str
_ = c(ctx, cmd)
return cmd
}

// FTHybrid - Executes a hybrid search combining full-text search and vector similarity
// The 'index' parameter specifies the index to search, 'searchExpr' is the search query,
// 'vectorField' is the name of the vector field, and 'vectorData' is the vector to search with.
func (c cmdable) FTHybrid(ctx context.Context, index string, searchExpr string, vectorField string, vectorData Vector) *FTHybridCmd {
options := &FTHybridOptions{
CountExpressions: 2,
SearchExpressions: []FTHybridSearchExpression{
{Query: searchExpr},
},
VectorExpressions: []FTHybridVectorExpression{
{VectorField: vectorField, VectorData: vectorData},
},
}
return c.FTHybridWithArgs(ctx, index, options)
}

// FTHybridWithArgs - Executes a hybrid search with advanced options
func (c cmdable) FTHybridWithArgs(ctx context.Context, index string, options *FTHybridOptions) *FTHybridCmd {
args := []interface{}{"FT.HYBRID", index}

if options != nil {
// Add count expressions if specified
if options.CountExpressions > 0 {
args = append(args, options.CountExpressions)
} else {
// Default to 2 expressions (1 search + 1 vector)
args = append(args, 2)
}

// Add search expressions
for _, searchExpr := range options.SearchExpressions {
args = append(args, "SEARCH", searchExpr.Query)

if searchExpr.Scorer != "" {
args = append(args, "SCORER", searchExpr.Scorer)
if len(searchExpr.ScorerParams) > 0 {
args = append(args, searchExpr.ScorerParams...)
}
}

if searchExpr.YieldScoreAs != "" {
args = append(args, "YIELD_SCORE_AS", searchExpr.YieldScoreAs)
}
}

// Add vector expressions
for _, vectorExpr := range options.VectorExpressions {
args = append(args, "VSIM", "@"+vectorExpr.VectorField)
args = append(args, vectorExpr.VectorData.Value()...)

if vectorExpr.Method != "" {
args = append(args, vectorExpr.Method)
if len(vectorExpr.MethodParams) > 0 {
args = append(args, vectorExpr.MethodParams...)
}
}

if vectorExpr.Filter != "" {
args = append(args, "FILTER", vectorExpr.Filter)
}

if vectorExpr.YieldScoreAs != "" {
args = append(args, "YIELD_SCORE_AS", vectorExpr.YieldScoreAs)
}
}

// Add combine/fusion options
if options.Combine != nil {
args = append(args, "COMBINE", string(options.Combine.Method))

if options.Combine.Count > 0 {
args = append(args, options.Combine.Count)
}

switch options.Combine.Method {
case FTHybridCombineRRF:
if options.Combine.Window > 0 {
args = append(args, "WINDOW", options.Combine.Window)
}
if options.Combine.Constant > 0 {
args = append(args, "CONSTANT", options.Combine.Constant)
}
case FTHybridCombineLinear:
if options.Combine.Alpha > 0 {
args = append(args, "ALPHA", options.Combine.Alpha)
}
if options.Combine.Beta > 0 {
args = append(args, "BETA", options.Combine.Beta)
}
}

if options.Combine.YieldScoreAs != "" {
args = append(args, "YIELD_SCORE_AS", options.Combine.YieldScoreAs)
}
}

// Add LOAD (projected fields)
if len(options.Load) > 0 {
args = append(args, "LOAD", len(options.Load))
for _, field := range options.Load {
args = append(args, field)
}
}

// Add GROUPBY
if options.GroupBy != nil {
args = append(args, "GROUPBY", options.GroupBy.Count)
for _, field := range options.GroupBy.Fields {
args = append(args, field)
}
if options.GroupBy.ReduceFunc != "" {
args = append(args, "REDUCE", options.GroupBy.ReduceFunc, options.GroupBy.ReduceCount)
args = append(args, options.GroupBy.ReduceParams...)
}
}

// Add APPLY transformations
for _, apply := range options.Apply {
args = append(args, "APPLY", apply.Expression, "AS", apply.AsField)
}

// Add SORTBY
if len(options.SortBy) > 0 {
args = append(args, "SORTBY", len(options.SortBy))
for _, sortBy := range options.SortBy {
args = append(args, sortBy.FieldName)
if sortBy.Asc && sortBy.Desc {
cmd := newFTHybridCmd(ctx, options, args...)
cmd.SetErr(fmt.Errorf("FT.HYBRID: ASC and DESC are mutually exclusive"))
return cmd
}
if sortBy.Asc {
args = append(args, "ASC")
}
if sortBy.Desc {
args = append(args, "DESC")
}
}
}

// Add FILTER (post-filter)
if options.Filter != "" {
args = append(args, "FILTER", options.Filter)
}

// Add LIMIT
if options.LimitOffset >= 0 && options.Limit > 0 || options.LimitOffset > 0 && options.Limit == 0 {
args = append(args, "LIMIT", options.LimitOffset, options.Limit)
}

// Add PARAMS
if len(options.Params) > 0 {
args = append(args, "PARAMS", len(options.Params)*2)
for key, value := range options.Params {
args = append(args, key, value)
}
}

// Add EXPLAINSCORE
if options.ExplainScore {
args = append(args, "EXPLAINSCORE")
}

// Add TIMEOUT
if options.Timeout > 0 {
args = append(args, "TIMEOUT", options.Timeout)
}

// Add WITHCURSOR support
if options.WithCursor {
args = append(args, "WITHCURSOR")
if options.WithCursorOptions != nil {
if options.WithCursorOptions.Count > 0 {
args = append(args, "COUNT", options.WithCursorOptions.Count)
}
if options.WithCursorOptions.MaxIdle > 0 {
args = append(args, "MAXIDLE", options.WithCursorOptions.MaxIdle)
}
}
}
}

cmd := newFTHybridCmd(ctx, options, args...)
_ = c(ctx, cmd)
return cmd
}
Loading
Loading