1mod adjacency;
25mod build_kernels;
26mod fastscan;
27pub(crate) mod kernels;
28#[cfg(not(target_arch = "wasm32"))]
29mod update;
30mod vertex;
31#[cfg(not(target_arch = "wasm32"))]
32pub use update::{ConsolidationPolicy, UpdatableVGraph, UpdateError};
33
34#[cfg(feature = "experimental")]
35pub mod diagnostics;
36
37#[cfg(feature = "experimental")]
38pub mod ivf;
39
40use std::cell::RefCell;
41
42use rayon::prelude::*;
43
44use crate::core::error::HmsError;
45use adjacency::Adjacency;
46
47use kernels::{
48 as_f32, as_f32_mut, as_u32, as_u32_mut, dot, dot_i8_kernel, DotI8, QueryCode, Rotation,
49 SplitMix,
50};
51pub use vertex::{Residency, SearchKnobs, VGraph, VSearchParams, VSearcher};
52
53pub const DEFAULT_DEGREE: usize = 32;
55const NONE: u32 = u32::MAX;
56
57fn invalid(details: String) -> HmsError {
59 HmsError::InvalidParam { details }
60}
61const UPPER_DEGREE: usize = 16;
62const LAYER_RATIO: usize = 16;
63const MIN_LAYER: usize = 64;
64const BUILD_CODES_MIN_DIM: usize = 128;
68const FIRST_PASS_EF: usize = 200;
74
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
77pub enum Metric {
78 #[default]
80 Angular,
81 Euclidean,
86}
87
88impl Metric {
89 fn code(self) -> u64 {
90 match self {
91 Self::Angular => 0,
92 Self::Euclidean => 1,
93 }
94 }
95}
96
97#[derive(Clone, Debug)]
98pub struct BuildParams {
99 pub metric: Metric,
101 pub build_ef: usize,
104 pub degree: usize,
106 pub alpha: f32,
108 pub code_bits: usize,
111 pub seed: u64,
112 pub quantized_search: bool,
117}
118
119impl Default for BuildParams {
120 fn default() -> Self {
121 Self {
122 metric: Metric::Angular,
123 build_ef: 128,
124 degree: DEFAULT_DEGREE,
125 alpha: 1.0,
126 code_bits: 0,
127 seed: 0x5EED,
128 quantized_search: false,
129 }
130 }
131}
132
133#[derive(Clone, Copy, Debug)]
134pub struct SearchParams {
135 pub ef: usize,
137 pub max_exact: usize,
139}
140
141pub struct QGraph {
142 n: usize,
143 dim: usize,
144 degree: usize,
145 words: usize,
146 rotation: Rotation,
147 stride: usize,
148 codes_off: usize,
149 factors_off: usize,
150 ids_off: usize,
151 blocks: Vec<u64>,
152 entry: u32,
153 upper_ids: Vec<u32>,
155 layers: Vec<Vec<Vec<u32>>>,
157}
158
159#[derive(Clone, Copy)]
160struct Candidate {
161 dist: f32,
162 id: u32,
163 done: bool,
164}
165
166struct Visited {
167 marks: Vec<u32>,
168 epoch: u32,
169}
170
171impl Visited {
172 fn new(n: usize) -> Self {
173 Self {
174 marks: vec![0; n],
175 epoch: 0,
176 }
177 }
178
179 fn next(&mut self) {
180 self.epoch = self.epoch.wrapping_add(1);
181 if self.epoch == 0 {
182 self.marks.fill(0);
183 self.epoch = 1;
184 }
185 }
186
187 fn insert(&mut self, id: u32) -> bool {
189 let m = &mut self.marks[id as usize];
190 let fresh = *m != self.epoch;
191 *m = self.epoch;
192 fresh
193 }
194}
195
196#[derive(Default)]
199struct BuildVisited {
200 bits: Vec<u64>,
201}
202
203impl BuildVisited {
204 fn reset(&mut self, n: usize) {
205 self.bits.clear();
206 self.bits.resize(n.div_ceil(64), 0);
207 }
208
209 fn insert(&mut self, id: u32) -> bool {
211 let (w, b) = (id as usize / 64, 1u64 << (id % 64));
212 let fresh = self.bits[w] & b == 0;
213 self.bits[w] |= b;
214 fresh
215 }
216}
217
218mod prof {
221 use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
222 use std::sync::OnceLock;
223 use std::time::Instant;
224
225 pub(super) static GREEDY: AtomicU64 = AtomicU64::new(0);
226 pub(super) static PRUNE: AtomicU64 = AtomicU64::new(0);
227 pub(super) static REV: AtomicU64 = AtomicU64::new(0);
228 pub(super) static FRESH: AtomicU64 = AtomicU64::new(0);
229 pub(super) static EXACT: AtomicU64 = AtomicU64::new(0);
230
231 pub(super) fn on() -> bool {
232 static ON: OnceLock<bool> = OnceLock::new();
233 *ON.get_or_init(|| std::env::var_os("HMS_QGRAPH_BUILD_PROFILE").is_some())
234 }
235
236 pub(super) fn start() -> Option<Instant> {
237 on().then(Instant::now)
238 }
239
240 pub(super) fn add(c: &AtomicU64, t: Option<Instant>) {
241 if let Some(t) = t {
242 c.fetch_add(t.elapsed().as_nanos() as u64, Relaxed);
243 }
244 }
245
246 pub(super) fn count(c: &AtomicU64, n: u64) {
247 if on() {
248 c.fetch_add(n, Relaxed);
249 }
250 }
251
252 pub(super) fn report(phase: &str, t: Option<Instant>) {
253 let Some(t) = t else { return };
254 let secs = |c: &AtomicU64| c.swap(0, Relaxed) as f64 * 1e-9;
255 eprintln!(
256 "PHASE {phase} wall={:.2}s greedy_cpu={:.1}s prune_cpu={:.1}s rev_wall={:.2}s fresh={} exact={}",
257 t.elapsed().as_secs_f64(),
258 secs(&GREEDY),
259 secs(&PRUNE),
260 secs(&REV),
261 FRESH.swap(0, Relaxed),
262 EXACT.swap(0, Relaxed),
263 );
264 }
265
266 pub(super) fn graph_hash(h: impl std::hash::Hash) {
267 if on() {
268 use std::hash::Hasher;
269 let mut s = std::collections::hash_map::DefaultHasher::new();
270 h.hash(&mut s);
271 eprintln!("GRAPH_HASH {:016x}", s.finish());
272 }
273 }
274}
275thread_local! {
276 static BUILD_VISITED: RefCell<BuildVisited> = RefCell::new(BuildVisited::default());
277}
278
279struct Rows<'a> {
280 data: &'a [f32],
281 dim: usize,
282 norms: Option<&'a [f32]>,
284 codes: Option<&'a BuildCodes>,
287 quantized: bool,
288}
289
290struct BuildCodes {
295 width: usize,
296 q: Vec<i8>,
297 meta: Vec<CodeMeta>,
299 dot: DotI8,
300 euclidean: bool,
302 rel_slack: f32,
304}
305
306#[derive(Clone, Copy, Default)]
308#[repr(C, align(16))]
309struct CodeMeta {
310 scale: f32,
311 err: f32,
312 n2: f32,
313 n: f32,
314}
315
316impl BuildCodes {
317 fn new(data: &[f32], dim: usize, norms: Option<&[f32]>) -> Self {
318 let n = data.len() / dim;
319 let width = dim.next_multiple_of(16);
320 let mut q = vec![0i8; n * width];
321 let mut meta = vec![CodeMeta::default(); n];
322 q.par_chunks_mut(width)
323 .zip(meta.par_iter_mut())
324 .zip(data.par_chunks(dim))
325 .for_each(|((q, m), x)| {
326 let (s, e) = (&mut m.scale, &mut m.err);
327 let max = x.iter().fold(0f32, |m, v| m.max(v.abs()));
328 *s = if max > 0.0 { max / 127.0 } else { 0.0 };
329 let mut e2 = 0f64;
330 for (o, &v) in q.iter_mut().zip(x) {
331 let c = if *s > 0.0 {
332 (v / *s).round().clamp(-127.0, 127.0)
333 } else {
334 0.0
335 };
336 *o = c as i8;
337 let r = f64::from(v) - f64::from(*s) * f64::from(c);
338 e2 += r * r;
339 }
340 *e = (e2.sqrt() * (1.0 + 1e-6)) as f32 + 1e-7;
342 });
343 if let Some(norms) = norms {
344 for (m, &n2) in meta.iter_mut().zip(norms) {
345 (m.n2, m.n) = (n2, n2.sqrt());
346 }
347 }
348 Self {
349 width,
350 q,
351 meta,
352 dot: dot_i8_kernel(),
353 euclidean: norms.is_some(),
354 rel_slack: 1e-4f32.max((dim as f32 + 4.0) * 2.0 * f32::EPSILON),
357 }
358 }
359
360 fn code(&self, i: u32) -> &[i8] {
361 let i = i as usize;
362 &self.q[i * self.width..(i + 1) * self.width]
363 }
364
365 #[inline]
367 fn prefetch(&self, i: u32) {
368 build_kernels::prefetch_code(self.code(i));
369 build_kernels::prefetch_meta(&self.meta[i as usize]);
370 }
371
372 fn d2_lower(&self, p: u32, u: u32) -> f32 {
377 self.d2_bounds(p, u).0
378 }
379
380 #[inline]
384 fn d2_est(&self, p: u32, u: u32) -> f32 {
385 let raw = (self.dot)(self.code(p), self.code(u)) as f32;
386 let (mp, mu) = (&self.meta[p as usize], &self.meta[u as usize]);
387 if self.euclidean {
388 (mp.n2 + mu.n2 - 2.0 * mp.scale * mu.scale * raw).max(0.0)
389 } else {
390 (2.0 - 2.0 * mp.scale * mu.scale * raw).max(0.0)
391 }
392 }
393
394 fn d2_bounds(&self, p: u32, u: u32) -> (f32, f32) {
398 const SLACK: f32 = 1e-4;
399 let (mp, mu) = (&self.meta[p as usize], &self.meta[u as usize]);
400 let raw = (self.dot)(self.code(p), self.code(u)) as f32;
401 let ep = mp.err;
402 let est = mp.scale * mu.scale * raw;
403 if !self.euclidean {
404 let margin = ep + (1.0 + ep) * mu.err + SLACK;
405 return (2.0 - 2.0 * (est + margin), 2.0 - 2.0 * (est - margin));
406 }
407 let ((np2, np), (nu2, nu)) = ((mp.n2, mp.n), (mu.n2, mu.n));
408 let margin = ep * nu + (np + ep) * mu.err;
409 let slack = self.rel_slack * (np2 + nu2 + 2.0 * np * nu);
410 let base = np2 + nu2;
411 (
412 base - 2.0 * (est + margin) - slack,
413 base - 2.0 * (est - margin) + slack,
414 )
415 }
416}
417
418impl Rows<'_> {
419 fn row(&self, i: u32) -> &[f32] {
420 let i = i as usize;
421 &self.data[i * self.dim..(i + 1) * self.dim]
422 }
423
424 #[inline]
426 fn prefetch(&self, u: u32) {
427 build_kernels::prefetch_row(self.row(u));
428 if let Some(n) = self.norms {
429 build_kernels::prefetch_meta(&n[u as usize]);
430 }
431 }
432
433 #[inline]
436 fn dist(&self, a: u32, b: u32, dot: f32) -> f32 {
437 match self.norms {
438 None => (2.0 - 2.0 * dot).max(0.0),
439 Some(n) => (n[a as usize] + n[b as usize] - 2.0 * dot).max(0.0),
440 }
441 }
442
443 fn d2(&self, a: u32, b: u32) -> f32 {
445 self.dist(a, b, dot(self.row(a), self.row(b)))
446 }
447
448 fn d2_many(&self, p: u32, ids: &[u32], out: &mut Vec<f32>) {
450 out.clear();
451 let target = self.row(p);
452 let (quads, rest) = ids.as_chunks::<4>();
453 for q in quads {
454 let d = build_kernels::dot4(target, q.map(|u| self.row(u)));
455 out.extend(d.iter().zip(q).map(|(&x, &u)| self.dist(p, u, x)));
456 }
457 out.extend(rest.iter().map(|&u| self.d2(p, u)));
458 }
459
460 fn d2_stream(&self, p: u32, ids: &[u32], out: &mut Vec<f32>) {
462 const AHEAD: usize = 16;
463 out.clear();
464 let target = self.row(p);
465 for &u in ids.iter().take(AHEAD) {
466 self.prefetch(u);
467 }
468 let (quads, rest) = ids.as_chunks::<4>();
469 for (i, q) in quads.iter().enumerate() {
470 for &u in ids.iter().skip(AHEAD + 4 * i).take(4) {
471 self.prefetch(u);
472 }
473 let d = build_kernels::dot4(target, q.map(|u| self.row(u)));
474 out.extend(d.iter().zip(q).map(|(&x, &u)| self.dist(p, u, x)));
475 }
476 out.extend(rest.iter().map(|&u| self.d2(p, u)));
477 }
478
479 fn occluded(&self, c: u32, kept: &[u32], dc: f32, a2: f32) -> bool {
481 let rc = self.row(c);
482 if let Some(codes) = self.codes {
483 let mut open = [0u32; 4];
486 let mut n_open = 0;
487 for &s in kept {
488 let (lo, hi) = codes.d2_bounds(c, s);
489 if a2 * hi <= dc {
490 return true;
491 }
492 if a2 * lo > dc {
493 continue;
494 }
495 open[n_open] = s;
496 n_open += 1;
497 if n_open == 4 {
498 n_open = 0;
499 let d = build_kernels::dot4(rc, open.map(|u| self.row(u)));
500 if d.iter()
501 .zip(&open)
502 .any(|(&x, &s)| a2 * self.dist(c, s, x) <= dc)
503 {
504 return true;
505 }
506 }
507 }
508 return open[..n_open]
509 .iter()
510 .any(|&s| a2 * self.dist(s, c, dot(self.row(s), rc)) <= dc);
511 }
512 let (quads, rest) = kept.as_chunks::<4>();
513 for q in quads {
514 let d = build_kernels::dot4(rc, q.map(|u| self.row(u)));
515 if d.iter()
516 .zip(q)
517 .any(|(&x, &s)| a2 * self.dist(c, s, x) <= dc)
518 {
519 return true;
520 }
521 }
522 rest.iter()
523 .any(|&s| a2 * self.dist(s, c, dot(self.row(s), rc)) <= dc)
524 }
525
526 fn greedy(
528 &self,
529 graph: &Adjacency,
530 start: u32,
531 p: u32,
532 l: usize,
533 visited: &mut BuildVisited,
534 ) -> Vec<(f32, u32)> {
535 let (mut n_fresh, mut n_exact) = (0u64, 0u64);
536 visited.reset(graph.len());
537 visited.insert(start);
538 let mut pool = vec![Candidate {
539 dist: self.d2(p, start),
540 id: start,
541 done: false,
542 }];
543 let mut expanded = Vec::new();
544 let mut fresh = Vec::new();
545 let mut dists = Vec::new();
546 let mut cur = 0;
547 while cur < pool.len() {
548 pool[cur].done = true;
549 let v = pool[cur].id;
550 expanded.push((pool[cur].dist, v));
551 fresh.clear();
553 match self.codes.filter(|_| self.quantized || pool.len() >= l) {
556 Some(codes) if self.quantized => {
557 for &u in graph.get(v as usize) {
558 if visited.insert(u) {
559 codes.prefetch(u);
560 fresh.push(u);
561 }
562 }
563 n_fresh += fresh.len() as u64;
564 }
565 Some(codes) => {
566 for &u in graph.get(v as usize) {
567 if visited.insert(u) {
568 codes.prefetch(u);
569 fresh.push(u);
570 }
571 }
572 let bound = pool[l - 1].dist;
573 n_fresh += fresh.len() as u64;
574 fresh.retain(|&u| codes.d2_lower(p, u) < bound);
575 for &u in &fresh {
576 build_kernels::prefetch_row(self.row(u));
577 }
578 }
579 None => {
580 for &u in graph.get(v as usize) {
581 if visited.insert(u) {
582 build_kernels::prefetch_row(self.row(u));
583 fresh.push(u);
584 }
585 }
586 n_fresh += fresh.len() as u64;
587 }
588 }
589 match self.codes.filter(|_| self.quantized) {
590 Some(codes) => {
591 dists.clear();
592 dists.extend(fresh.iter().map(|&u| codes.d2_est(p, u)));
593 }
594 None => {
595 n_exact += fresh.len() as u64;
596 self.d2_many(p, &fresh, &mut dists);
597 }
598 }
599 let mut best = usize::MAX;
600 for (&u, &du) in fresh.iter().zip(&dists) {
601 if pool.len() >= l && du >= pool[l - 1].dist {
602 continue;
603 }
604 let pos = pool.partition_point(|c| c.dist <= du);
605 pool.insert(
606 pos,
607 Candidate {
608 dist: du,
609 id: u,
610 done: false,
611 },
612 );
613 pool.truncate(l);
614 best = best.min(pos);
615 build_kernels::prefetch_meta(graph.len_word(u as usize));
617 }
618 cur = best.min(cur + 1);
619 while cur < pool.len() && pool[cur].done {
620 cur += 1;
621 }
622 if let Some(c) = pool.get(cur) {
623 build_kernels::prefetch_ids(graph.get(c.id as usize));
624 }
625 }
626 if self.quantized {
627 let ids: Vec<u32> = expanded.iter().map(|e| e.1).collect();
629 let mut d = Vec::with_capacity(ids.len());
630 self.d2_stream(p, &ids, &mut d);
631 n_exact += ids.len() as u64;
632 for (e, d) in expanded.iter_mut().zip(d) {
633 e.0 = d;
634 }
635 }
636 prof::count(&prof::FRESH, n_fresh);
637 prof::count(&prof::EXACT, n_exact);
638 expanded
639 }
640
641 fn prune(&self, p: u32, mut cands: Vec<(f32, u32)>, alpha: f32, r: usize) -> Vec<u32> {
643 cands.retain(|c| c.1 != p);
644 cands.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
645 cands.dedup_by_key(|c| c.1);
646 let a2 = alpha * alpha;
647 let mut out: Vec<u32> = Vec::with_capacity(r);
648 for &(dc, c) in &cands {
649 if out.len() == r {
650 break;
651 }
652 if !self.occluded(c, &out, dc, a2) {
653 out.push(c);
654 }
655 }
656 out
657 }
658
659 fn prune_merged(&self, t: u32, list: &[u32], closed: usize, alpha: f32, r: usize) -> Vec<u32> {
664 let mut d = Vec::with_capacity(list.len());
665 self.d2_stream(t, list, &mut d);
666 let mut cands: Vec<(f32, u32, bool)> = d
667 .into_iter()
668 .zip(list)
669 .enumerate()
670 .map(|(i, (d, &u))| (d, u, i < closed))
671 .collect();
672 cands.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
673 let a2 = alpha * alpha;
674 let mut out: Vec<u32> = Vec::with_capacity(r);
675 let mut open: Vec<u32> = Vec::new();
676 for &(dc, c, was_kept) in &cands {
677 if out.len() == r {
678 break;
679 }
680 if !self.occluded(c, if was_kept { &open } else { &out }, dc, a2) {
681 out.push(c);
682 if !was_kept {
683 open.push(c);
684 }
685 }
686 }
687 out
688 }
689
690 #[allow(clippy::too_many_arguments)]
693 fn insert_pass(
694 &self,
695 graph: &mut Adjacency,
696 order: &[u32],
697 entry: u32,
698 l: usize,
699 alpha: f32,
700 r: usize,
701 ) {
702 let n = graph.len();
703 let mut closed = vec![0u32; n];
705 let max_batch = (n / 50).max(64);
706 let mut outs = Adjacency::new(max_batch.min(order.len()), r);
707 let (mut start, mut size) = (0, 1);
708 while start < order.len() {
709 let end = (start + size).min(order.len());
710 let batch = &order[start..end];
711 let g: &Adjacency = graph;
712 outs.par_set(batch.len(), |i| {
713 let p = batch[i];
714 let t0 = prof::start();
715 let mut cands = BUILD_VISITED.with(|cell| {
716 let mut vis = cell.borrow_mut();
717 self.greedy(g, entry, p, l, &mut vis)
718 });
719 prof::add(&prof::GREEDY, t0);
720 let t0 = prof::start();
721 let adj = g.get(p as usize);
722 let mut d = Vec::with_capacity(adj.len());
723 self.d2_stream(p, adj, &mut d);
724 cands.extend(d.into_iter().zip(adj.iter().copied()));
725 let out = self.prune(p, cands, alpha, r);
726 prof::add(&prof::PRUNE, t0);
727 out
728 });
729 let trev = prof::start();
730 let mut rev: Vec<(u32, u32)> = batch
731 .iter()
732 .enumerate()
733 .flat_map(|(i, &p)| outs.get(i).iter().map(move |&u| (u, p)))
734 .collect();
735 for (i, &p) in batch.iter().enumerate() {
736 let out = outs.get(i);
737 closed[p as usize] = out.len() as u32;
738 graph.set(p as usize, out);
739 }
740 rev.par_sort_unstable();
741 let groups: Vec<_> = rev.chunk_by(|a, b| a.0 == b.0).collect();
743 let targets = || groups.iter().map(|grp| grp[0].0 as usize);
744 let closed_of = pick_mut(&mut closed, targets());
745 let jobs: Vec<_> = graph
746 .rows_mut(targets())
747 .zip(closed_of)
748 .zip(&groups)
749 .collect();
750 jobs.into_par_iter()
751 .for_each(|(((row, len), closed), grp)| {
752 let t = grp[0].0;
753 let mut merged = row[..*len as usize].to_vec();
754 for &(_, s) in grp.iter() {
755 if !merged.contains(&s) {
756 merged.push(s);
757 }
758 }
759 if merged.len() > r {
760 merged = self.prune_merged(t, &merged, *closed as usize, alpha, r);
761 *closed = merged.len() as u32;
762 }
763 row[..merged.len()].copy_from_slice(&merged);
764 *len = merged.len() as u32;
765 });
766 prof::add(&prof::REV, trev);
767 start = end;
768 size = (size * 2).min(max_batch);
769 }
770 }
771
772 fn fill(&self, graph: &mut Adjacency, r: usize) {
775 graph.par_append(|g, v| {
776 let have = g.get(v);
777 if have.len() >= r {
778 return Vec::new();
779 }
780 let mut ids: Vec<u32> = have
781 .iter()
782 .flat_map(|&u| g.get(u as usize).iter().copied())
783 .collect();
784 ids.sort_unstable();
785 ids.dedup();
786 let mut skip = have.to_vec();
788 skip.push(v as u32);
789 skip.sort_unstable();
790 let mut at = 0;
791 ids.retain(|&u| {
792 while at < skip.len() && skip[at] < u {
793 at += 1;
794 }
795 skip.get(at) != Some(&u)
796 });
797 let k = r - have.len();
798 if let Some(codes) = self.codes.filter(|_| ids.len() > k) {
799 const AHEAD: usize = 8;
802 ids.iter().take(AHEAD).for_each(|&u| codes.prefetch(u));
803 let bounds: Vec<(f32, f32)> = ids
804 .iter()
805 .enumerate()
806 .map(|(i, &u)| {
807 if let Some(&w) = ids.get(i + AHEAD) {
808 codes.prefetch(w);
809 }
810 codes.d2_bounds(v as u32, u)
811 })
812 .collect();
813 let mut upper: Vec<f32> = bounds.iter().map(|b| b.1).collect();
814 let cut = *upper.select_nth_unstable_by(k - 1, f32::total_cmp).1;
815 let mut b = bounds.iter();
816 ids.retain(|_| b.next().is_some_and(|b| b.0 <= cut));
817 }
818 let mut d = Vec::new();
819 self.d2_stream(v as u32, &ids, &mut d);
820 let mut c: Vec<(f32, u32)> = d.into_iter().zip(ids).collect();
821 c.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
822 c.into_iter().take(r - have.len()).map(|x| x.1).collect()
823 });
824 }
825}
826
827fn pick_mut<T>(values: &mut [T], ids: impl Iterator<Item = usize>) -> impl Iterator<Item = &mut T> {
829 let mut rest = values;
830 let mut next = 0;
831 ids.map(move |v| {
832 assert!(v >= next, "ids must be strictly increasing");
833 let (x, tail) = std::mem::take(&mut rest)[v - next..]
834 .split_first_mut()
835 .expect("id in range");
836 rest = tail;
837 next = v + 1;
838 x
839 })
840}
841
842fn edge_factors(pv: &[f32], pu: &[f32], code: &mut [u64]) -> (f32, f32, f32) {
845 let (mut r2, mut l1, mut sv, mut vw) = (0f32, 0f32, 0f32, 0f32);
846 for ((a64, b64), word) in pv
848 .as_chunks::<64>()
849 .0
850 .iter()
851 .zip(pu.as_chunks::<64>().0)
852 .zip(code.iter_mut())
853 {
854 let mut bits = 0u64;
855 for (i, (&a, &b)) in a64.iter().zip(b64).enumerate() {
856 let w = b - a;
857 r2 += w * w;
858 l1 += w.abs();
859 vw += a * w;
860 let up = w > 0.0;
861 sv += if up { a } else { -a };
862 bits |= u64::from(up) << i;
863 }
864 *word = bits;
865 }
866 let m = if l1 > 0.0 { r2 / l1 } else { 0.0 };
867 let pop = code.iter().map(|c| c.count_ones()).sum::<u32>() as f32;
868 (vw - m * sv, m, pop)
869}
870
871impl QGraph {
872 pub fn build(data: &[f32], dim: usize, params: &BuildParams) -> Result<Self, HmsError> {
875 Self::from_graph(&Graph::build(data, dim, params)?, params)
876 }
877
878 pub fn from_graph(g: &Graph, params: &BuildParams) -> Result<Self, HmsError> {
880 let (n, dim, r) = (g.n, g.dim, g.degree);
881 if r != params.degree {
882 return Err(invalid(format!(
883 "graph built with degree {r}, parameters say {}",
884 params.degree
885 )));
886 }
887 if g.metric != Metric::Angular {
888 return Err(invalid(
889 "the edge index is angular only; use VGraph for Euclidean data".into(),
890 ));
891 }
892 let rotation = Rotation::new(dim, params.code_bits, params.seed);
893 let words = rotation.padded() / 64;
894 let rows = g.rows();
895 let graph = &g.adj;
896 let codes_off = dim.div_ceil(2);
897 let factors_off = codes_off + r * words;
898 let ids_off = factors_off + 3 * r / 2;
899 let stride = ids_off + r / 2;
900 let padded = rotation.padded();
902 let mut rotated = vec![0f32; n * padded];
903 rotated
904 .par_chunks_mut(padded)
905 .enumerate()
906 .for_each(|(v, out)| rotation.apply(rows.row(v as u32), out));
907 let rot_row = |v: u32| &rotated[v as usize * padded..(v as usize + 1) * padded];
908 let mut blocks = vec![0u64; n * stride];
909 blocks
910 .par_chunks_mut(stride)
911 .enumerate()
912 .for_each(|(v, b)| {
913 let adj = graph.get(v);
914 let (vec_w, rest) = b.split_at_mut(codes_off);
915 let (codes, rest) = rest.split_at_mut(r * words);
916 let (fac, ids) = rest.split_at_mut(3 * r / 2);
917 as_f32_mut(vec_w)[..dim].copy_from_slice(rows.row(v as u32));
918 let fac = as_f32_mut(fac);
919 let ids = as_u32_mut(ids);
920 ids.fill(NONE);
921 let pv = rot_row(v as u32);
922 for &u in adj.iter().take(4) {
923 build_kernels::prefetch_row(rot_row(u));
924 }
925 for (j, &u) in adj.iter().enumerate() {
926 if let Some(&next) = adj.get(j + 4) {
927 build_kernels::prefetch_row(rot_row(next));
928 }
929 ids[j] = u;
930 let (k, m, pop) =
931 edge_factors(pv, rot_row(u), &mut codes[j * words..(j + 1) * words]);
932 fac[j] = k;
933 fac[r + j] = m;
934 fac[2 * r + j] = pop;
935 }
936 });
937 Ok(Self {
938 n,
939 dim,
940 degree: r,
941 words,
942 rotation,
943 stride,
944 codes_off,
945 factors_off,
946 ids_off,
947 blocks,
948 entry: g.entry,
949 upper_ids: g.upper_ids.clone(),
950 layers: g.layers.clone(),
951 })
952 }
953}
954
955pub struct Graph {
959 n: usize,
960 dim: usize,
961 degree: usize,
962 metric: Metric,
963 vectors: Vec<f32>,
965 norms: Vec<f32>,
967 adj: Adjacency,
968 entry: u32,
969 upper_ids: Vec<u32>,
970 layers: Vec<Vec<Vec<u32>>>,
971 build: BuildParams,
973}
974
975fn prepare_rows(data: &mut [f32], dim: usize, metric: Metric) -> Vec<f32> {
978 match metric {
979 Metric::Angular => {
980 data.par_chunks_mut(dim).for_each(|x| {
981 let norm = dot(x, x).sqrt();
982 if norm > 0.0 {
983 x.iter_mut().for_each(|o| *o /= norm);
984 }
985 });
986 Vec::new()
987 }
988 Metric::Euclidean => data.par_chunks(dim).map(|x| dot(x, x)).collect(),
989 }
990}
991
992fn norms_opt(norms: &[f32]) -> Option<&[f32]> {
994 (!norms.is_empty()).then_some(norms)
995}
996
997impl Graph {
998 fn rows(&self) -> Rows<'_> {
999 Rows {
1000 data: &self.vectors,
1001 dim: self.dim,
1002 norms: norms_opt(&self.norms),
1003 codes: None,
1004 quantized: false,
1005 }
1006 }
1007
1008 pub fn metric(&self) -> Metric {
1010 self.metric
1011 }
1012
1013 pub(crate) fn neighbors(&self, v: usize) -> &[u32] {
1015 self.adj.get(v)
1016 }
1017
1018 pub fn structure_hash(&self) -> u64 {
1021 let mut h = Fnv::new();
1022 h.word(self.entry);
1023 for list in self.adj.iter() {
1024 h.word(list.len() as u32);
1025 list.iter().for_each(|&u| h.word(u));
1026 }
1027 for layer in &self.layers {
1028 h.word(layer.len() as u32);
1029 for list in layer {
1030 h.word(list.len() as u32);
1031 list.iter().for_each(|&u| h.word(u));
1032 }
1033 }
1034 h.finish()
1035 }
1036
1037 pub fn build(data: &[f32], dim: usize, params: &BuildParams) -> Result<Self, HmsError> {
1041 Self::build_owned(data.to_vec(), dim, params)
1042 }
1043
1044 pub fn build_owned(
1048 mut data: Vec<f32>,
1049 dim: usize,
1050 params: &BuildParams,
1051 ) -> Result<Self, HmsError> {
1052 if dim == 0 || !data.len().is_multiple_of(dim) {
1053 return Err(invalid(format!(
1054 "ragged data: {} values are not a multiple of dimension {dim}",
1055 data.len()
1056 )));
1057 }
1058 let n = data.len() / dim;
1059 if n == 0 {
1060 return Err(invalid("empty index: no vectors to build from".into()));
1061 }
1062 if n >= NONE as usize {
1063 return Err(invalid(format!(
1064 "index too large: {n} vectors, the limit is {}",
1065 NONE - 1
1066 )));
1067 }
1068 if let Some(at) = data.iter().position(|v| !v.is_finite()) {
1069 return Err(invalid(format!(
1070 "non-finite value in row {} (coordinate {})",
1071 at / dim,
1072 at % dim
1073 )));
1074 }
1075 if params.build_ef == 0 {
1076 return Err(invalid("build_ef must be positive".into()));
1077 }
1078 let r = params.degree;
1079 if r == 0 || !r.is_multiple_of(2) {
1080 return Err(invalid(format!(
1081 "degree must be positive and even, not {r}"
1082 )));
1083 }
1084 let metric = params.metric;
1085 let norms = prepare_rows(&mut data, dim, metric);
1086 if let Some(at) = norms.iter().position(|v| !v.is_finite()) {
1087 return Err(invalid(format!("row {at} overflows its squared norm")));
1088 }
1089 let vectors = data;
1090 let codes = (params.quantized_search || dim >= BUILD_CODES_MIN_DIM)
1091 .then(|| BuildCodes::new(&vectors, dim, norms_opt(&norms)));
1092 let rows = Rows {
1093 data: &vectors,
1094 dim,
1095 norms: norms_opt(&norms),
1096 codes: codes.as_ref(),
1097 quantized: params.quantized_search,
1098 };
1099
1100 let mut centroid = vec![0f32; dim];
1101 for x in vectors.chunks_exact(dim) {
1102 centroid.iter_mut().zip(x).for_each(|(c, v)| *c += v);
1103 }
1104 let entry = match metric {
1105 Metric::Angular => {
1106 (0..n as u32)
1107 .into_par_iter()
1108 .map(|i| (dot(rows.row(i), ¢roid), i))
1109 .reduce(
1110 || (f32::NEG_INFINITY, NONE),
1111 |a, b| {
1112 if b.0 > a.0 || (b.0 == a.0 && b.1 < a.1) {
1113 b
1114 } else {
1115 a
1116 }
1117 },
1118 )
1119 .1
1120 }
1121 Metric::Euclidean => {
1122 centroid.iter_mut().for_each(|c| *c /= n as f32);
1125 (0..n as u32)
1126 .into_par_iter()
1127 .map(|i| (norms[i as usize] - 2.0 * dot(rows.row(i), ¢roid), i))
1128 .reduce(
1129 || (f32::INFINITY, NONE),
1130 |a, b| {
1131 if b.0 < a.0 || (b.0 == a.0 && b.1 < a.1) {
1132 b
1133 } else {
1134 a
1135 }
1136 },
1137 )
1138 .1
1139 }
1140 };
1141
1142 let mut rng = SplitMix(params.seed ^ 0xA5A5_A5A5);
1143 let mut order: Vec<u32> = (0..n as u32).collect();
1144 for i in (1..n).rev() {
1145 order.swap(i, (rng.next_u64() % (i as u64 + 1)) as usize);
1146 }
1147 let mut graph = Adjacency::new(n, r);
1148 let first_ef = params.build_ef.min(FIRST_PASS_EF);
1149 for (l, alpha) in [(first_ef, 1.0), (params.build_ef, params.alpha)] {
1150 let t = prof::start();
1151 rows.insert_pass(&mut graph, &order, entry, l, alpha, r);
1152 prof::report("pass", t);
1153 }
1154 let t = prof::start();
1155 rows.fill(&mut graph, r);
1156 prof::report("fill", t);
1157 let tup = prof::start();
1158
1159 let mut layers = Vec::new();
1162 let mut size = n / LAYER_RATIO;
1163 while size >= MIN_LAYER {
1164 let sub: Vec<f32> = order[..size]
1165 .iter()
1166 .flat_map(|&g| rows.row(g).iter().copied())
1167 .collect();
1168 let sub_norms: Vec<f32> = match rows.norms {
1169 Some(n) => order[..size].iter().map(|&g| n[g as usize]).collect(),
1170 None => Vec::new(),
1171 };
1172 let sub_codes = (params.quantized_search || dim >= BUILD_CODES_MIN_DIM)
1173 .then(|| BuildCodes::new(&sub, dim, norms_opt(&sub_norms)));
1174 let sub_rows = Rows {
1175 data: &sub,
1176 dim,
1177 norms: norms_opt(&sub_norms),
1178 codes: sub_codes.as_ref(),
1179 quantized: params.quantized_search,
1180 };
1181 let local: Vec<u32> = (0..size as u32).collect();
1182 let mut g = Adjacency::new(size, UPPER_DEGREE);
1183 sub_rows.insert_pass(
1184 &mut g,
1185 &local,
1186 0,
1187 params.build_ef,
1188 params.alpha,
1189 UPPER_DEGREE,
1190 );
1191 layers.push(g.to_lists());
1192 size /= LAYER_RATIO;
1193 }
1194 prof::report("upper", tup);
1195 prof::graph_hash((&graph, &layers, entry));
1196 let upper_ids = order[..n / LAYER_RATIO].to_vec();
1197 Ok(Self {
1198 n,
1199 dim,
1200 degree: r,
1201 metric,
1202 vectors,
1203 norms,
1204 adj: graph,
1205 entry,
1206 upper_ids,
1207 layers,
1208 build: params.clone(),
1209 })
1210 }
1211}
1212
1213struct Fnv(u64);
1215
1216impl Fnv {
1217 fn new() -> Self {
1218 Self(0xcbf2_9ce4_8422_2325)
1219 }
1220
1221 fn word(&mut self, w: u32) {
1222 self.0 = (self.0 ^ u64::from(w)).wrapping_mul(0x0000_0100_0000_01b3);
1223 }
1224
1225 fn finish(&self) -> u64 {
1226 self.0
1227 }
1228}
1229
1230fn upper_bytes(upper_ids: &[u32], layers: &[Vec<Vec<u32>>]) -> usize {
1232 let adj: usize = layers.iter().flatten().map(|a| a.len() * 4).sum();
1233 upper_ids.len() * 4 + adj
1234}
1235
1236impl QGraph {
1237 pub fn len(&self) -> usize {
1238 self.n
1239 }
1240
1241 pub fn is_empty(&self) -> bool {
1242 self.n == 0
1243 }
1244
1245 pub fn index_bytes(&self) -> usize {
1247 self.blocks.len() * 8 + upper_bytes(&self.upper_ids, &self.layers)
1248 }
1249
1250 fn vector(&self, v: u32) -> &[f32] {
1251 &as_f32(&self.block(v)[..self.codes_off])[..self.dim]
1252 }
1253
1254 fn descend(&self, pq: &[f32]) -> (u32, usize) {
1257 if self.layers.is_empty() {
1258 return (self.entry, 0);
1259 }
1260 let mut p = 0u32;
1261 let mut best = -dot(pq, self.vector(self.upper_ids[0]));
1262 let mut evals = 1;
1263 for layer in self.layers.iter().rev() {
1264 loop {
1265 let mut moved = false;
1266 for &u in &layer[p as usize] {
1267 let d = -dot(pq, self.vector(self.upper_ids[u as usize]));
1268 evals += 1;
1269 if d < best {
1270 best = d;
1271 p = u;
1272 moved = true;
1273 }
1274 }
1275 if !moved {
1276 break;
1277 }
1278 }
1279 }
1280 (self.upper_ids[p as usize], evals)
1281 }
1282
1283 pub fn searcher(&self) -> Searcher<'_> {
1284 Searcher {
1285 index: self,
1286 visited: Visited::new(self.n),
1287 pq: vec![0.0; self.rotation.padded()],
1288 code: QueryCode::new(self.words),
1289 raw: vec![0; self.degree],
1290 est: vec![0.0; self.degree],
1291 pool: Vec::new(),
1292 results: Vec::new(),
1293 }
1294 }
1295
1296 fn block(&self, v: u32) -> &[u64] {
1297 let v = v as usize;
1298 &self.blocks[v * self.stride..(v + 1) * self.stride]
1299 }
1300}
1301
1302pub struct Searcher<'a> {
1304 index: &'a QGraph,
1305 visited: Visited,
1306 pq: Vec<f32>,
1307 code: QueryCode,
1308 raw: Vec<u32>,
1309 est: Vec<f32>,
1310 pool: Vec<Candidate>,
1311 results: Vec<(f32, u32)>,
1312}
1313
1314impl Searcher<'_> {
1315 pub fn search(
1318 &mut self,
1319 query: &[f32],
1320 k: usize,
1321 params: SearchParams,
1322 out: &mut Vec<u32>,
1323 ) -> usize {
1324 let idx = self.index;
1325 let ef = params.ef.max(1);
1326 idx.rotation.apply(query, &mut self.pq);
1327 self.code.encode(&self.pq);
1328 let (lo2, d2q, sq) = (2.0 * self.code.lo, 2.0 * self.code.delta, self.code.sum);
1329
1330 self.visited.next();
1331 let (entry, mut exact) = idx.descend(query);
1332 let mut expanded = 0;
1333 self.visited.insert(entry);
1334 self.pool.clear();
1335 self.results.clear();
1336 self.pool.push(Candidate {
1337 dist: 0.0,
1338 id: entry,
1339 done: false,
1340 });
1341 let mut cur = 0;
1342 while cur < self.pool.len() {
1343 if params.max_exact > 0 && expanded >= params.max_exact {
1344 break;
1345 }
1346 let v = self.pool.remove(cur).id;
1347 if let Some(next) = self.pool[cur..].iter().find(|c| !c.done) {
1348 kernels::prefetch(idx.block(next.id));
1349 }
1350 let b = idx.block(v);
1351 let ipv = dot(query, &as_f32(&b[..idx.codes_off])[..idx.dim]);
1352 exact += 1;
1353 expanded += 1;
1354 push_top(&mut self.results, k, -ipv, v);
1355 let pos = self.pool.partition_point(|c| c.dist <= -ipv);
1358 if pos < ef {
1359 self.pool.insert(
1360 pos,
1361 Candidate {
1362 dist: -ipv,
1363 id: v,
1364 done: true,
1365 },
1366 );
1367 self.pool.truncate(ef);
1368 }
1369
1370 kernels::raw_estimates(
1371 &b[idx.codes_off..idx.factors_off],
1372 &self.code.planes,
1373 idx.words,
1374 &mut self.raw,
1375 );
1376 let fac = as_f32(&b[idx.factors_off..idx.ids_off]);
1377 let (kf, rest) = fac.split_at(idx.degree);
1378 let (mf, pf) = rest.split_at(idx.degree);
1379 for ((((e, &kj), &mj), &pj), &rj) in
1380 self.est.iter_mut().zip(kf).zip(mf).zip(pf).zip(&self.raw)
1381 {
1382 *e = -(ipv + kj + mj * (lo2 * pj - sq + d2q * rj as f32));
1383 }
1384
1385 let mut best = usize::MAX;
1386 for (&u, &d) in as_u32(&b[idx.ids_off..]).iter().zip(&self.est) {
1387 if u == NONE || !self.visited.insert(u) {
1388 continue;
1389 }
1390 if self.pool.len() >= ef && d >= self.pool[ef - 1].dist {
1391 continue;
1392 }
1393 let pos = self.pool.partition_point(|c| c.dist <= d);
1394 self.pool.insert(
1395 pos,
1396 Candidate {
1397 dist: d,
1398 id: u,
1399 done: false,
1400 },
1401 );
1402 self.pool.truncate(ef);
1403 best = best.min(pos);
1404 }
1405 cur = best.min(cur);
1406 while cur < self.pool.len() && self.pool[cur].done {
1407 cur += 1;
1408 }
1409 }
1410 out.clear();
1411 out.extend(self.results.iter().map(|r| r.1));
1412 exact
1413 }
1414}
1415
1416fn push_top(top: &mut Vec<(f32, u32)>, k: usize, dist: f32, id: u32) {
1417 if top.len() >= k && top.last().is_none_or(|l| dist >= l.0) {
1418 return;
1419 }
1420 let pos = top.partition_point(|c| c.0 <= dist);
1421 top.insert(pos, (dist, id));
1422 top.truncate(k);
1423}
1424
1425#[cfg(test)]
1426mod tests {
1427 use super::*;
1428 use kernels::raw_estimates;
1429
1430 pub(super) fn gaussian(rng: &mut SplitMix) -> f32 {
1431 let u = rng.next_f32().max(1e-7);
1432 let v = rng.next_f32();
1433 (-2.0 * u.ln()).sqrt() * (std::f32::consts::TAU * v).cos()
1434 }
1435
1436 fn unit(mut x: Vec<f32>) -> Vec<f32> {
1437 let n = dot(&x, &x).sqrt();
1438 x.iter_mut().for_each(|v| *v /= n);
1439 x
1440 }
1441
1442 #[test]
1447 fn edge_estimator_is_unbiased_over_rotations() {
1448 let d = 100;
1449 let mut rng = SplitMix(42);
1450 for bits in [0, 0, 0, 512, 512, 512] {
1451 let q = unit((0..d).map(|_| gaussian(&mut rng)).collect());
1452 let v = unit((0..d).map(|_| gaussian(&mut rng)).collect());
1453 let u = unit(v.iter().map(|x| x + 0.15 * gaussian(&mut rng)).collect());
1454 let truth = dot(&q, &u);
1455 let (mut float_sum, mut quant_sum) = (0f64, 0f64);
1456 let seeds = 400;
1457 for seed in 0..seeds {
1458 let rot = Rotation::new(d, bits, seed);
1459 let p = rot.padded();
1460 let (mut pq, mut pv, mut pu) = (vec![0.0; p], vec![0.0; p], vec![0.0; p]);
1461 rot.apply(&q, &mut pq);
1462 rot.apply(&v, &mut pv);
1463 rot.apply(&u, &mut pu);
1464 let mut code = vec![0u64; p / 64];
1465 let (k, m, pop) = edge_factors(&pv, &pu, &mut code);
1466 let ipv = dot(&pq, &pv);
1467 let sq: f32 = pq
1468 .iter()
1469 .enumerate()
1470 .map(|(i, x)| {
1471 if code[i / 64] >> (i % 64) & 1 == 1 {
1472 *x
1473 } else {
1474 -*x
1475 }
1476 })
1477 .sum();
1478 float_sum += f64::from(ipv + k + m * sq);
1479 let mut qc = QueryCode::new(p / 64);
1480 qc.encode(&pq);
1481 let mut raw = [0u32];
1482 raw_estimates(&code, &qc.planes, p / 64, &mut raw);
1483 let est =
1484 ipv + k + m * (2.0 * qc.lo * pop - qc.sum + 2.0 * qc.delta * raw[0] as f32);
1485 quant_sum += f64::from(est);
1486 }
1487 let float_mean = float_sum / f64::from(seeds as u32);
1488 let quant_mean = quant_sum / f64::from(seeds as u32);
1489 assert!(
1490 (float_mean - f64::from(truth)).abs() < 0.02,
1491 "{float_mean} vs {truth}"
1492 );
1493 assert!(
1494 (quant_mean - f64::from(truth)).abs() < 0.03,
1495 "{quant_mean} vs {truth}"
1496 );
1497 }
1498 }
1499
1500 pub(super) fn clustered(n: usize, d: usize, seed: u64) -> Vec<f32> {
1501 let mut rng = SplitMix(seed);
1502 let centers: Vec<Vec<f32>> = (0..20)
1503 .map(|_| (0..d).map(|_| gaussian(&mut rng)).collect())
1504 .collect();
1505 (0..n)
1506 .flat_map(|i| {
1507 let c = ¢ers[i % centers.len()];
1508 c.iter()
1509 .map(|x| x + 0.4 * gaussian(&mut rng))
1510 .collect::<Vec<_>>()
1511 })
1512 .collect()
1513 }
1514
1515 pub(super) fn brute_force(data: &[f32], d: usize, q: &[f32], k: usize) -> Vec<u32> {
1516 let mut s: Vec<(f32, u32)> = data
1517 .chunks_exact(d)
1518 .enumerate()
1519 .map(|(i, x)| (-dot(x, q) / dot(x, x).sqrt(), i as u32))
1520 .collect();
1521 s.sort_by(|a, b| a.0.total_cmp(&b.0));
1522 s.into_iter().take(k).map(|x| x.1).collect()
1523 }
1524
1525 #[test]
1526 fn recall_on_clustered_data() {
1527 let d = 24;
1528 let data = clustered(4000, d, 1);
1529 let queries = clustered(100, d, 2);
1530 let index = QGraph::build(&data, d, &BuildParams::default()).expect("build");
1531 let mut s = index.searcher();
1532 let mut out = Vec::new();
1533 let mut hits = 0;
1534 for q in queries.chunks_exact(d) {
1535 let truth = brute_force(&data, d, q, 10);
1536 s.search(
1537 q,
1538 10,
1539 SearchParams {
1540 ef: 64,
1541 max_exact: 0,
1542 },
1543 &mut out,
1544 );
1545 hits += out.iter().filter(|x| truth.contains(x)).count();
1546 }
1547 let recall = hits as f64 / 1000.0;
1548 assert!(recall >= 0.9, "recall {recall}");
1550 }
1551
1552 #[test]
1555 fn prune_merged_matches_prune() {
1556 let d = 12;
1557 let mut data = clustered(600, d, 5);
1558 data.chunks_exact_mut(d).for_each(|x| {
1559 let n = dot(x, x).sqrt();
1560 x.iter_mut().for_each(|v| *v /= n);
1561 });
1562 let rows = Rows {
1563 data: &data,
1564 dim: d,
1565 norms: None,
1566 quantized: false,
1567 codes: None,
1568 };
1569 let mut rng = SplitMix(11);
1570 for trial in 0..200 {
1571 let t = (rng.next_u64() % 600) as u32;
1572 let (r, alpha) = (4 + trial % 13, [1.0, 1.2][trial % 2]);
1573 let all = |ids: &[u32]| -> Vec<(f32, u32)> {
1574 ids.iter().map(|&u| (rows.d2(t, u), u)).collect()
1575 };
1576 let pool: Vec<u32> = (0..60)
1577 .map(|_| (rng.next_u64() % 600) as u32)
1578 .filter(|&u| u != t)
1579 .collect();
1580 let mut list = rows.prune(t, all(&pool), alpha, r);
1581 let closed = list.len();
1582 for _ in 0..1 + trial % 9 {
1583 let u = (rng.next_u64() % 600) as u32;
1584 if u != t && !list.contains(&u) {
1585 list.push(u);
1586 }
1587 }
1588 assert_eq!(
1589 rows.prune_merged(t, &list, closed, alpha, r),
1590 rows.prune(t, all(&list), alpha, r),
1591 "trial {trial}"
1592 );
1593 }
1594 }
1595
1596 #[test]
1601 fn build_codes_leave_the_graph_unchanged() {
1602 for metric in [Metric::Angular, Metric::Euclidean] {
1603 for d in [100, 160] {
1604 let n = 1200;
1605 let mut data = clustered(n, d, 3);
1606 let mut rng = SplitMix(5);
1607 if metric == Metric::Euclidean {
1608 for (i, x) in data.chunks_exact_mut(d).enumerate() {
1609 let s = 0.1 * 100f32.powf((i * 7919 % n) as f32 / n as f32);
1610 x.iter_mut().for_each(|v| *v = s * (*v + 3.0));
1611 }
1612 }
1613 for i in (0..n).step_by(7) {
1614 let j = (rng.next_u64() % n as u64) as usize;
1615 for k in 0..d {
1616 data[i * d + k] = data[j * d + k] * (1.0 + 1e-6 * gaussian(&mut rng));
1617 }
1618 }
1619 data[5 * d..6 * d].fill(0.0);
1620 let norms = prepare_rows(&mut data, d, metric);
1621 let codes = BuildCodes::new(&data, d, norms_opt(&norms));
1622 let build = |codes: Option<&BuildCodes>| {
1623 let rows = Rows {
1624 data: &data,
1625 dim: d,
1626 norms: norms_opt(&norms),
1627 codes,
1628 quantized: false,
1629 };
1630 let order: Vec<u32> = (0..n as u32).rev().collect();
1631 let mut graph = Adjacency::new(n, 12);
1632 for alpha in [1.0, 1.2] {
1633 rows.insert_pass(&mut graph, &order, 0, 40, alpha, 12);
1634 }
1635 rows.fill(&mut graph, 12);
1636 graph
1637 };
1638 assert_eq!(build(Some(&codes)), build(None), "{metric:?}, d = {d}");
1639 }
1640 }
1641 }
1642
1643 #[test]
1644 fn build_and_search_are_deterministic() {
1645 let d = 16;
1646 let data = clustered(3000, d, 3);
1647 let params = BuildParams {
1648 seed: 9,
1649 ..BuildParams::default()
1650 };
1651 let a = QGraph::build(&data, d, ¶ms).expect("build");
1652 let b = QGraph::build(&data, d, ¶ms).expect("build");
1653 assert_eq!(a.entry, b.entry);
1654 assert!(a.blocks == b.blocks);
1655 let (mut sa, mut sb) = (a.searcher(), b.searcher());
1656 let (mut oa, mut ob) = (Vec::new(), Vec::new());
1657 let p = SearchParams {
1658 ef: 32,
1659 max_exact: 0,
1660 };
1661 for q in data.chunks_exact(d).take(50) {
1662 sa.search(q, 10, p, &mut oa);
1663 sb.search(q, 10, p, &mut ob);
1664 assert_eq!(oa, ob);
1665 }
1666 }
1667
1668 #[test]
1671 fn build_reproduces_the_pinned_graphs() {
1672 let cases = [
1673 (Metric::Angular, false, 16, 0x2b16_3721_02ba_f39b),
1674 (Metric::Euclidean, true, 128, 0xe014_c782_9bed_e9aa),
1675 ];
1676 for (metric, quantized_search, d, want) in cases {
1677 let data = clustered(2500, d, 11);
1678 let params = BuildParams {
1679 metric,
1680 quantized_search,
1681 degree: 16,
1682 build_ef: 48,
1683 seed: 7,
1684 ..BuildParams::default()
1685 };
1686 let g = Graph::build(&data, d, ¶ms).expect("build");
1687 assert_eq!(g.structure_hash(), want, "{metric:?}");
1688 }
1689 }
1690
1691 #[test]
1692 fn exact_budget_caps_work() {
1693 let d = 16;
1694 let data = clustered(2000, d, 4);
1695 let index = QGraph::build(&data, d, &BuildParams::default()).expect("build");
1696 let mut s = index.searcher();
1697 let mut out = Vec::new();
1698 let q = &data[..d];
1699 let capped = s.search(
1700 q,
1701 10,
1702 SearchParams {
1703 ef: 128,
1704 max_exact: 20,
1705 },
1706 &mut out,
1707 );
1708 assert_eq!(out.len(), 10);
1709 let free = s.search(
1710 q,
1711 10,
1712 SearchParams {
1713 ef: 128,
1714 max_exact: 0,
1715 },
1716 &mut out,
1717 );
1718 assert!(capped < free);
1719 assert_eq!(capped - index.descend(q).1, 20);
1720 }
1721}