use crate::VectorId;
use std::collections::BinaryHeap;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Neighbor {
pub id: VectorId,
pub score: f32,
}
impl Neighbor {
#[inline]
#[must_use]
pub const fn new(id: VectorId, score: f32) -> Self {
Self { id, score }
}
}
#[inline]
#[must_use]
pub fn cmp_ascending(a: &Neighbor, b: &Neighbor) -> core::cmp::Ordering {
a.score
.total_cmp(&b.score)
.then_with(|| a.id.cmp(&b.id))
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Ranked(Neighbor);
impl Eq for Ranked {}
impl PartialOrd for Ranked {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Ranked {
#[inline]
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
cmp_ascending(&self.0, &other.0)
}
}
#[derive(Debug)]
pub struct TopK {
k: usize,
heap: BinaryHeap<Ranked>,
}
impl TopK {
#[must_use]
pub fn new(k: usize) -> Self {
Self {
k,
heap: BinaryHeap::with_capacity(k),
}
}
pub fn offer(&mut self, candidate: Neighbor) {
if self.k == 0 {
return;
}
if self.heap.len() < self.k {
self.heap.push(Ranked(candidate));
return;
}
if let Some(worst) = self.heap.peek() {
if cmp_ascending(&candidate, &worst.0) == core::cmp::Ordering::Less {
self.heap.pop();
self.heap.push(Ranked(candidate));
}
}
}
#[must_use]
pub fn worst_score(&self) -> Option<f32> {
self.heap.peek().map(|r| r.0.score)
}
#[must_use]
pub fn is_full(&self) -> bool {
self.heap.len() >= self.k
}
#[must_use]
pub fn len(&self) -> usize {
self.heap.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.heap.is_empty()
}
#[must_use]
pub fn into_sorted_vec(self) -> Vec<Neighbor> {
let mut out: Vec<Neighbor> = self.heap.into_iter().map(|r| r.0).collect();
out.sort_unstable_by(cmp_ascending);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::cmp::Ordering;
fn n(id: u64, score: f32) -> Neighbor {
Neighbor::new(VectorId::new(id), score)
}
#[test]
fn orders_by_score_then_id() {
let a = Neighbor::new(VectorId::new(5), 1.0);
let b = Neighbor::new(VectorId::new(2), 2.0);
assert_eq!(cmp_ascending(&a, &b), Ordering::Less);
}
#[test]
fn breaks_ties_by_id() {
let a = Neighbor::new(VectorId::new(2), 1.0);
let b = Neighbor::new(VectorId::new(5), 1.0);
assert_eq!(cmp_ascending(&a, &b), Ordering::Less);
assert_eq!(cmp_ascending(&b, &a), Ordering::Greater);
}
#[test]
fn nan_sorts_last() {
let real = Neighbor::new(VectorId::new(1), 1.0);
let nan = Neighbor::new(VectorId::new(0), f32::NAN);
assert_eq!(cmp_ascending(&real, &nan), Ordering::Less);
}
#[test]
fn keeps_k_smallest() {
let mut t = TopK::new(3);
for (i, s) in [5.0, 1.0, 4.0, 2.0, 3.0].iter().enumerate() {
t.offer(n(i as u64, *s));
}
let got = t.into_sorted_vec();
let scores: Vec<f32> = got.iter().map(|x| x.score).collect();
assert_eq!(scores, vec![1.0, 2.0, 3.0]);
}
#[test]
fn k_larger_than_input_keeps_all_sorted() {
let mut t = TopK::new(10);
t.offer(n(0, 2.0));
t.offer(n(1, 1.0));
let got = t.into_sorted_vec();
assert_eq!(got.len(), 2);
assert_eq!(got[0].id, VectorId::new(1));
assert_eq!(got[1].id, VectorId::new(0));
}
#[test]
fn k_zero_keeps_nothing() {
let mut t = TopK::new(0);
t.offer(n(0, 1.0));
assert!(t.is_empty());
assert!(t.into_sorted_vec().is_empty());
}
#[test]
fn deterministic_tie_break_by_id() {
let mut t = TopK::new(2);
for id in [9, 3, 7, 1, 5] {
t.offer(n(id, 1.0));
}
let got = t.into_sorted_vec();
let ids: Vec<u64> = got.iter().map(|x| x.id.get()).collect();
assert_eq!(ids, vec![1, 3]);
}
#[test]
fn worst_score_and_fullness_track_the_beam() {
let mut t = TopK::new(2);
assert!(!t.is_full());
assert_eq!(t.worst_score(), None);
t.offer(n(0, 3.0));
assert!(!t.is_full());
assert_eq!(t.worst_score(), Some(3.0));
t.offer(n(1, 1.0));
assert!(t.is_full());
assert_eq!(t.worst_score(), Some(3.0));
t.offer(n(2, 2.0));
assert!(t.is_full());
assert_eq!(t.worst_score(), Some(2.0));
}
}