@@ -118,3 +118,181 @@ def test_sparse_in_memory_key_filter_returns_results(qdrant: QdrantClient):
118118 ).points
119119
120120 assert [r .id for r in search_result ] == [4 , 2 ]
121+
122+
123+ def test_fusion_rrf_score_threshold (qdrant : QdrantClient ):
124+ """Test that RRF fusion with score_threshold correctly filters results.
125+
126+ RRF scores in local mode are normalized and for 5 points we get roughly:
127+ - ID 1: 1.0
128+ - ID 2: 0.667
129+ - ID 3: 0.5
130+ - ID 5: 0.4
131+ - ID 4: 0.333
132+
133+ A threshold of 0.45 should filter out IDs 4 and 5.
134+ """
135+ qdrant .create_collection (
136+ collection_name = "test_collection" ,
137+ vectors_config = {
138+ "text" : models .VectorParams (size = 4 , distance = models .Distance .COSINE ),
139+ "image" : models .VectorParams (size = 4 , distance = models .Distance .COSINE ),
140+ },
141+ )
142+
143+ qdrant .upsert (
144+ collection_name = "test_collection" ,
145+ wait = True ,
146+ points = [
147+ models .PointStruct (
148+ id = 1 ,
149+ vector = {"text" : [1.0 , 0.0 , 0.0 , 0.0 ], "image" : [1.0 , 0.0 , 0.0 , 0.0 ]},
150+ ),
151+ models .PointStruct (
152+ id = 2 ,
153+ vector = {"text" : [0.9 , 0.1 , 0.0 , 0.0 ], "image" : [0.9 , 0.1 , 0.0 , 0.0 ]},
154+ ),
155+ models .PointStruct (
156+ id = 3 ,
157+ vector = {"text" : [0.5 , 0.5 , 0.0 , 0.0 ], "image" : [0.5 , 0.5 , 0.0 , 0.0 ]},
158+ ),
159+ models .PointStruct (
160+ id = 4 ,
161+ vector = {"text" : [0.0 , 1.0 , 0.0 , 0.0 ], "image" : [0.0 , 1.0 , 0.0 , 0.0 ]},
162+ ),
163+ models .PointStruct (
164+ id = 5 ,
165+ vector = {"text" : [0.0 , 0.0 , 1.0 , 0.0 ], "image" : [0.0 , 0.0 , 1.0 , 0.0 ]},
166+ ),
167+ ],
168+ )
169+
170+ query_vector = [1.0 , 0.0 , 0.0 , 0.0 ]
171+
172+ # Without score_threshold - should return all 5 points
173+ result_no_threshold = qdrant .query_points (
174+ collection_name = "test_collection" ,
175+ prefetch = [
176+ models .Prefetch (query = query_vector , using = "text" , limit = 10 ),
177+ models .Prefetch (query = query_vector , using = "image" , limit = 10 ),
178+ ],
179+ query = models .FusionQuery (fusion = models .Fusion .RRF ),
180+ limit = 10 ,
181+ )
182+ assert len (result_no_threshold .points ) == 5
183+
184+ # Find points with scores below 0.45 - IDs 4 (0.333) and 5 (0.4) should be filtered
185+ low_score_count = sum (1 for p in result_no_threshold .points if p .score < 0.45 )
186+ assert low_score_count == 2 , f"Expected 2 low-scoring points, got { low_score_count } "
187+
188+ # With a threshold of 0.45, points with scores below should be filtered
189+ result_with_threshold = qdrant .query_points (
190+ collection_name = "test_collection" ,
191+ prefetch = [
192+ models .Prefetch (query = query_vector , using = "text" , limit = 10 ),
193+ models .Prefetch (query = query_vector , using = "image" , limit = 10 ),
194+ ],
195+ query = models .FusionQuery (fusion = models .Fusion .RRF ),
196+ score_threshold = 0.45 ,
197+ limit = 10 ,
198+ )
199+
200+ # Verify all returned points have score >= threshold
201+ for point in result_with_threshold .points :
202+ assert point .score >= 0.45 , f"Score { point .score } is below threshold 0.45"
203+
204+ # Key assertion: filtering should reduce the count from 5 to 3
205+ assert len (result_with_threshold .points ) == 3 , (
206+ f"Expected 3 points after filtering (threshold 0.45), got { len (result_with_threshold .points )} . "
207+ f"Scores: { [p .score for p in result_no_threshold .points ]} "
208+ )
209+
210+
211+ def test_fusion_dbsf_score_threshold (qdrant : QdrantClient ):
212+ """Test that DBSF fusion with score_threshold correctly filters results.
213+
214+ DBSF scores for the test data:
215+ - ID 1: ~1.30
216+ - ID 2: ~1.30
217+ - ID 3: ~1.11
218+ - ID 4: ~0.64
219+ - ID 5: ~0.64
220+
221+ A threshold of 1.0 should filter out IDs 4 and 5.
222+ """
223+ qdrant .create_collection (
224+ collection_name = "test_collection" ,
225+ vectors_config = {
226+ "text" : models .VectorParams (size = 4 , distance = models .Distance .COSINE ),
227+ "image" : models .VectorParams (size = 4 , distance = models .Distance .COSINE ),
228+ },
229+ )
230+
231+ qdrant .upsert (
232+ collection_name = "test_collection" ,
233+ wait = True ,
234+ points = [
235+ models .PointStruct (
236+ id = 1 ,
237+ vector = {"text" : [1.0 , 0.0 , 0.0 , 0.0 ], "image" : [1.0 , 0.0 , 0.0 , 0.0 ]},
238+ ),
239+ models .PointStruct (
240+ id = 2 ,
241+ vector = {"text" : [0.9 , 0.1 , 0.0 , 0.0 ], "image" : [0.9 , 0.1 , 0.0 , 0.0 ]},
242+ ),
243+ models .PointStruct (
244+ id = 3 ,
245+ vector = {"text" : [0.5 , 0.5 , 0.0 , 0.0 ], "image" : [0.5 , 0.5 , 0.0 , 0.0 ]},
246+ ),
247+ models .PointStruct (
248+ id = 4 ,
249+ vector = {"text" : [0.0 , 1.0 , 0.0 , 0.0 ], "image" : [0.0 , 1.0 , 0.0 , 0.0 ]},
250+ ),
251+ models .PointStruct (
252+ id = 5 ,
253+ vector = {"text" : [0.0 , 0.0 , 1.0 , 0.0 ], "image" : [0.0 , 0.0 , 1.0 , 0.0 ]},
254+ ),
255+ ],
256+ )
257+
258+ query_vector = [1.0 , 0.0 , 0.0 , 0.0 ]
259+
260+ # Without score_threshold - should return all 5 points
261+ result_no_threshold = qdrant .query_points (
262+ collection_name = "test_collection" ,
263+ prefetch = [
264+ models .Prefetch (query = query_vector , using = "text" , limit = 10 ),
265+ models .Prefetch (query = query_vector , using = "image" , limit = 10 ),
266+ ],
267+ query = models .FusionQuery (fusion = models .Fusion .DBSF ),
268+ limit = 10 ,
269+ )
270+ assert len (result_no_threshold .points ) == 5
271+
272+ # Find points with scores below 1.0 - IDs 4 and 5 (~0.64) should be filtered
273+ low_score_count = sum (1 for p in result_no_threshold .points if p .score < 1.0 )
274+ assert low_score_count == 2 , f"Expected 2 low-scoring points, got { low_score_count } "
275+
276+ # With score_threshold of 1.0, points below should be filtered
277+ result_with_threshold = qdrant .query_points (
278+ collection_name = "test_collection" ,
279+ prefetch = [
280+ models .Prefetch (query = query_vector , using = "text" , limit = 10 ),
281+ models .Prefetch (query = query_vector , using = "image" , limit = 10 ),
282+ ],
283+ query = models .FusionQuery (fusion = models .Fusion .DBSF ),
284+ score_threshold = 1.0 ,
285+ limit = 10 ,
286+ )
287+
288+ # Verify all returned points have score >= threshold
289+ for point in result_with_threshold .points :
290+ assert point .score >= 1.0 , f"Score { point .score } is below threshold 1.0"
291+
292+ # Key assertion: filtering should reduce the count from 5 to 3
293+ assert len (result_with_threshold .points ) == 3 , (
294+ f"Expected 3 points after filtering (threshold 1.0), got { len (result_with_threshold .points )} . "
295+ f"Scores: { [p .score for p in result_no_threshold .points ]} "
296+ )
297+
298+
0 commit comments