perf(agent): replace BM25 WAND top-k re-sort with a min-heap
top_k_scores.sort_by(...) ran over the full k-sized buffer for every matching document that beat the running threshold (twice in the full branch), plus another full sort on first reaching k results — O(m·k log k) for m matching documents. Replace the Vec<f32> buffer with a BinaryHeap<Reverse<HeapScore>> min-heap of size k, giving O(m log k). Existing wand_returns_same_results_as_exhaustive test confirms results are unchanged. INT-11
This commit is contained in:
@@ -8,7 +8,28 @@
|
|||||||
//! - Sorted posting lists by doc_id for cache-friendly access
|
//! - Sorted posting lists by doc_id for cache-friendly access
|
||||||
//! - Block-Max WAND early termination
|
//! - Block-Max WAND early termination
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::cmp::Reverse;
|
||||||
|
use std::collections::{BinaryHeap, HashMap};
|
||||||
|
|
||||||
|
/// `f32` wrapper providing a total order (via `total_cmp`) so BM25 scores can
|
||||||
|
/// be kept in a `BinaryHeap`. Scores are always finite in practice (no NaN
|
||||||
|
/// inputs reach this path), so `total_cmp`'s NaN ordering is never exercised.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
struct HeapScore(f32);
|
||||||
|
|
||||||
|
impl Eq for HeapScore {}
|
||||||
|
|
||||||
|
impl PartialOrd for HeapScore {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for HeapScore {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.0.total_cmp(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Default BM25 term-frequency saturation parameter.
|
/// Default BM25 term-frequency saturation parameter.
|
||||||
const DEFAULT_K1: f32 = 1.2;
|
const DEFAULT_K1: f32 = 1.2;
|
||||||
@@ -97,9 +118,11 @@ impl BM25Index {
|
|||||||
|
|
||||||
let total_max_contribution: f32 = max_tf_score.iter().sum();
|
let total_max_contribution: f32 = max_tf_score.iter().sum();
|
||||||
|
|
||||||
// Threshold for WAND early termination
|
// Threshold for WAND early termination. `top_k_heap` is a min-heap of
|
||||||
|
// size k (worst-of-the-top-k at the head) so it can be maintained in
|
||||||
|
// O(log k) per update instead of re-sorting the whole buffer.
|
||||||
let mut threshold = 0.0f32;
|
let mut threshold = 0.0f32;
|
||||||
let mut top_k_scores: Vec<f32> = Vec::with_capacity(k);
|
let mut top_k_heap: BinaryHeap<Reverse<HeapScore>> = BinaryHeap::with_capacity(k);
|
||||||
|
|
||||||
for (term_idx, (_, idf, postings)) in query_terms.iter().enumerate() {
|
for (term_idx, (_, idf, postings)) in query_terms.iter().enumerate() {
|
||||||
for &(doc_id, freq) in *postings {
|
for &(doc_id, freq) in *postings {
|
||||||
@@ -118,24 +141,17 @@ impl BM25Index {
|
|||||||
if term_idx == query_terms.len() - 1 {
|
if term_idx == query_terms.len() - 1 {
|
||||||
// Last term: check if this doc beats threshold
|
// Last term: check if this doc beats threshold
|
||||||
let final_score = *entry;
|
let final_score = *entry;
|
||||||
if final_score > threshold && top_k_scores.len() >= k {
|
if top_k_heap.len() >= k {
|
||||||
// Update threshold
|
if final_score > threshold {
|
||||||
top_k_scores
|
// Replace the current worst-of-top-k.
|
||||||
.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
|
top_k_heap.pop();
|
||||||
if final_score > top_k_scores[k - 1] {
|
top_k_heap.push(Reverse(HeapScore(final_score)));
|
||||||
top_k_scores[k - 1] = final_score;
|
threshold = top_k_heap.peek().map(|Reverse(s)| s.0).unwrap_or(0.0);
|
||||||
top_k_scores.sort_by(|a, b| {
|
|
||||||
b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
});
|
|
||||||
threshold = top_k_scores[k - 1];
|
|
||||||
}
|
}
|
||||||
} else if top_k_scores.len() < k {
|
} else {
|
||||||
top_k_scores.push(final_score);
|
top_k_heap.push(Reverse(HeapScore(final_score)));
|
||||||
if top_k_scores.len() == k {
|
if top_k_heap.len() == k {
|
||||||
top_k_scores.sort_by(|a, b| {
|
threshold = top_k_heap.peek().map(|Reverse(s)| s.0).unwrap_or(0.0);
|
||||||
b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
});
|
|
||||||
threshold = top_k_scores[k - 1];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user