1+ import { strict as assert } from 'node:assert' ;
2+ import testUtils , { GLOBAL } from '../test-utils' ;
3+ import HYBRID from './HYBRID' ;
4+ import { BasicCommandParser } from '@redis/client/lib/client/parser' ;
5+
6+ describe ( 'FT.HYBRID' , ( ) => {
7+ describe ( 'parseCommand' , ( ) => {
8+ it ( 'minimal command' , ( ) => {
9+ const parser = new BasicCommandParser ( ) ;
10+ HYBRID . parseCommand ( parser , 'index' ) ;
11+ assert . deepEqual (
12+ parser . redisArgs ,
13+ [ 'FT.HYBRID' , 'index' , '2' , 'DIALECT' , '2' ]
14+ ) ;
15+ } ) ;
16+
17+ it ( 'with count expressions' , ( ) => {
18+ const parser = new BasicCommandParser ( ) ;
19+ HYBRID . parseCommand ( parser , 'index' , {
20+ countExpressions : 3
21+ } ) ;
22+ assert . deepEqual (
23+ parser . redisArgs ,
24+ [ 'FT.HYBRID' , 'index' , '3' , 'DIALECT' , '2' ]
25+ ) ;
26+ } ) ;
27+
28+ it ( 'with SEARCH expression' , ( ) => {
29+ const parser = new BasicCommandParser ( ) ;
30+ HYBRID . parseCommand ( parser , 'index' , {
31+ SEARCH : {
32+ query : '@description: bikes'
33+ }
34+ } ) ;
35+ assert . deepEqual (
36+ parser . redisArgs ,
37+ [ 'FT.HYBRID' , 'index' , '2' , 'SEARCH' , '@description: bikes' , 'DIALECT' , '2' ]
38+ ) ;
39+ } ) ;
40+
41+ it ( 'with SEARCH expression and SCORER' , ( ) => {
42+ const parser = new BasicCommandParser ( ) ;
43+ HYBRID . parseCommand ( parser , 'index' , {
44+ SEARCH : {
45+ query : '@description: bikes' ,
46+ SCORER : {
47+ algorithm : 'TFIDF.DOCNORM' ,
48+ params : [ 'param1' , 'param2' ]
49+ } ,
50+ YIELD_SCORE_AS : 'search_score'
51+ }
52+ } ) ;
53+ assert . deepEqual (
54+ parser . redisArgs ,
55+ [
56+ 'FT.HYBRID' , 'index' , '2' , 'SEARCH' , '@description: bikes' ,
57+ 'SCORER' , 'TFIDF.DOCNORM' , 'param1' , 'param2' ,
58+ 'YIELD_SCORE_AS' , 'search_score' , 'DIALECT' , '2'
59+ ]
60+ ) ;
61+ } ) ;
62+
63+ it ( 'with VSIM expression and KNN method' , ( ) => {
64+ const parser = new BasicCommandParser ( ) ;
65+ HYBRID . parseCommand ( parser , 'index' , {
66+ VSIM : {
67+ field : '@vector_field' ,
68+ vectorData : 'BLOB_DATA' ,
69+ method : {
70+ KNN : {
71+ K : 10 ,
72+ EF_RUNTIME : 50 ,
73+ YIELD_DISTANCE_AS : 'vector_dist'
74+ }
75+ }
76+ }
77+ } ) ;
78+ assert . deepEqual (
79+ parser . redisArgs ,
80+ [
81+ 'FT.HYBRID' , 'index' , '2' , 'VSIM' , '@vector_field' , 'BLOB_DATA' ,
82+ 'KNN' , '1' , 'K' , '10' , 'EF_RUNTIME' , '50' , 'YIELD_DISTANCE_AS' , 'vector_dist' ,
83+ 'DIALECT' , '2'
84+ ]
85+ ) ;
86+ } ) ;
87+
88+ it ( 'with VSIM expression and RANGE method' , ( ) => {
89+ const parser = new BasicCommandParser ( ) ;
90+ HYBRID . parseCommand ( parser , 'index' , {
91+ VSIM : {
92+ field : '@vector_field' ,
93+ vectorData : 'BLOB_DATA' ,
94+ method : {
95+ RANGE : {
96+ RADIUS : 0.5 ,
97+ EPSILON : 0.01 ,
98+ YIELD_DISTANCE_AS : 'vector_dist'
99+ }
100+ }
101+ }
102+ } ) ;
103+ assert . deepEqual (
104+ parser . redisArgs ,
105+ [
106+ 'FT.HYBRID' , 'index' , '2' , 'VSIM' , '@vector_field' , 'BLOB_DATA' ,
107+ 'RANGE' , '1' , 'RADIUS' , '0.5' , 'EPSILON' , '0.01' , 'YIELD_DISTANCE_AS' , 'vector_dist' ,
108+ 'DIALECT' , '2'
109+ ]
110+ ) ;
111+ } ) ;
112+
113+ it ( 'with VSIM expression and FILTER' , ( ) => {
114+ const parser = new BasicCommandParser ( ) ;
115+ HYBRID . parseCommand ( parser , 'index' , {
116+ VSIM : {
117+ field : '@vector_field' ,
118+ vectorData : 'BLOB_DATA' ,
119+ FILTER : {
120+ expression : '@category:{bikes}' ,
121+ POLICY : 'BATCHES' ,
122+ BATCHES : {
123+ BATCH_SIZE : 100
124+ }
125+ } ,
126+ YIELD_SCORE_AS : 'vsim_score'
127+ }
128+ } ) ;
129+ assert . deepEqual (
130+ parser . redisArgs ,
131+ [
132+ 'FT.HYBRID' , 'index' , '2' , 'VSIM' , '@vector_field' , 'BLOB_DATA' ,
133+ 'FILTER' , '@category:{bikes}' , 'POLICY' , 'BATCHES' , 'BATCHES' , 'BATCH_SIZE' , '100' ,
134+ 'YIELD_SCORE_AS' , 'vsim_score' , 'DIALECT' , '2'
135+ ]
136+ ) ;
137+ } ) ;
138+
139+ it ( 'with RRF COMBINE method' , ( ) => {
140+ const parser = new BasicCommandParser ( ) ;
141+ HYBRID . parseCommand ( parser , 'index' , {
142+ COMBINE : {
143+ method : {
144+ RRF : {
145+ count : 2 ,
146+ WINDOW : 10 ,
147+ CONSTANT : 60
148+ }
149+ } ,
150+ YIELD_SCORE_AS : 'combined_score'
151+ }
152+ } ) ;
153+ assert . deepEqual (
154+ parser . redisArgs ,
155+ [
156+ 'FT.HYBRID' , 'index' , '2' , 'COMBINE' , 'RRF' , '2' , 'WINDOW' , '10' , 'CONSTANT' , '60' ,
157+ 'YIELD_SCORE_AS' , 'combined_score' , 'DIALECT' , '2'
158+ ]
159+ ) ;
160+ } ) ;
161+
162+ it ( 'with LINEAR COMBINE method' , ( ) => {
163+ const parser = new BasicCommandParser ( ) ;
164+ HYBRID . parseCommand ( parser , 'index' , {
165+ COMBINE : {
166+ method : {
167+ LINEAR : {
168+ count : 2 ,
169+ ALPHA : 0.7 ,
170+ BETA : 0.3
171+ }
172+ }
173+ }
174+ } ) ;
175+ assert . deepEqual (
176+ parser . redisArgs ,
177+ [
178+ 'FT.HYBRID' , 'index' , '2' , 'COMBINE' , 'LINEAR' , '2' , 'ALPHA' , '0.7' , 'BETA' , '0.3' ,
179+ 'DIALECT' , '2'
180+ ]
181+ ) ;
182+ } ) ;
183+
184+ it ( 'with LOAD, SORTBY, and LIMIT' , ( ) => {
185+ const parser = new BasicCommandParser ( ) ;
186+ HYBRID . parseCommand ( parser , 'index' , {
187+ LOAD : [ 'field1' , 'field2' ] ,
188+ SORTBY : {
189+ count : 1 ,
190+ fields : [
191+ { field : 'score' , direction : 'DESC' }
192+ ]
193+ } ,
194+ LIMIT : {
195+ offset : 0 ,
196+ num : 10
197+ }
198+ } ) ;
199+ assert . deepEqual (
200+ parser . redisArgs ,
201+ [
202+ 'FT.HYBRID' , 'index' , '2' , 'LOAD' , '2' , 'field1' , 'field2' ,
203+ 'SORTBY' , '1' , 'score' , 'DESC' , 'LIMIT' , '0' , '10' , 'DIALECT' , '2'
204+ ]
205+ ) ;
206+ } ) ;
207+
208+ it ( 'with GROUPBY and REDUCE' , ( ) => {
209+ const parser = new BasicCommandParser ( ) ;
210+ HYBRID . parseCommand ( parser , 'index' , {
211+ GROUPBY : {
212+ fields : [ '@category' ] ,
213+ REDUCE : {
214+ function : 'COUNT' ,
215+ count : 0 ,
216+ args : [ ]
217+ }
218+ }
219+ } ) ;
220+ assert . deepEqual (
221+ parser . redisArgs ,
222+ [
223+ 'FT.HYBRID' , 'index' , '2' , 'GROUPBY' , '1' , '@category' , 'REDUCE' , 'COUNT' , '0' ,
224+ 'DIALECT' , '2'
225+ ]
226+ ) ;
227+ } ) ;
228+
229+ it ( 'with APPLY' , ( ) => {
230+ const parser = new BasicCommandParser ( ) ;
231+ HYBRID . parseCommand ( parser , 'index' , {
232+ APPLY : {
233+ expression : '@score * 2' ,
234+ AS : 'double_score'
235+ }
236+ } ) ;
237+ assert . deepEqual (
238+ parser . redisArgs ,
239+ [ 'FT.HYBRID' , 'index' , '2' , 'APPLY' , '@score * 2' , 'AS' , 'double_score' , 'DIALECT' , '2' ]
240+ ) ;
241+ } ) ;
242+
243+ it ( 'with FILTER and post-processing' , ( ) => {
244+ const parser = new BasicCommandParser ( ) ;
245+ HYBRID . parseCommand ( parser , 'index' , {
246+ FILTER : '@price:[100 500]'
247+ } ) ;
248+ assert . deepEqual (
249+ parser . redisArgs ,
250+ [ 'FT.HYBRID' , 'index' , '2' , 'FILTER' , '@price:[100 500]' , 'DIALECT' , '2' ]
251+ ) ;
252+ } ) ;
253+
254+ it ( 'with PARAMS' , ( ) => {
255+ const parser = new BasicCommandParser ( ) ;
256+ HYBRID . parseCommand ( parser , 'index' , {
257+ PARAMS : {
258+ query_vector : 'BLOB_DATA' ,
259+ min_price : 100
260+ }
261+ } ) ;
262+ assert . deepEqual (
263+ parser . redisArgs ,
264+ [
265+ 'FT.HYBRID' , 'index' , '2' , 'PARAMS' , '4' , 'query_vector' , 'BLOB_DATA' , 'min_price' , '100' ,
266+ 'DIALECT' , '2'
267+ ]
268+ ) ;
269+ } ) ;
270+
271+ it ( 'with EXPLAINSCORE and TIMEOUT' , ( ) => {
272+ const parser = new BasicCommandParser ( ) ;
273+ HYBRID . parseCommand ( parser , 'index' , {
274+ EXPLAINSCORE : true ,
275+ TIMEOUT : 5000
276+ } ) ;
277+ assert . deepEqual (
278+ parser . redisArgs ,
279+ [ 'FT.HYBRID' , 'index' , '2' , 'EXPLAINSCORE' , 'TIMEOUT' , '5000' , 'DIALECT' , '2' ]
280+ ) ;
281+ } ) ;
282+
283+ it ( 'with WITHCURSOR' , ( ) => {
284+ const parser = new BasicCommandParser ( ) ;
285+ HYBRID . parseCommand ( parser , 'index' , {
286+ WITHCURSOR : {
287+ COUNT : 100 ,
288+ MAXIDLE : 300000
289+ }
290+ } ) ;
291+ assert . deepEqual (
292+ parser . redisArgs ,
293+ [
294+ 'FT.HYBRID' , 'index' , '2' , 'WITHCURSOR' , 'COUNT' , '100' , 'MAXIDLE' , '300000' ,
295+ 'DIALECT' , '2'
296+ ]
297+ ) ;
298+ } ) ;
299+
300+ it ( 'complete example with all options' , ( ) => {
301+ const parser = new BasicCommandParser ( ) ;
302+ HYBRID . parseCommand ( parser , 'index' , {
303+ countExpressions : 2 ,
304+ SEARCH : {
305+ query : '@description: bikes' ,
306+ SCORER : {
307+ algorithm : 'TFIDF.DOCNORM'
308+ } ,
309+ YIELD_SCORE_AS : 'text_score'
310+ } ,
311+ VSIM : {
312+ field : '@vector_field' ,
313+ vectorData : '$query_vector' ,
314+ method : {
315+ KNN : {
316+ K : 5
317+ }
318+ } ,
319+ YIELD_SCORE_AS : 'vector_score'
320+ } ,
321+ COMBINE : {
322+ method : {
323+ RRF : {
324+ count : 2 ,
325+ CONSTANT : 60
326+ }
327+ } ,
328+ YIELD_SCORE_AS : 'final_score'
329+ } ,
330+ LOAD : [ 'description' , 'price' ] ,
331+ SORTBY : {
332+ count : 1 ,
333+ fields : [ { field : 'final_score' , direction : 'DESC' } ]
334+ } ,
335+ LIMIT : {
336+ offset : 0 ,
337+ num : 10
338+ } ,
339+ PARAMS : {
340+ query_vector : 'BLOB_DATA'
341+ }
342+ } ) ;
343+ assert . deepEqual (
344+ parser . redisArgs ,
345+ [
346+ 'FT.HYBRID' , 'index' , '2' ,
347+ 'SEARCH' , '@description: bikes' , 'SCORER' , 'TFIDF.DOCNORM' , 'YIELD_SCORE_AS' , 'text_score' ,
348+ 'VSIM' , '@vector_field' , '$query_vector' , 'KNN' , '1' , 'K' , '5' , 'YIELD_SCORE_AS' , 'vector_score' ,
349+ 'COMBINE' , 'RRF' , '2' , 'CONSTANT' , '60' , 'YIELD_SCORE_AS' , 'final_score' ,
350+ 'LOAD' , '2' , 'description' , 'price' ,
351+ 'SORTBY' , '1' , 'final_score' , 'DESC' ,
352+ 'LIMIT' , '0' , '10' ,
353+ 'PARAMS' , '2' , 'query_vector' , 'BLOB_DATA' ,
354+ 'DIALECT' , '2'
355+ ]
356+ ) ;
357+ } ) ;
358+
359+ it ( 'with custom DIALECT' , ( ) => {
360+ const parser = new BasicCommandParser ( ) ;
361+ HYBRID . parseCommand ( parser , 'index' , {
362+ DIALECT : 3
363+ } ) ;
364+ assert . deepEqual (
365+ parser . redisArgs ,
366+ [ 'FT.HYBRID' , 'index' , '2' , 'DIALECT' , '3' ]
367+ ) ;
368+ } ) ;
369+ } ) ;
370+
371+ // Integration tests would need to be added when RediSearch supports FT.HYBRID
372+ // For now, we'll skip them as this is a new command that may not be available yet
373+ describe . skip ( 'client.ft.hybrid' , ( ) => {
374+ testUtils . testWithClient ( 'basic hybrid search' , async client => {
375+ // This would require a test index and data setup
376+ // similar to how other FT commands are tested
377+ } , GLOBAL . SERVERS . OPEN ) ;
378+ } ) ;
379+ } ) ;
0 commit comments