@@ -290,7 +290,7 @@ def testProfileVector(env):
290290
291291 actual_res = conn .execute_command ('ft.profile' , 'idx' , 'search' , 'query' , '*=>[KNN 3 @v $vec]' ,
292292 'SORTBY' , '__v_score' , 'PARAMS' , '2' , 'vec' , 'aaaaaaaa' , 'nocontent' )
293- expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 3 ]
293+ expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 3 , 'Vector search mode' , 'STANDARD_KNN' ]
294294 expected_vecsim_rp_res = ['Type' , 'Metrics Applier' , 'Results processed' , 3 ]
295295 env .assertEqual (actual_res [0 ], [3 , '4' , '2' , '1' ])
296296 actual_profile = to_dict (actual_res [1 ][1 ][0 ])
@@ -301,7 +301,7 @@ def testProfileVector(env):
301301 # Range query - uses metric iterator. Radius is set so that the closest 2 vectors will be in the range
302302 actual_res = conn .execute_command ('ft.profile' , 'idx' , 'search' , 'query' , '@v:[VECTOR_RANGE 3e36 $vec]=>{$yield_distance_as:dist}' ,
303303 'SORTBY' , 'dist' , 'PARAMS' , '2' , 'vec' , 'aaaaaaaa' , 'nocontent' )
304- expected_iterators_res = ['Type' , 'METRIC - VECTOR DISTANCE' , 'Number of reading operations' , 2 ]
304+ expected_iterators_res = ['Type' , 'METRIC - VECTOR DISTANCE' , 'Number of reading operations' , 2 , 'Vector search mode' , 'RANGE_QUERY' ]
305305 expected_vecsim_rp_res = ['Type' , 'Metrics Applier' , 'Results processed' , 2 ]
306306 env .assertEqual (actual_res [0 ], [2 , '4' , '2' ])
307307 actual_profile = to_dict (actual_res [1 ][1 ][0 ])
@@ -313,7 +313,7 @@ def testProfileVector(env):
313313 # Expect ad-hoc BF to take place - going over child iterator exactly once (reading 2 results)
314314 actual_res = conn .execute_command ('ft.profile' , 'idx' , 'search' , 'query' , '(@t:hello world)=>[KNN 3 @v $vec]' ,
315315 'SORTBY' , '__v_score' , 'PARAMS' , '2' , 'vec' , 'aaaaaaaa' , 'nocontent' )
316- expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 2 , 'Child iterator' ,
316+ expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 2 , 'Vector search mode' , 'HYBRID_ADHOC_BF' , ' Child iterator' ,
317317 ['Type' , 'INTERSECT' , 'Number of reading operations' , 2 , 'Child iterators' , [
318318 ['Type' , 'TEXT' , 'Term' , 'world' , 'Number of reading operations' , 2 , 'Estimated number of matches' , 2 ],
319319 ['Type' , 'TEXT' , 'Term' , 'hello' , 'Number of reading operations' , 2 , 'Estimated number of matches' , 5 ]]]]
@@ -332,7 +332,7 @@ def testProfileVector(env):
332332 actual_res = conn .execute_command ('ft.profile' , 'idx' , 'search' , 'query' , '(@t:hello world)=>[KNN 3 @v $vec]' ,
333333 'SORTBY' , '__v_score' , 'PARAMS' , '2' , 'vec' , 'aaaaaaaa' , 'nocontent' )
334334 env .assertEqual (actual_res [0 ], [3 , '4' , '6' , '7' ])
335- expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 3 , 'Batches number' , 2 , 'Child iterator' ,
335+ expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 3 , 'Vector search mode' , 'HYBRID_BATCHES' , ' Batches number' , 2 , 'Largest batch size' , 4 , 'Largest batch iteration (zero based)' , 0 , 'Child iterator' ,
336336 ['Type' , 'INTERSECT' , 'Number of reading operations' , 8 , 'Child iterators' , [
337337 ['Type' , 'TEXT' , 'Term' , 'world' , 'Number of reading operations' , 8 , 'Estimated number of matches' , 9997 ],
338338 ['Type' , 'TEXT' , 'Term' , 'hello' , 'Number of reading operations' , 8 , 'Estimated number of matches' , 10000 ]]]]
@@ -348,7 +348,7 @@ def testProfileVector(env):
348348
349349 # expected results that pass the filter is index_size/2. after two iterations with no results,
350350 # we should move ad-hoc BF.
351- expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 0 , 'Batches number' , 2 , 'Child iterator' ,
351+ expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 0 , 'Vector search mode' , 'HYBRID_BATCHES_TO_ADHOC_BF' , ' Batches number' , 2 , 'Largest batch size' , 13 , 'Largest batch iteration (zero based)' , 1 , 'Child iterator' ,
352352 ['Type' , 'INTERSECT' , 'Number of reading operations' , 2 , 'Child iterators' , [
353353 ['Type' , 'TEXT' , 'Term' , 'hello' , 'Number of reading operations' , 5 , 'Estimated number of matches' , 10000 ],
354354 ['Type' , 'TEXT' , 'Term' , 'other' , 'Number of reading operations' , 3 , 'Estimated number of matches' , 10000 ]]]]
@@ -363,7 +363,7 @@ def testProfileVector(env):
363363 # index after the 13th batch.
364364 actual_res = conn .execute_command ('ft.profile' , 'idx' , 'search' , 'query' , '(@t:hello other)=>[KNN 2 @v $vec HYBRID_POLICY BATCHES]' ,
365365 'SORTBY' , '__v_score' , 'PARAMS' , '2' , 'vec' , '????????' , 'nocontent' )
366- expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 0 , 'Batches number' , 13 , 'Child iterator' ,
366+ expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 0 , 'Vector search mode' , 'HYBRID_BATCHES' , ' Batches number' , 13 , 'Largest batch size' , 20001 , 'Largest batch iteration (zero based)' , 12 , 'Child iterator' ,
367367 ['Type' , 'INTERSECT' , 'Number of reading operations' , 12 , 'Child iterators' , [
368368 ['Type' , 'TEXT' , 'Term' , 'hello' , 'Number of reading operations' , 25 , 'Estimated number of matches' , 10000 ],
369369 ['Type' , 'TEXT' , 'Term' , 'other' , 'Number of reading operations' , 13 , 'Estimated number of matches' , 10000 ]]]]
@@ -375,7 +375,7 @@ def testProfileVector(env):
375375 # After 200 iterations, we should go over the entire index.
376376 actual_res = conn .execute_command ('ft.profile' , 'idx' , 'search' , 'query' , '(@t:hello other)=>[KNN 2 @v $vec HYBRID_POLICY BATCHES BATCH_SIZE 100]' ,
377377 'SORTBY' , '__v_score' , 'PARAMS' , '2' , 'vec' , '????????' , 'nocontent' , 'timeout' , '100000' )
378- expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 0 , 'Batches number' , 200 , 'Child iterator' ,
378+ expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 0 , 'Vector search mode' , 'HYBRID_BATCHES' , ' Batches number' , 200 , 'Largest batch size' , 100 , 'Largest batch iteration (zero based)' , 0 , 'Child iterator' ,
379379 ['Type' , 'INTERSECT' , 'Number of reading operations' , 199 , 'Child iterators' , [
380380 ['Type' , 'TEXT' , 'Term' , 'hello' , 'Number of reading operations' , 399 , 'Estimated number of matches' , 10000 ],
381381 ['Type' , 'TEXT' , 'Term' , 'other' , 'Number of reading operations' , 200 , 'Estimated number of matches' , 10000 ]]]]
@@ -389,7 +389,7 @@ def testProfileVector(env):
389389 # every iteration that returned 0 results.
390390 actual_res = conn .execute_command ('ft.profile' , 'idx' , 'search' , 'query' , '(@t:hello other)=>[KNN 2 @v $vec BATCH_SIZE 100]' ,
391391 'SORTBY' , '__v_score' , 'PARAMS' , '2' , 'vec' , '????????' , 'nocontent' )
392- expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 0 , 'Batches number' , 2 , 'Child iterator' ,
392+ expected_iterators_res = ['Type' , 'VECTOR' , 'Number of reading operations' , 0 , 'Vector search mode' , 'HYBRID_BATCHES_TO_ADHOC_BF' , ' Batches number' , 2 , 'Largest batch size' , 100 , 'Largest batch iteration (zero based)' , 0 , 'Child iterator' ,
393393 ['Type' , 'INTERSECT' , 'Number of reading operations' , 2 , 'Child iterators' , [
394394 ['Type' , 'TEXT' , 'Term' , 'hello' , 'Number of reading operations' , 5 , 'Estimated number of matches' , 10000 ],
395395 ['Type' , 'TEXT' , 'Term' , 'other' , 'Number of reading operations' , 3 , 'Estimated number of matches' , 10000 ]]]]
@@ -693,3 +693,82 @@ def testProfileBM25NormMax(env):
693693 env .assertTrue (recursive_contains (aggregate_response , "Score Max Normalizer" ))
694694 search_response = env .cmd ('FT.PROFILE' , 'idx' , 'SEARCH' , 'query' , 'hello' , 'WITHSCORES' , 'SCORER' , 'BM25STD.NORM' )
695695 env .assertTrue (recursive_contains (search_response , "Score Max Normalizer" ))
696+
697+ def testProfileVectorSearchMode ():
698+ """Test Vector search mode field in FT.PROFILE for both SEARCH and AGGREGATE"""
699+ env = Env (moduleArgs = 'DEFAULT_DIALECT 2' , protocol = 3 ) # Use RESP3 for easier dict access
700+ conn = getConnectionByEnv (env )
701+
702+ env .expect ('FT.CREATE' , 'idx' , 'SCHEMA' , 'v' , 'VECTOR' , 'FLAT' , '6' , 'TYPE' , 'FLOAT32' , 'DIM' , '2' , 'DISTANCE_METRIC' , 'L2' , 't' , 'TEXT' ).ok ()
703+
704+ conn .execute_command ('hset' , '1' , 'v' , 'bababaca' , 't' , "hello" )
705+ conn .execute_command ('hset' , '2' , 'v' , 'babababa' , 't' , "hello" )
706+ conn .execute_command ('hset' , '3' , 'v' , 'aabbaabb' , 't' , "hello" )
707+ conn .execute_command ('hset' , '4' , 'v' , 'bbaabbaa' , 't' , "hello world" )
708+ conn .execute_command ('hset' , '5' , 'v' , 'aaaabbbb' , 't' , "hello world" )
709+
710+ # Helper function to test both SEARCH and AGGREGATE
711+ def verify_search_mode (query_type , query , params , expected_mode , expected_iterator_type = 'VECTOR' ):
712+ scenario_message = f"query_type: { query_type } , query: { query } , params: { params } , expected_mode: { expected_mode } "
713+ """
714+ Verify that Vector search mode appears in profile for both SEARCH and AGGREGATE
715+ query_type: 'SEARCH' or 'AGGREGATE'
716+ query: the query string
717+ params: list of params (e.g., ['vec', 'aaaaaaaa'])
718+ expected_mode: expected search mode string
719+ expected_iterator_type: 'VECTOR' or 'METRIC - VECTOR DISTANCE'
720+ """
721+ cmd = ['FT.PROFILE' , 'idx' , query_type , 'QUERY' , query ]
722+ cmd .extend (['PARAMS' ] + [str (len (params ))] + params )
723+
724+ res = env .cmd (* cmd )
725+
726+ # Navigate to iterator profile (RESP3 dict structure)
727+ shards = res ['Profile' ]['Shards' ]
728+ env .assertGreater (len (shards ), 0 , message = scenario_message )
729+
730+ # Check at least one shard has the expected search mode
731+ # res['Profile']['Shards'][0]['Iterators profile']['Vector search mode']
732+ found = False
733+ for shard in shards :
734+ iter_profile = shard ['Iterators profile' ]
735+ if iter_profile ['Type' ] == expected_iterator_type :
736+ env .assertEqual (iter_profile ['Vector search mode' ], expected_mode , message = scenario_message )
737+ found = True
738+ break
739+ env .assertTrue (found , message = f"{ scenario_message } : Expected iterator type { expected_iterator_type } not found" )
740+
741+ # Test 1: STANDARD_KNN
742+ verify_search_mode ('SEARCH' , '*=>[KNN 3 @v $vec]' , ['vec' , 'aaaaaaaa' ], 'STANDARD_KNN' )
743+ verify_search_mode ('AGGREGATE' , '*=>[KNN 3 @v $vec]' , ['vec' , 'aaaaaaaa' ], 'STANDARD_KNN' )
744+
745+ # Test 2: HYBRID_ADHOC_BF
746+ verify_search_mode ('SEARCH' , '(@t:hello world)=>[KNN 3 @v $vec]' , ['vec' , 'aaaaaaaa' ], 'HYBRID_ADHOC_BF' )
747+ verify_search_mode ('AGGREGATE' , '(@t:hello world)=>[KNN 3 @v $vec]' , ['vec' , 'aaaaaaaa' ], 'HYBRID_ADHOC_BF' )
748+
749+ # Test 3: RANGE_QUERY (uses METRIC_ITERATOR)
750+ verify_search_mode ('SEARCH' , '@v:[VECTOR_RANGE 3e36 $vec]=>{$yield_distance_as:dist}' ,
751+ ['vec' , 'aaaaaaaa' ], 'RANGE_QUERY' , 'METRIC - VECTOR DISTANCE' )
752+ verify_search_mode ('AGGREGATE' , '@v:[VECTOR_RANGE 3e36 $vec]=>{$yield_distance_as:dist}' ,
753+ ['vec' , 'aaaaaaaa' ], 'RANGE_QUERY' , 'METRIC - VECTOR DISTANCE' )
754+
755+ # Test 4: HYBRID_BATCHES
756+ verify_search_mode ('SEARCH' , '(@t:hello world)=>[KNN 3 @v $vec HYBRID_POLICY BATCHES BATCH_SIZE 100]' , ['vec' , 'aaaaaaaa' ], 'HYBRID_BATCHES' )
757+ verify_search_mode ('AGGREGATE' , '(@t:hello world)=>[KNN 3 @v $vec HYBRID_POLICY BATCHES BATCH_SIZE 100]' , ['vec' , 'aaaaaaaa' ], 'HYBRID_BATCHES' )
758+
759+ # Running HYBRID_BATCHES_TO_ADHOC_BF on cluster requires much more data and doesn't add a significant value
760+ if env .isCluster ():
761+ return
762+
763+ for i in range (6 , 5000 ):
764+ conn .execute_command ('hset' , str (i ), 'v' , 'bababada' , 't' , "hello" )
765+
766+ # Add another 10K docs with "other" tag for HYBRID_BATCHES_TO_ADHOC_BF test
767+ for i in range (5000 , 10001 ):
768+ conn .execute_command ('hset' , str (i ), 'v' , '????????' , 't' , "other" )
769+
770+ # Test 5: HYBRID_BATCHES_TO_ADHOC_BF
771+ # Query: "hello" (10K docs) AND "other" (10K docs) → intersection is 0 (disjoint sets)
772+ # High estimated results → starts BATCHES, but 0 actual results → switches to ADHOC_BF
773+ verify_search_mode ('SEARCH' , '(@t:hello other)=>[KNN 3 @v $vec BATCH_SIZE 100]' , ['vec' , '????????' ], 'HYBRID_BATCHES_TO_ADHOC_BF' )
774+ verify_search_mode ('AGGREGATE' , '(@t:hello other)=>[KNN 3 @v $vec BATCH_SIZE 100]' , ['vec' , '????????' ], 'HYBRID_BATCHES_TO_ADHOC_BF' )
0 commit comments