1use std::sync::{Mutex, OnceLock};
16
17use super::hnsw::{AnnIndex, FlatEmbeddings, brute_force_topk};
18
19pub const ANN_MIN_VECTORS: usize = 2_500;
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
36pub fn clear() {
42 if let Ok(mut guard) = cache().lock() {
43 *guard = None;
44 }
45}
46
47#[must_use]
51pub fn memory_usage_bytes() -> usize {
52 let Ok(guard) = cache().lock() else {
53 return 0;
54 };
55 guard.as_ref().map_or(0, |c| c.index.memory_usage_bytes())
56}
57
58#[must_use]
67pub fn topk(embeddings: &FlatEmbeddings, query: &[f32], top_k: usize) -> Vec<(usize, f32)> {
68 topk_gated(embeddings, query, top_k, ANN_MIN_VECTORS)
69}
70
71fn topk_gated(
74 embeddings: &FlatEmbeddings,
75 query: &[f32],
76 top_k: usize,
77 min_vectors: usize,
78) -> Vec<(usize, f32)> {
79 if embeddings.n_vectors() < min_vectors {
80 return brute_force_topk(embeddings, query, top_k);
81 }
82
83 let fp = fingerprint(embeddings);
84 let Ok(mut guard) = cache().lock() else {
85 return brute_force_topk(embeddings, query, top_k);
86 };
87
88 let needs_build = match guard.as_ref() {
89 Some(c) => c.fingerprint != fp,
90 None => true,
91 };
92 if needs_build {
93 *guard = Some(Cached {
94 fingerprint: fp,
95 index: AnnIndex::build(embeddings.clone()),
96 });
97 }
98
99 match guard.as_ref() {
100 Some(c) => c.index.search(query, top_k),
101 None => brute_force_topk(embeddings, query, top_k),
102 }
103}
104
105fn fingerprint(embeddings: &FlatEmbeddings) -> u64 {
108 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
109 macro_rules! mix {
110 ($x:expr_2021) => {{
111 h ^= $x;
112 h = h.wrapping_mul(0x0000_0100_0000_01b3);
113 }};
114 }
115 let n = embeddings.n_vectors();
116 mix!(n as u64);
117 mix!(embeddings.dim as u64);
118 for i in 0..n {
119 let v = embeddings.get(i);
120 mix!(i as u64);
121 if let Some(&f) = v.first() {
122 mix!(u64::from(f.to_bits()));
123 }
124 if let Some(&f) = v.get(v.len() / 2) {
125 mix!(u64::from(f.to_bits()));
126 }
127 if let Some(&f) = v.last() {
128 mix!(u64::from(f.to_bits()));
129 }
130 }
131 h
132}
133
134#[cfg(test)]
141pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> {
142 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
143 LOCK.get_or_init(|| Mutex::new(()))
144 .lock()
145 .unwrap_or_else(std::sync::PoisonError::into_inner)
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use std::collections::HashSet;
152
153 const TEST_GATE: usize = 1000;
156
157 fn serial() -> std::sync::MutexGuard<'static, ()> {
158 super::test_lock()
159 }
160
161 fn cached_fingerprint() -> Option<u64> {
164 cache()
165 .lock()
166 .ok()
167 .and_then(|g| g.as_ref().map(|c| c.fingerprint))
168 }
169
170 fn random_vec(dim: usize, seed: u64) -> Vec<f32> {
171 let mut v = Vec::with_capacity(dim);
172 let mut s = seed;
173 for _ in 0..dim {
174 s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
175 v.push((s as f32 / u64::MAX as f32) * 2.0 - 1.0);
176 }
177 v
178 }
179
180 fn flat_from(vecs: Vec<Vec<f32>>) -> FlatEmbeddings {
181 FlatEmbeddings::from_vecs(vecs)
182 }
183
184 fn jitter(base: &[f32], seed: u64, scale: f32) -> Vec<f32> {
186 base.iter()
187 .enumerate()
188 .map(|(i, &b)| {
189 let s = seed
190 .wrapping_add(i as u64)
191 .wrapping_mul(6_364_136_223_846_793_005)
192 .wrapping_add(1);
193 b + ((s as f32 / u64::MAX as f32) * 2.0 - 1.0) * scale
194 })
195 .collect()
196 }
197
198 fn clustered(
199 n_clusters: usize,
200 per_cluster: usize,
201 dim: usize,
202 ) -> (FlatEmbeddings, Vec<Vec<f32>>) {
203 let centers: Vec<Vec<f32>> = (0..n_clusters)
204 .map(|c| random_vec(dim, (c as u64 + 1) * 1_000))
205 .collect();
206 let mut vectors = Vec::with_capacity(n_clusters * per_cluster);
207 for (c, center) in centers.iter().enumerate() {
208 for j in 0..per_cluster {
209 vectors.push(jitter(center, (c * per_cluster + j) as u64 + 7, 0.02));
210 }
211 }
212 (flat_from(vectors), centers)
213 }
214
215 #[test]
216 fn small_corpus_matches_brute_force_exactly() {
217 let flat = flat_from((0..200).map(|i| random_vec(32, i)).collect());
218 let query = random_vec(32, 9_999);
219
220 let via_cache = topk(&flat, &query, 8);
222 let exact = brute_force_topk(&flat, &query, 8);
223
224 assert_eq!(via_cache.len(), exact.len());
225 for (a, b) in via_cache.iter().zip(exact.iter()) {
226 assert_eq!(a.0, b.0, "below threshold must be exact brute force");
227 }
228 }
229
230 #[test]
231 fn hnsw_path_recall_matches_brute_force_on_clusters() {
232 let _serial = serial();
233 let (flat, centers) = clustered(24, 60, 32); let query = ¢ers[5];
235 let k = 20;
236
237 let ann = topk_gated(&flat, query, k, TEST_GATE); let exact = brute_force_topk(&flat, query, k);
239 assert_eq!(ann.len(), k);
240
241 let exact_set: HashSet<usize> = exact.iter().map(|(i, _)| *i).collect();
242 let overlap = ann.iter().filter(|(i, _)| exact_set.contains(i)).count();
243 assert!(
244 overlap * 100 >= k * 50,
245 "HNSW recall@{k} too low: {overlap}/{k}"
246 );
247 }
248
249 #[test]
250 fn hnsw_path_results_are_descending() {
251 let _serial = serial();
252 let (flat, centers) = clustered(20, 60, 24); let results = topk_gated(&flat, ¢ers[3], 10, TEST_GATE);
254 for w in results.windows(2) {
255 assert!(
256 w[0].1 >= w[1].1,
257 "results must be sorted by descending similarity"
258 );
259 }
260 }
261
262 #[test]
263 fn rebuilds_when_corpus_changes() {
264 let _serial = serial();
265 let (a, ca) = clustered(20, 55, 32); let (b, cb) = clustered(18, 60, 32); let _ = topk_gated(&a, &ca[7], 5, TEST_GATE);
269 assert_eq!(
270 cached_fingerprint(),
271 Some(fingerprint(&a)),
272 "first query caches corpus A's index"
273 );
274
275 let _ = topk_gated(&b, &cb[4], 5, TEST_GATE);
276 assert_eq!(
277 cached_fingerprint(),
278 Some(fingerprint(&b)),
279 "a different corpus must force a rebuild to B"
280 );
281
282 let _ = topk_gated(&a, &ca[7], 5, TEST_GATE);
283 assert_eq!(
284 cached_fingerprint(),
285 Some(fingerprint(&a)),
286 "re-querying A must rebuild A — never serve stale B"
287 );
288 }
289
290 #[test]
291 fn fingerprint_differs_on_content_change() {
292 let a = flat_from((0..10).map(|i| random_vec(8, i)).collect());
293 let mut b_vecs: Vec<Vec<f32>> = (0..10).map(|i| random_vec(8, i)).collect();
294 b_vecs[3][0] += 0.5;
295 let b = flat_from(b_vecs);
296 assert_ne!(fingerprint(&a), fingerprint(&b));
297 }
298}