Skip to content

fix(vector/search): rescore multi-segment hits on a shared basis (#927) - #928

Merged
mosuka merged 1 commit into
mainfrom
fix/927-cross-segment-score
Aug 1, 2026
Merged

fix(vector/search): rescore multi-segment hits on a shared basis (#927)#928
mosuka merged 1 commit into
mainfrom
fix/927-cross-segment-score

Conversation

@mosuka

@mosuka mosuka commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the cross-segment score comparability bug: per-segment similarities come from quantized kernels running against per-segment affine params, with the query clamped into each segment's value range, so the fan-out's global sort compared meaningless numbers. Any segment whose value range excluded the query reported its boundary doc as an exact match (similarity = 1.0) — a single-doc segment (one upsert commit, routine under segment-per-commit) topped every out-of-range query. Repro: [7, 20, 10, 0, 1] where brute force says [0, 1, 2, 4, 5], with the top three at sim = 1.0.

Changes (segment/fanout.rs + one stamp in hnsw/searcher.rs)

  1. Shared-basis rescore: after each segment's newest-wins masking, every surviving hit's distance/similarity is recomputed as distance(prepared_raw_query, dequantized_vector) (hit-carried vector, else get_vector). Candidate generation stays on the quantized kernels — clamping shifts a segment's distances by a per-segment constant, so segment-internal ordering was always correct; only the absolute scores needed a shared basis. min_similarity re-applies on final scores (the local filter can only be lenient, never over-strict, so no hit is lost).
  2. Sort by distance, not similarity (second defect found while testing): exp(-d) similarity underflows to 0.0 at long range (and DotProduct similarity saturates via clamp(0,1)), collapsing distant hits into ties whose unstable-sort order leaked segment order into results. The merged sort now uses ascending distance (lower-is-more-similar for every metric, precise at long range) with a doc-id tiebreak.
  3. Stage-2 rerank scores preserved (third defect, caught by the existing rerank e2e test): sidecar-reranked scores are distance(raw_query, true_f32_vector) — already cross-segment-comparable and more precise than the dequantized basis; the first version of the rescore overwrote them, silently discarding the sidecar's benefit. The searcher now stamps query_metadata["score_basis"] = "f32-rerank" when the rerank arm actually ran and the fan-out skips its rescore for such segments (per-segment decision, so mixed sidecar/non-sidecar indexes still get clamp-corrected scores where needed).

Flat/IVF segmented indexes share the fan-out and get all three for free. No public API changes.

Tests

  • New segment_score_comparability_test.rs: the discovery repro as a regression test (4 disjoint-range segments incl. the degenerate single-doc one, cross-segment stale duplicates, a deletion; queries inside/outside/between ranges must match brute force exactly; a second test pins min_similarity on the shared basis). Query positions are tie-free by construction — an exact tie's rescored order would depend on per-segment reconstruction error, which is not what the test pins.
  • RED-proved via reversible sed: disabling the rescore reproduces the original [7, 20, 10, ...] corruption.
  • The Stage-2 interaction is pinned by the existing vector_rerank_engine_test.rs (it caught defect 3 during development).

Verification

Closes #927. Found while implementing #926; blocks #926's correctness test.

…r-segment quantized scores are not comparable (#927)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(vector/search): cross-segment scores are not comparable — out-of-range queries clamp to similarity 1.0 per segment

1 participant