1use alloc::vec::Vec;
34
35#[cfg(feature = "counters")]
36use core::cell::Cell;
37
38use plugmem_arena::{Arena, ArenaCfg, ChunkPool, ChunkPoolCfg, ListHandle, ShardMode, Slot, key};
39use xxhash_rust::xxh3::xxh3_64;
40
41use crate::error::Error;
42use crate::id::NONE_U32;
43use crate::index::vecpool::{VecPool, dot_i8};
44
45const MAX_LEVEL: usize = 16;
49
50const UPPER_SHARDS: usize = 64;
52
53const NEIGHBOR_BYTES: usize = core::mem::size_of::<u32>();
55
56const META_BYTES: usize = 2 * core::mem::size_of::<u32>();
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62struct UpperSlot {
63 slot: u32,
65 level: u32,
67 handle: ListHandle,
69}
70
71impl Slot for UpperSlot {
72 const SIZE: usize = 20;
73 const KEY_LEN: usize = 8;
74
75 fn write(&self, out: &mut [u8]) {
76 key::write_u32(out, self.slot);
77 key::write_u32(&mut out[4..], self.level);
78 out[8..20].copy_from_slice(&self.handle.to_bytes());
79 }
80
81 fn read(bytes: &[u8]) -> Self {
82 Self {
83 slot: key::read_u32(bytes),
84 level: key::read_u32(&bytes[4..]),
85 handle: ListHandle::from_bytes(bytes[8..20].try_into().unwrap()),
86 }
87 }
88}
89
90#[derive(Debug, Default)]
92pub struct HnswScratch {
93 visited: Vec<u32>,
95 epoch: u32,
97 cand: Vec<(f32, u32)>,
100 found: Vec<(f32, u32)>,
102 nbrs: Vec<u32>,
104 sel: Vec<u32>,
106 pruned: Vec<u32>,
108 relink: Vec<(f32, u32)>,
110}
111
112#[inline]
115fn better(a: (f32, u32), b: (f32, u32)) -> core::cmp::Ordering {
116 a.0.total_cmp(&b.0).then(b.1.cmp(&a.1))
117}
118
119pub struct HnswGraph<'a> {
126 m: usize,
128 m0: usize,
130 level0: Vec<u32>,
132 upper: Arena<'a, UpperSlot>,
134 lists: ChunkPool<'a>,
136 entry: u32,
138 indexed: u32,
140 thresholds: [u64; MAX_LEVEL],
143 #[cfg(feature = "counters")]
146 dist_evals: Cell<u64>,
147}
148
149impl<'a> HnswGraph<'a> {
150 pub fn new(m: usize, m0: usize, max_bytes: usize) -> Result<Self, Error> {
153 let mut thresholds = [0u64; MAX_LEVEL];
154 let mut t = u64::MAX;
155 for slot in &mut thresholds {
156 t /= m as u64;
157 *slot = t;
158 }
159 Ok(Self {
160 m,
161 m0,
162 level0: Vec::new(),
163 upper: Arena::new(
164 ArenaCfg::new(UPPER_SHARDS, ShardMode::Uniform).with_max_bytes(max_bytes),
165 )?,
166 lists: ChunkPool::new(ChunkPoolCfg::new().with_max_bytes(max_bytes)),
167 entry: NONE_U32,
168 indexed: 0,
169 thresholds,
170 #[cfg(feature = "counters")]
171 dist_evals: Cell::new(0),
172 })
173 }
174
175 pub fn indexed(&self) -> u32 {
177 self.indexed
178 }
179
180 fn level_of(&self, fact: u32) -> usize {
183 let h = xxh3_64(&fact.to_le_bytes());
184 self.thresholds.iter().take_while(|&&t| h < t).count()
185 }
186
187 #[inline]
189 fn sim_q(&self, pool: &VecPool<'_>, q: (f32, &[u8]), slot: u32) -> f32 {
190 #[cfg(feature = "counters")]
191 self.dist_evals.set(self.dist_evals.get() + 1);
192 let (s, qb) = pool.quant(slot as usize);
193 q.0 * s * dot_i8(q.1, qb) as f32
194 }
195
196 #[inline]
198 fn block(&self, slot: u32) -> &[u32] {
199 let at = slot as usize * self.m0;
200 &self.level0[at..at + self.m0]
201 }
202
203 fn neighbors_into(&self, slot: u32, level: usize, out: &mut Vec<u32>) {
205 out.clear();
206 if level == 0 {
207 out.extend(
208 self.block(slot)
209 .iter()
210 .copied()
211 .take_while(|&n| n != NONE_U32),
212 );
213 return;
214 }
215 let mut kb = [0u8; 8];
216 key::write_u32(&mut kb, slot);
217 key::write_u32(&mut kb[4..], level as u32);
218 let Some(entry) = self.upper.get(&kb) else {
219 return;
220 };
221 for chunk in self.lists.iter(&entry.handle) {
222 for raw in chunk.chunks_exact(4) {
223 out.push(u32::from_le_bytes(raw.try_into().unwrap()));
224 }
225 }
226 }
227
228 #[inline]
230 fn visit(scratch: &mut HnswScratch, slot: u32) -> bool {
231 let at = slot as usize;
232 if scratch.visited[at] == scratch.epoch {
233 return true;
234 }
235 scratch.visited[at] = scratch.epoch;
236 false
237 }
238
239 fn search_layer(
243 &self,
244 pool: &VecPool<'_>,
245 q: (f32, &[u8]),
246 level: usize,
247 ep: u32,
248 ef: usize,
249 scratch: &mut HnswScratch,
250 ) {
251 scratch.epoch = scratch.epoch.wrapping_add(1);
252 if scratch.epoch == 0 {
253 scratch.visited.fill(u32::MAX);
255 scratch.epoch = 1;
256 }
257 scratch
258 .visited
259 .resize(self.indexed as usize, scratch.epoch.wrapping_sub(1));
260 scratch.cand.clear();
261 scratch.found.clear();
262
263 Self::visit(scratch, ep);
264 let s = self.sim_q(pool, q, ep);
265 scratch.cand.push((s, ep));
266 scratch.found.push((s, ep));
267
268 while let Some(best) = scratch.cand.pop() {
269 if scratch.found.len() >= ef && better(best, scratch.found[0]).is_lt() {
272 break;
273 }
274 let nbrs = core::mem::take(&mut scratch.nbrs);
275 let mut nbrs = nbrs;
276 self.neighbors_into(best.1, level, &mut nbrs);
277 for &nb in &nbrs {
278 if Self::visit(scratch, nb) {
279 continue;
280 }
281 let s = self.sim_q(pool, q, nb);
282 let entry = (s, nb);
283 if scratch.found.len() < ef || better(entry, scratch.found[0]).is_gt() {
284 let at = scratch.found.partition_point(|&e| better(e, entry).is_lt());
285 scratch.found.insert(at, entry);
286 if scratch.found.len() > ef {
287 scratch.found.remove(0);
288 }
289 let at = scratch.cand.partition_point(|&e| better(e, entry).is_lt());
290 scratch.cand.insert(at, entry);
291 }
292 }
293 scratch.nbrs = nbrs;
294 }
295 }
296
297 fn select_neighbors(&self, pool: &VecPool<'_>, cap: usize, scratch: &mut HnswScratch) {
302 scratch.sel.clear();
303 scratch.pruned.clear();
304 for i in (0..scratch.found.len()).rev() {
305 let (sim, cand) = scratch.found[i];
306 if scratch.sel.len() >= cap {
307 break;
308 }
309 let dominated = scratch.sel.iter().any(|&kept| {
310 #[cfg(feature = "counters")]
311 self.dist_evals.set(self.dist_evals.get() + 1);
312 pool.sim(cand, kept) > sim
313 });
314 if dominated {
315 scratch.pruned.push(cand);
316 } else {
317 scratch.sel.push(cand);
318 }
319 }
320 for &p in scratch.pruned.iter() {
321 if scratch.sel.len() >= cap {
322 break;
323 }
324 scratch.sel.push(p);
325 }
326 }
327
328 fn write_list(&mut self, slot: u32, level: usize, sel: &[u32]) -> Result<(), Error> {
331 if level == 0 {
332 let at = slot as usize * self.m0;
333 let block = &mut self.level0[at..at + self.m0];
334 block.fill(NONE_U32);
335 block[..sel.len()].copy_from_slice(sel);
336 return Ok(());
337 }
338 let mut kb = [0u8; 8];
339 key::write_u32(&mut kb, slot);
340 key::write_u32(&mut kb[4..], level as u32);
341 let mut handle = match self.upper.get(&kb) {
342 Some(entry) => {
343 let mut h = entry.handle;
344 self.lists.free(&mut h);
345 h
346 }
347 None => ListHandle::EMPTY,
348 };
349 for &n in sel {
350 self.lists.push(&mut handle, &n.to_le_bytes())?;
351 }
352 let updated = UpperSlot {
353 slot,
354 level: level as u32,
355 handle,
356 };
357 if self.upper.contains(&kb) {
358 let payload = self.upper.payload_mut(&kb).expect("checked above");
359 let mut full = [0u8; UpperSlot::SIZE];
360 updated.write(&mut full);
361 payload.copy_from_slice(&full[UpperSlot::KEY_LEN..]);
362 } else {
363 self.upper.insert(&updated)?;
364 }
365 Ok(())
366 }
367
368 fn add_link(
371 &mut self,
372 pool: &VecPool<'_>,
373 v: u32,
374 new: u32,
375 level: usize,
376 scratch: &mut HnswScratch,
377 ) -> Result<(), Error> {
378 let cap = if level == 0 { self.m0 } else { self.m };
379 let mut nbrs = core::mem::take(&mut scratch.nbrs);
380 self.neighbors_into(v, level, &mut nbrs);
381 if nbrs.len() < cap {
382 nbrs.push(new);
383 let sel = core::mem::take(&mut scratch.sel);
384 let mut sel = sel;
385 sel.clear();
386 sel.extend_from_slice(&nbrs);
387 let res = self.write_list(v, level, &sel);
388 scratch.sel = sel;
389 scratch.nbrs = nbrs;
390 return res;
391 }
392 let vq = pool.quant(v as usize);
394 scratch.relink.clear();
395 for &n in nbrs.iter().chain(core::iter::once(&new)) {
396 let s = self.sim_q(pool, (vq.0, vq.1), n);
397 scratch.relink.push((s, n));
398 }
399 scratch.nbrs = nbrs;
400 scratch.relink.sort_unstable_by(|a, b| better(*a, *b));
401 scratch.found.clear();
402 scratch.found.extend_from_slice(&scratch.relink);
403 self.select_neighbors(pool, cap, scratch);
404 let sel = core::mem::take(&mut scratch.sel);
405 let res = self.write_list(v, level, &sel);
406 scratch.sel = sel;
407 res
408 }
409
410 pub fn insert_bulk(
414 &mut self,
415 pool: &VecPool<'_>,
416 upto: u32,
417 ef_construction: usize,
418 scratch: &mut HnswScratch,
419 ) -> Result<(), Error> {
420 debug_assert!(upto as usize <= pool.len());
421 self.level0.resize(upto as usize * self.m0, NONE_U32);
422 for slot in self.indexed..upto {
423 self.insert_one(pool, slot, ef_construction, scratch)?;
424 self.indexed = slot + 1;
427 }
428 Ok(())
429 }
430
431 fn insert_one(
432 &mut self,
433 pool: &VecPool<'_>,
434 slot: u32,
435 ef_construction: usize,
436 scratch: &mut HnswScratch,
437 ) -> Result<(), Error> {
438 let level = self.level_of(pool.slot_fact(slot as usize));
439 if self.entry == NONE_U32 {
440 self.entry = slot;
441 return Ok(());
442 }
443 let q = pool.quant(slot as usize);
444 let q = (q.0, q.1);
445 let top = self.level_of(pool.slot_fact(self.entry as usize));
446 let mut ep = self.entry;
447 let mut lev = top;
449 while lev > level {
450 self.search_layer(pool, q, lev, ep, 1, scratch);
451 ep = scratch.found.last().expect("entry is always found").1;
452 lev -= 1;
453 }
454 let mut lev = level.min(top);
456 loop {
457 self.search_layer(pool, q, lev, ep, ef_construction, scratch);
458 ep = scratch.found.last().expect("entry is always found").1;
459 let cap = if lev == 0 { self.m0 } else { self.m };
460 self.select_neighbors(pool, cap, scratch);
461 let sel = core::mem::take(&mut scratch.sel);
462 self.write_list(slot, lev, &sel)?;
463 for &nb in &sel {
464 self.add_link(pool, nb, slot, lev, scratch)?;
465 }
466 scratch.sel = sel;
467 if lev == 0 {
468 break;
469 }
470 lev -= 1;
471 }
472 if level > top {
473 self.entry = slot;
474 }
475 Ok(())
476 }
477
478 pub fn search(
488 &self,
489 pool: &VecPool<'_>,
490 query: &[f32],
491 ef: usize,
492 vec_scratch: &mut crate::index::vecpool::VecScratch,
493 scratch: &mut HnswScratch,
494 out: &mut Vec<(u32, f32)>,
495 ) -> Result<(), Error> {
496 pool.quantize_query(query, vec_scratch)?;
497 let q = pool.quantized(vec_scratch);
498 self.search_quantized(pool, q, ef, scratch, out);
499 Ok(())
500 }
501
502 pub(crate) fn search_quantized(
506 &self,
507 pool: &VecPool<'_>,
508 q: (f32, &[u8]),
509 ef: usize,
510 scratch: &mut HnswScratch,
511 out: &mut Vec<(u32, f32)>,
512 ) {
513 out.clear();
514 if self.entry == NONE_U32 {
515 return;
516 }
517 let mut ep = self.entry;
518 let top = self.level_of(pool.slot_fact(self.entry as usize));
519 for lev in (1..=top).rev() {
520 self.search_layer(pool, q, lev, ep, 1, scratch);
521 ep = scratch.found.last().expect("entry is always found").1;
522 }
523 self.search_layer(pool, q, 0, ep, ef.max(1), scratch);
524 for &(sim, slot) in scratch.found.iter().rev() {
525 out.push((slot, sim));
526 }
527 }
528
529 pub(crate) fn pool_bytes(&self) -> usize {
531 self.level0.len() * NEIGHBOR_BYTES + self.upper.pool_bytes() + self.lists.pool_bytes()
532 }
533
534 pub(crate) fn remapped(
540 &self,
541 map: &[u32],
542 new_pool: &VecPool<'_>,
543 max_bytes: usize,
544 ) -> Result<HnswGraph<'static>, Error> {
545 let mut g: HnswGraph<'static> = HnswGraph::new(self.m, self.m0, max_bytes)?;
548 let old_indexed = self.indexed as usize;
549 let new_indexed = map[..old_indexed]
550 .iter()
551 .filter(|&&m| m != NONE_U32)
552 .count() as u32;
553 g.level0 = alloc::vec![NONE_U32; new_indexed as usize * self.m0];
554 g.indexed = new_indexed;
555 let mut nbrs = Vec::new();
556 let mut sel = Vec::new();
557 for old in 0..old_indexed as u32 {
558 let new = map[old as usize];
559 if new == NONE_U32 {
560 continue;
561 }
562 self.neighbors_into(old, 0, &mut nbrs);
563 sel.clear();
564 sel.extend(
565 nbrs.iter()
566 .map(|&n| map[n as usize])
567 .filter(|&n| n != NONE_U32),
568 );
569 g.write_list(new, 0, &sel)?;
570 let levels = g.level_of(new_pool.slot_fact(new as usize));
571 for level in 1..=levels {
572 self.neighbors_into(old, level, &mut nbrs);
573 if nbrs.is_empty() {
574 continue;
575 }
576 sel.clear();
577 sel.extend(
578 nbrs.iter()
579 .map(|&n| map[n as usize])
580 .filter(|&n| n != NONE_U32),
581 );
582 g.write_list(new, level, &sel)?;
583 }
584 }
585 g.entry = if self.entry != NONE_U32 && map[self.entry as usize] != NONE_U32 {
586 map[self.entry as usize]
587 } else {
588 let mut best = NONE_U32;
591 let mut best_level = 0usize;
592 for slot in 0..new_indexed {
593 let level = g.level_of(new_pool.slot_fact(slot as usize));
594 if best == NONE_U32 || level > best_level {
595 best = slot;
596 best_level = level;
597 }
598 }
599 best
600 };
601 Ok(g)
602 }
603
604 pub(crate) fn dump_meta(&self) -> Vec<u8> {
606 let mut out = Vec::with_capacity(META_BYTES);
607 out.extend_from_slice(&self.entry.to_le_bytes());
608 out.extend_from_slice(&self.indexed.to_le_bytes());
609 out
610 }
611
612 pub(crate) fn dump_level0(&self) -> Vec<u8> {
614 let mut out = Vec::with_capacity(self.level0.len() * NEIGHBOR_BYTES);
615 for &n in &self.level0 {
616 out.extend_from_slice(&n.to_le_bytes());
617 }
618 out
619 }
620
621 pub(crate) fn dump_upper(&self) -> [Vec<u8>; 4] {
623 let (mut am, mut ap) = (Vec::new(), Vec::new());
624 self.upper.dump_meta(&mut am);
625 self.upper.dump_pool(&mut ap);
626 let (mut cm, mut cp) = (Vec::new(), Vec::new());
627 self.lists.dump_meta(&mut cm);
628 self.lists.dump_pool(&mut cp);
629 [am, ap, cm, cp]
630 }
631
632 #[allow(clippy::too_many_arguments)]
636 pub(crate) fn from_parts(
637 m: usize,
638 m0: usize,
639 max_bytes: usize,
640 meta: &[u8],
641 level0: &[u8],
642 upper_meta: &[u8],
643 upper_pool: &[u8],
644 lists_meta: &[u8],
645 lists_pool: &[u8],
646 ) -> Result<Self, Error> {
647 let mut g = Self::new(m, m0, max_bytes)?;
648 if meta.len() != META_BYTES {
649 return Err(Error::Corrupt("hnsw meta section has a wrong length"));
650 }
651 g.entry = u32::from_le_bytes(meta[0..4].try_into().unwrap());
652 g.indexed = u32::from_le_bytes(meta[4..8].try_into().unwrap());
653 if level0.len() as u64 != u64::from(g.indexed) * m0 as u64 * NEIGHBOR_BYTES as u64 {
654 return Err(Error::Corrupt("hnsw level0 length mismatch"));
655 }
656 g.level0 = level0
657 .chunks_exact(NEIGHBOR_BYTES)
658 .map(|b| u32::from_le_bytes(b.try_into().unwrap()))
659 .collect();
660 g.upper = Arena::load(
661 ArenaCfg::new(UPPER_SHARDS, ShardMode::Uniform).with_max_bytes(max_bytes),
662 upper_meta,
663 upper_pool,
664 )?;
665 g.lists = ChunkPool::load(
666 ChunkPoolCfg::new().with_max_bytes(max_bytes),
667 lists_meta,
668 lists_pool,
669 )?;
670 Ok(g)
671 }
672
673 #[allow(clippy::too_many_arguments)]
679 pub(crate) fn from_parts_borrowed(
680 m: usize,
681 m0: usize,
682 max_bytes: usize,
683 meta: &[u8],
684 level0: &[u8],
685 upper_meta: &[u8],
686 upper_pool: &'a [u8],
687 lists_meta: &[u8],
688 lists_pool: &'a [u8],
689 ) -> Result<Self, Error> {
690 let mut g = Self::new(m, m0, max_bytes)?;
691 if meta.len() != META_BYTES {
692 return Err(Error::Corrupt("hnsw meta section has a wrong length"));
693 }
694 g.entry = u32::from_le_bytes(meta[0..4].try_into().unwrap());
695 g.indexed = u32::from_le_bytes(meta[4..8].try_into().unwrap());
696 if level0.len() as u64 != u64::from(g.indexed) * m0 as u64 * NEIGHBOR_BYTES as u64 {
697 return Err(Error::Corrupt("hnsw level0 length mismatch"));
698 }
699 g.level0 = level0
700 .chunks_exact(NEIGHBOR_BYTES)
701 .map(|b| u32::from_le_bytes(b.try_into().unwrap()))
702 .collect();
703 g.upper = Arena::load_borrowed(
704 ArenaCfg::new(UPPER_SHARDS, ShardMode::Uniform).with_max_bytes(max_bytes),
705 upper_meta,
706 upper_pool,
707 )?;
708 g.lists = ChunkPool::load_borrowed(
709 ChunkPoolCfg::new().with_max_bytes(max_bytes),
710 lists_meta,
711 lists_pool,
712 )?;
713 Ok(g)
714 }
715
716 pub(crate) fn validate(&self, pool: &VecPool<'_>) -> Result<(), Error> {
723 if self.indexed as usize > pool.len() {
724 return Err(Error::Corrupt("hnsw indexes more slots than the pool"));
725 }
726 if self.level0.len() != self.indexed as usize * self.m0 {
727 return Err(Error::Corrupt("hnsw level0 disagrees with indexed"));
728 }
729 if self.indexed == 0 {
730 if self.entry != NONE_U32 || !self.upper.is_empty() || self.lists.chunks() != 0 {
731 return Err(Error::Corrupt("hnsw empty graph carries state"));
732 }
733 return Ok(());
734 }
735 if self.entry >= self.indexed {
736 return Err(Error::Corrupt("hnsw entry out of range"));
737 }
738 for slot in 0..self.indexed {
739 let block = self.block(slot);
740 let mut ended = false;
741 for &n in block {
742 if n == NONE_U32 {
743 ended = true;
744 continue;
745 }
746 if ended {
747 return Err(Error::Corrupt("hnsw level0 padding is not canonical"));
748 }
749 if n >= self.indexed || n == slot {
750 return Err(Error::Corrupt("hnsw level0 neighbor out of range"));
751 }
752 }
753 }
754 let mut visited = alloc::vec![false; self.lists.chunks()];
755 for entry in self.upper.iter() {
756 if entry.slot >= self.indexed {
757 return Err(Error::Corrupt("hnsw upper handle out of range"));
758 }
759 let max_level = self.level_of(pool.slot_fact(entry.slot as usize));
760 if entry.level == 0 || entry.level as usize > max_level {
761 return Err(Error::Corrupt("hnsw upper level disagrees with the hash"));
762 }
763 self.lists.validate_chain(&entry.handle, &mut visited)?;
764 let mut count = 0u32;
765 for chunk in self.lists.iter(&entry.handle) {
766 if !chunk.len().is_multiple_of(4) {
767 return Err(Error::Corrupt("hnsw upper list is not a slot sequence"));
768 }
769 for raw in chunk.chunks_exact(4) {
770 let n = u32::from_le_bytes(raw.try_into().unwrap());
771 if n >= self.indexed || n == entry.slot {
772 return Err(Error::Corrupt("hnsw upper neighbor out of range"));
773 }
774 count += 1;
775 }
776 }
777 if count != entry.handle.len() || count as usize > self.m {
778 return Err(Error::Corrupt("hnsw upper list disagrees with its handle"));
779 }
780 }
781 if self.lists.orphan_count(&visited) != 0 {
782 return Err(Error::Corrupt("hnsw list pool has orphan chunks"));
783 }
784 Ok(())
785 }
786
787 #[cfg(feature = "counters")]
789 pub fn dist_evals(&self) -> u64 {
790 self.dist_evals.get()
791 }
792
793 #[cfg(feature = "counters")]
795 pub fn reset_dist_evals(&self) {
796 self.dist_evals.set(0);
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803 use crate::id::FactId;
804 use alloc::vec;
805
806 struct Lcg(u64);
808 impl Lcg {
809 fn next(&mut self) -> f32 {
810 self.0 = self
811 .0
812 .wrapping_mul(6_364_136_223_846_793_005)
813 .wrapping_add(1_442_695_040_888_963_407);
814 ((self.0 >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0
815 }
816 }
817
818 fn cluster_pool(n: usize, dim: usize, clusters: usize, seed: u64) -> VecPool<'static> {
820 let mut rng = Lcg(seed);
821 let centers: Vec<Vec<f32>> = (0..clusters)
822 .map(|_| (0..dim).map(|_| rng.next()).collect())
823 .collect();
824 let mut pool = VecPool::new(dim, usize::MAX);
825 for i in 0..n {
826 let c = ¢ers[i % clusters];
827 let v: Vec<f32> = c.iter().map(|&x| x + rng.next() * 0.3).collect();
828 pool.push(FactId(i as u32), &v).unwrap();
829 }
830 pool
831 }
832
833 fn build(pool: &VecPool<'_>, m: usize, m0: usize) -> HnswGraph<'static> {
835 let mut g = HnswGraph::new(m, m0, usize::MAX).unwrap();
836 let mut scratch = HnswScratch::default();
837 g.insert_bulk(pool, pool.len() as u32, 200, &mut scratch)
838 .unwrap();
839 g
840 }
841
842 fn brute_force(pool: &VecPool<'_>, q: u32, k: usize) -> Vec<u32> {
844 let mut all: Vec<(f32, u32)> = (0..pool.len() as u32)
845 .map(|i| (pool.sim(q, i), i))
846 .collect();
847 all.sort_unstable_by(|a, b| better(*b, *a));
848 all.into_iter().take(k).map(|(_, s)| s).collect()
849 }
850
851 #[test]
854 #[cfg_attr(miri, ignore)] fn levels_are_geometric_and_pure() {
856 let g = HnswGraph::new(16, 32, usize::MAX).unwrap();
857 let n = 100_000u32;
858 let mut per_level = [0usize; 4];
859 for fact in 0..n {
860 let l = g.level_of(fact).min(3);
861 per_level[l] += 1;
862 }
863 let at_least_1: usize = per_level[1..].iter().sum();
865 assert!(
866 (4_000..9_000).contains(&at_least_1),
867 "level>=1 count {at_least_1} is out of band"
868 );
869 let at_least_2: usize = per_level[2..].iter().sum();
870 assert!(
871 (150..800).contains(&at_least_2),
872 "level>=2 count {at_least_2} is out of band"
873 );
874 assert_eq!(g.level_of(42), g.level_of(42));
876 }
877
878 #[test]
881 #[cfg_attr(miri, ignore)] fn recall_against_brute_force() {
883 let dim = 32;
884 let pool = cluster_pool(2_000, dim, 64, 0xA11CE);
885 let g = build(&pool, 16, 32);
886 let mut scratch = HnswScratch::default();
887 let mut out = Vec::new();
888 let mut hits = 0usize;
889 let mut total = 0usize;
890 for q in (0..2_000u32).step_by(97) {
891 let truth = brute_force(&pool, q, 10);
892 let (scale, qb) = pool.quant(q as usize);
893 g.search_quantized(&pool, (scale, qb), 64, &mut scratch, &mut out);
894 let got: Vec<u32> = out.iter().take(10).map(|&(s, _)| s).collect();
895 hits += truth.iter().filter(|t| got.contains(t)).count();
896 total += truth.len();
897 }
898 let recall = hits as f64 / total as f64;
899 assert!(recall >= 0.9, "recall@10 {recall} below the 0.9 gate");
900 }
901
902 #[test]
905 #[cfg_attr(miri, ignore)] fn build_is_deterministic() {
907 let pool = cluster_pool(600, 24, 16, 7);
908 let a = build(&pool, 8, 16);
909 let b = build(&pool, 8, 16);
910 assert_eq!(a.level0, b.level0);
911 assert_eq!(a.entry, b.entry);
912 assert_eq!(a.indexed, b.indexed);
913 let (mut am, mut bm) = (Vec::new(), Vec::new());
914 a.upper.dump_meta(&mut am);
915 b.upper.dump_meta(&mut bm);
916 assert_eq!(am, bm);
917 let (mut ap, mut bp) = (Vec::new(), Vec::new());
918 a.lists.dump_pool(&mut ap);
919 b.lists.dump_pool(&mut bp);
920 assert_eq!(ap, bp);
921 }
922
923 #[test]
925 #[cfg_attr(miri, ignore)] fn degree_caps_hold() {
927 let pool = cluster_pool(800, 16, 8, 3);
928 let g = build(&pool, 6, 12);
929 let mut nbrs = Vec::new();
930 for slot in 0..g.indexed() {
931 g.neighbors_into(slot, 0, &mut nbrs);
932 assert!(nbrs.len() <= 12);
933 assert!(!nbrs.contains(&slot));
935 let mut sorted = nbrs.clone();
936 sorted.sort_unstable();
937 sorted.dedup();
938 assert_eq!(sorted.len(), nbrs.len());
939 assert!(nbrs.iter().all(|&n| n < g.indexed()));
940 for level in 1..=g.level_of(pool.slot_fact(slot as usize)) {
941 g.neighbors_into(slot, level, &mut nbrs);
942 assert!(nbrs.len() <= 6, "level {level} degree overflow");
943 }
944 }
945 }
946
947 #[test]
951 #[cfg_attr(miri, ignore)] fn remap_survives_a_dead_entry() {
953 let pool = cluster_pool(300, 16, 8, 21);
954 let g = build(&pool, 6, 12);
955 let entry = g.entry;
956 let mut map = alloc::vec![NONE_U32; 300];
959 let mut new_pool = VecPool::new(16, usize::MAX);
960 let mut next = 0u32;
961 for old in 0..300u32 {
962 if old == entry || old % 7 == 0 {
963 continue;
964 }
965 map[old as usize] = next;
966 new_pool.copy_slot(&pool, old);
967 next += 1;
968 }
969 let remapped = g.remapped(&map, &new_pool, usize::MAX).unwrap();
970 assert_eq!(remapped.indexed(), next);
971 assert_ne!(remapped.entry, NONE_U32, "a survivor takes the entry");
972 remapped.validate(&new_pool).unwrap();
973 let probe_old = 1u32; let probe_old = if probe_old == entry { 2 } else { probe_old };
976 let probe_new = map[probe_old as usize];
977 let (scale, qb) = new_pool.quant(probe_new as usize);
978 let mut scratch = HnswScratch::default();
979 let mut out = Vec::new();
980 remapped.search_quantized(&new_pool, (scale, qb), 32, &mut scratch, &mut out);
981 assert_eq!(out[0].0, probe_new);
982 }
983
984 #[test]
988 fn validate_rejects_malformed_graphs() {
989 let pool = cluster_pool(50, 8, 4, 5);
990 let g = build(&pool, 4, 8);
991 let dump = (g.dump_meta(), g.dump_level0(), g.dump_upper());
992 let load = |meta: &[u8], level0: &[u8]| {
993 HnswGraph::from_parts(
994 4,
995 8,
996 usize::MAX,
997 meta,
998 level0,
999 &dump.2[0],
1000 &dump.2[1],
1001 &dump.2[2],
1002 &dump.2[3],
1003 )
1004 };
1005 load(&dump.0, &dump.1).unwrap().validate(&pool).unwrap();
1007 let mut meta = dump.0.clone();
1009 meta[0..4].copy_from_slice(&999u32.to_le_bytes());
1010 assert!(load(&meta, &dump.1).unwrap().validate(&pool).is_err());
1011 let mut level0 = dump.1.clone();
1013 level0[0..4].copy_from_slice(&500u32.to_le_bytes());
1014 assert!(load(&dump.0, &level0).unwrap().validate(&pool).is_err());
1015 let mut level0 = dump.1.clone();
1017 level0[0..4].copy_from_slice(&NONE_U32.to_le_bytes());
1018 level0[4..8].copy_from_slice(&1u32.to_le_bytes());
1019 assert!(load(&dump.0, &level0).unwrap().validate(&pool).is_err());
1020 assert!(load(&dump.0, &dump.1[..dump.1.len() - 4]).is_err());
1022 let mut meta = dump.0.clone();
1024 meta[4..8].copy_from_slice(&0u32.to_le_bytes());
1025 assert!(load(&meta, &[]).unwrap().validate(&pool).is_err());
1026 }
1027
1028 #[test]
1030 fn tiny_graphs() {
1031 let pool = cluster_pool(1, 8, 1, 1);
1032 let mut g = HnswGraph::new(4, 8, usize::MAX).unwrap();
1033 let mut scratch = HnswScratch::default();
1034 let mut out = vec![(0u32, 0.0f32)];
1035 let (scale, qb) = pool.quant(0);
1036 g.search_quantized(&pool, (scale, qb), 8, &mut scratch, &mut out);
1037 assert!(out.is_empty(), "an empty graph must answer empty");
1038 g.insert_bulk(&pool, 1, 50, &mut scratch).unwrap();
1039 g.search_quantized(&pool, (scale, qb), 8, &mut scratch, &mut out);
1040 assert_eq!(out.len(), 1);
1041 assert_eq!(out[0].0, 0);
1042 }
1043}