use ordered_float::OrderedFloat;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
struct Candidate<T> {
distance: OrderedFloat<f64>,
seq: u64,
item: T,
}
impl<T> Candidate<T> {
fn key(&self) -> (OrderedFloat<f64>, u64) {
(self.distance, self.seq)
}
}
impl<T> PartialEq for Candidate<T> {
fn eq(&self, other: &Self) -> bool {
self.key() == other.key()
}
}
impl<T> Eq for Candidate<T> {}
impl<T> Ord for Candidate<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.key().cmp(&other.key())
}
}
impl<T> PartialOrd for Candidate<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub(crate) struct KnnHeap<T> {
k: usize,
seq: u64,
heap: BinaryHeap<Candidate<T>>,
}
impl<T> KnnHeap<T> {
pub(crate) fn new(k: usize) -> Self {
KnnHeap {
k,
seq: 0,
heap: BinaryHeap::with_capacity(k.min(64)),
}
}
pub(crate) fn worst(&self) -> f64 {
if self.k == 0 {
return f64::NEG_INFINITY;
}
if self.heap.len() < self.k {
return f64::INFINITY;
}
self.heap
.peek()
.map_or(f64::INFINITY, |candidate| candidate.distance.into_inner())
}
pub(crate) fn offer(&mut self, distance: f64, item: T) {
if self.k == 0 {
return;
}
if self.heap.len() == self.k {
if distance >= self.worst() {
return;
}
self.heap.pop();
}
self.seq += 1;
self.heap.push(Candidate {
distance: OrderedFloat(distance),
seq: self.seq,
item,
});
}
pub(crate) fn into_sorted_vec(self) -> Vec<T> {
self.heap
.into_sorted_vec()
.into_iter()
.map(|candidate| candidate.item)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keeps_the_nearest_k_in_order() {
let mut heap = KnnHeap::new(3);
for (d, name) in [(5.0, "e"), (1.0, "a"), (4.0, "d"), (2.0, "b"), (3.0, "c")] {
heap.offer(d, name);
}
assert_eq!(heap.into_sorted_vec(), vec!["a", "b", "c"]);
}
#[test]
fn test_worst_is_infinite_until_full() {
let mut heap = KnnHeap::new(2);
assert_eq!(heap.worst(), f64::INFINITY);
heap.offer(1.0, "a");
assert_eq!(heap.worst(), f64::INFINITY);
heap.offer(4.0, "b");
assert_eq!(heap.worst(), 4.0);
heap.offer(9.0, "c");
assert_eq!(heap.worst(), 4.0);
heap.offer(2.0, "d");
assert_eq!(heap.worst(), 2.0);
assert_eq!(heap.into_sorted_vec(), vec!["a", "d"]);
}
#[test]
fn test_zero_k_keeps_nothing_and_prunes_everything() {
let mut heap: KnnHeap<&str> = KnnHeap::new(0);
heap.offer(1.0, "a");
assert_eq!(heap.worst(), f64::NEG_INFINITY);
assert!(
0.0 > heap.worst(),
"any candidate distance must fail the prune test"
);
assert!(heap.into_sorted_vec().is_empty());
}
#[test]
fn test_ties_are_deterministic() {
let mut heap = KnnHeap::new(4);
for name in ["a", "b", "c", "d"] {
heap.offer(7.0, name);
}
assert_eq!(heap.into_sorted_vec(), vec!["a", "b", "c", "d"]);
}
#[test]
fn test_equal_distance_does_not_evict() {
let mut heap = KnnHeap::new(1);
heap.offer(3.0, "first");
heap.offer(3.0, "second");
assert_eq!(heap.into_sorted_vec(), vec!["first"]);
}
}