Skip to content

Commit bbddaf9

Browse files
updated fthybrid and its tests
1 parent 51de3df commit bbddaf9

File tree

2 files changed

+243
-70
lines changed

2 files changed

+243
-70
lines changed

search_commands.go

Lines changed: 177 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,21 +1901,39 @@ func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) {
19011901
}
19021902

19031903
// FTHybridResult represents the result of a hybrid search operation
1904-
type FTHybridResult = FTSearchResult
1904+
type FTHybridResult struct {
1905+
TotalResults int
1906+
Results []map[string]interface{}
1907+
Warnings []string
1908+
ExecutionTime float64
1909+
}
1910+
1911+
// FTHybridCursorResult represents cursor result for hybrid search
1912+
type FTHybridCursorResult struct {
1913+
SearchCursorID int
1914+
VsimCursorID int
1915+
}
19051916

19061917
type FTHybridCmd struct {
19071918
baseCmd
1908-
val FTHybridResult
1909-
options *FTHybridOptions
1919+
val FTHybridResult
1920+
cursorVal *FTHybridCursorResult
1921+
options *FTHybridOptions
1922+
withCursor bool
19101923
}
19111924

19121925
func newFTHybridCmd(ctx context.Context, options *FTHybridOptions, args ...interface{}) *FTHybridCmd {
1926+
withCursor := false
1927+
if options != nil && options.WithCursor {
1928+
withCursor = true
1929+
}
19131930
return &FTHybridCmd{
19141931
baseCmd: baseCmd{
19151932
ctx: ctx,
19161933
args: args,
19171934
},
1918-
options: options,
1935+
options: options,
1936+
withCursor: withCursor,
19191937
}
19201938
}
19211939

@@ -1931,10 +1949,18 @@ func (cmd *FTHybridCmd) Result() (FTHybridResult, error) {
19311949
return cmd.val, cmd.err
19321950
}
19331951

1952+
func (cmd *FTHybridCmd) CursorResult() (*FTHybridCursorResult, error) {
1953+
return cmd.cursorVal, cmd.err
1954+
}
1955+
19341956
func (cmd *FTHybridCmd) Val() FTHybridResult {
19351957
return cmd.val
19361958
}
19371959

1960+
func (cmd *FTHybridCmd) CursorVal() *FTHybridCursorResult {
1961+
return cmd.cursorVal
1962+
}
1963+
19381964
func (cmd *FTHybridCmd) RawVal() interface{} {
19391965
return cmd.rawVal
19401966
}
@@ -1943,20 +1969,132 @@ func (cmd *FTHybridCmd) RawResult() (interface{}, error) {
19431969
return cmd.rawVal, cmd.err
19441970
}
19451971

1972+
func parseFTHybrid(data []interface{}, withCursor bool) (FTHybridResult, *FTHybridCursorResult, error) {
1973+
// Convert to map
1974+
resultMap := make(map[string]interface{})
1975+
for i := 0; i < len(data); i += 2 {
1976+
if i+1 < len(data) {
1977+
key, ok := data[i].(string)
1978+
if !ok {
1979+
return FTHybridResult{}, nil, fmt.Errorf("invalid key type at index %d", i)
1980+
}
1981+
resultMap[key] = data[i+1]
1982+
}
1983+
}
1984+
1985+
fmt.Println("resultMap", resultMap)
1986+
1987+
// Handle cursor result
1988+
if withCursor {
1989+
searchCursorID, ok1 := resultMap["SEARCH"].(int64)
1990+
vsimCursorID, ok2 := resultMap["VSIM"].(int64)
1991+
if !ok1 || !ok2 {
1992+
return FTHybridResult{}, nil, fmt.Errorf("invalid cursor result format")
1993+
}
1994+
return FTHybridResult{}, &FTHybridCursorResult{
1995+
SearchCursorID: int(searchCursorID),
1996+
VsimCursorID: int(vsimCursorID),
1997+
}, nil
1998+
}
1999+
2000+
// Parse regular result
2001+
totalResults, ok := resultMap["total_results"].(int64)
2002+
if !ok {
2003+
return FTHybridResult{}, nil, fmt.Errorf("invalid total_results format")
2004+
}
2005+
2006+
resultsData, ok := resultMap["results"].([]interface{})
2007+
if !ok {
2008+
return FTHybridResult{}, nil, fmt.Errorf("invalid results format")
2009+
}
2010+
2011+
// Parse each result item
2012+
// In RESP3, the results are already parsed as maps
2013+
results := make([]map[string]interface{}, 0, len(resultsData))
2014+
for _, item := range resultsData {
2015+
// Try as map first (RESP3)
2016+
if itemMap, ok := item.(map[interface{}]interface{}); ok {
2017+
// Convert map[interface{}]interface{} to map[string]interface{}
2018+
convertedMap := make(map[string]interface{})
2019+
for k, v := range itemMap {
2020+
if keyStr, ok := k.(string); ok {
2021+
convertedMap[keyStr] = v
2022+
}
2023+
}
2024+
results = append(results, convertedMap)
2025+
continue
2026+
}
2027+
2028+
// Fallback to array format (RESP2)
2029+
itemData, ok := item.([]interface{})
2030+
if !ok {
2031+
return FTHybridResult{}, nil, fmt.Errorf("invalid result item format")
2032+
}
2033+
2034+
itemMap := make(map[string]interface{})
2035+
for i := 0; i < len(itemData); i += 2 {
2036+
if i+1 < len(itemData) {
2037+
key, ok := itemData[i].(string)
2038+
if !ok {
2039+
return FTHybridResult{}, nil, fmt.Errorf("invalid item key format")
2040+
}
2041+
itemMap[key] = itemData[i+1]
2042+
}
2043+
}
2044+
results = append(results, itemMap)
2045+
}
2046+
2047+
// Parse warnings (optional field)
2048+
warnings := make([]string, 0)
2049+
if warningsData, ok := resultMap["warnings"].([]interface{}); ok {
2050+
for _, w := range warningsData {
2051+
if ws, ok := w.(string); ok {
2052+
warnings = append(warnings, ws)
2053+
}
2054+
}
2055+
}
2056+
2057+
// Parse execution time (optional field)
2058+
var executionTime float64
2059+
if execTimeVal, exists := resultMap["execution_time"]; exists {
2060+
switch v := execTimeVal.(type) {
2061+
case string:
2062+
var err error
2063+
executionTime, err = strconv.ParseFloat(v, 64)
2064+
if err != nil {
2065+
return FTHybridResult{}, nil, fmt.Errorf("invalid execution_time format: %v", err)
2066+
}
2067+
case float64:
2068+
executionTime = v
2069+
case int64:
2070+
executionTime = float64(v)
2071+
}
2072+
}
2073+
2074+
return FTHybridResult{
2075+
TotalResults: int(totalResults),
2076+
Results: results,
2077+
Warnings: warnings,
2078+
ExecutionTime: executionTime,
2079+
}, nil, nil
2080+
}
2081+
19462082
func (cmd *FTHybridCmd) readReply(rd *proto.Reader) (err error) {
19472083
data, err := rd.ReadSlice()
19482084
if err != nil {
19492085
return err
19502086
}
1951-
// Parse hybrid search results similarly to FT.SEARCH
1952-
// We can reuse the FTSearch parser since the result format should be similar
1953-
searchResult, err := parseFTSearch(data, false, true, false, false)
2087+
fmt.Println("data", data)
2088+
result, cursorResult, err := parseFTHybrid(data, cmd.withCursor)
19542089
if err != nil {
19552090
return err
19562091
}
19572092

1958-
// FTSearchResult and FTHybridResult are aliases
1959-
cmd.val = searchResult
2093+
if cmd.withCursor {
2094+
cmd.cursorVal = cursorResult
2095+
} else {
2096+
cmd.val = result
2097+
}
19602098
return nil
19612099
}
19622100

@@ -2375,11 +2513,23 @@ func (c cmdable) FTHybridWithArgs(ctx context.Context, index string, options *FT
23752513
// Add vector expressions
23762514
for _, vectorExpr := range options.VectorExpressions {
23772515
args = append(args, "VSIM", "@"+vectorExpr.VectorField)
2378-
args = append(args, vectorExpr.VectorData.Value()...)
2516+
2517+
// For FT.HYBRID, we need to send just the raw vector bytes, not the Value() format
2518+
// Value() returns [format, data] but FT.HYBRID expects just the blob
2519+
vectorValue := vectorExpr.VectorData.Value()
2520+
if len(vectorValue) >= 2 {
2521+
// vectorValue is [format, data, ...] - we only want the data part
2522+
args = append(args, vectorValue[1])
2523+
} else {
2524+
// Fallback for unexpected format
2525+
args = append(args, vectorValue...)
2526+
}
23792527

23802528
if vectorExpr.Method != "" {
23812529
args = append(args, vectorExpr.Method)
23822530
if len(vectorExpr.MethodParams) > 0 {
2531+
// MethodParams should be key-value pairs, count them
2532+
args = append(args, len(vectorExpr.MethodParams))
23832533
args = append(args, vectorExpr.MethodParams...)
23842534
}
23852535
}
@@ -2395,39 +2545,45 @@ func (c cmdable) FTHybridWithArgs(ctx context.Context, index string, options *FT
23952545

23962546
// Add combine/fusion options
23972547
if options.Combine != nil {
2398-
args = append(args, "COMBINE", string(options.Combine.Method))
2399-
2400-
if options.Combine.Count > 0 {
2401-
args = append(args, options.Combine.Count)
2402-
}
2548+
// Build combine parameters
2549+
combineParams := []interface{}{}
24032550

24042551
switch options.Combine.Method {
24052552
case FTHybridCombineRRF:
24062553
if options.Combine.Window > 0 {
2407-
args = append(args, "WINDOW", options.Combine.Window)
2554+
combineParams = append(combineParams, "WINDOW", options.Combine.Window)
24082555
}
24092556
if options.Combine.Constant > 0 {
2410-
args = append(args, "CONSTANT", options.Combine.Constant)
2557+
combineParams = append(combineParams, "CONSTANT", options.Combine.Constant)
24112558
}
24122559
case FTHybridCombineLinear:
24132560
if options.Combine.Alpha > 0 {
2414-
args = append(args, "ALPHA", options.Combine.Alpha)
2561+
combineParams = append(combineParams, "ALPHA", options.Combine.Alpha)
24152562
}
24162563
if options.Combine.Beta > 0 {
2417-
args = append(args, "BETA", options.Combine.Beta)
2564+
combineParams = append(combineParams, "BETA", options.Combine.Beta)
24182565
}
24192566
}
24202567

24212568
if options.Combine.YieldScoreAs != "" {
2422-
args = append(args, "YIELD_SCORE_AS", options.Combine.YieldScoreAs)
2569+
combineParams = append(combineParams, "YIELD_SCORE_AS", options.Combine.YieldScoreAs)
2570+
}
2571+
2572+
// Add COMBINE with method and parameter count
2573+
args = append(args, "COMBINE", string(options.Combine.Method))
2574+
if len(combineParams) > 0 {
2575+
args = append(args, len(combineParams))
2576+
args = append(args, combineParams...)
24232577
}
24242578
}
24252579

24262580
// Add LOAD (projected fields)
24272581
if len(options.Load) > 0 {
24282582
args = append(args, "LOAD", len(options.Load))
24292583
for _, field := range options.Load {
2430-
args = append(args, field)
2584+
// Redis requires field names in LOAD to be prefixed with '@' (or '$' for JSON paths).
2585+
// Tests pass plain field names (e.g. "description"), so add the '@' prefix here.
2586+
args = append(args, "@"+field)
24312587
}
24322588
}
24332589

0 commit comments

Comments
 (0)