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