perf(agent): unranked BM25 scores and a top-k merge — same rankings, 4-5x faster

Fusion min-max normalises over every keyword match, so hybrid_search asked
BM25 for a ranked list of the whole corpus: a hash insert per posting, then a
sort of every match, then the merge sorted every candidate again to keep k.

- BM25Index::scores returns every match unsorted, accumulated in a dense array
  (contributions are strictly positive, so zero means untouched). search() is
  built on it with the bounded heap.
- merge_vector_keyword partitions out its top k (select_nth) and orders only
  those, with the same score-then-id order.
- Both hybrid paths use scores().

Rankings are identical (equivalence tests for both changes). p50 0.24 -> 0.07
ms (1K), 2.1 -> 0.49 ms (10K), 23 -> 4.65 ms (100K).

The harness gains --fusion-study, which measured the alternative — capping the
keyword pool — and found it changes the top-10 for most queries (overlap
0.83-0.92, different #1 for 10-35%) for only a 2x saving. Not adopted.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
osobh
2026-09-19 13:27:31 -07:00
co-authored by Claude Fable 5.1
parent f15bf2eb22
commit 390a2e3836
6 changed files with 216 additions and 30 deletions
+3 -1
View File
@@ -35,7 +35,9 @@ impl HDF5Memory {
.into_iter()
.map(|(id, dist)| (id, 1.0 - dist))
.collect();
let kw_scores = bm25.search(query_text, self.cache.len());
// Fusion normalises over every keyword match, so it needs all
// the scores — but not ranked.
let kw_scores = bm25.scores(query_text);
hybrid::merge_vector_keyword(
vec_scores,
kw_scores,