1use crate::VectorId;
10use std::collections::BinaryHeap;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct Neighbor {
20 pub id: VectorId,
22 pub score: f32,
24}
25
26impl Neighbor {
27 #[inline]
29 #[must_use]
30 pub const fn new(id: VectorId, score: f32) -> Self {
31 Self { id, score }
32 }
33}
34
35#[inline]
43#[must_use]
44pub fn cmp_ascending(a: &Neighbor, b: &Neighbor) -> core::cmp::Ordering {
45 a.score
46 .total_cmp(&b.score)
47 .then_with(|| a.id.cmp(&b.id))
48}
49
50#[derive(Debug, Clone, Copy, PartialEq)]
56struct Ranked(Neighbor);
57
58impl Eq for Ranked {}
59
60impl PartialOrd for Ranked {
61 #[inline]
62 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
63 Some(self.cmp(other))
64 }
65}
66
67impl Ord for Ranked {
68 #[inline]
69 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
70 cmp_ascending(&self.0, &other.0)
71 }
72}
73
74#[derive(Debug)]
80pub struct TopK {
81 k: usize,
82 heap: BinaryHeap<Ranked>,
84}
85
86impl TopK {
87 #[must_use]
91 pub fn new(k: usize) -> Self {
92 Self {
93 k,
94 heap: BinaryHeap::with_capacity(k),
95 }
96 }
97
98 pub fn offer(&mut self, candidate: Neighbor) {
100 if self.k == 0 {
101 return;
102 }
103 if self.heap.len() < self.k {
104 self.heap.push(Ranked(candidate));
105 return;
106 }
107 if let Some(worst) = self.heap.peek() {
110 if cmp_ascending(&candidate, &worst.0) == core::cmp::Ordering::Less {
111 self.heap.pop();
112 self.heap.push(Ranked(candidate));
113 }
114 }
115 }
116
117 #[must_use]
122 pub fn worst_score(&self) -> Option<f32> {
123 self.heap.peek().map(|r| r.0.score)
124 }
125
126 #[must_use]
128 pub fn is_full(&self) -> bool {
129 self.heap.len() >= self.k
130 }
131
132 #[must_use]
134 pub fn len(&self) -> usize {
135 self.heap.len()
136 }
137
138 #[must_use]
140 pub fn is_empty(&self) -> bool {
141 self.heap.is_empty()
142 }
143
144 #[must_use]
146 pub fn into_sorted_vec(self) -> Vec<Neighbor> {
147 let mut out: Vec<Neighbor> = self.heap.into_iter().map(|r| r.0).collect();
148 out.sort_unstable_by(cmp_ascending);
149 out
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use core::cmp::Ordering;
157
158 fn n(id: u64, score: f32) -> Neighbor {
159 Neighbor::new(VectorId::new(id), score)
160 }
161
162 #[test]
163 fn orders_by_score_then_id() {
164 let a = Neighbor::new(VectorId::new(5), 1.0);
165 let b = Neighbor::new(VectorId::new(2), 2.0);
166 assert_eq!(cmp_ascending(&a, &b), Ordering::Less);
167 }
168
169 #[test]
170 fn breaks_ties_by_id() {
171 let a = Neighbor::new(VectorId::new(2), 1.0);
172 let b = Neighbor::new(VectorId::new(5), 1.0);
173 assert_eq!(cmp_ascending(&a, &b), Ordering::Less);
174 assert_eq!(cmp_ascending(&b, &a), Ordering::Greater);
175 }
176
177 #[test]
178 fn nan_sorts_last() {
179 let real = Neighbor::new(VectorId::new(1), 1.0);
180 let nan = Neighbor::new(VectorId::new(0), f32::NAN);
181 assert_eq!(cmp_ascending(&real, &nan), Ordering::Less);
184 }
185
186 #[test]
187 fn keeps_k_smallest() {
188 let mut t = TopK::new(3);
189 for (i, s) in [5.0, 1.0, 4.0, 2.0, 3.0].iter().enumerate() {
190 t.offer(n(i as u64, *s));
191 }
192 let got = t.into_sorted_vec();
193 let scores: Vec<f32> = got.iter().map(|x| x.score).collect();
194 assert_eq!(scores, vec![1.0, 2.0, 3.0]);
195 }
196
197 #[test]
198 fn k_larger_than_input_keeps_all_sorted() {
199 let mut t = TopK::new(10);
200 t.offer(n(0, 2.0));
201 t.offer(n(1, 1.0));
202 let got = t.into_sorted_vec();
203 assert_eq!(got.len(), 2);
204 assert_eq!(got[0].id, VectorId::new(1));
205 assert_eq!(got[1].id, VectorId::new(0));
206 }
207
208 #[test]
209 fn k_zero_keeps_nothing() {
210 let mut t = TopK::new(0);
211 t.offer(n(0, 1.0));
212 assert!(t.is_empty());
213 assert!(t.into_sorted_vec().is_empty());
214 }
215
216 #[test]
217 fn deterministic_tie_break_by_id() {
218 let mut t = TopK::new(2);
220 for id in [9, 3, 7, 1, 5] {
221 t.offer(n(id, 1.0));
222 }
223 let got = t.into_sorted_vec();
224 let ids: Vec<u64> = got.iter().map(|x| x.id.get()).collect();
225 assert_eq!(ids, vec![1, 3]);
226 }
227
228 #[test]
229 fn worst_score_and_fullness_track_the_beam() {
230 let mut t = TopK::new(2);
231 assert!(!t.is_full());
232 assert_eq!(t.worst_score(), None);
233 t.offer(n(0, 3.0));
234 assert!(!t.is_full());
235 assert_eq!(t.worst_score(), Some(3.0));
236 t.offer(n(1, 1.0));
237 assert!(t.is_full());
238 assert_eq!(t.worst_score(), Some(3.0));
239 t.offer(n(2, 2.0));
241 assert!(t.is_full());
242 assert_eq!(t.worst_score(), Some(2.0));
243 }
244}