use std::cmp::Ordering;
use nodedb_types::Surrogate;
#[derive(Debug, Clone, Copy)]
pub struct ScoredDoc {
pub score: f32,
pub doc_id: Surrogate,
}
fn rank_cmp(a: &ScoredDoc, b: &ScoredDoc) -> Ordering {
a.score
.total_cmp(&b.score)
.then_with(|| b.doc_id.cmp(&a.doc_id))
}
pub struct TopKHeap {
data: Vec<ScoredDoc>,
capacity: usize,
}
const INITIAL_HEAP_CAPACITY: usize = 1024;
impl TopKHeap {
pub fn new(k: usize) -> Self {
Self {
data: Vec::with_capacity(k.min(INITIAL_HEAP_CAPACITY)),
capacity: k,
}
}
pub fn threshold(&self) -> f32 {
if self.data.len() < self.capacity {
0.0
} else {
self.data[0].score
}
}
pub fn insert(&mut self, score: f32, doc_id: Surrogate) {
let candidate = ScoredDoc { score, doc_id };
if self.data.len() < self.capacity {
self.data.push(candidate);
if self.data.len() == self.capacity {
self.build_heap();
}
} else if rank_cmp(&candidate, &self.data[0]) == Ordering::Greater {
self.data[0] = candidate;
self.sift_down(0);
}
}
pub fn into_sorted(mut self) -> Vec<ScoredDoc> {
self.data.sort_by(|a, b| rank_cmp(b, a));
self.data
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
fn build_heap(&mut self) {
let n = self.data.len();
for i in (0..n / 2).rev() {
self.sift_down(i);
}
}
fn sift_down(&mut self, mut pos: usize) {
let n = self.data.len();
loop {
let left = 2 * pos + 1;
let right = 2 * pos + 2;
let mut smallest = pos;
if left < n && rank_cmp(&self.data[left], &self.data[smallest]) == Ordering::Less {
smallest = left;
}
if right < n && rank_cmp(&self.data[right], &self.data[smallest]) == Ordering::Less {
smallest = right;
}
if smallest == pos {
break;
}
self.data.swap(pos, smallest);
pos = smallest;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_top_k() {
let mut heap = TopKHeap::new(3);
heap.insert(1.0, Surrogate(1));
heap.insert(5.0, Surrogate(5));
heap.insert(3.0, Surrogate(3));
heap.insert(2.0, Surrogate(2));
heap.insert(4.0, Surrogate(4));
let sorted = heap.into_sorted();
assert_eq!(sorted.len(), 3);
assert_eq!(sorted[0].doc_id, Surrogate(5));
assert_eq!(sorted[1].doc_id, Surrogate(4));
assert_eq!(sorted[2].doc_id, Surrogate(3));
}
#[test]
fn threshold_while_filling() {
let mut heap = TopKHeap::new(3);
assert_eq!(heap.threshold(), 0.0);
heap.insert(5.0, Surrogate(1));
assert_eq!(heap.threshold(), 0.0); heap.insert(3.0, Surrogate(2));
assert_eq!(heap.threshold(), 0.0);
heap.insert(1.0, Surrogate(3));
assert!((heap.threshold() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn rejects_below_threshold() {
let mut heap = TopKHeap::new(2);
heap.insert(5.0, Surrogate(1));
heap.insert(3.0, Surrogate(2));
heap.insert(1.0, Surrogate(99));
let sorted = heap.into_sorted();
assert_eq!(sorted.len(), 2);
assert!(sorted.iter().all(|d| d.doc_id != Surrogate(99)));
}
#[test]
fn empty_heap() {
let heap = TopKHeap::new(5);
assert!(heap.is_empty());
assert_eq!(heap.threshold(), 0.0);
}
#[test]
fn unbounded_k_allocates_at_most_the_initial_capacity() {
let mut heap = TopKHeap::new(usize::MAX);
assert!(heap.data.capacity() <= INITIAL_HEAP_CAPACITY);
for i in 1..=3000u32 {
heap.insert(i as f32, Surrogate(i));
}
assert_eq!(heap.len(), 3000, "an unbounded heap keeps every match");
assert_eq!(heap.into_sorted()[0].doc_id, Surrogate(3000));
}
#[test]
fn eviction_among_ties_keeps_the_lowest_surrogates() {
let mut heap = TopKHeap::new(2);
heap.insert(1.0, Surrogate(1));
heap.insert(1.0, Surrogate(2));
heap.insert(2.0, Surrogate(3));
let kept: Vec<Surrogate> = heap.into_sorted().iter().map(|d| d.doc_id).collect();
assert_eq!(kept, vec![Surrogate(3), Surrogate(1)]);
let mut heap = TopKHeap::new(3);
for id in [7, 4, 9, 2] {
heap.insert(1.0, Surrogate(id));
}
let kept: Vec<Surrogate> = heap.into_sorted().iter().map(|d| d.doc_id).collect();
assert_eq!(kept, vec![Surrogate(2), Surrogate(4), Surrogate(7)]);
}
#[test]
fn single_element() {
let mut heap = TopKHeap::new(1);
heap.insert(3.0, Surrogate(1));
heap.insert(5.0, Surrogate(2));
heap.insert(1.0, Surrogate(3));
let sorted = heap.into_sorted();
assert_eq!(sorted.len(), 1);
assert_eq!(sorted[0].doc_id, Surrogate(2));
}
}