Skip to content

[Local Mode] score_threshold not applied to FusionQuery results #1136

@cbcoutinho

Description

@cbcoutinho

Description

When using qdrant-client in local mode (:memory: or path-based), the score_threshold parameter is not applied to FusionQuery (RRF/DBSF) results. The same parameter works correctly for regular vector searches and when connecting to a remote Qdrant server.

Proposed Fix:

Root Cause

The bug is in qdrant_client/local/local_collection.py in the _merge_sources function (around lines 800-839).

The function receives score_threshold as a parameter but never uses it for FusionQuery or RrfQuery:

https://github.com/qdrant/qdrant-client/blob/b51cdee7ea8f2e20fd2a49f81eb9f7099f2e3fe3/qdrant_client/local/local_collection.py#L800-L839

def _merge_sources(
    self,
    sources: list[list[types.ScoredPoint]],
    query: types.Query,
    limit: int,
    offset: int,
    using: Optional[str] = None,
    query_filter: Optional[types.Filter] = None,
    score_threshold: Optional[float] = None,  # <-- RECEIVED BUT NEVER USED FOR FUSION
    ...
) -> list[types.ScoredPoint]:
    if isinstance(query, (models.FusionQuery, models.RrfQuery)):
        # ... fusion logic ...
        return fused[offset:]  # <-- score_threshold NOT applied!

Compare to the Qdrant server implementation which correctly applies score_threshold:
https://github.com/qdrant/qdrant/blob/main/lib/collection/src/shards/local_shard/query.rs#L430-L438

let top_fused: Vec<_> = if let Some(score_threshold) = score_threshold {
    fused
        .into_iter()
        .take_while(|point| point.score >= score_threshold)
        .take(limit)
        .collect()
} else {
    fused.into_iter().take(limit).collect()
};

Reproduction

import asyncio
from qdrant_client import AsyncQdrantClient, models

async def main():
    client = AsyncQdrantClient(":memory:")  # Local mode

    await client.create_collection(
        collection_name="test",
        vectors_config={"dense": models.VectorParams(size=4, distance=models.Distance.COSINE)},
    )

    await client.upsert(
        collection_name="test",
        points=[
            models.PointStruct(id=1, vector={"dense": [1.0, 0.0, 0.0, 0.0]}, payload={"name": "a"}),
            models.PointStruct(id=2, vector={"dense": [0.9, 0.1, 0.0, 0.0]}, payload={"name": "b"}),
            models.PointStruct(id=3, vector={"dense": [0.5, 0.5, 0.0, 0.0]}, payload={"name": "c"}),
            models.PointStruct(id=4, vector={"dense": [0.0, 1.0, 0.0, 0.0]}, payload={"name": "d"}),
        ],
    )

    # BUG: All 4 results returned despite score_threshold=0.4
    results = await client.query_points(
        collection_name="test",
        prefetch=[models.Prefetch(query=[1.0, 0.0, 0.0, 0.0], using="dense", limit=10)],
        query=models.FusionQuery(fusion=models.Fusion.RRF),
        limit=10,
        score_threshold=0.4,  # Should filter results with score < 0.4
    )

    print(f"Results: {len(results.points)}")
    for p in results.points:
        print(f"  id={p.id}, score={p.score:.4f}")

    # Output shows results with score < 0.4:
    #   id=1, score=0.5000
    #   id=2, score=0.3333  <-- below threshold!
    #   id=3, score=0.2500  <-- below threshold!
    #   id=4, score=0.2000  <-- below threshold!

asyncio.run(main())

Expected Behavior

Fusion query results should be filtered by score_threshold, returning only results with score >= score_threshold.

Actual Behavior

All fusion results are returned regardless of score_threshold.

Suggested Fix

Apply score_threshold filtering after fusion in _merge_sources:

if isinstance(query, (models.FusionQuery, models.RrfQuery)):
    # ... existing fusion logic ...

    # Apply score_threshold (matching server behavior)
    if score_threshold is not None:
        fused = [p for p in fused if p.score >= score_threshold]

    # Fetch payload and vectors
    # ...
    return fused[offset:]

Environment

  • qdrant-client version: 1.13.3
  • Python version: 3.12
  • Affects: Local mode only (:memory: and path-based storage)
  • Does NOT affect: Remote server connections (server applies threshold correctly)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions