1use std::collections::BinaryHeap;
18use std::sync::OnceLock;
19
20use rayon::prelude::*;
21
22use super::adjacency::Adjacency;
23use super::fastscan::{pack32, scan32, BLOCK};
24use super::kernels::{
25 dot_f32_i4, dot_f32_i8, dot_i4_inline, dot_i4_inline_x4, dot_i8_inline, interleave_for_i4,
26 prefetch, prefetch_bytes, raw_one, QueryCode, Rotation, SplitMix,
27};
28use super::{invalid, prepare_rows, upper_bytes, BuildParams, Graph, Metric, NONE};
29use crate::core::error::HmsError;
30
31mod file;
32pub use file::Residency;
33use file::Slab;
34
35#[derive(Clone, Copy, Debug)]
37pub struct VSearchParams {
38 pub ef: usize,
40 pub rerank: usize,
43 pub patience: usize,
46 pub screen_sigmas: f32,
50 pub screen: bool,
52 pub fuse: bool,
56 pub neighbour_cap: usize,
59}
60
61impl Default for VSearchParams {
62 fn default() -> Self {
63 Self {
64 ef: 64,
65 rerank: 0,
66 patience: 0,
67 screen_sigmas: 1.0,
68 screen: true,
69 fuse: false,
70 neighbour_cap: 0,
71 }
72 }
73}
74
75#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct SearchKnobs {
99 pub prefetch: u8,
101 pub batch_est: bool,
103 pub frontier: f32,
105 pub block_screen: bool,
107}
108
109impl SearchKnobs {
110 pub const OFF: Self = Self {
112 prefetch: 0,
113 batch_est: false,
114 frontier: 1.0,
115 block_screen: false,
116 };
117
118 pub fn from_env() -> Self {
121 static KNOBS: OnceLock<SearchKnobs> = OnceLock::new();
122 *KNOBS.get_or_init(|| Self {
123 prefetch: env_knob::<u8>("HMS_QGRAPH_PREFETCH").unwrap_or(0).min(3),
124 batch_est: env_knob::<u8>("HMS_QGRAPH_BATCH_EST").unwrap_or(0) != 0,
125 frontier: env_knob::<f32>("HMS_QGRAPH_FRONTIER")
126 .filter(|f| f.is_finite())
127 .map_or(1.0, |f| f.clamp(0.0, 1.0)),
128 block_screen: env_knob::<u8>("HMS_QGRAPH_BLOCK_SCREEN").unwrap_or(0) != 0,
129 })
130 }
131}
132
133impl Default for SearchKnobs {
134 fn default() -> Self {
135 Self::OFF
136 }
137}
138
139fn env_knob<T: std::str::FromStr>(name: &str) -> Option<T> {
142 let value = std::env::var(name).ok()?;
143 match value.trim().parse::<T>() {
144 Ok(v) => Some(v),
145 Err(_) => {
146 eprintln!("{name}={value:?} is not a valid value; using the default");
147 None
148 }
149 }
150}
151
152fn huge_pages() -> bool {
157 static HUGE: OnceLock<bool> = OnceLock::new();
158 *HUGE.get_or_init(|| env_knob::<u8>("HMS_QGRAPH_HUGEPAGES").unwrap_or(0) != 0)
159}
160
161fn advise_huge_pages(ptr: *const u8, len: usize) {
167 #[cfg(target_os = "linux")]
168 {
169 const HUGE: usize = 2 << 20;
170 const MADV_HUGEPAGE: i32 = 14;
171 extern "C" {
172 fn madvise(addr: *mut std::ffi::c_void, len: usize, advice: i32) -> i32;
173 }
174 let start = (ptr as usize).next_multiple_of(HUGE);
175 let end = (ptr as usize + len) & !(HUGE - 1);
176 if start < end {
177 unsafe {
180 madvise(start as *mut std::ffi::c_void, end - start, MADV_HUGEPAGE);
181 }
182 }
183 }
184 #[cfg(not(target_os = "linux"))]
185 let _ = (ptr, len);
186}
187
188fn huge_capacity<T>(ptr: *const T, capacity: usize) {
191 if huge_pages() {
192 advise_huge_pages(ptr.cast::<u8>(), capacity * std::mem::size_of::<T>());
193 }
194}
195
196#[repr(C, align(128))]
198#[derive(Clone, Copy)]
199struct Line([i8; 128]);
200
201struct Lines {
203 lines: Slab<Line>,
205 len: usize,
206}
207
208impl Lines {
209 fn zeroed(len: usize) -> Self {
210 let count = len.div_ceil(128);
211 let mut lines = Vec::with_capacity(count);
212 huge_capacity(lines.as_ptr(), count);
213 lines.resize(count, Line([0; 128]));
214 Self {
215 lines: lines.into(),
216 len,
217 }
218 }
219
220 #[inline(always)]
221 fn bytes(&self) -> &[i8] {
222 unsafe { std::slice::from_raw_parts(self.lines.as_ptr().cast::<i8>(), self.len) }
225 }
226
227 fn bytes_mut(&mut self) -> &mut [i8] {
228 let len = self.len;
229 let lines = self.lines.as_mut_slice();
230 unsafe { std::slice::from_raw_parts_mut(lines.as_mut_ptr().cast::<i8>(), len) }
232 }
233}
234
235pub struct VGraph {
236 n: usize,
237 dim: usize,
238 padded: usize,
240 bits: u8,
242 cbytes: usize,
244 stride: usize,
247 degree: usize,
248 mean: Vec<f32>,
249 half_norms: Slab<f32>,
252 adj: Slab<u32>,
254 adj3: Slab<u8>,
256 adjv: Slab<u8>,
260 adjv_off: Slab<u32>,
261 codes: Lines,
262 scales: Slab<f32>,
263 res_bits: u8,
265 res_stride: usize,
267 residual: Slab<i8>,
268 res_scales: Slab<f32>,
269 entry: u32,
270 upper_ids: Vec<u32>,
271 layers: Vec<Vec<Vec<u32>>>,
272 ids: Slab<u32>,
274 screen: Slab<u64>,
278 screen_words: usize,
279 screen_rot: Option<Rotation>,
280 screen_sigma: f32,
283 block_bits: u8,
286 block_dim: usize,
288 block_m: usize,
290 blocks_per_vertex: usize,
292 blocks: Lines,
295 block_rot: Option<Rotation>,
296 block_kappa: f32,
299}
300
301const SCREEN_SEED: u64 = 0x5C12_EE00;
303const BLOCK_SEED: u64 = 0xB10C_5CA0;
305
306fn block_record_bytes(m: usize) -> usize {
308 16 * m + 8 * BLOCK
309}
310
311#[inline]
313fn block_level(nibbles: &[u8], bits: u8, i: usize) -> f32 {
314 match bits {
315 1 => {
316 if nibbles[i / 4] >> (i % 4) & 1 == 1 {
317 1.0
318 } else {
319 -1.0
320 }
321 }
322 2 => 2.0 * f32::from(nibbles[i / 2] >> (2 * (i % 2)) & 3) - 3.0,
323 _ => f32::from(nibbles[i]) - 7.0,
324 }
325}
326
327#[inline]
329fn le_f32(bytes: &[u8], i: usize) -> f32 {
330 f32::from_le_bytes([
331 bytes[4 * i],
332 bytes[4 * i + 1],
333 bytes[4 * i + 2],
334 bytes[4 * i + 3],
335 ])
336}
337
338fn encode_block_code(x: &[f32], bits: u8, nibbles: &mut [u8]) -> f32 {
348 nibbles.fill(0);
349 let m = x.iter().fold(0f32, |a, &v| a.max(v.abs()));
350 if m == 0.0 {
351 return 0.0;
352 }
353 match bits {
354 1 => {
355 let (mut norm2, mut abs) = (0f32, 0f32);
356 for (i, &v) in x.iter().enumerate() {
357 norm2 += v * v;
358 abs += v.abs();
359 if v >= 0.0 {
360 nibbles[i / 4] |= 1 << (i % 4);
361 }
362 }
363 norm2 / abs
364 }
365 2 => {
366 let err = |s: f32| -> f32 {
368 x.iter()
369 .map(|&v| {
370 let t = ((v / s + 3.0) / 2.0).round().clamp(0.0, 3.0);
371 let d = v - s * (2.0 * t - 3.0);
372 d * d
373 })
374 .sum()
375 };
376 let (mut best, mut best_err) = (m / 3.0, f32::INFINITY);
377 for k in 0..8 {
378 let s = m / 3.0 * (1.0 - 0.1 * k as f32);
379 let e = err(s);
380 if e < best_err {
381 (best, best_err) = (s, e);
382 }
383 }
384 let mut dot = 0f32;
385 for (i, &v) in x.iter().enumerate() {
386 let t = ((v / best + 3.0) / 2.0).round().clamp(0.0, 3.0) as u8;
387 nibbles[i / 2] |= t << (2 * (i % 2));
388 dot += v * (2.0 * f32::from(t) - 3.0);
389 }
390 unbiased_scale(x, dot, best)
391 }
392 _ => {
393 let err = |s: f32| -> f32 {
394 x.iter()
395 .map(|&v| {
396 let d = v - s * (v / s).round().clamp(-7.0, 7.0);
397 d * d
398 })
399 .sum()
400 };
401 let (mut best, mut best_err) = (m / 7.0, f32::INFINITY);
402 for k in 0..8 {
403 let s = m / 7.0 * (1.0 - 0.06 * k as f32);
404 let e = err(s);
405 if e < best_err {
406 (best, best_err) = (s, e);
407 }
408 }
409 let mut dot = 0f32;
410 for (i, &v) in x.iter().enumerate() {
411 let level = (v / best).round().clamp(-7.0, 7.0);
412 nibbles[i] = (level as i8 + 7) as u8;
413 dot += v * level;
414 }
415 unbiased_scale(x, dot, best)
416 }
417 }
418}
419
420fn unbiased_scale(x: &[f32], dot: f32, fallback: f32) -> f32 {
423 let norm2: f32 = x.iter().map(|v| v * v).sum();
424 if dot > 0.0 {
425 norm2 / dot
426 } else {
427 fallback
428 }
429}
430
431fn block_table(q: &[f32], bits: u8, m: usize, t: &mut [f32]) {
435 match bits {
436 1 => {
437 for j in 0..m {
438 let qs = &q[4 * j..4 * j + 4];
439 let total: f32 = qs.iter().sum();
440 for v in 0..16usize {
441 let on: f32 = (0..4).filter(|b| v >> b & 1 == 1).map(|b| qs[b]).sum();
442 t[16 * j + v] = 2.0 * on - total;
443 }
444 }
445 }
446 2 => {
447 for j in 0..m {
448 let (qa, qb) = (q[2 * j], q[2 * j + 1]);
449 for v in 0..16usize {
450 let (ta, tb) = ((v & 3) as f32, (v >> 2) as f32);
451 t[16 * j + v] = qa * (2.0 * ta - 3.0) + qb * (2.0 * tb - 3.0);
452 }
453 }
454 }
455 _ => {
456 for j in 0..m {
457 for v in 0..16usize {
458 t[16 * j + v] = q[j] * (v as f32 - 7.0);
459 }
460 }
461 }
462 }
463}
464
465pub struct BlockQuery {
469 table: Vec<f32>,
470 lut: Vec<u8>,
471 delta: f32,
472 min_sum: f32,
473 inv_sq: f32,
475}
476
477impl BlockQuery {
478 fn new(m: usize) -> Self {
479 Self {
480 table: vec![0.0; 16 * m],
481 lut: vec![0; 16 * m],
482 delta: 1.0,
483 min_sum: 0.0,
484 inv_sq: 1.0,
485 }
486 }
487
488 fn quantize(&mut self, m: usize) {
491 let mut range = 0f32;
492 let mut min_sum = 0f32;
493 for g in self.table.as_chunks::<16>().0 {
494 let lo = g.iter().fold(f32::INFINITY, |a, &v| a.min(v));
495 let hi = g.iter().fold(f32::NEG_INFINITY, |a, &v| a.max(v));
496 min_sum += lo;
497 range = range.max(hi - lo);
498 }
499 let delta = if range > 0.0 { range / 255.0 } else { 1.0 };
500 for (j, g) in self.table.as_chunks::<16>().0.iter().enumerate() {
501 let lo = g.iter().fold(f32::INFINITY, |a, &v| a.min(v));
502 for (v, &e) in g.iter().enumerate() {
503 self.lut[16 * j + v] = ((e - lo) / delta).round().clamp(0.0, 255.0) as u8;
504 }
505 }
506 debug_assert_eq!(self.lut.len(), 16 * m);
507 self.delta = delta;
508 self.min_sum = min_sum;
509 }
510}
511
512#[inline]
514fn key_dist(k: u64) -> f32 {
515 let o = (k >> 32) as u32;
516 f32::from_bits(if o >> 31 == 1 { o & 0x7FFF_FFFF } else { !o })
517}
518
519pub(super) fn quantize(x: &[f32], code: &mut [i8]) -> f32 {
522 let m = x.iter().fold(0f32, |a, &v| a.max(v.abs()));
523 code.fill(0);
524 if m == 0.0 {
525 return 0.0;
526 }
527 let scale = m / 127.0;
528 for (c, &v) in code.iter_mut().zip(x) {
529 *c = (v / scale).round().clamp(-127.0, 127.0) as i8;
530 }
531 scale
532}
533
534pub(super) fn quantize4(x: &[f32], code: &mut [i8]) -> f32 {
537 let m = x.iter().fold(0f32, |a, &v| a.max(v.abs()));
538 code.fill(0);
539 if m == 0.0 {
540 return 0.0;
541 }
542 let scale = m / 7.0;
543 let q = |i: usize| {
544 x.get(i)
545 .map_or(0, |&v| (v / scale).round().clamp(-7.0, 7.0) as i8)
546 };
547 for (j, c) in code.iter_mut().enumerate() {
548 *c = ((q(2 * j) as u8 & 0x0F) | ((q(2 * j + 1) as u8) << 4)) as i8;
549 }
550 scale
551}
552
553fn bfs_order(g: &Graph) -> Vec<u32> {
556 let mut seen = vec![false; g.n];
557 let mut order = Vec::with_capacity(g.n);
558 for s in std::iter::once(g.entry as usize).chain(0..g.n) {
559 if seen[s] {
560 continue;
561 }
562 seen[s] = true;
563 let mut head = order.len();
564 order.push(s as u32);
565 while head < order.len() {
566 let v = order[head] as usize;
567 head += 1;
568 for &u in g.neighbors(v) {
569 if !std::mem::replace(&mut seen[u as usize], true) {
570 order.push(u);
571 }
572 }
573 }
574 }
575 order
576}
577
578pub(super) fn code_at(c: &[i8], bits: u8, i: usize) -> i8 {
580 if bits == 8 {
581 c.get(i).copied().unwrap_or(0)
582 } else {
583 c.get(i / 2).map_or(0, |&b| {
584 if i.is_multiple_of(2) {
585 (((b as u8) << 4) as i8) >> 4
586 } else {
587 b >> 4
588 }
589 })
590 }
591}
592
593const NONE3: u32 = 0x00FF_FFFF;
595
596#[inline]
597fn put_varint(out: &mut Vec<u8>, mut v: u64) {
599 while v >= 0x80 {
600 out.push((v as u8) | 0x80);
601 v >>= 7;
602 }
603 out.push(v as u8);
604}
605
606#[inline]
608fn get_varint(bytes: &[u8]) -> (u64, usize) {
609 let mut v = 0u64;
610 for (i, &b) in bytes.iter().enumerate() {
611 v |= u64::from(b & 0x7f) << (7 * i);
612 if b & 0x80 == 0 {
613 return (v, i + 1);
614 }
615 }
616 (v, bytes.len())
617}
618
619fn zigzag(v: i64) -> u64 {
620 ((v << 1) ^ (v >> 63)) as u64
621}
622
623fn unzigzag(v: u64) -> i64 {
624 ((v >> 1) as i64) ^ -((v & 1) as i64)
625}
626
627fn prefetch_u8(bytes: &[u8]) {
628 prefetch_bytes(unsafe { std::slice::from_raw_parts(bytes.as_ptr().cast::<i8>(), bytes.len()) });
630}
631
632#[inline]
633fn prefetch_f32(x: &f32) {
634 prefetch_bytes(unsafe { std::slice::from_raw_parts((x as *const f32).cast::<i8>(), 4) });
636}
637
638#[inline]
639fn prefetch_ids(ids: &[u32]) {
640 prefetch_bytes(unsafe { std::slice::from_raw_parts(ids.as_ptr().cast::<i8>(), ids.len() * 4) });
642}
643
644#[inline]
646fn key(dist: f32, id: u32) -> u64 {
647 let b = dist.to_bits();
648 let o = if b >> 31 == 1 { !b } else { b | 0x8000_0000 };
649 (u64::from(o) << 32) | u64::from(id)
650}
651
652#[inline]
653fn key_id(k: u64) -> u32 {
654 k as u32
655}
656
657impl VGraph {
658 pub fn from_graph(g: &Graph, residual: bool) -> Result<Self, HmsError> {
661 Self::from_graph_with(g, 8, if residual { 8 } else { 0 }, false, false, 4, false)
662 }
663
664 pub fn from_graph_with(
673 g: &Graph,
674 bits: u8,
675 residual_bits: u8,
676 align: bool,
677 reorder: bool,
678 id_bytes: u8,
679 screen: bool,
680 ) -> Result<Self, HmsError> {
681 if !matches!(bits, 4 | 8) {
682 return Err(invalid(format!(
683 "vertex code bits must be 4 or 8, not {bits}"
684 )));
685 }
686 if !(id_bytes == 4 || id_bytes == 0 || (id_bytes == 3 && (g.n as u64) < u64::from(NONE3))) {
687 return Err(invalid(format!(
688 "id_bytes must be 4, 0 (delta varints), or 3 below 2^24 - 1 vertices (got {id_bytes} for {} vertices)",
689 g.n
690 )));
691 }
692 if !matches!(residual_bits, 0 | 4 | 8) {
693 return Err(invalid(format!(
694 "residual code bits must be 0, 4 or 8, not {residual_bits}"
695 )));
696 }
697 let euclidean = g.metric == Metric::Euclidean;
698 if euclidean && screen {
699 return Err(invalid("the 1-bit screen is angular only".into()));
700 }
701 let (n, dim) = (g.n, g.dim);
702 let padded = dim.next_multiple_of(if bits == 4 { 32 } else { 16 });
703 let cbytes = if bits == 4 { padded / 2 } else { padded };
704 let stride = match (align, cbytes) {
705 (false, _) => cbytes,
706 (true, c) if c <= 64 => c.next_power_of_two(),
707 (true, c) => c.next_multiple_of(128),
708 };
709 let res_stride = match residual_bits {
710 8 => padded,
711 4 => padded / 2,
712 _ => 0,
713 };
714 let mut sum = vec![0f64; dim];
715 for x in g.vectors.chunks_exact(dim) {
716 sum.iter_mut().zip(x).for_each(|(s, &v)| *s += f64::from(v));
717 }
718 let mut mean: Vec<f32> = sum.iter().map(|s| (s / n as f64) as f32).collect();
719 mean.resize(padded, 0.0);
720
721 let ids = if reorder { bfs_order(g) } else { Vec::new() };
722 let old = |v: usize| if reorder { ids[v] as usize } else { v };
723 let mut new_id: Vec<u32> = Vec::new();
724 if reorder {
725 new_id = vec![0; n];
726 for (v, &o) in ids.iter().enumerate() {
727 new_id[o as usize] = v as u32;
728 }
729 }
730 let new = |o: u32| if reorder { new_id[o as usize] } else { o };
731 let half_norms: Vec<f32> = if euclidean {
732 (0..n).map(|v| 0.5 * g.norms[old(v)]).collect()
733 } else {
734 Vec::new()
735 };
736
737 let screen_rot = screen.then(|| Rotation::new(dim, 0, SCREEN_SEED));
738 let screen_words = screen_rot.as_ref().map_or(0, |r| r.padded() / 64);
739 let centred = |v: usize| -> Vec<f32> {
741 let o = old(v);
742 g.vectors[o * dim..(o + 1) * dim]
743 .iter()
744 .zip(&mean)
745 .map(|(a, m)| a - m)
746 .collect()
747 };
748 let mut codes = Lines::zeroed(n * stride);
750 let mut scales = Vec::with_capacity(n);
751 huge_capacity(scales.as_ptr(), n);
752 scales.resize(n, 0f32);
753 codes
754 .bytes_mut()
755 .par_chunks_exact_mut(stride)
756 .zip(scales.par_iter_mut())
757 .enumerate()
758 .for_each(|(v, (dst, s))| {
759 let x = centred(v);
760 *s = if bits == 8 {
761 quantize(&x, &mut dst[..cbytes])
762 } else {
763 quantize4(&x, &mut dst[..cbytes])
764 };
765 });
766 let mut residual = Vec::with_capacity(n * res_stride);
767 huge_capacity(residual.as_ptr(), n * res_stride);
768 residual.resize(n * res_stride, 0i8);
769 let mut res_scales = vec![0f32; if residual_bits > 0 { n } else { 0 }];
770 if residual_bits > 0 {
771 let code_bytes = codes.bytes();
772 residual
773 .par_chunks_exact_mut(res_stride)
774 .zip(res_scales.par_iter_mut())
775 .enumerate()
776 .for_each(|(v, (r, sr))| {
777 let mut x = centred(v);
778 let c = &code_bytes[v * stride..v * stride + cbytes];
779 let s = scales[v];
780 x.iter_mut()
781 .enumerate()
782 .for_each(|(i, a)| *a -= s * f32::from(code_at(c, bits, i)));
783 *sr = if residual_bits == 8 {
784 quantize(&x, r)
785 } else {
786 quantize4(&x, r)
787 };
788 });
789 }
790 let mut screen_rows = vec![0u64; if screen { n * (screen_words + 1) } else { 0 }];
791 if let Some(rot) = &screen_rot {
792 screen_rows
793 .par_chunks_exact_mut(screen_words + 1)
794 .enumerate()
795 .for_each(|(v, srow)| {
796 let x = centred(v);
797 let mut xr = vec![0f32; rot.padded()];
798 rot.apply(&x, &mut xr);
799 let (mut norm2, mut abs) = (0f32, 0f32);
800 let mut pop = 0u32;
801 for (i, &a) in xr.iter().enumerate() {
802 norm2 += a * a;
803 abs += a.abs();
804 if a >= 0.0 {
805 srow[i / 64] |= 1u64 << (i % 64);
806 pop += 1;
807 }
808 }
809 let f = if abs > 0.0 { norm2 / abs } else { 0.0 };
811 srow[screen_words] = u64::from(f.to_bits()) | (u64::from(pop) << 32);
812 });
813 }
814
815 let r = g.degree;
816 let list = |v: usize| g.neighbors(old(v)).iter().take(r).map(|&u| new(u));
818 let (mut adj, mut adj3) = (Vec::new(), Vec::new());
819 let (mut adjv, mut adjv_off) = (Vec::new(), Vec::new());
820 if id_bytes == 3 {
821 adj3 = Vec::<u8>::with_capacity(3 * n * r);
822 huge_capacity(adj3.as_ptr(), adj3.capacity());
823 for v in 0..n {
824 let mut count = 0;
825 for u in list(v) {
826 adj3.extend_from_slice(&u.min(NONE3).to_le_bytes()[..3]);
827 count += 1;
828 }
829 for _ in count..r {
830 adj3.extend_from_slice(&NONE3.to_le_bytes()[..3]);
831 }
832 }
833 } else if id_bytes == 4 {
834 adj = vec![NONE; n * r];
835 for (v, dst) in adj.chunks_exact_mut(r).enumerate() {
836 for (d, u) in dst.iter_mut().zip(list(v)) {
837 *d = u;
838 }
839 }
840 } else {
841 adjv_off.reserve(n + 1);
842 let mut sorted: Vec<u32> = Vec::with_capacity(r);
843 for v in 0..n {
844 adjv_off.push(adjv.len() as u32);
845 sorted.clear();
846 sorted.extend(list(v).take_while(|&u| u != NONE));
847 sorted.sort_unstable();
848 let list = &sorted;
849 let mut prev = v as u32;
850 for (i, &u) in list.iter().enumerate() {
851 let delta = if i == 0 {
852 zigzag(i64::from(u) - i64::from(prev))
853 } else {
854 u64::from(u - prev - 1)
855 };
856 put_varint(&mut adjv, delta);
857 prev = u;
858 }
859 }
860 adjv_off.push(adjv.len() as u32);
861 }
862 let entry = new(g.entry);
863 let upper_ids = g.upper_ids.iter().map(|&u| new(u)).collect();
864 let mut out = Self {
865 n,
866 dim,
867 padded,
868 bits,
869 cbytes,
870 stride,
871 degree: r,
872 mean,
873 half_norms: half_norms.into(),
874 adj: adj.into(),
875 adj3: adj3.into(),
876 adjv: adjv.into(),
877 adjv_off: adjv_off.into(),
878 codes,
879 scales: scales.into(),
880 res_bits: residual_bits,
881 res_stride,
882 residual: residual.into(),
883 res_scales: res_scales.into(),
884 entry,
885 upper_ids,
886 layers: g.layers.clone(),
887 ids: ids.into(),
888 screen: screen_rows.into(),
889 screen_words,
890 screen_rot,
891 screen_sigma: 0.0,
892 block_bits: 0,
893 block_dim: 0,
894 block_m: 0,
895 blocks_per_vertex: 0,
896 blocks: Lines::zeroed(0),
897 block_rot: None,
898 block_kappa: 0.0,
899 };
900 if screen {
901 out.screen_sigma = out.measure_screen_sigma(g);
902 }
903 Ok(out)
904 }
905
906 pub fn with_blocks(mut self, g: &Graph, bits: u8) -> Result<Self, HmsError> {
912 if !matches!(bits, 1 | 2 | 4) {
913 return Err(invalid(format!("block bits must be 1, 2 or 4, not {bits}")));
914 }
915 if !self.half_norms.is_empty() {
916 return Err(invalid("neighbour blocks are angular only".into()));
917 }
918 let (n, dim, r) = (self.n, self.dim, self.degree);
919 let rot = Rotation::new(dim, 0, BLOCK_SEED);
920 let bd = rot.padded();
921 let m = bd * usize::from(bits) / 4;
922 let per_vertex = r.div_ceil(BLOCK);
923 let record = block_record_bytes(m);
924 let ids = &self.ids;
925 let old = |v: usize| if ids.is_empty() { v } else { ids[v] as usize };
926 let row = |v: usize| &g.vectors[old(v) * dim..(old(v) + 1) * dim];
927 let mut blocks = Lines::zeroed(n * per_vertex * record);
928 blocks
929 .bytes_mut()
930 .par_chunks_exact_mut(per_vertex * record)
931 .enumerate()
932 .for_each(|(v, dst)| {
933 let xv = row(v);
940 let mut xr = vec![0f32; bd];
941 rot.apply(xv, &mut xr);
942 let mut diff = vec![0f32; dim];
943 let mut rr = vec![0f32; bd];
944 let mut nibbles = vec![0u8; BLOCK * m];
945 let mut scales = [0f32; BLOCK];
946 let mut offsets = [0f32; BLOCK];
947 let mut list: Vec<u32> = Vec::with_capacity(r);
948 self.for_each_neighbour(v, |u| list.push(u));
949 for (b, rec) in dst.chunks_exact_mut(record).enumerate() {
950 nibbles.fill(0);
951 scales.fill(0.0);
952 offsets.fill(0.0);
953 for i in 0..BLOCK {
954 let Some(&u) = list.get(b * BLOCK + i) else {
955 break;
956 };
957 let xu = row(u as usize);
958 for (d, (a, b)) in diff.iter_mut().zip(xu.iter().zip(xv)) {
959 *d = a - b;
960 }
961 rot.apply(&diff, &mut rr);
962 let nib = &mut nibbles[i * m..(i + 1) * m];
963 let s = encode_block_code(&rr, bits, nib);
964 let dot_lv: f32 = (0..bd).map(|j| block_level(nib, bits, j) * xr[j]).sum();
965 let dot_vr: f32 = xv.iter().zip(&diff).map(|(a, b)| a * b).sum();
966 scales[i] = s;
967 offsets[i] = dot_vr - s * dot_lv;
968 }
969 let rec_u8 = unsafe {
971 std::slice::from_raw_parts_mut(rec.as_mut_ptr().cast::<u8>(), rec.len())
972 };
973 pack32(&nibbles, m, &mut rec_u8[..16 * m]);
974 for (i, (s, e)) in scales.iter().zip(&offsets).enumerate() {
975 let at = 16 * m + 4 * i;
976 rec_u8[at..at + 4].copy_from_slice(&s.to_le_bytes());
977 let at = 16 * m + 4 * BLOCK + 4 * i;
978 rec_u8[at..at + 4].copy_from_slice(&e.to_le_bytes());
979 }
980 }
981 });
982 self.block_bits = bits;
983 self.block_dim = bd;
984 self.block_m = m;
985 self.blocks_per_vertex = per_vertex;
986 self.blocks = blocks;
987 self.block_rot = Some(rot);
988 self.block_kappa = self.measure_block_kappa(g);
989 Ok(self)
990 }
991
992 fn unit_row<'g>(&self, g: &'g Graph, v: usize) -> &'g [f32] {
994 let o = if self.ids.is_empty() {
995 v
996 } else {
997 self.ids[v] as usize
998 };
999 &g.vectors[o * self.dim..(o + 1) * self.dim]
1000 }
1001
1002 fn measure_block_kappa(&self, g: &Graph) -> f32 {
1006 let (nq, nv) = (32.min(self.n), 64.min(self.n));
1007 let mut rng = SplitMix(BLOCK_SEED ^ 1);
1008 let mut s = self.searcher();
1009 let (mut num, mut den) = (0f64, 0f64);
1010 let (mut diag_sum2, mut diag_n) = (0f64, 0f64);
1011 let mut scan = [0u32; BLOCK];
1012 for _ in 0..nq {
1013 let qi = (rng.next_u64() % self.n as u64) as usize;
1014 let q = self.unit_row(g, qi).to_vec();
1015 s.scratch.qf[..self.dim].copy_from_slice(&q);
1016 let sq = self.encode_query(
1017 &s.scratch.qf,
1018 &mut s.scratch.qc,
1019 &mut s.scratch.qtmp,
1020 &mut s.scratch.qrot,
1021 &mut s.scratch.qs,
1022 );
1023 if sq == 0.0 {
1024 continue;
1025 }
1026 self.encode_block_query(&s.scratch.qf, sq, &mut s.scratch.bqrot, &mut s.scratch.bq);
1027 let centred = |x: &[f32]| -> f32 {
1028 q.iter()
1029 .zip(x)
1030 .zip(&self.mean)
1031 .map(|((a, b), m)| a * (b - m))
1032 .sum()
1033 };
1034 let mut list = Vec::with_capacity(self.degree);
1035 for _ in 0..nv {
1036 let v = (rng.next_u64() % self.n as u64) as usize;
1037 let xv = self.unit_row(g, v);
1038 let ip_v = centred(xv);
1039 let d2: f32 = q.iter().zip(xv).map(|(a, b)| (a - b) * (a - b)).sum();
1040 list.clear();
1041 self.for_each_neighbour(v, |u| list.push(u));
1042 for b in 0..self.blocks_per_vertex {
1043 let (codes, scales, offsets) = self.block(v, b);
1044 scan32(codes, &s.scratch.bq.lut, self.block_m, &mut scan);
1045 for (i, &sum) in scan.iter().enumerate() {
1046 let Some(&u) = list.get(b * BLOCK + i) else {
1047 break;
1048 };
1049 let sc = le_f32(scales, i);
1050 let est = ip_v
1051 + sc * (s.scratch.bq.delta * sum as f32 + s.scratch.bq.min_sum)
1052 + le_f32(offsets, i);
1053 let exact = centred(self.unit_row(g, u as usize));
1054 let err = f64::from(est - exact);
1055 num += err * err;
1056 den += f64::from(sc * sc * d2);
1057 diag_sum2 += f64::from(exact - ip_v).powi(2);
1058 diag_n += 1.0;
1059 }
1060 }
1061 }
1062 }
1063 if std::env::var_os("HMS_BLOCK_DIAG").is_some() && diag_n > 0.0 {
1064 eprintln!(
1065 "block diag: bits {} kappa {:.4} rms_err {:.5} rms_residual_ip {:.5} pairs {}",
1066 self.block_bits,
1067 num / den.max(1e-30),
1068 (num / diag_n).sqrt(),
1069 (diag_sum2 / diag_n).sqrt(),
1070 diag_n
1071 );
1072 }
1073 if den > 0.0 {
1074 (num / den) as f32
1075 } else {
1076 0.0
1077 }
1078 }
1079
1080 pub fn has_blocks(&self) -> bool {
1082 self.block_bits > 0
1083 }
1084
1085 pub fn block_bytes(&self) -> usize {
1087 self.blocks.len
1088 }
1089
1090 #[inline]
1093 fn block(&self, v: usize, b: usize) -> (&[u8], &[u8], &[u8]) {
1094 let record = block_record_bytes(self.block_m);
1095 let at = (v * self.blocks_per_vertex + b) * record;
1096 let bytes = self.blocks.bytes();
1097 let rec =
1099 unsafe { std::slice::from_raw_parts(bytes.as_ptr().add(at).cast::<u8>(), record) };
1100 let (codes, rest) = rec.split_at(16 * self.block_m);
1101 let (scales, offsets) = rest.split_at(4 * BLOCK);
1102 (codes, scales, offsets)
1103 }
1104
1105 #[inline]
1106 fn prefetch_blocks(&self, v: usize) {
1107 let record = block_record_bytes(self.block_m) * self.blocks_per_vertex;
1108 let at = v * record;
1109 prefetch_bytes(&self.blocks.bytes()[at..at + record]);
1110 }
1111
1112 fn encode_block_query(&self, qf: &[f32], sq: f32, qrot: &mut [f32], bq: &mut BlockQuery) {
1114 let rot = self.block_rot.as_ref().expect("blocks present");
1115 rot.apply(&qf[..self.dim], qrot);
1116 block_table(qrot, self.block_bits, self.block_m, &mut bq.table);
1117 bq.quantize(self.block_m);
1118 bq.inv_sq = if sq > 0.0 { 1.0 / sq } else { 1.0 };
1119 }
1120
1121 fn measure_screen_sigma(&self, g: &Graph) -> f32 {
1124 let (nq, nv) = (128.min(self.n), 1024.min(self.n));
1125 let mut rng = SplitMix(SCREEN_SEED ^ 1);
1126 let mut qc = vec![0i8; self.padded];
1127 let mut qtmp = vec![0i8; self.padded];
1128 let mut qf = vec![0f32; self.padded];
1129 let mut qrot = vec![0f32; self.screen_words * 64];
1130 let mut qs = QueryCode::new(self.screen_words);
1131 let (mut sum, mut sum2, mut count) = (0f64, 0f64, 0usize);
1132 for _ in 0..nq {
1133 let q = (rng.next_u64() % self.n as u64) as usize;
1134 qf[..self.dim].copy_from_slice(&g.vectors[q * self.dim..(q + 1) * self.dim]);
1135 let sq = self.encode_query(&qf, &mut qc, &mut qtmp, &mut qrot, &mut qs);
1136 if sq == 0.0 {
1137 continue;
1138 }
1139 for _ in 0..nv {
1140 let v = (rng.next_u64() % self.n as u64) as u32;
1141 let d = f64::from(self.screen_est(&qs, v) - self.est(&qc, v, 1.0));
1142 sum += d;
1143 sum2 += d * d;
1144 count += 1;
1145 }
1146 }
1147 if count == 0 {
1148 return 0.0;
1149 }
1150 let mean = sum / count as f64;
1151 ((sum2 / count as f64) - mean * mean).max(0.0).sqrt() as f32
1152 }
1153
1154 fn encode_query(
1157 &self,
1158 qf: &[f32],
1159 qc: &mut [i8],
1160 qtmp: &mut [i8],
1161 qrot: &mut [f32],
1162 qs: &mut QueryCode,
1163 ) -> f32 {
1164 let sq = if self.bits == 8 {
1165 quantize(qf, qc)
1166 } else {
1167 let s = quantize(qf, qtmp);
1168 interleave_for_i4(qtmp, qc);
1169 s
1170 };
1171 if let Some(rot) = &self.screen_rot {
1172 rot.apply(&qf[..self.dim], qrot);
1173 if sq > 0.0 {
1174 qrot.iter_mut().for_each(|v| *v /= sq);
1177 }
1178 qs.encode(qrot);
1179 }
1180 sq
1181 }
1182
1183 pub fn has_screen(&self) -> bool {
1185 !self.screen.is_empty()
1186 }
1187
1188 pub fn screen_bytes(&self) -> usize {
1190 self.screen.len() * 8
1191 }
1192
1193 #[inline]
1196 fn screen_est(&self, qs: &QueryCode, v: u32) -> f32 {
1197 let w = self.screen_words;
1198 let row = &self.screen[v as usize * (w + 1)..(v as usize + 1) * (w + 1)];
1199 let raw = raw_one(&row[..w], &qs.planes, w) as f32;
1200 let meta = row[w];
1201 let f = f32::from_bits(meta as u32);
1202 let pop = (meta >> 32) as f32;
1203 -(f * (2.0 * (qs.lo * pop + qs.delta * raw) - qs.sum))
1204 }
1205
1206 #[inline]
1207 fn prefetch_screen(&self, v: u32) {
1208 let w = self.screen_words;
1209 prefetch(&self.screen[v as usize * (w + 1)..(v as usize + 1) * (w + 1)]);
1210 }
1211
1212 pub fn len(&self) -> usize {
1213 self.n
1214 }
1215
1216 pub fn is_empty(&self) -> bool {
1217 self.n == 0
1218 }
1219
1220 pub fn has_residual(&self) -> bool {
1221 self.res_bits > 0
1222 }
1223
1224 pub fn index_bytes(&self) -> usize {
1226 self.adj.len() * 4
1227 + self.adj3.len()
1228 + self.adjv.len()
1229 + self.adjv_off.len() * 4
1230 + self.n * self.stride
1231 + self.scales.len() * 4
1232 + self.residual_bytes()
1233 + self.mean.len() * 4
1234 + self.half_norms.len() * 4
1235 + self.ids.len() * 4
1236 + self.screen_bytes()
1237 + self.block_bytes()
1238 + upper_bytes(&self.upper_ids, &self.layers)
1239 }
1240
1241 pub fn residual_bytes(&self) -> usize {
1243 self.residual.len() + self.res_scales.len() * 4
1244 }
1245
1246 #[inline]
1247 fn code(&self, v: u32) -> &[i8] {
1248 let at = v as usize * self.stride;
1249 &self.codes.bytes()[at..at + self.cbytes]
1250 }
1251
1252 #[inline]
1254 fn for_each_neighbour(&self, v: usize, mut f: impl FnMut(u32)) {
1255 let r = self.degree;
1256 if !self.adjv_off.is_empty() {
1257 return self.for_each_delta_neighbour(v, f);
1258 }
1259 if self.adj3.is_empty() {
1260 for &u in &self.adj[v * r..(v + 1) * r] {
1261 if u == NONE {
1262 break;
1263 }
1264 f(u);
1265 }
1266 } else {
1267 for b in self.adj3[3 * v * r..3 * (v + 1) * r].as_chunks::<3>().0 {
1268 let u = u32::from_le_bytes([b[0], b[1], b[2], 0]);
1269 if u == NONE3 {
1270 break;
1271 }
1272 f(u);
1273 }
1274 }
1275 }
1276
1277 #[cold]
1279 #[inline(never)]
1280 fn for_each_delta_neighbour(&self, v: usize, mut f: impl FnMut(u32)) {
1281 let span = &self.adjv[self.adjv_off[v] as usize..self.adjv_off[v + 1] as usize];
1282 let mut at = 0;
1283 let mut prev = v as u32;
1284 let mut first = true;
1285 while at < span.len() {
1286 let (delta, used) = get_varint(&span[at..]);
1287 at += used;
1288 let u = if first {
1289 (i64::from(prev) + unzigzag(delta)) as u32
1290 } else {
1291 prev + delta as u32 + 1
1292 };
1293 first = false;
1294 prev = u;
1295 f(u);
1296 }
1297 }
1298
1299 #[inline]
1300 fn prefetch_neighbours(&self, v: usize) {
1301 let r = self.degree;
1302 if !self.adjv_off.is_empty() {
1303 prefetch_u8(&self.adjv[self.adjv_off[v] as usize..self.adjv_off[v + 1] as usize]);
1304 } else if self.adj3.is_empty() {
1305 prefetch_ids(&self.adj[v * r..(v + 1) * r]);
1306 } else {
1307 prefetch_u8(&self.adj3[3 * v * r..3 * (v + 1) * r]);
1308 }
1309 }
1310
1311 #[inline]
1313 fn prefetch_scale(&self, v: u32) {
1314 prefetch_f32(&self.scales[v as usize]);
1315 }
1316
1317 #[inline]
1319 fn prefetch_rerank_rows(&self, v: u32) {
1320 prefetch_bytes(self.code(v));
1321 self.prefetch_scale(v);
1322 if self.has_residual() {
1323 let v = v as usize;
1324 prefetch_bytes(&self.residual[v * self.res_stride..(v + 1) * self.res_stride]);
1325 prefetch_f32(&self.res_scales[v]);
1326 }
1327 }
1328
1329 #[inline]
1330 fn original(&self, v: u32) -> u32 {
1331 if self.ids.is_empty() {
1332 v
1333 } else {
1334 self.ids[v as usize]
1335 }
1336 }
1337
1338 #[inline]
1342 fn est(&self, qc: &[i8], v: u32, w: f32) -> f32 {
1343 let dot = if self.bits == 8 {
1344 dot_i8_inline(qc, self.code(v))
1345 } else {
1346 dot_i4_inline(qc, self.code(v))
1347 };
1348 self.order(v, self.scales[v as usize] * dot as f32, w)
1349 }
1350
1351 #[inline]
1354 fn order(&self, v: u32, ip: f32, w: f32) -> f32 {
1355 if self.half_norms.is_empty() {
1356 -ip
1357 } else {
1358 self.half_norms[v as usize] * w - ip
1359 }
1360 }
1361
1362 #[inline]
1365 fn est4(&self, qc: &[i8], v: [u32; 4], w: f32) -> [f32; 4] {
1366 let dots = if self.bits == 8 {
1367 v.map(|u| dot_i8_inline(qc, self.code(u)))
1368 } else {
1369 dot_i4_inline_x4(qc, v.map(|u| self.code(u)))
1370 };
1371 std::array::from_fn(|i| self.order(v[i], self.scales[v[i] as usize] * dots[i] as f32, w))
1372 }
1373
1374 fn fine(&self, qf: &[f32], v: u32) -> f32 {
1377 let primary = if self.bits == 8 {
1378 dot_f32_i8(qf, self.code(v))
1379 } else {
1380 dot_f32_i4(qf, self.code(v))
1381 };
1382 let mut ip = self.scales[v as usize] * primary;
1383 if self.has_residual() {
1384 let v = v as usize;
1385 let r = &self.residual[v * self.res_stride..(v + 1) * self.res_stride];
1386 let d = if self.res_bits == 8 {
1387 dot_f32_i8(qf, r)
1388 } else {
1389 dot_f32_i4(qf, r)
1390 };
1391 ip += self.res_scales[v] * d;
1392 }
1393 self.order(v, ip, 1.0)
1394 }
1395
1396 fn descend(&self, qc: &[i8], w: f32) -> (u32, usize) {
1399 if self.layers.is_empty() {
1400 return (self.entry, 0);
1401 }
1402 let mut p = 0u32;
1403 let mut best = self.est(qc, self.upper_ids[0], w);
1404 let mut evals = 1;
1405 for layer in self.layers.iter().rev() {
1406 loop {
1407 let mut moved = false;
1408 for &u in &layer[p as usize] {
1409 let d = self.est(qc, self.upper_ids[u as usize], w);
1410 evals += 1;
1411 if d < best {
1412 best = d;
1413 p = u;
1414 moved = true;
1415 }
1416 }
1417 if !moved {
1418 break;
1419 }
1420 }
1421 }
1422 (self.upper_ids[p as usize], evals)
1423 }
1424
1425 pub fn searcher(&self) -> VSearcher<'_> {
1427 self.searcher_with(SearchKnobs::from_env())
1428 }
1429
1430 pub fn searcher_with(&self, knobs: SearchKnobs) -> VSearcher<'_> {
1432 VSearcher {
1433 index: self,
1434 scratch: SearchScratch {
1435 #[cfg(not(target_arch = "wasm32"))]
1436 dim: self.dim,
1437 knobs,
1438 visited: Marks {
1439 marks: vec![0; self.n],
1440 epoch: 0,
1441 traces: vec![Trace::default(); if self.has_blocks() { self.n } else { 0 }],
1442 },
1443 qf: vec![0.0; self.padded],
1444 qc: vec![0; self.padded],
1445 qtmp: vec![0; self.padded],
1446 qrot: vec![0.0; self.screen_words * 64],
1447 qs: QueryCode::new(self.screen_words.max(1)),
1448 bqrot: vec![0.0; self.block_dim],
1449 bq: BlockQuery::new(self.block_m),
1450 scan: [0; BLOCK],
1451 fresh: Vec::with_capacity(self.degree),
1452 survivors: Vec::with_capacity(self.degree),
1453 screened: 0,
1454 pool: Pool {
1455 keys: Vec::new(),
1456 expanded: Vec::new(),
1457 cap: 1,
1458 cursor: 0,
1459 },
1460 expanded: BinaryHeap::new(),
1461 keys: Vec::new(),
1462 results: Vec::new(),
1463 },
1464 }
1465 }
1466
1467 #[cfg(not(target_arch = "wasm32"))]
1468 pub(super) fn update_encoding(&self) -> super::update::Encoding {
1469 super::update::Encoding {
1470 bits: self.bits,
1471 residual_bits: self.res_bits,
1472 align: self.stride != self.cbytes,
1473 reorder: !self.ids.is_empty(),
1474 id_bytes: if !self.adjv_off.is_empty() {
1475 0
1476 } else if !self.adj3.is_empty() {
1477 3
1478 } else {
1479 4
1480 },
1481 screen: self.has_screen(),
1482 block_bits: self.block_bits,
1483 }
1484 }
1485
1486 #[cfg(not(target_arch = "wasm32"))]
1487 pub(super) fn search_reusing(
1488 &self,
1489 cache: &mut Option<SearchScratch>,
1490 query: &[f32],
1491 k: usize,
1492 params: VSearchParams,
1493 out: &mut Vec<u32>,
1494 ) -> Result<usize, HmsError> {
1495 let compatible = cache.as_ref().is_some_and(|s| {
1496 s.dim == self.dim
1497 && s.visited.marks.len() == self.n
1498 && s.qf.len() == self.padded
1499 && s.qrot.len() == self.screen_words * 64
1500 && s.bqrot.len() == self.block_dim
1501 && s.bq.table.len() == 16 * self.block_m
1502 && s.visited.traces.len() == if self.has_blocks() { self.n } else { 0 }
1503 });
1504 if !compatible {
1505 *cache = Some(self.searcher().scratch);
1506 }
1507 cache
1508 .as_mut()
1509 .expect("initialized search scratch")
1510 .search_validated(self, query, k, params, out)
1511 }
1512}
1513
1514struct Marks {
1517 marks: Vec<u8>,
1518 epoch: u8,
1519 traces: Vec<Trace>,
1521}
1522
1523#[derive(Clone, Copy, Default)]
1527struct Trace {
1528 epoch: u8,
1529 expanded: bool,
1530 w: f32,
1531 wsum: f32,
1532 key: u64,
1534}
1535
1536impl Marks {
1537 fn next(&mut self) {
1538 self.epoch = self.epoch.wrapping_add(1);
1539 if self.epoch == 0 {
1540 self.marks.fill(0);
1541 self.traces.fill(Trace::default());
1542 self.epoch = 1;
1543 }
1544 }
1545
1546 #[inline]
1548 fn trace(&mut self, id: u32) -> (bool, &mut Trace) {
1549 let t = &mut self.traces[id as usize];
1550 let fresh = t.epoch != self.epoch;
1551 if fresh {
1552 *t = Trace {
1553 epoch: self.epoch,
1554 key: u64::MAX,
1555 ..Trace::default()
1556 };
1557 }
1558 (fresh, t)
1559 }
1560
1561 #[inline]
1563 fn seen(&self, id: u32) -> bool {
1564 self.marks[id as usize] == self.epoch
1565 }
1566
1567 #[inline]
1569 fn insert(&mut self, id: u32) -> bool {
1570 let m = &mut self.marks[id as usize];
1571 let fresh = *m != self.epoch;
1572 *m = self.epoch;
1573 fresh
1574 }
1575}
1576
1577struct Pool {
1585 keys: Vec<u64>,
1586 expanded: Vec<bool>,
1587 cap: usize,
1588 cursor: usize,
1589}
1590
1591impl Pool {
1592 fn reset(&mut self, cap: usize) {
1593 self.keys.clear();
1594 self.expanded.clear();
1595 self.cap = cap.max(1);
1596 self.cursor = 0;
1597 }
1598
1599 fn is_full(&self) -> bool {
1600 self.keys.len() >= self.cap
1601 }
1602
1603 fn worst(&self) -> Option<u64> {
1605 self.is_full().then(|| self.keys[self.keys.len() - 1])
1606 }
1607
1608 #[inline]
1611 fn insert(&mut self, key: u64) -> Option<(usize, Option<u64>)> {
1612 let n = self.keys.len();
1613 if n >= self.cap && key >= self.keys[n - 1] {
1614 return None;
1615 }
1616 let pos = self.keys.partition_point(|&t| t < key);
1617 let mut evicted = None;
1618 if n >= self.cap {
1619 evicted = self.keys.pop();
1620 self.expanded.pop();
1621 }
1622 self.keys.insert(pos, key);
1623 self.expanded.insert(pos, false);
1624 if pos < self.cursor {
1625 self.cursor = pos;
1626 }
1627 Some((pos, evicted))
1628 }
1629
1630 fn remove(&mut self, key: u64) {
1632 let pos = self.keys.partition_point(|&t| t < key);
1633 if pos < self.keys.len() && self.keys[pos] == key {
1634 self.keys.remove(pos);
1635 self.expanded.remove(pos);
1636 if pos < self.cursor {
1637 self.cursor -= 1;
1638 }
1639 }
1640 }
1641
1642 #[inline]
1644 fn next(&mut self) -> Option<u64> {
1645 while self.cursor < self.keys.len() {
1646 if !self.expanded[self.cursor] {
1647 self.expanded[self.cursor] = true;
1648 return Some(self.keys[self.cursor]);
1649 }
1650 self.cursor += 1;
1651 }
1652 None
1653 }
1654
1655 #[inline]
1657 fn peek_next(&self) -> Option<u64> {
1658 self.peek_unexpanded(0)
1659 }
1660
1661 #[inline]
1663 fn peek_unexpanded(&self, n: usize) -> Option<u64> {
1664 (self.cursor..self.keys.len())
1665 .filter(|&i| !self.expanded[i])
1666 .nth(n)
1667 .map(|i| self.keys[i])
1668 }
1669}
1670
1671pub struct VSearcher<'a> {
1673 index: &'a VGraph,
1674 scratch: SearchScratch,
1675}
1676
1677pub(super) struct SearchScratch {
1678 #[cfg(not(target_arch = "wasm32"))]
1679 dim: usize,
1680 visited: Marks,
1681 qf: Vec<f32>,
1682 qc: Vec<i8>,
1684 qtmp: Vec<i8>,
1685 qrot: Vec<f32>,
1687 qs: QueryCode,
1688 bqrot: Vec<f32>,
1690 bq: BlockQuery,
1691 scan: [u32; BLOCK],
1692 fresh: Vec<u32>,
1693 survivors: Vec<u32>,
1695 screened: usize,
1697 pool: Pool,
1699 expanded: BinaryHeap<u64>,
1702 keys: Vec<u64>,
1703 results: Vec<(f32, u32)>,
1704 knobs: SearchKnobs,
1705}
1706
1707impl VSearcher<'_> {
1708 pub fn last_screened(&self) -> usize {
1710 self.scratch.screened
1711 }
1712
1713 pub fn knobs(&self) -> SearchKnobs {
1715 self.scratch.knobs
1716 }
1717
1718 #[inline]
1720 pub fn search(
1721 &mut self,
1722 query: &[f32],
1723 k: usize,
1724 params: VSearchParams,
1725 out: &mut Vec<u32>,
1726 ) -> Result<usize, HmsError> {
1727 self.scratch.search(self.index, query, k, params, out)
1728 }
1729}
1730
1731impl SearchScratch {
1732 #[inline]
1737 fn score_fresh(
1738 &mut self,
1739 g: &VGraph,
1740 k: usize,
1741 patience: usize,
1742 stale: &mut usize,
1743 w: f32,
1744 ) -> usize {
1745 let qc = &self.qc[..];
1746 let n = self.fresh.len();
1747 let mut i = 0;
1748 if self.knobs.batch_est {
1749 while i + 4 <= n {
1750 let ids = [
1751 self.fresh[i],
1752 self.fresh[i + 1],
1753 self.fresh[i + 2],
1754 self.fresh[i + 3],
1755 ];
1756 for (e, u) in g.est4(qc, ids, w).into_iter().zip(ids) {
1757 if let Some((pos, _)) = self.pool.insert(key(e, u)) {
1758 if patience > 0 && pos < k {
1759 *stale = 0;
1760 }
1761 }
1762 }
1763 i += 4;
1764 }
1765 }
1766 for &u in &self.fresh[i..] {
1767 let kk = key(g.est(qc, u, w), u);
1768 if let Some((pos, _)) = self.pool.insert(kk) {
1769 if patience > 0 && pos < k {
1770 *stale = 0;
1771 }
1772 }
1773 }
1774 n
1775 }
1776
1777 fn search(
1783 &mut self,
1784 g: &VGraph,
1785 query: &[f32],
1786 k: usize,
1787 params: VSearchParams,
1788 out: &mut Vec<u32>,
1789 ) -> Result<usize, HmsError> {
1790 if query.len() != g.dim {
1791 return Err(invalid(format!(
1792 "query has {} dimensions, the index has {}",
1793 query.len(),
1794 g.dim
1795 )));
1796 }
1797 if let Some(at) = query.iter().position(|v| !v.is_finite()) {
1798 return Err(invalid(format!(
1799 "non-finite query value at coordinate {at}"
1800 )));
1801 }
1802 if g.half_norms.is_empty() && query.iter().all(|&v| v == 0.0) {
1803 return Err(invalid("zero query: cosine similarity is undefined".into()));
1804 }
1805 self.search_validated(g, query, k, params, out)
1806 }
1807
1808 fn search_validated(
1810 &mut self,
1811 g: &VGraph,
1812 query: &[f32],
1813 k: usize,
1814 params: VSearchParams,
1815 out: &mut Vec<u32>,
1816 ) -> Result<usize, HmsError> {
1817 let ef = params.ef.max(k).max(1);
1818 self.qf[..g.dim].copy_from_slice(query);
1821 let sq = g.encode_query(
1822 &self.qf,
1823 &mut self.qc,
1824 &mut self.qtmp,
1825 &mut self.qrot,
1826 &mut self.qs,
1827 );
1828 let w = if sq > 0.0 { 1.0 / sq } else { 1.0 };
1831 let qc = &self.qc[..];
1832 let blocks = g.has_blocks();
1833 if blocks {
1834 g.encode_block_query(&self.qf, sq, &mut self.bqrot, &mut self.bq);
1835 }
1836 let knobs = self.knobs;
1837 let block_screen = blocks && knobs.block_screen;
1838 let screen = g.has_screen() && params.screen && !blocks;
1839 let margin = params.screen_sigmas * g.screen_sigma;
1840 self.screened = 0;
1841
1842 self.visited.next();
1843 let (entry, mut evals) = g.descend(qc, w);
1844 self.visited.insert(entry);
1845 let fuse = blocks && params.fuse && !block_screen;
1846 if fuse {
1847 self.visited.trace(entry);
1848 }
1849 let (qn2, qm) = if blocks {
1852 let q = &self.qf[..g.dim];
1853 (
1854 q.iter().map(|a| a * a).sum::<f32>(),
1855 q.iter().zip(&g.mean).map(|(a, m)| a * m).sum::<f32>(),
1856 )
1857 } else {
1858 (0.0, 0.0)
1859 };
1860 self.pool.reset(ef);
1861 self.expanded.clear();
1862 let take = params.rerank.max(k).min(g.n);
1863 let k0 = key(g.est(qc, entry, w), entry);
1864 self.pool.insert(k0);
1865 if fuse {
1866 self.visited.trace(entry).1.key = k0;
1867 }
1868 evals += 1;
1869 let patience = params.patience;
1874 let frontier = if knobs.frontier.is_finite() {
1875 knobs.frontier.clamp(0.0, 1.0)
1876 } else {
1877 1.0
1878 };
1879 let mut stale = 0usize;
1880 while let Some(c) = self.pool.next() {
1881 if patience > 0 && stale >= patience {
1882 break;
1883 }
1884 if frontier < 1.0 && k > 0 && self.pool.is_full() {
1885 let len = self.pool.keys.len();
1889 let kth = key_dist(self.pool.keys[k.min(len) - 1]);
1890 let worst = key_dist(self.pool.keys[len - 1]);
1891 if key_dist(c) > kth + frontier * (worst - kth) {
1892 break;
1893 }
1894 }
1895 if fuse {
1896 let (_, t) = self.visited.trace(key_id(c));
1898 if t.expanded {
1899 continue;
1900 }
1901 t.expanded = true;
1902 }
1903 stale += 1;
1904 if let Some(next) = self.pool.peek_next() {
1905 g.prefetch_neighbours(key_id(next) as usize);
1906 if blocks {
1907 g.prefetch_blocks(key_id(next) as usize);
1908 if !block_screen {
1909 prefetch_bytes(g.code(key_id(next)));
1910 }
1911 }
1912 }
1913 if knobs.prefetch >= 2 {
1914 if let Some(second) = self.pool.peek_unexpanded(1) {
1915 g.prefetch_neighbours(key_id(second) as usize);
1916 if blocks {
1917 g.prefetch_blocks(key_id(second) as usize);
1918 }
1919 }
1920 }
1921 if block_screen {
1922 let v = key_id(c) as usize;
1928 let ip_v = -key_dist(c) * sq;
1929 let d2_v = (qn2 + 1.0 - 2.0 * (ip_v + qm)).max(1e-6);
1930 let (delta, min_sum, inv_sq) = (self.bq.delta, self.bq.min_sum, self.bq.inv_sq);
1931 let worst = self.pool.worst().map(key_dist);
1932 let sigmas = params.screen_sigmas;
1933 self.fresh.clear();
1934 let list = &mut self.fresh;
1935 g.for_each_neighbour(v, |u| list.push(u));
1936 self.survivors.clear();
1937 'screen: for b in 0..g.blocks_per_vertex {
1938 let (codes, scales, offsets) = g.block(v, b);
1939 scan32(codes, &self.bq.lut, g.block_m, &mut self.scan);
1940 for i in 0..BLOCK {
1941 let Some(&u) = self.fresh.get(b * BLOCK + i) else {
1942 break 'screen;
1943 };
1944 if !self.visited.insert(u) {
1945 continue;
1946 }
1947 let s = le_f32(scales, i);
1948 let ip =
1949 ip_v + s * (delta * self.scan[i] as f32 + min_sum) + le_f32(offsets, i);
1950 if let Some(w) = worst {
1951 let sigma = (g.block_kappa * s * s * d2_v).sqrt();
1952 if -(ip + sigmas * sigma) * inv_sq >= w {
1953 self.screened += 1;
1954 continue;
1955 }
1956 }
1957 prefetch_bytes(g.code(u));
1958 if knobs.prefetch >= 1 {
1959 g.prefetch_scale(u);
1960 }
1961 self.survivors.push(u);
1962 }
1963 }
1964 std::mem::swap(&mut self.fresh, &mut self.survivors);
1965 evals += self.score_fresh(g, k, patience, &mut stale, w);
1966 continue;
1967 }
1968 if blocks {
1969 let v = key_id(c) as usize;
1975 let est_v = g.est(&self.qc, v as u32, w);
1976 let ek = key(est_v, v as u32);
1977 evals += 1;
1978 let ip_v = -est_v * sq;
1980 let d2_v = (qn2 + 1.0 - 2.0 * (ip_v + qm)).max(1e-6);
1981 if self.expanded.len() < take {
1982 self.expanded.push(ek);
1983 } else if let Some(mut worst) = self.expanded.peek_mut() {
1984 if ek < *worst {
1985 *worst = ek;
1986 }
1987 }
1988 let (delta, min_sum, inv_sq) = (self.bq.delta, self.bq.min_sum, self.bq.inv_sq);
1989 self.fresh.clear();
1990 let list = &mut self.fresh;
1991 g.for_each_neighbour(v, |u| list.push(u));
1992 'blocks: for b in 0..g.blocks_per_vertex {
1993 let (codes, scales, offsets) = g.block(v, b);
1994 scan32(codes, &self.bq.lut, g.block_m, &mut self.scan);
1995 for i in 0..BLOCK {
1996 let Some(&u) = self.fresh.get(b * BLOCK + i) else {
1997 break 'blocks;
1998 };
1999 let s = le_f32(scales, i);
2000 let ip =
2001 ip_v + s * (delta * self.scan[i] as f32 + min_sum) + le_f32(offsets, i);
2002 if !fuse {
2003 if !self.visited.insert(u) {
2006 continue;
2007 }
2008 evals += 1;
2009 if let Some((pos, _)) = self.pool.insert(key(-ip * inv_sq, u)) {
2010 if patience > 0 && pos < k {
2011 stale = 0;
2012 }
2013 }
2014 continue;
2015 }
2016 let w = 1.0 / (g.block_kappa * s * s * d2_v + 1e-12);
2018 let (fresh, t) = self.visited.trace(u);
2019 let ip = if fresh {
2020 t.w = w;
2021 t.wsum = w * ip;
2022 ip
2023 } else {
2024 if t.expanded {
2025 continue;
2026 }
2027 t.w += w;
2028 t.wsum += w * ip;
2029 t.wsum / t.w
2030 };
2031 let kk = key(-ip * inv_sq, u);
2032 let old = t.key;
2033 evals += 1;
2034 if !fresh && kk >= old {
2035 continue;
2036 }
2037 if old != u64::MAX {
2038 self.pool.remove(old);
2039 }
2040 let Some((pos, evicted)) = self.pool.insert(kk) else {
2041 if old != u64::MAX {
2042 self.visited.trace(u).1.key = u64::MAX;
2043 }
2044 continue;
2045 };
2046 self.visited.trace(u).1.key = kk;
2047 if let Some(e) = evicted {
2048 self.visited.trace(key_id(e)).1.key = u64::MAX;
2049 }
2050 if patience > 0 && pos < k {
2051 stale = 0;
2052 }
2053 }
2054 }
2055 continue;
2056 }
2057 self.fresh.clear();
2058 let (visited, fresh) = (&mut self.visited, &mut self.fresh);
2059 let scale_too = knobs.prefetch >= 1;
2060 let admit = |u: u32, visited: &mut Marks, fresh: &mut Vec<u32>| {
2061 if visited.insert(u) {
2062 if screen {
2063 g.prefetch_screen(u);
2064 } else {
2065 prefetch_bytes(g.code(u));
2066 if scale_too {
2067 g.prefetch_scale(u);
2068 }
2069 }
2070 fresh.push(u);
2071 }
2072 };
2073 if params.neighbour_cap == 0 {
2074 g.for_each_neighbour(key_id(c) as usize, |u| admit(u, visited, fresh));
2075 } else {
2076 let mut seen = 0usize;
2077 g.for_each_neighbour(key_id(c) as usize, |u| {
2078 seen += 1;
2079 if seen <= params.neighbour_cap {
2080 admit(u, visited, fresh);
2081 }
2082 });
2083 }
2084 if screen {
2085 let worst = self.pool.worst().map(key_dist);
2088 self.survivors.clear();
2089 for &u in &self.fresh {
2090 if let Some(w) = worst {
2091 if g.screen_est(&self.qs, u) - margin >= w {
2092 self.screened += 1;
2093 continue;
2094 }
2095 }
2096 prefetch_bytes(g.code(u));
2097 if scale_too {
2098 g.prefetch_scale(u);
2099 }
2100 self.survivors.push(u);
2101 }
2102 std::mem::swap(&mut self.fresh, &mut self.survivors);
2103 }
2104 evals += self.score_fresh(g, k, patience, &mut stale, w);
2105 if knobs.prefetch >= 3 {
2106 if let Some(next) = self.pool.peek_next() {
2109 let visited = &self.visited;
2110 g.for_each_neighbour(key_id(next) as usize, |u| {
2111 if !visited.seen(u) {
2112 if screen {
2113 g.prefetch_screen(u);
2114 } else {
2115 prefetch_bytes(g.code(u));
2116 g.prefetch_scale(u);
2117 }
2118 }
2119 });
2120 }
2121 }
2122 }
2123
2124 self.keys.clear();
2125 if blocks && !block_screen {
2126 self.keys.extend(self.expanded.drain());
2127 self.keys.sort_unstable();
2128 } else {
2129 self.keys.extend_from_slice(&self.pool.keys);
2130 }
2131 self.results.clear();
2132 let take = take.min(self.keys.len());
2133 if params.rerank > 0 {
2134 if knobs.prefetch >= 1 {
2135 for &c in &self.keys[..take] {
2136 g.prefetch_rerank_rows(key_id(c));
2137 }
2138 }
2139 self.results.extend(self.keys[..take].iter().map(|&c| {
2141 let id = key_id(c);
2142 (g.fine(&self.qf, id), g.original(id))
2143 }));
2144 evals += take;
2145 self.results
2146 .sort_unstable_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
2147 } else {
2148 self.results.extend(
2149 self.keys[..take]
2150 .iter()
2151 .map(|&c| (0.0, g.original(key_id(c)))),
2152 );
2153 }
2154 out.clear();
2155 out.extend(self.results.iter().take(k).map(|r| r.1));
2156 Ok(evals)
2157 }
2158}
2159
2160const CACHE_MAGIC: &[u8; 8] = b"HMSGRF04";
2164
2165fn data_fingerprint(vectors: &[f32]) -> u64 {
2167 let mut h = super::Fnv::new();
2168 vectors.iter().for_each(|x| h.word(x.to_bits()));
2169 h.finish()
2170}
2171
2172fn put_u32s(w: &mut impl std::io::Write, xs: &[u32]) -> std::io::Result<()> {
2173 w.write_all(&(xs.len() as u64).to_le_bytes())?;
2174 for x in xs {
2175 w.write_all(&x.to_le_bytes())?;
2176 }
2177 Ok(())
2178}
2179
2180fn get_u64(r: &mut impl std::io::Read) -> std::io::Result<u64> {
2181 let mut b = [0u8; 8];
2182 r.read_exact(&mut b)?;
2183 Ok(u64::from_le_bytes(b))
2184}
2185
2186fn get_u32s(r: &mut impl std::io::Read, max: u64) -> std::io::Result<Vec<u32>> {
2187 let len = get_u64(r)?;
2188 if len > max {
2189 return Err(std::io::Error::new(
2190 std::io::ErrorKind::InvalidData,
2191 "graph cache: list longer than the graph",
2192 ));
2193 }
2194 let mut bytes = vec![0u8; len as usize * 4];
2195 r.read_exact(&mut bytes)?;
2196 Ok(bytes
2197 .as_chunks::<4>()
2198 .0
2199 .iter()
2200 .map(|c| u32::from_le_bytes(*c))
2201 .collect())
2202}
2203
2204fn put_lists<'a>(
2205 w: &mut impl std::io::Write,
2206 lists: impl Iterator<Item = &'a [u32]> + Clone,
2207) -> std::io::Result<()> {
2208 let lens: Vec<u32> = lists.clone().map(|l| l.len() as u32).collect();
2209 put_u32s(w, &lens)?;
2210 put_u32s(w, &lists.flatten().copied().collect::<Vec<u32>>())
2211}
2212
2213fn get_lists(r: &mut impl std::io::Read, n: u64) -> std::io::Result<Vec<Vec<u32>>> {
2214 let lens = get_u32s(r, n)?;
2215 let flat = get_u32s(r, lens.iter().map(|&l| u64::from(l)).sum())?;
2216 let mut out = Vec::with_capacity(lens.len());
2217 let mut at = 0;
2218 for l in lens {
2219 out.push(flat[at..at + l as usize].to_vec());
2220 at += l as usize;
2221 }
2222 Ok(out)
2223}
2224
2225impl Graph {
2228 pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
2232 let mut w = std::io::BufWriter::new(std::fs::File::create(path)?);
2233 use std::io::Write;
2234 w.write_all(CACHE_MAGIC)?;
2235 let header = [
2236 self.n as u64,
2237 self.dim as u64,
2238 self.degree as u64,
2239 self.build.build_ef as u64,
2240 u64::from(self.build.alpha.to_bits()),
2241 self.build.seed,
2242 u64::from(self.entry),
2243 data_fingerprint(&self.vectors),
2244 u64::from(self.build.quantized_search),
2245 self.metric.code(),
2246 ];
2247 for x in header {
2248 w.write_all(&x.to_le_bytes())?;
2249 }
2250 put_lists(&mut w, self.adj.iter())?;
2251 put_u32s(&mut w, &self.upper_ids)?;
2252 w.write_all(&(self.layers.len() as u64).to_le_bytes())?;
2253 for l in &self.layers {
2254 put_lists(&mut w, l.iter().map(Vec::as_slice))?;
2255 }
2256 w.flush()
2257 }
2258
2259 pub fn load(
2265 data: &[f32],
2266 dim: usize,
2267 path: &std::path::Path,
2268 params: &BuildParams,
2269 ) -> std::io::Result<Self> {
2270 let bad = |m: String| std::io::Error::new(std::io::ErrorKind::InvalidData, m);
2271 let mut r = std::io::BufReader::new(std::fs::File::open(path)?);
2272 let mut magic = [0u8; 8];
2273 std::io::Read::read_exact(&mut r, &mut magic)?;
2274 if &magic != CACHE_MAGIC {
2275 return Err(bad(
2276 "graph cache: bad magic (an older format or not a cache file); delete it to rebuild"
2277 .into(),
2278 ));
2279 }
2280 let mut header = [0u64; 10];
2281 for x in &mut header {
2282 *x = get_u64(&mut r)?;
2283 }
2284 let [n, d, degree, build_ef, alpha_bits, seed, entry, fingerprint, quantized, metric] =
2285 header;
2286 if dim == 0 || !data.len().is_multiple_of(dim) {
2287 return Err(bad("graph cache: ragged data".into()));
2288 }
2289 let mut vectors = data.to_vec();
2290 let norms = prepare_rows(&mut vectors, dim, params.metric);
2291 let checks: [(&str, u64, u64); 9] = [
2292 ("metric", metric, params.metric.code()),
2293 ("n", n, (data.len() / dim) as u64),
2294 ("dim", d, dim as u64),
2295 ("degree", degree, params.degree as u64),
2296 ("build_ef", build_ef, params.build_ef as u64),
2297 ("alpha", alpha_bits, u64::from(params.alpha.to_bits())),
2298 ("seed", seed, params.seed),
2299 ("data fingerprint", fingerprint, data_fingerprint(&vectors)),
2300 (
2301 "quantized_search",
2302 quantized,
2303 u64::from(params.quantized_search),
2304 ),
2305 ];
2306 if let Some((what, file, want)) = checks.iter().find(|(_, a, b)| a != b) {
2307 let show = |x: u64| {
2308 if *what == "alpha" {
2309 f32::from_bits(x as u32).to_string()
2310 } else {
2311 format!("{x:#x}")
2312 }
2313 };
2314 return Err(bad(format!(
2315 "graph cache: {what} {} in the file, {} requested; delete the file to rebuild",
2316 show(*file),
2317 show(*want)
2318 )));
2319 }
2320 let n = n as usize;
2321 if entry >= n as u64 {
2322 return Err(bad("graph cache: entry out of range".into()));
2323 }
2324 let lens = get_u32s(&mut r, n as u64)?;
2325 let flat = get_u32s(&mut r, lens.iter().map(|&l| u64::from(l)).sum())?;
2326 let adj = Adjacency::from_flat(lens, &flat, degree as usize)
2327 .ok_or_else(|| bad("graph cache: list longer than the degree".into()))?;
2328 drop(flat);
2329 let upper_ids = get_u32s(&mut r, n as u64)?;
2330 let nl = get_u64(&mut r)?;
2331 if nl > 64 {
2332 return Err(bad("graph cache: too many layers".into()));
2333 }
2334 let layers = (0..nl)
2335 .map(|_| get_lists(&mut r, n as u64))
2336 .collect::<std::io::Result<Vec<_>>>()?;
2337 let ok = adj.len() == n
2338 && adj.iter().flatten().all(|&u| (u as usize) < n)
2339 && upper_ids.iter().all(|&u| (u as usize) < n)
2340 && layers.iter().all(|l| {
2341 l.len() <= upper_ids.len() && l.iter().flatten().all(|&u| (u as usize) < l.len())
2342 });
2343 if !ok {
2344 return Err(bad("graph cache: id out of range".into()));
2345 }
2346 Ok(Self {
2347 n,
2348 dim,
2349 degree: degree as usize,
2350 metric: params.metric,
2351 vectors,
2352 norms,
2353 adj,
2354 entry: entry as u32,
2355 upper_ids,
2356 layers,
2357 build: params.clone(),
2358 })
2359 }
2360}
2361
2362#[cfg(test)]
2363mod tests {
2364 use super::super::kernels::SplitMix;
2365 use super::super::tests::{brute_force, clustered, gaussian};
2366 use super::*;
2367
2368 #[test]
2371 fn graph_cache_round_trips_and_rejects_mismatches() {
2372 let d = 16;
2373 let data = clustered(600, d, 4);
2374 let params = BuildParams {
2375 build_ef: 32,
2376 degree: 8,
2377 alpha: 1.1,
2378 seed: 9,
2379 ..BuildParams::default()
2380 };
2381 let graph = Graph::build(&data, d, ¶ms).expect("build");
2382 let path = std::env::temp_dir().join(format!("hms-qgraph-cache-{}", std::process::id()));
2383 graph.save(&path).unwrap();
2384 let back = Graph::load(&data, d, &path, ¶ms).unwrap();
2385 assert_eq!(back.structure_hash(), graph.structure_hash());
2386 assert!(back.adj == graph.adj && back.layers == graph.layers);
2387 assert_eq!(
2388 (back.entry, &back.upper_ids),
2389 (graph.entry, &graph.upper_ids)
2390 );
2391 assert_eq!(back.vectors, graph.vectors);
2392 let rejects = |data: &[f32], p: &BuildParams, what: &str| {
2393 let err = Graph::load(data, d, &path, p)
2394 .err()
2395 .expect("a mismatched cache was accepted")
2396 .to_string();
2397 assert!(err.contains(what), "{what}: {err}");
2398 };
2399 for (what, p) in [
2400 (
2401 "degree",
2402 BuildParams {
2403 degree: 10,
2404 ..params.clone()
2405 },
2406 ),
2407 (
2408 "build_ef",
2409 BuildParams {
2410 build_ef: 33,
2411 ..params.clone()
2412 },
2413 ),
2414 (
2415 "alpha",
2416 BuildParams {
2417 alpha: 1.0,
2418 ..params.clone()
2419 },
2420 ),
2421 (
2422 "seed",
2423 BuildParams {
2424 seed: 10,
2425 ..params.clone()
2426 },
2427 ),
2428 (
2429 "metric",
2430 BuildParams {
2431 metric: Metric::Euclidean,
2432 ..params.clone()
2433 },
2434 ),
2435 ] {
2436 rejects(&data, &p, what);
2437 }
2438 let mut other = data.clone();
2439 other[d * 7 + 3] += 0.5;
2440 rejects(&other, ¶ms, "data fingerprint");
2441 rejects(&data[..d * 599], ¶ms, "n");
2442 std::fs::write(&path, b"HMSGRF01").unwrap();
2443 rejects(&data, ¶ms, "bad magic");
2444 std::fs::remove_file(&path).unwrap();
2445 }
2446
2447 #[test]
2452 fn euclidean_index_finds_euclidean_neighbours() {
2453 let (d, n, k) = (32, 4000, 10);
2454 let spread = |rows: &mut [f32], salt: usize| {
2455 let count = rows.len() / d;
2456 for (i, x) in rows.chunks_exact_mut(d).enumerate() {
2457 let s = 0.1 * 100f32.powf(((i * 7919 + salt) % count) as f32 / count as f32);
2458 x.iter_mut().for_each(|v| *v *= s);
2459 }
2460 };
2461 let mut data = clustered(n + 100, d, 7);
2463 spread(&mut data, 0);
2464 let queries = data.split_off(n * d);
2465 let l2 = |a: &[f32], b: &[f32]| -> f64 {
2466 a.iter()
2467 .zip(b)
2468 .map(|(x, y)| (f64::from(*x) - f64::from(*y)).powi(2))
2469 .sum()
2470 };
2471 let recall = |metric: Metric| -> f64 {
2472 let params = BuildParams {
2473 metric,
2474 degree: 32,
2475 build_ef: 100,
2476 ..BuildParams::default()
2477 };
2478 let g = Graph::build(&data, d, ¶ms).expect("build");
2479 let index = VGraph::from_graph_with(&g, 4, 8, true, true, 3, false).expect("encode");
2480 let mut s = index.searcher_with(SearchKnobs::OFF);
2481 let mut out = Vec::new();
2482 let mut hits = 0;
2483 for q in queries.chunks_exact(d) {
2484 let mut all: Vec<(f64, u32)> = data
2485 .chunks_exact(d)
2486 .enumerate()
2487 .map(|(i, x)| (l2(q, x), i as u32))
2488 .collect();
2489 all.sort_by(|a, b| a.0.total_cmp(&b.0));
2490 let truth: Vec<u32> = all[..k].iter().map(|t| t.1).collect();
2491 let params = VSearchParams {
2492 ef: 64,
2493 rerank: 32,
2494 ..VSearchParams::default()
2495 };
2496 s.search(q, k, params, &mut out).expect("search");
2497 hits += out.iter().filter(|x| truth.contains(x)).count();
2498 }
2499 hits as f64 / (k * 100) as f64
2500 };
2501 let (euclidean, angular) = (recall(Metric::Euclidean), recall(Metric::Angular));
2502 assert!(euclidean >= 0.95, "Euclidean recall {euclidean}");
2503 assert!(
2504 angular < euclidean - 0.3,
2505 "angular recall {angular} against Euclidean {euclidean}"
2506 );
2507 }
2508
2509 #[test]
2512 fn euclidean_index_limits() {
2513 let d = 16;
2514 let mut data = clustered(500, d, 2);
2515 for (i, x) in data.chunks_exact_mut(d).enumerate() {
2516 x.iter_mut().for_each(|v| *v *= 1.0 + i as f32 / 50.0);
2517 }
2518 let params = BuildParams {
2519 metric: Metric::Euclidean,
2520 degree: 16,
2521 build_ef: 64,
2522 ..BuildParams::default()
2523 };
2524 let g = Graph::build(&data, d, ¶ms).expect("build");
2525 assert!(VGraph::from_graph_with(&g, 8, 0, false, false, 4, true).is_err());
2526 assert!(super::super::QGraph::from_graph(&g, ¶ms).is_err());
2527 let blocks = VGraph::from_graph(&g, false)
2528 .expect("encode")
2529 .with_blocks(&g, 4);
2530 assert!(blocks.is_err());
2531 let index = VGraph::from_graph(&g, true).expect("encode");
2532 let mut out = Vec::new();
2533 let params = VSearchParams {
2534 ef: 500,
2535 rerank: 500,
2536 ..VSearchParams::default()
2537 };
2538 index
2539 .searcher_with(SearchKnobs::OFF)
2540 .search(&[0.0; 16], 1, params, &mut out)
2541 .expect("zero query");
2542 let smallest = data
2543 .chunks_exact(d)
2544 .enumerate()
2545 .min_by(|a, b| {
2546 let n2 = |x: &[f32]| x.iter().map(|v| v * v).sum::<f32>();
2547 n2(a.1).total_cmp(&n2(b.1))
2548 })
2549 .map(|m| m.0 as u32);
2550 assert_eq!(out.first().copied(), smallest);
2551 }
2552
2553 #[test]
2556 fn quantization_error_is_bounded() {
2557 let mut rng = SplitMix(12);
2558 let x: Vec<f32> = (0..100).map(|_| gaussian(&mut rng)).collect();
2559 let mut c = vec![0i8; 112];
2560 let s = quantize(&x, &mut c);
2561 let mut r: Vec<f32> = x
2562 .iter()
2563 .zip(&c)
2564 .map(|(a, &q)| a - s * f32::from(q))
2565 .collect();
2566 assert!(r.iter().all(|e| e.abs() <= s / 2.0 + 1e-6));
2567 assert!(c[100..].iter().all(|&q| q == 0));
2568 let mut rc = vec![0i8; 112];
2569 let sr = quantize(&r, &mut rc);
2570 r.iter_mut()
2571 .zip(&rc)
2572 .for_each(|(a, &q)| *a -= sr * f32::from(q));
2573 assert!(r.iter().all(|e| e.abs() <= s / 254.0 + 1e-6));
2574 assert_eq!(quantize(&[0.0; 4], &mut c[..16]), 0.0);
2575 }
2576
2577 fn recall(index: &VGraph, data: &[f32], queries: &[f32], d: usize, p: VSearchParams) -> f64 {
2578 let mut s = index.searcher();
2579 let mut out = Vec::new();
2580 let mut hits = 0;
2581 for q in queries.chunks_exact(d) {
2582 let truth = brute_force(data, d, q, 10);
2583 s.search(q, 10, p, &mut out).expect("search");
2584 assert_eq!(out.len(), 10);
2585 hits += out.iter().filter(|x| truth.contains(x)).count();
2586 }
2587 hits as f64 / (queries.len() / d * 10) as f64
2588 }
2589
2590 #[test]
2591 fn recall_on_clustered_data() {
2592 let d = 24;
2593 let data = clustered(4000, d, 1);
2594 let queries = clustered(100, d, 2);
2595 let graph = Graph::build(&data, d, &BuildParams::default()).expect("build");
2596 let plain = VGraph::from_graph(&graph, false).expect("encode");
2597 let fine = VGraph::from_graph(&graph, true).expect("encode");
2598 assert!(plain.index_bytes() < fine.index_bytes());
2599 let pool = VSearchParams {
2600 ef: 64,
2601 ..VSearchParams::default()
2602 };
2603 let rr = VSearchParams {
2604 ef: 64,
2605 rerank: 32,
2606 ..VSearchParams::default()
2607 };
2608 let r0 = recall(&plain, &data, &queries, d, pool);
2609 let r1 = recall(&fine, &data, &queries, d, rr);
2610 assert!(r0 >= 0.9, "recall {r0}");
2612 assert!(r1 >= r0, "rerank {r1} below pool order {r0}");
2613 let fine4 = VGraph::from_graph_with(&graph, 8, 4, false, false, 4, false).expect("encode");
2614 assert!(plain.index_bytes() < fine4.index_bytes());
2615 assert!(fine4.index_bytes() < fine.index_bytes());
2616 let r2 = recall(&fine4, &data, &queries, d, rr);
2617 assert!(r2 >= r0 - 0.02, "4-bit rerank {r2} below pool order {r0}");
2618 let lvq48 = VGraph::from_graph_with(&graph, 4, 8, true, false, 4, false).expect("encode");
2619 assert!(lvq48.index_bytes() < fine.index_bytes());
2620 let wide = VSearchParams {
2622 ef: 96,
2623 rerank: 64,
2624 ..VSearchParams::default()
2625 };
2626 let r3 = recall(&lvq48, &data, &queries, d, wide);
2627 assert!(
2628 r3 >= r0 - 0.05,
2629 "4-bit traversal with 8-bit residual {r3} vs {r0}"
2630 );
2631 }
2632
2633 #[test]
2637 fn block_codes_keep_recall() {
2638 let d = 24;
2639 let data = clustered(4000, d, 1);
2640 let queries = clustered(100, d, 2);
2641 let graph = Graph::build(&data, d, &BuildParams::default()).expect("build");
2642 let plain = VGraph::from_graph(&graph, true).expect("encode");
2643 let rr = VSearchParams {
2644 ef: 96,
2645 rerank: 64,
2646 ..VSearchParams::default()
2647 };
2648 let r0 = recall(&plain, &data, &queries, d, rr);
2649 for bits in [4u8, 2, 1] {
2650 for (reorder, id_bytes) in [(false, 4u8), (true, 3)] {
2651 let b = VGraph::from_graph_with(&graph, 8, 8, true, reorder, id_bytes, false)
2652 .expect("encode")
2653 .with_blocks(&graph, bits)
2654 .expect("blocks");
2655 assert!(b.has_blocks());
2656 assert!(b.index_bytes() > plain.index_bytes());
2657 let r = recall(&b, &data, &queries, d, rr);
2658 let slack = match bits {
2662 4 => 0.03,
2663 2 => 0.06,
2664 _ => 0.30,
2665 };
2666 assert!(
2667 r >= r0 - slack,
2668 "{bits}-bit blocks (reorder {reorder}, ids {id_bytes}) recall {r} vs {r0}"
2669 );
2670 }
2671 }
2672 let e = match VGraph::from_graph(&graph, true)
2673 .expect("encode")
2674 .with_blocks(&graph, 3)
2675 {
2676 Ok(_) => panic!("3 bits accepted"),
2677 Err(e) => e.to_string(),
2678 };
2679 assert!(e.contains("block bits"), "{e}");
2680 }
2681
2682 #[test]
2685 fn block_estimates_track_exact_inner_products() {
2686 let d = 64;
2687 let data = clustered(3000, d, 5);
2688 let graph = Graph::build(&data, d, &BuildParams::default()).expect("build");
2689 let unit = &graph.vectors;
2691 for (bits, bar) in [(4u8, 0.998), (2, 0.98), (1, 0.90)] {
2692 let g = VGraph::from_graph(&graph, false)
2693 .expect("encode")
2694 .with_blocks(&graph, bits)
2695 .expect("blocks");
2696 let mut s = g.searcher();
2697 let q: Vec<f32> = unit[7 * d..8 * d].to_vec();
2698 s.scratch.qf[..d].copy_from_slice(&q);
2699 let sq = g.encode_query(
2700 &s.scratch.qf,
2701 &mut s.scratch.qc,
2702 &mut s.scratch.qtmp,
2703 &mut s.scratch.qrot,
2704 &mut s.scratch.qs,
2705 );
2706 g.encode_block_query(&s.scratch.qf, sq, &mut s.scratch.bqrot, &mut s.scratch.bq);
2707 let centred =
2708 |u: usize| -> f32 { (0..d).map(|j| q[j] * (unit[u * d + j] - g.mean[j])).sum() };
2709 let (mut xs, mut ys) = (Vec::new(), Vec::new());
2710 for v in 0..64usize {
2711 let ip_v = centred(v);
2713 let mut list = Vec::new();
2714 g.for_each_neighbour(v, |u| list.push(u));
2715 for b in 0..g.blocks_per_vertex {
2716 let (codes, scales, offsets) = g.block(v, b);
2717 scan32(codes, &s.scratch.bq.lut, g.block_m, &mut s.scratch.scan);
2718 for i in 0..BLOCK {
2719 let Some(&u) = list.get(b * BLOCK + i) else {
2720 break;
2721 };
2722 let ip = ip_v
2723 + le_f32(scales, i)
2724 * (s.scratch.bq.delta * s.scratch.scan[i] as f32
2725 + s.scratch.bq.min_sum)
2726 + le_f32(offsets, i);
2727 xs.push(f64::from(ip));
2728 ys.push(f64::from(centred(u as usize)));
2729 }
2730 }
2731 }
2732 let n = xs.len() as f64;
2733 let (mx, my) = (xs.iter().sum::<f64>() / n, ys.iter().sum::<f64>() / n);
2734 let (mut sxy, mut sxx, mut syy) = (0.0, 0.0, 0.0);
2735 for (x, y) in xs.iter().zip(&ys) {
2736 sxy += (x - mx) * (y - my);
2737 sxx += (x - mx) * (x - mx);
2738 syy += (y - my) * (y - my);
2739 }
2740 let r = sxy / (sxx * syy).sqrt();
2741 eprintln!(
2742 "{bits}-bit block estimates: correlation {r:.4} over {} pairs",
2743 xs.len()
2744 );
2745 assert!(
2746 r > bar,
2747 "{bits}-bit block estimates: correlation {r} below {bar}"
2748 );
2749 }
2750 }
2751
2752 #[test]
2756 fn delta_coded_adjacency_round_trips() {
2757 let d = 24;
2758 let data = clustered(4000, d, 1);
2759 let queries = clustered(50, d, 2);
2760 let graph = Graph::build(&data, d, &BuildParams::default()).expect("build");
2761 let wide = VGraph::from_graph_with(&graph, 4, 8, true, true, 4, false).expect("encode");
2762 let delta = VGraph::from_graph_with(&graph, 4, 8, true, true, 0, false).expect("encode");
2763 assert!(
2764 delta.index_bytes() < wide.index_bytes(),
2765 "{} vs {}",
2766 delta.index_bytes(),
2767 wide.index_bytes()
2768 );
2769 for v in 0..wide.n {
2770 let (mut a, mut b) = (Vec::new(), Vec::new());
2771 wide.for_each_neighbour(v, |u| a.push(u));
2772 delta.for_each_neighbour(v, |u| b.push(u));
2773 a.sort_unstable();
2774 assert_eq!(a, b, "vertex {v}");
2775 }
2776 let p = VSearchParams {
2777 ef: 48,
2778 rerank: 16,
2779 ..VSearchParams::default()
2780 };
2781 let (mut s1, mut s2) = (wide.searcher(), delta.searcher());
2782 let (mut o1, mut o2) = (Vec::new(), Vec::new());
2783 for q in queries.chunks_exact(d) {
2784 s1.search(q, 10, p, &mut o1).expect("search");
2785 s2.search(q, 10, p, &mut o2).expect("search");
2786 assert_eq!(o1, o2);
2787 }
2788 let blocks = VGraph::from_graph_with(&graph, 8, 8, true, true, 0, false)
2789 .expect("encode")
2790 .with_blocks(&graph, 2)
2791 .expect("blocks");
2792 assert!(recall(&blocks, &data, &queries, d, p) > 0.8);
2793 assert_eq!(zigzag(-1), 1);
2794 assert_eq!(unzigzag(zigzag(-123_456)), -123_456);
2795 let mut buf = Vec::new();
2796 put_varint(&mut buf, 300);
2797 assert_eq!(get_varint(&buf), (300, 2));
2798 }
2799
2800 #[test]
2803 fn reordered_aligned_index_returns_the_same_ids() {
2804 let d = 24;
2805 let data = clustered(3000, d, 5);
2806 let graph = Graph::build(&data, d, &BuildParams::default()).expect("build");
2807 let a = VGraph::from_graph(&graph, true).expect("encode");
2808 let b = VGraph::from_graph_with(&graph, 8, 8, true, true, 4, false).expect("encode");
2809 assert_eq!(b.index_bytes(), a.index_bytes() + b.n * 4);
2811 let (mut sa, mut sb) = (a.searcher(), b.searcher());
2812 let (mut oa, mut ob) = (Vec::new(), Vec::new());
2813 let c = VGraph::from_graph_with(&graph, 8, 8, true, true, 3, false).expect("encode");
2814 assert_eq!(c.index_bytes(), b.index_bytes() - c.n * c.degree);
2815 let mut sc = c.searcher();
2816 let mut oc = Vec::new();
2817 let mut same = 0;
2818 for q in data.chunks_exact(d).take(200) {
2819 for p in [
2820 VSearchParams {
2821 ef: 32,
2822 ..VSearchParams::default()
2823 },
2824 VSearchParams {
2825 ef: 48,
2826 rerank: 16,
2827 ..VSearchParams::default()
2828 },
2829 ] {
2830 sa.search(q, 10, p, &mut oa).expect("search");
2831 sb.search(q, 10, p, &mut ob).expect("search");
2832 sc.search(q, 10, p, &mut oc).expect("search");
2833 assert_eq!(ob, oc, "3-byte ids changed the search");
2834 same += usize::from(oa == ob);
2835 }
2836 }
2837 assert!(same >= 396, "{same} of 400 searches agree");
2838 }
2839
2840 #[test]
2841 fn build_and_search_are_deterministic() {
2842 let d = 20;
2843 let data = clustered(3000, d, 3);
2844 let params = BuildParams {
2845 seed: 9,
2846 ..BuildParams::default()
2847 };
2848 let a = VGraph::from_graph(&Graph::build(&data, d, ¶ms).expect("build"), true)
2849 .expect("encode");
2850 let b = VGraph::from_graph(&Graph::build(&data, d, ¶ms).expect("build"), true)
2851 .expect("encode");
2852 assert!(
2853 a.codes.bytes() == b.codes.bytes()
2854 && a.residual[..] == b.residual[..]
2855 && a.adj[..] == b.adj[..]
2856 );
2857 assert!(a.scales[..] == b.scales[..] && a.res_scales[..] == b.res_scales[..]);
2858 assert_eq!((a.entry, &a.mean), (b.entry, &b.mean));
2859 let (mut sa, mut sb) = (a.searcher(), b.searcher());
2860 let (mut oa, mut ob) = (Vec::new(), Vec::new());
2861 let p = VSearchParams {
2862 ef: 32,
2863 rerank: 16,
2864 ..VSearchParams::default()
2865 };
2866 for q in data.chunks_exact(d).take(50) {
2867 sa.search(q, 10, p, &mut oa).expect("search");
2868 sb.search(q, 10, p, &mut ob).expect("search");
2869 assert_eq!(oa, ob);
2870 }
2871 }
2872
2873 #[test]
2875 fn invalid_inputs_are_errors_that_name_the_cause() {
2876 let d = 16;
2877 let data = clustered(500, d, 4);
2878 let err = |r: Result<Graph, HmsError>| match r {
2879 Err(e) => e.to_string(),
2880 Ok(_) => panic!("an invalid input was accepted"),
2881 };
2882 assert!(err(Graph::build(&[], d, &BuildParams::default())).contains("empty"));
2883 assert!(err(Graph::build(&data[..d + 1], d, &BuildParams::default())).contains("ragged"));
2884 assert!(err(Graph::build(&data, 0, &BuildParams::default())).contains("ragged"));
2885 let mut bad = data.clone();
2886 bad[3 * d + 2] = f32::NAN;
2887 let e = err(Graph::build(&bad, d, &BuildParams::default()));
2888 assert!(e.contains("non-finite") && e.contains("row 3"), "{e}");
2889 let odd = BuildParams {
2890 degree: 7,
2891 ..BuildParams::default()
2892 };
2893 assert!(err(Graph::build(&data, d, &odd)).contains("even"));
2894 let graph = Graph::build(&data, d, &BuildParams::default()).expect("build");
2895 let e = match VGraph::from_graph_with(&graph, 5, 8, false, false, 4, false) {
2896 Err(e) => e.to_string(),
2897 Ok(_) => panic!("bits 5 was accepted"),
2898 };
2899 assert!(e.contains("bits"), "{e}");
2900 let index = VGraph::from_graph(&graph, true).expect("encode");
2901 let mut s = index.searcher();
2902 let mut out = Vec::new();
2903 let p = VSearchParams::default();
2904 let e = s
2905 .search(&data[..d - 1], 10, p, &mut out)
2906 .expect_err("short query")
2907 .to_string();
2908 assert!(e.contains("dimension"), "{e}");
2909 let mut q = data[..d].to_vec();
2910 q[5] = f32::INFINITY;
2911 let e = s.search(&q, 10, p, &mut out).expect_err("inf").to_string();
2912 assert!(e.contains("non-finite") && e.contains('5'), "{e}");
2913 let e = s
2914 .search(&vec![0.0; d], 10, p, &mut out)
2915 .expect_err("zero")
2916 .to_string();
2917 assert!(e.contains("zero"), "{e}");
2918 let huge = VSearchParams {
2921 ef: usize::MAX,
2922 rerank: usize::MAX,
2923 ..VSearchParams::default()
2924 };
2925 let evals = s.search(&data[..d], 10, huge, &mut out).expect("search");
2926 assert_eq!(out.len(), 10);
2927 assert!(evals <= 2 * index.len() + 10);
2928 s.search(&data[..d], 5000, huge, &mut out).expect("search");
2929 assert!(out.len() <= index.len());
2930 let mut with_zero = data.clone();
2932 with_zero[..d].fill(0.0);
2933 let g0 = Graph::build(&with_zero, d, &BuildParams::default()).expect("build");
2934 let i0 = VGraph::from_graph(&g0, true).expect("encode");
2935 let mut s0 = i0.searcher();
2936 s0.search(&data[d..2 * d], 10, p, &mut out).expect("search");
2937 assert_eq!(out.len(), 10);
2938 assert!(out.iter().all(|&v| (v as usize) < i0.len()));
2939 }
2940
2941 #[test]
2944 fn screen_is_transparent_at_a_wide_margin() {
2945 let d = 24;
2946 let data = clustered(3000, d, 5);
2947 let queries = clustered(100, d, 2);
2948 let graph = Graph::build(&data, d, &BuildParams::default()).expect("build");
2949 let plain = VGraph::from_graph(&graph, true).expect("encode");
2950 let screened =
2951 VGraph::from_graph_with(&graph, 8, 8, false, false, 4, true).expect("encode");
2952 assert!(screened.has_screen() && screened.index_bytes() > plain.index_bytes());
2953 let (mut sa, mut sb) = (plain.searcher(), screened.searcher());
2954 let (mut oa, mut ob) = (Vec::new(), Vec::new());
2955 let wide = VSearchParams {
2956 ef: 64,
2957 rerank: 16,
2958 screen_sigmas: 1e6,
2959 ..VSearchParams::default()
2960 };
2961 for q in queries.chunks_exact(d) {
2962 let ea = sa.search(q, 10, wide, &mut oa).expect("search");
2963 let eb = sb.search(q, 10, wide, &mut ob).expect("search");
2964 assert_eq!((ea, &oa), (eb, &ob));
2965 assert_eq!(sb.last_screened(), 0);
2966 }
2967 let one = VSearchParams {
2968 ef: 64,
2969 rerank: 16,
2970 ..VSearchParams::default()
2971 };
2972 let r0 = recall(&plain, &data, &queries, d, one);
2973 let r1 = recall(&screened, &data, &queries, d, one);
2974 assert!(r1 >= r0 - 0.02, "screened {r1} vs {r0}");
2975 let mut skipped = 0;
2976 for q in queries.chunks_exact(d) {
2977 sb.search(q, 10, one, &mut ob).expect("search");
2978 skipped += sb.last_screened();
2979 }
2980 assert!(skipped > 0, "the screen never skipped a neighbour");
2981 }
2982}