1use std::sync::{Arc, Mutex, OnceLock};
19
20use super::hnsw::{brute_force_topk, AnnIndex};
21
22pub const ANN_MIN_VECTORS: usize = 50_000;
25
26struct Cached {
27 fingerprint: u64,
28 index: AnnIndex,
29}
30
31fn cache() -> &'static Mutex<Option<Cached>> {
32 static CACHE: OnceLock<Mutex<Option<Cached>>> = OnceLock::new();
33 CACHE.get_or_init(|| Mutex::new(None))
34}
35
36#[must_use]
46pub fn topk(embeddings: &Arc<[Vec<f32>]>, query: &[f32], top_k: usize) -> Vec<(usize, f32)> {
47 topk_gated(embeddings, query, top_k, ANN_MIN_VECTORS)
48}
49
50fn topk_gated(
53 embeddings: &Arc<[Vec<f32>]>,
54 query: &[f32],
55 top_k: usize,
56 min_vectors: usize,
57) -> Vec<(usize, f32)> {
58 if embeddings.len() < min_vectors {
59 return brute_force_topk(embeddings, query, top_k);
60 }
61
62 let fp = fingerprint(embeddings);
63 let Ok(mut guard) = cache().lock() else {
64 return brute_force_topk(embeddings, query, top_k);
65 };
66
67 let needs_build = match guard.as_ref() {
68 Some(c) => c.fingerprint != fp,
69 None => true,
70 };
71 if needs_build {
72 *guard = Some(Cached {
73 fingerprint: fp,
74 index: AnnIndex::build(Arc::clone(embeddings)),
76 });
77 }
78
79 match guard.as_ref() {
80 Some(c) => c.index.search(query, top_k),
81 None => brute_force_topk(embeddings, query, top_k),
82 }
83}
84
85fn fingerprint(embeddings: &[Vec<f32>]) -> u64 {
89 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
90 macro_rules! mix {
91 ($x:expr) => {{
92 h ^= $x;
93 h = h.wrapping_mul(0x0000_0100_0000_01b3);
94 }};
95 }
96 mix!(embeddings.len() as u64);
97 for (i, v) in embeddings.iter().enumerate() {
98 mix!(v.len() as u64);
99 mix!(i as u64);
100 if let Some(&f) = v.first() {
101 mix!(u64::from(f.to_bits()));
102 }
103 if let Some(&f) = v.get(v.len() / 2) {
104 mix!(u64::from(f.to_bits()));
105 }
106 if let Some(&f) = v.last() {
107 mix!(u64::from(f.to_bits()));
108 }
109 }
110 h
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use std::collections::HashSet;
117
118 const TEST_GATE: usize = 1000;
121
122 static TEST_LOCK: Mutex<()> = Mutex::new(());
127
128 fn serial() -> std::sync::MutexGuard<'static, ()> {
129 TEST_LOCK
130 .lock()
131 .unwrap_or_else(std::sync::PoisonError::into_inner)
132 }
133
134 fn cached_fingerprint() -> Option<u64> {
137 cache()
138 .lock()
139 .ok()
140 .and_then(|g| g.as_ref().map(|c| c.fingerprint))
141 }
142
143 fn random_vec(dim: usize, seed: u64) -> Vec<f32> {
144 let mut v = Vec::with_capacity(dim);
145 let mut s = seed;
146 for _ in 0..dim {
147 s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
148 v.push((s as f32 / u64::MAX as f32) * 2.0 - 1.0);
149 }
150 v
151 }
152
153 fn jitter(base: &[f32], seed: u64, scale: f32) -> Vec<f32> {
157 base.iter()
158 .enumerate()
159 .map(|(i, &b)| {
160 let s = seed
161 .wrapping_add(i as u64)
162 .wrapping_mul(6_364_136_223_846_793_005)
163 .wrapping_add(1);
164 b + ((s as f32 / u64::MAX as f32) * 2.0 - 1.0) * scale
165 })
166 .collect()
167 }
168
169 fn clustered(
170 n_clusters: usize,
171 per_cluster: usize,
172 dim: usize,
173 ) -> (Vec<Vec<f32>>, Vec<Vec<f32>>) {
174 let centers: Vec<Vec<f32>> = (0..n_clusters)
175 .map(|c| random_vec(dim, (c as u64 + 1) * 1_000))
176 .collect();
177 let mut vectors = Vec::with_capacity(n_clusters * per_cluster);
178 for (c, center) in centers.iter().enumerate() {
179 for j in 0..per_cluster {
180 vectors.push(jitter(center, (c * per_cluster + j) as u64 + 7, 0.02));
181 }
182 }
183 (vectors, centers)
184 }
185
186 #[test]
187 fn small_corpus_matches_brute_force_exactly() {
188 let vectors: Arc<[Vec<f32>]> = (0..200).map(|i| random_vec(32, i)).collect();
189 let query = random_vec(32, 9_999);
190
191 let via_cache = topk(&vectors, &query, 8);
193 let exact = brute_force_topk(&vectors, &query, 8);
194
195 assert_eq!(via_cache.len(), exact.len());
196 for (a, b) in via_cache.iter().zip(exact.iter()) {
197 assert_eq!(a.0, b.0, "below threshold must be exact brute force");
198 }
199 }
200
201 #[test]
202 fn hnsw_path_recall_matches_brute_force_on_clusters() {
203 let _serial = serial();
204 let (vectors, centers) = clustered(24, 60, 32); let vectors: Arc<[Vec<f32>]> = Arc::from(vectors);
206 let query = centers[5].clone();
207 let k = 20;
208
209 let ann = topk_gated(&vectors, &query, k, TEST_GATE); let exact = brute_force_topk(&vectors, &query, k);
211 assert_eq!(ann.len(), k);
212
213 let exact_set: HashSet<usize> = exact.iter().map(|(i, _)| *i).collect();
214 let overlap = ann.iter().filter(|(i, _)| exact_set.contains(i)).count();
215 assert!(
216 overlap * 100 >= k * 50,
217 "HNSW recall@{k} too low: {overlap}/{k}"
218 );
219 }
220
221 #[test]
222 fn hnsw_path_results_are_descending() {
223 let _serial = serial();
224 let (vectors, centers) = clustered(20, 60, 24); let vectors: Arc<[Vec<f32>]> = Arc::from(vectors);
226 let results = topk_gated(&vectors, ¢ers[3], 10, TEST_GATE);
227 for w in results.windows(2) {
228 assert!(
229 w[0].1 >= w[1].1,
230 "results must be sorted by descending similarity"
231 );
232 }
233 }
234
235 #[test]
236 fn rebuilds_when_corpus_changes() {
237 let _serial = serial();
238 let (a, ca) = clustered(20, 55, 32); let (b, cb) = clustered(18, 60, 32); let a: Arc<[Vec<f32>]> = Arc::from(a);
245 let b: Arc<[Vec<f32>]> = Arc::from(b);
246
247 let _ = topk_gated(&a, &ca[7], 5, TEST_GATE);
248 assert_eq!(
249 cached_fingerprint(),
250 Some(fingerprint(&a)),
251 "first query caches corpus A's index"
252 );
253
254 let _ = topk_gated(&b, &cb[4], 5, TEST_GATE);
255 assert_eq!(
256 cached_fingerprint(),
257 Some(fingerprint(&b)),
258 "a different corpus must force a rebuild to B"
259 );
260
261 let _ = topk_gated(&a, &ca[7], 5, TEST_GATE);
262 assert_eq!(
263 cached_fingerprint(),
264 Some(fingerprint(&a)),
265 "re-querying A must rebuild A — never serve stale B"
266 );
267 }
268
269 #[test]
270 fn fingerprint_differs_on_content_change() {
271 let a: Vec<Vec<f32>> = (0..10).map(|i| random_vec(8, i)).collect();
272 let mut b = a.clone();
273 b[3][0] += 0.5;
274 assert_ne!(fingerprint(&a), fingerprint(&b));
275 }
276}