1use std::cmp::Reverse;
9use std::collections::BinaryHeap;
10
11#[inline(always)]
15fn prefetch_t0(ptr: *const u8) {
16 #[cfg(target_arch = "x86_64")]
17 {
18 unsafe {
21 core::arch::x86_64::_mm_prefetch::<{ core::arch::x86_64::_MM_HINT_T0 }>(
22 ptr as *const i8,
23 );
24 }
25 }
26 #[cfg(not(target_arch = "x86_64"))]
29 let _ = ptr;
30}
31
32use roaring::RoaringBitmap;
33
34use crate::error::{VectorError, check_dim};
35use crate::hnsw::graph::{Candidate, HnswIndex, Probe, SearchResult};
36
37const MAX_EF: usize = 8192;
39
40impl HnswIndex {
41 pub fn search(
49 &self,
50 query: &[f32],
51 k: usize,
52 ef: usize,
53 ) -> Result<Vec<SearchResult>, VectorError> {
54 self.search_inner(query, k, ef.max(k).min(MAX_EF), None, 0)
55 }
56
57 pub fn search_filtered(
59 &self,
60 query: &[f32],
61 k: usize,
62 ef: usize,
63 filter: &RoaringBitmap,
64 ) -> Result<Vec<SearchResult>, VectorError> {
65 self.search_filtered_offset(query, k, ef, filter, 0)
66 }
67
68 pub fn search_filtered_offset(
74 &self,
75 query: &[f32],
76 k: usize,
77 ef: usize,
78 filter: &RoaringBitmap,
79 id_offset: u32,
80 ) -> Result<Vec<SearchResult>, VectorError> {
81 self.search_inner(query, k, ef.max(k), Some(filter), id_offset)
82 }
83
84 pub fn search_with_bitmap_bytes(
86 &self,
87 query: &[f32],
88 k: usize,
89 ef: usize,
90 bitmap_bytes: &[u8],
91 ) -> Result<Vec<SearchResult>, VectorError> {
92 self.search_with_bitmap_bytes_offset(query, k, ef, bitmap_bytes, 0)
93 }
94
95 pub fn search_with_bitmap_bytes_offset(
102 &self,
103 query: &[f32],
104 k: usize,
105 ef: usize,
106 bitmap_bytes: &[u8],
107 id_offset: u32,
108 ) -> Result<Vec<SearchResult>, VectorError> {
109 let bitmap = decode_filter_bitmap(bitmap_bytes)?;
110 self.search_filtered_offset(query, k, ef, &bitmap, id_offset)
111 }
112
113 fn search_inner(
116 &self,
117 query: &[f32],
118 k: usize,
119 ef: usize,
120 filter: Option<&RoaringBitmap>,
121 id_offset: u32,
122 ) -> Result<Vec<SearchResult>, VectorError> {
123 check_dim(self.dim, query.len())?;
124 if self.is_empty() {
125 return Ok(Vec::new());
126 }
127 let Some(ep) = self.entry_point else {
128 return Ok(Vec::new());
129 };
130
131 let probe = self.probe(query);
132
133 let mut current_ep = ep;
135 for layer in (1..=self.max_layer).rev() {
136 let results = search_layer(self, &probe, current_ep, 1, layer, None, 0);
137 if let Some(nearest) = results.first() {
138 current_ep = nearest.id;
139 }
140 }
141
142 let results = search_layer(self, &probe, current_ep, ef, 0, filter, id_offset);
144
145 Ok(results
146 .into_iter()
147 .take(k)
148 .map(|c| SearchResult {
149 id: c.id,
150 distance: c.dist,
151 })
152 .collect())
153 }
154}
155
156pub fn decode_filter_bitmap(bytes: &[u8]) -> Result<RoaringBitmap, VectorError> {
158 RoaringBitmap::deserialize_from(bytes).map_err(|e| VectorError::InvalidFilterBitmap {
159 detail: e.to_string(),
160 })
161}
162
163pub(crate) fn search_layer(
177 index: &HnswIndex,
178 probe: &Probe<'_>,
179 entry_point: u32,
180 ef: usize,
181 layer: usize,
182 filter: Option<&RoaringBitmap>,
183 id_offset: u32,
184) -> Vec<Candidate> {
185 let metric = index.params.metric;
186 let mut arena = index.arena.borrow_mut();
187
188 arena.reset();
190
191 arena.visited.insert(entry_point);
192
193 let ep_dist = index.dist_to_node(probe, entry_point, metric);
194 let ep_candidate = Candidate {
195 dist: ep_dist.unwrap_or(f32::INFINITY),
196 id: entry_point,
197 };
198
199 let mut cand_vec = std::mem::take(&mut arena.candidates);
204 cand_vec.push(Reverse(ep_candidate));
205 let mut candidates = BinaryHeap::from(cand_vec);
206
207 let mut res_vec = std::mem::take(&mut arena.results);
208
209 let passes_filter = |id: u32| -> bool {
210 if index.nodes[id as usize].deleted {
211 return false;
212 }
213 match filter {
214 Some(f) => f.contains(id + id_offset),
215 None => true,
216 }
217 };
218
219 if ep_dist.is_some() && passes_filter(entry_point) {
220 res_vec.push(ep_candidate);
221 }
222 let mut results = BinaryHeap::from(res_vec);
223
224 while let Some(Reverse(current)) = candidates.pop() {
225 if let Some(worst) = results.peek()
226 && current.dist > worst.dist
227 && results.len() >= ef
228 {
229 break;
230 }
231
232 if let Some(Reverse(next)) = candidates.peek()
236 && let Some(node) = index.nodes.get(next.id as usize)
237 {
238 prefetch_t0(node.storage.as_bytes().as_ptr());
239 }
240
241 let neighbors = index.neighbors_at(current.id, layer);
242 if neighbors.is_empty() && layer >= index.node_num_layers(current.id) {
243 continue;
244 }
245
246 for &neighbor_id in neighbors {
247 if !arena.visited.insert(neighbor_id) {
248 continue;
249 }
250
251 let scored = index.dist_to_node(probe, neighbor_id, metric);
252 let neighbor = Candidate {
253 dist: scored.unwrap_or(f32::INFINITY),
254 id: neighbor_id,
255 };
256
257 let worst_dist = results.peek().map_or(f32::INFINITY, |w| w.dist);
258 let should_explore = neighbor.dist < worst_dist || results.len() < ef;
259
260 if should_explore {
261 candidates.push(Reverse(neighbor));
262 }
263
264 if scored.is_some() && passes_filter(neighbor_id) {
265 results.push(neighbor);
266 if results.len() > ef {
267 results.pop();
268 }
269 }
270 }
271 }
272
273 arena.candidates = candidates.into_vec();
275
276 let mut result_vec = results.into_vec();
277 drop(arena);
279
280 result_vec.sort_unstable_by(|a, b| a.dist.total_cmp(&b.dist));
281 result_vec
282}
283
284#[cfg(test)]
285mod tests {
286 use crate::distance::DistanceMetric;
287 use crate::hnsw::{HnswIndex, HnswParams};
288 use roaring::RoaringBitmap;
289
290 fn build_index(n: usize, dim: usize) -> HnswIndex {
291 let mut idx = HnswIndex::with_seed(
292 dim,
293 HnswParams {
294 m: 16,
295 m0: 32,
296 ef_construction: 100,
297 metric: DistanceMetric::L2,
298 dtype: nodedb_types::vector_dtype::VectorStorageDtype::F32,
299 },
300 42,
301 );
302 for i in 0..n {
303 let v: Vec<f32> = (0..dim).map(|d| (i * dim + d) as f32).collect();
304 idx.insert(v).unwrap();
305 }
306 idx
307 }
308
309 #[test]
310 fn search_empty_index() {
311 let idx = HnswIndex::new(3, HnswParams::default());
312 let results = idx.search(&[1.0, 2.0, 3.0], 5, 50).unwrap();
313 assert!(results.is_empty());
314 }
315
316 #[test]
317 fn search_single_element() {
318 let mut idx = HnswIndex::with_seed(
319 2,
320 HnswParams {
321 m: 4,
322 m0: 8,
323 ef_construction: 16,
324 metric: DistanceMetric::L2,
325 dtype: nodedb_types::vector_dtype::VectorStorageDtype::F32,
326 },
327 1,
328 );
329 idx.insert(vec![1.0, 0.0]).unwrap();
330 let results = idx.search(&[1.0, 0.0], 1, 10).unwrap();
331 assert_eq!(results.len(), 1);
332 assert_eq!(results[0].id, 0);
333 assert!(results[0].distance < 1e-6);
334 }
335
336 #[test]
337 fn search_finds_exact_match() {
338 let idx = build_index(50, 3);
339 let query = idx.get_vector(25).unwrap().to_vec();
340 let results = idx.search(&query, 1, 50).unwrap();
341 assert_eq!(results.len(), 1);
342 assert_eq!(results[0].id, 25);
343 assert!(results[0].distance < 1e-6);
344 }
345
346 #[test]
347 fn search_returns_sorted_by_distance() {
348 let idx = build_index(100, 4);
349 let query = vec![50.0, 50.0, 50.0, 50.0];
350 let results = idx.search(&query, 10, 64).unwrap();
351 assert_eq!(results.len(), 10);
352 for w in results.windows(2) {
353 assert!(w[0].distance <= w[1].distance);
354 }
355 }
356
357 #[test]
358 fn search_k_larger_than_index() {
359 let idx = build_index(5, 2);
360 let results = idx.search(&[0.0, 0.0], 20, 50).unwrap();
361 assert_eq!(results.len(), 5);
362 }
363
364 #[test]
365 fn search_recall_at_10() {
366 let idx = build_index(500, 3);
367 let query = vec![100.0, 100.0, 100.0];
368 let results = idx.search(&query, 10, 128).unwrap();
369
370 let mut truth: Vec<(u32, f32)> = (0..500)
371 .map(|i| {
372 let v = idx.get_vector(i).unwrap();
373 let d = crate::distance::l2_squared(&query, v);
374 (i, d)
375 })
376 .collect();
377 truth.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
378 let truth_top10: std::collections::HashSet<u32> = truth[..10].iter().map(|t| t.0).collect();
379
380 let found: std::collections::HashSet<u32> = results.iter().map(|r| r.id).collect();
381 let recall = found.intersection(&truth_top10).count() as f64 / 10.0;
382 assert!(recall >= 0.8, "recall@10 = {recall:.2}, expected >= 0.80");
383 }
384
385 #[test]
386 fn search_excludes_tombstoned() {
387 let mut idx = build_index(20, 3);
388 idx.delete(0);
389 let results = idx.search(&[0.0, 0.0, 0.0], 5, 32).unwrap();
390 for r in &results {
391 assert_ne!(r.id, 0, "tombstoned node appeared in results");
392 }
393 }
394
395 #[test]
396 fn search_filtered_respects_bitmap() {
397 let idx = build_index(50, 3);
398 let mut filter = RoaringBitmap::new();
399 for i in (0..50u32).step_by(2) {
400 filter.insert(i);
401 }
402 let results = idx
403 .search_filtered(&[0.0, 0.0, 0.0], 5, 64, &filter)
404 .unwrap();
405 assert_eq!(results.len(), 5);
406 for r in &results {
407 assert!(r.id % 2 == 0, "got odd id {}", r.id);
408 }
409 }
410
411 #[test]
412 fn search_filtered_empty_returns_empty() {
413 let idx = build_index(20, 3);
414 let filter = RoaringBitmap::new();
415 let results = idx
416 .search_filtered(&[0.0, 0.0, 0.0], 5, 64, &filter)
417 .unwrap();
418 assert!(results.is_empty());
419 }
420
421 #[test]
422 fn bitmap_bytes_roundtrip() {
423 let idx = build_index(50, 3);
424 let mut filter = RoaringBitmap::new();
425 for i in 0..25u32 {
426 filter.insert(i);
427 }
428 let mut bytes = Vec::new();
429 filter.serialize_into(&mut bytes).unwrap();
430 let results = idx
431 .search_with_bitmap_bytes(&[0.0, 0.0, 0.0], 5, 32, &bytes)
432 .unwrap();
433 for r in &results {
434 assert!(r.id < 25, "got filtered-out node {}", r.id);
435 }
436 }
437
438 #[test]
441 fn wrong_dimension_query_is_a_typed_error() {
442 use crate::error::VectorError;
443 let empty = HnswIndex::new(3, HnswParams::default());
444 let idx = build_index(20, 3);
445 let filter: RoaringBitmap = (0..20u32).collect();
446 let mut bytes = Vec::new();
447 filter.serialize_into(&mut bytes).unwrap();
448 let short = [0.0_f32, 0.0];
449 for result in [
450 empty.search(&short, 5, 32),
451 idx.search(&short, 5, 32),
452 idx.search_filtered(&short, 5, 32, &filter),
453 idx.search_filtered_offset(&short, 5, 32, &filter, 0),
454 idx.search_with_bitmap_bytes(&short, 5, 32, &bytes),
455 idx.search_with_bitmap_bytes_offset(&short, 5, 32, &bytes, 0),
456 ] {
457 assert!(
458 matches!(
459 result,
460 Err(VectorError::DimensionMismatch {
461 expected: 3,
462 got: 2
463 })
464 ),
465 "{result:?}"
466 );
467 }
468 }
469
470 #[test]
473 fn undecodable_filter_bitmap_is_a_typed_error() {
474 use crate::error::VectorError;
475 let idx = build_index(20, 3);
476 let result = idx.search_with_bitmap_bytes(&[0.0, 0.0, 0.0], 5, 32, &[0xff, 0x01]);
477 assert!(
478 matches!(result, Err(VectorError::InvalidFilterBitmap { .. })),
479 "{result:?}"
480 );
481 }
482}