Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/collector/top_score_collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,35 @@ where
}
self.buffer
}

/// The exact current n-th best sort key, or `None` while the buffer holds
/// fewer than `top_n` entries (with no externally-set threshold that is
/// exactly "fewer than `top_n` entries ever collected": nothing is
/// dropped before the first truncation, and truncation never shrinks the
/// buffer below `top_n`). Also `None` for `top_n == 0`, where "n-th best"
/// is undefined.
///
/// Side effect: a buffer holding more than `top_n` entries is
/// force-truncated to exactly `top_n` through the same machinery the
/// capacity overflow uses, which also updates `threshold` — so subsequent
/// [`push`](Self::push) / [`push_unordered`](Self::push_unordered)
/// early-drops kick in sooner than they would have without this call.
/// Each call is O(buffer); intended for coarse boundaries (e.g. once per
/// probed IVF cluster), not per push.
pub fn kth_best(&mut self) -> Option<TSortKey> {
if self.top_n == 0 || self.buffer.len() < self.top_n {
return None;
}
if self.buffer.len() > self.top_n {
self.truncate_top_n();
}
// The buffer now holds exactly the top_n best, unordered; the n-th
// best is its minimum under the comparator.
self.buffer
.iter()
.min_by(|a, b| self.comparator.compare(&a.sort_key, &b.sort_key))
.map(|entry| entry.sort_key.clone())
}
}

// Push an element provided there is enough capacity to do so.
Expand Down Expand Up @@ -994,6 +1023,99 @@ mod tests {
}
}

#[test]
fn test_kth_best_none_below_capacity() {
// Fewer than top_n entries collected: no n-th best exists yet.
let mut computer: TopNComputer<u32, u32, NaturalComparator> =
TopNComputer::new_with_comparator(3, NaturalComparator);
assert_eq!(computer.kth_best(), None);
computer.push(10u32, 1u32);
computer.push(20u32, 2u32);
assert_eq!(computer.kth_best(), None);

// top_n == 0 has no "n-th best" no matter how much is pushed.
let mut zero: TopNComputer<u32, u32, NaturalComparator> =
TopNComputer::new_with_comparator(0, NaturalComparator);
zero.push(1u32, 1u32);
assert_eq!(zero.kth_best(), None);
}

#[test]
fn test_kth_best_exact_at_capacity() {
// Exactly top_n entries: the n-th best is the worst of them.
let mut computer: TopNComputer<u32, u32, NaturalComparator> =
TopNComputer::new_with_comparator(3, NaturalComparator);
computer.push(30u32, 1u32);
computer.push(10u32, 2u32);
computer.push(20u32, 3u32);
assert_eq!(computer.kth_best(), Some(10u32));
}

#[test]
fn test_kth_best_tracks_improvement() {
// As better entries displace the tail, the n-th best rises — and it
// is exact at every boundary, not lagging like `threshold`.
let mut computer: TopNComputer<u32, u32, NaturalComparator> =
TopNComputer::new_with_comparator(2, NaturalComparator);
computer.push(10u32, 1u32);
computer.push(20u32, 2u32);
assert_eq!(computer.kth_best(), Some(10u32));
computer.push(30u32, 3u32);
assert_eq!(computer.kth_best(), Some(20u32));
computer.push(40u32, 4u32);
assert_eq!(computer.kth_best(), Some(30u32));
// A push below the current n-th best must not lower it.
computer.push(5u32, 5u32);
assert_eq!(computer.kth_best(), Some(30u32));
}

#[test]
fn test_kth_best_after_natural_truncation() {
// Overflow the 2·top_n buffer so `truncate_top_n` has already fired
// naturally; `kth_best` must still be the exact n-th best of
// everything pushed.
let top_n = 4usize;
let mut computer: TopNComputer<u32, u32, NaturalComparator> =
TopNComputer::new_with_comparator(top_n, NaturalComparator);
for doc in 0..20u32 {
computer.push(doc, doc); // scores 0..20, ascending
}
// Top 4 scores are 19, 18, 17, 16 — the 4th best is 16.
assert_eq!(computer.kth_best(), Some(16u32));
// The forced truncation set a threshold as a side effect.
assert!(computer.threshold.is_some());
// The final results are unaffected by the interleaved queries.
let docs: Vec<u32> = computer.into_sorted_vec().iter().map(|d| d.doc).collect();
assert_eq!(docs, vec![19, 18, 17, 16]);
}

#[test]
fn test_kth_best_with_push_unordered_natural_comparator() {
// The vector-backend shape: f32 scores, NaturalComparator (higher is
// better), out-of-order pushes. Interleave queries with pushes and
// confirm both the boundary values and the final top-K.
let mut computer: TopNComputer<Score, u32, NaturalComparator> =
TopNComputer::new_with_comparator(2, NaturalComparator);
computer.push_unordered(0.5f32, 10u32);
assert_eq!(computer.kth_best(), None);
computer.push_unordered(0.9f32, 3u32);
assert_eq!(computer.kth_best(), Some(0.5f32));
computer.push_unordered(0.7f32, 7u32);
assert_eq!(computer.kth_best(), Some(0.7f32));
// Below the n-th best: dropped by the truncation threshold, and the
// n-th best is unchanged.
computer.push_unordered(0.1f32, 1u32);
assert_eq!(computer.kth_best(), Some(0.7f32));
computer.push_unordered(1.5f32, 99u32);
assert_eq!(computer.kth_best(), Some(0.9f32));
let result: Vec<(Score, u32)> = computer
.into_sorted_vec()
.into_iter()
.map(|d| (d.sort_key, d.doc))
.collect();
assert_eq!(result, vec![(1.5f32, 99u32), (0.9f32, 3u32)]);
}

proptest! {
#[test]
fn test_topn_computer_asc_prop(
Expand Down
Loading
Loading