use std::cmp::Reverse;
use std::collections::BinaryHeap;
#[derive(Debug, Clone)]
pub(super) struct SearchResult {
pub(super) id: u32,
pub(super) similarity: f32,
}
impl PartialEq for SearchResult {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == std::cmp::Ordering::Equal
}
}
impl Eq for SearchResult {}
impl PartialOrd for SearchResult {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SearchResult {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.similarity
.total_cmp(&other.similarity)
.then_with(|| other.id.cmp(&self.id))
}
}
pub(super) struct TopK {
k: usize,
heap: BinaryHeap<Reverse<SearchResult>>,
}
impl TopK {
pub(super) fn new(k: usize) -> Self {
Self {
k,
heap: BinaryHeap::with_capacity(k.min(1024) + 1),
}
}
#[inline]
pub(super) fn push(&mut self, id: u32, similarity: f32) {
if self.k == 0 || similarity.is_nan() {
return;
}
let candidate = SearchResult { id, similarity };
if self.heap.len() < self.k {
self.heap.push(Reverse(candidate));
} else if let Some(Reverse(worst)) = self.heap.peek()
&& candidate > *worst
{
self.heap.pop();
self.heap.push(Reverse(candidate));
}
}
pub(super) fn into_sorted(self) -> Vec<SearchResult> {
self.heap
.into_sorted_vec()
.into_iter()
.map(|r| r.0)
.collect()
}
}
pub(super) fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f32>()
.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn top_k_keeps_the_best_k_sorted_with_id_tiebreak() {
let mut top = TopK::new(3);
for (id, sim) in [
(0, 0.1),
(1, 0.9),
(2, f32::NAN),
(3, 0.5),
(4, 0.9),
(5, -1.0),
] {
top.push(id, sim);
}
let got: Vec<_> = top.into_sorted().iter().map(|r| r.id).collect();
assert_eq!(got, vec![1, 4, 3]);
assert!(TopK::new(0).into_sorted().is_empty());
}
}