#10640·qdrant

Fusion inside a prefetch is computed per shard, so its scores and score_threshold depend on shard_number

Author: Kushal9889Created Sep 14, 2026Updated Sep 14, 2026

Current Behavior

When fusion (RRF or DBSF) is used inside a prefetch instead of as the root query, the result depends on the collection's shard_number. Same points, same query, exact search — different scores, different points, and score_threshold lets through a different number of points.

It looks like each shard fuses its own local prefetch results and the collection then merges those per-shard fused scores. DBSF normalizes each list against that shard's own mean ± 3σ, and RRF scores by that shard's local positions, so a point at the top of one shard's local list gets a near-maximal score no matter how it compares globally. The merged scores aren't on a common scale.

Root-level fusion is not affected: intermediates_to_final_list fuses the merged per-prefetch lists once at the collection level (lib/collection/src/collection/query.rs:381). Nested fusion goes through fusion_rescore on every shard (lib/collection/src/shards/local_shard/query.rs:424-437).

Measured on 1.19.1 (6ab21cac), 400 points, two named Cosine vectors, all leaf prefetches exact: true:

1 shard 2 shards 4 shards
nested DBSF, score_threshold: 0.9 2 points pass 7 44
nested RRF, score_threshold: 0.4 2 points pass 4 9
nested DBSF top 10 matching the same fusion computed client-side 10/10 4/10

The single-shard result is the one that matches the fusion computed by hand from the exact leaf results.

Steps to Reproduce

  1. Run a Qdrant 1.19.1 node on localhost:6333.
  2. Run the script below with plain python3 (standard library only). It creates fusion_1_shards, fusion_2_shards and fusion_4_shards with identical points.
  3. Compare the output across shard counts.
repro.py
python
import json
import math
import random
import sys
import urllib.request

BASE = f"http://localhost:{sys.argv[1] if len(sys.argv) > 1 else 6333}"
DIM, N = 8, 400


def call(method, path, body=None):
    req = urllib.request.Request(BASE + path, method=method,
                                 data=None if body is None else json.dumps(body).encode(),
                                 headers={"content-type": "application/json"})
    with urllib.request.urlopen(req) as r:
        return json.loads(r.read())


def query(coll, body):
    return call("POST", f"/collections/{coll}/points/query", body)["result"]["points"]


rng = random.Random(7)
points = [{"id": i, "vector": {"a": [rng.uniform(-1, 1) for _ in range(DIM)],
                               "b": [rng.uniform(-1, 1) for _ in range(DIM)]}} for i in range(N)]
qa = [rng.uniform(-1, 1) for _ in range(DIM)]
qb = [rng.uniform(-1, 1) for _ in range(DIM)]

for shards in (1, 2, 4):
    name = f"fusion_{shards}_shards"
    call("DELETE", f"/collections/{name}")
    call("PUT", f"/collections/{name}", {"shard_number": shards, "vectors": {
        "a": {"size": DIM, "distance": "Cosine"}, "b": {"size": DIM, "distance": "Cosine"}}})
    call("PUT", f"/collections/{name}/points?wait=true", {"points": points})

exact = {"exact": True}
leaf_a = {"query": qa, "using": "a", "limit": 40, "params": exact}
leaf_b = {"query": qb, "using": "b", "limit": 40, "params": exact}

print("1) score_threshold on a nested fusion (limit 80, so nothing is truncated)")
for method, threshold in (("dbsf", 0.9), ("rrf", 0.4)):
    thr = {"prefetch": [{"prefetch": [leaf_a, leaf_b], "query": {"fusion": method},
                         "limit": 80, "score_threshold": threshold}],
           "query": qa, "using": "a", "limit": 80, "params": exact}
    counts = [len(query(f"fusion_{shards}_shards", thr)) for shards in (1, 2, 4)]
    print(f"   {method} >= {threshold}: 1 shard {counts[0]}, 2 shards {counts[1]}, 4 shards {counts[2]} points pass")

print("\n2) nested DBSF ranking vs the same fusion computed client-side")
la, lb = query("fusion_1_shards", leaf_a), query("fusion_1_shards", leaf_b)
fused = {}
for pts in (la, lb):
    s = [p["score"] for p in pts]
    mean = sum(s) / len(s)
    std = math.sqrt(sum((x - mean) ** 2 for x in s) / (len(s) - 1))
    for p in pts:
        fused[p["id"]] = fused.get(p["id"], 0.0) + (p["score"] - (mean - 3 * std)) / (6 * std)
expected = [i for i, _ in sorted(fused.items(), key=lambda kv: -kv[1])][:10]
nested = {"prefetch": [{"prefetch": [leaf_a, leaf_b], "query": {"fusion": "dbsf"}, "limit": 80}],
          "query": {"fusion": "rrf"}, "limit": 10}
print(f"   expected  : {expected}")
for shards in (1, 4):
    got = [p["id"] for p in query(f"fusion_{shards}_shards", nested)]
    print(f"   {shards} shard(s): {got}  ({len(set(got) & set(expected))}/10 expected points)")

print("\n3) control: the same DBSF at the root is identical across shard counts")
root = {"prefetch": [leaf_a, leaf_b], "query": {"fusion": "dbsf"}, "limit": 10}
print("   identical:", [p["id"] for p in query("fusion_1_shards", root)]
      == [p["id"] for p in query("fusion_4_shards", root)])

Output:

1) score_threshold on a nested fusion (limit 80, so nothing is truncated)
   dbsf >= 0.9: 1 shard 2, 2 shards 7, 4 shards 44 points pass
   rrf >= 0.4: 1 shard 2, 2 shards 4, 4 shards 9 points pass

2) nested DBSF ranking vs the same fusion computed client-side
   expected  : [76, 178, 159, 396, 251, 336, 331, 9, 255, 306]
   1 shard(s): [76, 178, 159, 396, 251, 336, 331, 9, 255, 306]  (10/10 expected points)
   4 shard(s): [396, 178, 76, 255, 175, 23, 332, 302, 314, 316]  (4/10 expected points)

3) control: the same DBSF at the root is identical across shard counts
   identical: True

Expected Behavior

A fusion inside a prefetch should give the same scores, the same points, and the same score_threshold cut regardless of shard_number, the way root-level fusion already does.

Possible Solution

Nested fusion needs its normalization statistics (DBSF) and positions (RRF) taken over the merged candidates from all shards rather than per shard — the same thing root-level fusion already gets from merging the per-prefetch lists before fusing.

Context (Environment)

Official qdrant-aarch64-apple-darwin 1.19.1 binary, single node, macOS. The shards are local, so this doesn't need a cluster — shard_number > 1 on one node is enough.

Where it bites: a threshold on fused hybrid scores is a common relevance cutoff when retrieving chunks for RAG. Here the same score_threshold: 0.9 passes 2 points on one shard and 44 on four, with no change to the query, so moving a collection to more shards silently changes what gets retrieved.

Two things I checked are not involved, to save review time:

  • Not the per-shard prefetch limit from #7159. Every nested prefetch here has limit: 80, and the two leaves return at most 80 candidates, so nothing is truncated on any shard. With truncation ruled out, a nested fusion followed by a plain vector rerank at the root returns identical results on 1, 2 and 4 shards — only the things that consume the nested fused scores (a nested score_threshold, or a fusion at the root) diverge.
  • Not shard-level undersampling. Every limit + offset is below SHARD_QUERY_SUBSAMPLING_LIMIT (128) and every leaf is exact, so modify_shard_query_for_undersampling_limits returns the requests unchanged.

The ordering of points that tie on an equal RRF score also varies between shard counts, but that looks like #6814, not this.