@@ -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
19061917type FTHybridCmd struct {
19071918 baseCmd
1908- val FTHybridResult
1909- options * FTHybridOptions
1919+ val FTHybridResult
1920+ cursorVal * FTHybridCursorResult
1921+ options * FTHybridOptions
1922+ withCursor bool
19101923}
19111924
19121925func 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+
19341956func (cmd * FTHybridCmd ) Val () FTHybridResult {
19351957 return cmd .val
19361958}
19371959
1960+ func (cmd * FTHybridCmd ) CursorVal () * FTHybridCursorResult {
1961+ return cmd .cursorVal
1962+ }
1963+
19381964func (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+
19462082func (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