1use std::cmp::Reverse;
9use std::collections::BinaryHeap;
10use std::sync::Arc;
11
12use crate::dsl::IvfRoutingMode;
13use crate::structures::vector::progress::PhaseProgress;
14use rand::prelude::*;
15use serde::{Deserialize, Serialize};
16
17pub const HNSW_AUTO_THRESHOLD: usize = 4_096;
42
43pub const BINARY_HNSW_AUTO_THRESHOLD: usize = 32_768;
66
67pub const HIERARCHICAL_TRAINING_THRESHOLD: usize = 4_096;
74
75pub const DEFAULT_PARENT_BEAM_OVERSAMPLE: usize = 4;
79pub const MAX_PARENT_BEAM_OVERSAMPLE: usize = 16;
86const MIN_BUILD_PARENT_BEAM: usize = 4;
89
90const HNSW_M: usize = 32;
91const HNSW_EF_CONSTRUCTION: usize = 200;
92const HNSW_QUERY_OVERSAMPLE: usize = 4;
93const HNSW_MIN_EF_SEARCH: usize = 128;
94const HNSW_BUILD_OVERSAMPLE: usize = 8;
99pub(crate) const HNSW_MIN_EF_BUILD: usize = 128;
109
110const NEIGHBOR_BLOCK: usize = HNSW_M * 4;
114
115pub trait QueryDistance {
122 fn distance(&self, node: u32) -> f32;
123
124 fn distances(&self, nodes: &[u32], out: &mut [f32]) {
126 debug_assert_eq!(nodes.len(), out.len());
127 for (slot, &node) in out.iter_mut().zip(nodes) {
128 *slot = self.distance(node);
129 }
130 }
131}
132
133impl<F: Fn(u32) -> f32> QueryDistance for F {
134 #[inline]
135 fn distance(&self, node: u32) -> f32 {
136 self(node)
137 }
138}
139
140pub trait PairDistance {
142 fn distance(&self, left: u32, right: u32) -> f32;
143
144 fn distances_from(&self, left: u32, rights: &[u32], out: &mut [f32]) {
146 debug_assert_eq!(rights.len(), out.len());
147 for (slot, &right) in out.iter_mut().zip(rights) {
148 *slot = self.distance(left, right);
149 }
150 }
151}
152
153impl<F: Fn(u32, u32) -> f32> PairDistance for F {
154 #[inline]
155 fn distance(&self, left: u32, right: u32) -> f32 {
156 self(left, right)
157 }
158}
159
160struct PairQueryDistance<'a, P: ?Sized> {
163 pair: &'a P,
164 left: u32,
165}
166
167impl<P: PairDistance + ?Sized> QueryDistance for PairQueryDistance<'_, P> {
168 #[inline]
169 fn distance(&self, node: u32) -> f32 {
170 self.pair.distance(self.left, node)
171 }
172
173 #[inline]
174 fn distances(&self, nodes: &[u32], out: &mut [f32]) {
175 self.pair.distances_from(self.left, nodes, out);
176 }
177}
178
179#[derive(Clone, Copy, Debug)]
180struct GraphCandidate {
181 node: u32,
182 distance: f32,
183}
184
185impl PartialEq for GraphCandidate {
186 fn eq(&self, other: &Self) -> bool {
187 self.node == other.node && self.distance.to_bits() == other.distance.to_bits()
188 }
189}
190
191impl Eq for GraphCandidate {}
192
193impl PartialOrd for GraphCandidate {
194 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
195 Some(self.cmp(other))
196 }
197}
198
199impl Ord for GraphCandidate {
200 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
201 self.distance
202 .total_cmp(&other.distance)
203 .then_with(|| self.node.cmp(&other.node))
204 }
205}
206
207struct VisitedNodes {
208 epochs: Vec<u32>,
209 current: u32,
210}
211
212impl VisitedNodes {
213 fn new(nodes: usize) -> Self {
214 Self {
215 epochs: vec![0; nodes],
216 current: 0,
217 }
218 }
219
220 fn reset(&mut self) {
221 self.current = self.current.wrapping_add(1);
222 if self.current == 0 {
223 self.epochs.fill(0);
224 self.current = 1;
225 }
226 }
227
228 fn ensure_nodes(&mut self, nodes: usize) {
229 if self.epochs.len() < nodes {
230 self.epochs.resize(nodes, 0);
231 }
232 }
233
234 fn insert(&mut self, node: u32) -> bool {
235 let slot = &mut self.epochs[node as usize];
236 if *slot == self.current {
237 false
238 } else {
239 *slot = self.current;
240 true
241 }
242 }
243}
244
245struct HnswQueryScratch {
246 visited: VisitedNodes,
247 candidates: BinaryHeap<Reverse<GraphCandidate>>,
248 best: BinaryHeap<GraphCandidate>,
249 ordered: Vec<GraphCandidate>,
250 pending: Vec<u32>,
253 pending_distances: Vec<f32>,
254}
255
256impl HnswQueryScratch {
257 fn new() -> Self {
258 Self {
259 visited: VisitedNodes::new(0),
260 candidates: BinaryHeap::new(),
261 best: BinaryHeap::new(),
262 ordered: Vec::new(),
263 pending: Vec::new(),
264 pending_distances: Vec::new(),
265 }
266 }
267}
268
269thread_local! {
270 static HNSW_QUERY_SCRATCH: std::cell::RefCell<HnswQueryScratch> =
274 std::cell::RefCell::new(HnswQueryScratch::new());
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct HnswRoutingGraph {
282 m: u16,
283 ef_construction: u32,
284 entry_point: u32,
285 max_level: u8,
286 node_levels: Vec<u8>,
287 node_offsets: Vec<u32>,
290 level_offsets: Vec<u32>,
291 neighbors: Vec<u32>,
292}
293
294impl HnswRoutingGraph {
295 pub fn build(
296 node_count: usize,
297 distance: impl PairDistance,
298 seed: u64,
299 index_label: &str,
300 ) -> Self {
301 assert!(node_count > 0 && node_count <= u32::MAX as usize);
302 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
303 let level_multiplier = 1.0 / (HNSW_M as f64).ln();
304 let node_levels: Vec<u8> = (0..node_count)
305 .map(|_| {
306 let uniform = rng.random::<f64>().clamp(f64::MIN_POSITIVE, 1.0);
307 (-uniform.ln() * level_multiplier).floor().min(31.0) as u8
308 })
309 .collect();
310 let mut insertion_order: Vec<u32> = (0..node_count as u32).collect();
311 insertion_order.shuffle(&mut rng);
312 let mut links: Vec<Vec<Vec<u32>>> = node_levels
313 .iter()
314 .map(|&level| vec![Vec::new(); level as usize + 1])
315 .collect();
316 let mut visited = VisitedNodes::new(node_count);
317 let mut entry_point = insertion_order[0];
318 let mut max_level = node_levels[entry_point as usize];
319
320 let mut progress = PhaseProgress::start(
321 index_label,
322 "hnsw graph build",
323 format!("{node_count} centroids, M={HNSW_M}, ef={HNSW_EF_CONSTRUCTION}"),
324 node_count,
325 );
326 for (inserted, &node) in insertion_order.iter().enumerate().skip(1) {
327 progress.advance(inserted);
328 let node_level = node_levels[node as usize];
329 let mut entry = entry_point;
330 let node_distance = PairQueryDistance {
331 pair: &distance,
332 left: node,
333 };
334
335 for level in ((node_level as usize + 1)..=max_level as usize).rev() {
336 entry = greedy_search_level(&links, entry, level, &node_distance);
337 }
338
339 for level in (0..=usize::min(node_level as usize, max_level as usize)).rev() {
340 let candidates = search_graph_layer(
341 &links,
342 entry,
343 level,
344 HNSW_EF_CONSTRUCTION,
345 &node_distance,
346 &mut visited,
347 );
348 if let Some(best) = candidates.first() {
349 entry = best.node;
350 }
351 let max_connections = if level == 0 { HNSW_M * 2 } else { HNSW_M };
352 let selected =
353 select_diverse_neighbors(node, candidates, max_connections, &distance);
354 links[node as usize][level] = selected.clone();
355 for neighbor in selected {
356 let adjacency = &mut links[neighbor as usize][level];
357 if !adjacency.contains(&node) {
358 adjacency.push(node);
359 }
360 if adjacency.len() > max_connections {
361 let candidates = adjacency
362 .iter()
363 .copied()
364 .map(|candidate| GraphCandidate {
365 node: candidate,
366 distance: distance.distance(neighbor, candidate),
367 })
368 .collect();
369 *adjacency = select_diverse_neighbors(
370 neighbor,
371 candidates,
372 max_connections,
373 &distance,
374 );
375 }
376 }
377 }
378
379 if node_level > max_level {
380 entry_point = node;
381 max_level = node_level;
382 }
383 }
384 progress.finish();
385
386 Self::compact(
387 HNSW_M,
388 HNSW_EF_CONSTRUCTION,
389 entry_point,
390 max_level,
391 node_levels,
392 links,
393 )
394 }
395
396 fn compact(
397 m: usize,
398 ef_construction: usize,
399 entry_point: u32,
400 max_level: u8,
401 node_levels: Vec<u8>,
402 links: Vec<Vec<Vec<u32>>>,
403 ) -> Self {
404 let mut node_offsets = Vec::with_capacity(links.len() + 1);
405 let level_count: usize = links.iter().map(|levels| levels.len() + 1).sum();
406 let neighbor_count: usize = links
407 .iter()
408 .flat_map(|levels| levels.iter())
409 .map(Vec::len)
410 .sum();
411 let mut level_offsets = Vec::with_capacity(level_count);
412 let mut neighbors = Vec::with_capacity(neighbor_count);
413 for levels in links {
414 node_offsets.push(level_offsets.len() as u32);
415 for mut adjacency in levels {
416 adjacency.sort_unstable();
417 adjacency.dedup();
418 level_offsets.push(neighbors.len() as u32);
419 neighbors.extend(adjacency);
420 }
421 level_offsets.push(neighbors.len() as u32);
422 }
423 node_offsets.push(level_offsets.len() as u32);
424 Self {
425 m: m as u16,
426 ef_construction: ef_construction as u32,
427 entry_point,
428 max_level,
429 node_levels,
430 node_offsets,
431 level_offsets,
432 neighbors,
433 }
434 }
435
436 #[inline]
437 pub fn neighbors(&self, node: u32, level: usize) -> &[u32] {
438 if (self.node_levels[node as usize] as usize) < level {
439 return &[];
440 }
441 let offset_index = self.node_offsets[node as usize] as usize + level;
442 let start = self.level_offsets[offset_index] as usize;
443 let end = self.level_offsets[offset_index + 1] as usize;
444 &self.neighbors[start..end]
445 }
446
447 pub fn search(&self, query_distance: impl QueryDistance, take: usize) -> Vec<u32> {
448 let take = take.min(self.node_levels.len());
449 if take == 0 {
450 return Vec::new();
451 }
452 let ef_search = take
453 .saturating_mul(HNSW_QUERY_OVERSAMPLE)
454 .max(HNSW_MIN_EF_SEARCH)
455 .min(self.node_levels.len());
456 self.search_with_budget(query_distance, take, ef_search)
457 }
458
459 pub(crate) fn search_for_build(
461 &self,
462 query_distance: impl QueryDistance,
463 take: usize,
464 ) -> Vec<u32> {
465 let take = take.min(self.node_levels.len());
466 if take == 0 {
467 return Vec::new();
468 }
469 self.search_with_budget(query_distance, take, self.build_budget(take))
470 }
471
472 pub(crate) fn search_best_for_build(&self, query_distance: impl QueryDistance) -> Option<u32> {
479 if self.node_levels.is_empty() {
480 return None;
481 }
482 let ef_search = self.build_budget(1);
483 let mut entry = self.entry_point;
484 for level in (1..=self.max_level as usize).rev() {
485 entry = greedy_search_compact(self, entry, level, &query_distance);
486 }
487 HNSW_QUERY_SCRATCH.with(|scratch| {
488 let mut scratch = scratch.borrow_mut();
489 search_compact_layer_reusing(self, entry, ef_search, &query_distance, &mut scratch);
490 Some(
491 scratch
492 .best
493 .iter()
494 .min()
495 .map_or(entry, |candidate| candidate.node),
496 )
497 })
498 }
499
500 #[inline]
501 fn build_budget(&self, take: usize) -> usize {
502 take.saturating_mul(HNSW_BUILD_OVERSAMPLE)
503 .max(HNSW_MIN_EF_BUILD)
504 .min(self.node_levels.len())
505 }
506
507 #[cfg(test)]
510 pub(crate) fn search_best_with_ef(
511 &self,
512 query_distance: impl QueryDistance,
513 ef: usize,
514 ) -> Option<u32> {
515 if self.node_levels.is_empty() {
516 return None;
517 }
518 let ef_search = ef.clamp(1, self.node_levels.len());
519 let mut entry = self.entry_point;
520 for level in (1..=self.max_level as usize).rev() {
521 entry = greedy_search_compact(self, entry, level, &query_distance);
522 }
523 HNSW_QUERY_SCRATCH.with(|scratch| {
524 let mut scratch = scratch.borrow_mut();
525 search_compact_layer_reusing(self, entry, ef_search, &query_distance, &mut scratch);
526 Some(
527 scratch
528 .best
529 .iter()
530 .min()
531 .map_or(entry, |candidate| candidate.node),
532 )
533 })
534 }
535
536 fn search_with_budget(
537 &self,
538 query_distance: impl QueryDistance,
539 take: usize,
540 ef_search: usize,
541 ) -> Vec<u32> {
542 let mut entry = self.entry_point;
543 for level in (1..=self.max_level as usize).rev() {
544 entry = greedy_search_compact(self, entry, level, &query_distance);
545 }
546 HNSW_QUERY_SCRATCH.with(|scratch| {
547 let mut scratch = scratch.borrow_mut();
548 search_compact_layer_reusing(self, entry, ef_search, &query_distance, &mut scratch);
549 order_scratch_candidates(&mut scratch);
550 scratch
551 .ordered
552 .iter()
553 .take(take)
554 .map(|candidate| candidate.node)
555 .collect()
556 })
557 }
558
559 pub fn validate(&self, expected_nodes: usize) -> bool {
560 if self.m as usize != HNSW_M
561 || self.ef_construction as usize != HNSW_EF_CONSTRUCTION
562 || expected_nodes == 0
563 || self.node_levels.len() != expected_nodes
564 || self.node_offsets.len() != expected_nodes + 1
565 || self.node_offsets.first() != Some(&0)
566 || self.node_offsets.last().copied() != Some(self.level_offsets.len() as u32)
567 || self.node_offsets.windows(2).any(|pair| pair[0] > pair[1])
568 || self
569 .node_offsets
570 .iter()
571 .any(|&offset| offset as usize > self.level_offsets.len())
572 || self.entry_point as usize >= expected_nodes
573 || self.node_levels[self.entry_point as usize] != self.max_level
574 || self.node_levels.iter().copied().max() != Some(self.max_level)
575 || self.level_offsets.last().copied() != Some(self.neighbors.len() as u32)
576 || self.level_offsets.windows(2).any(|pair| pair[0] > pair[1])
577 || self
578 .neighbors
579 .iter()
580 .any(|&node| node as usize >= expected_nodes)
581 {
582 return false;
583 }
584 for node in 0..expected_nodes {
585 let start = self.node_offsets[node] as usize;
586 let end = self.node_offsets[node + 1] as usize;
587 if end.saturating_sub(start) != self.node_levels[node] as usize + 2 {
588 return false;
589 }
590 for level in 0..=self.node_levels[node] as usize {
591 let adjacency = self.neighbors(node as u32, level);
592 let max_connections = if level == 0 { HNSW_M * 2 } else { HNSW_M };
593 if adjacency.len() > max_connections
594 || adjacency.contains(&(node as u32))
595 || adjacency.windows(2).any(|pair| pair[0] >= pair[1])
596 {
597 return false;
598 }
599 }
600 }
601 true
602 }
603
604 pub fn size_bytes(&self) -> usize {
605 self.node_levels.len()
606 + self.node_offsets.len() * size_of::<u32>()
607 + self.level_offsets.len() * size_of::<u32>()
608 + self.neighbors.len() * size_of::<u32>()
609 + 32
610 }
611
612 #[cfg(feature = "native")]
615 pub(crate) fn visit_resident_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
616 visit("HNSW node levels", bytes_of_slice(&self.node_levels));
617 visit("HNSW node offsets", bytes_of_slice(&self.node_offsets));
618 visit("HNSW level offsets", bytes_of_slice(&self.level_offsets));
619 visit("HNSW neighbors", bytes_of_slice(&self.neighbors));
620 }
621}
622
623fn greedy_search_level(
624 links: &[Vec<Vec<u32>>],
625 mut current: u32,
626 level: usize,
627 query_distance: &impl QueryDistance,
628) -> u32 {
629 let mut current_distance = query_distance.distance(current);
630 let mut scores = [0f32; NEIGHBOR_BLOCK];
631 loop {
632 let mut changed = false;
633 for chunk in links[current as usize][level].chunks(NEIGHBOR_BLOCK) {
634 let scored = &mut scores[..chunk.len()];
635 query_distance.distances(chunk, scored);
636 for (&candidate, &distance) in chunk.iter().zip(scored.iter()) {
637 if distance < current_distance
638 || (distance == current_distance && candidate < current)
639 {
640 current = candidate;
641 current_distance = distance;
642 changed = true;
643 }
644 }
645 }
646 if !changed {
647 return current;
648 }
649 }
650}
651
652fn greedy_search_compact(
653 graph: &HnswRoutingGraph,
654 mut current: u32,
655 level: usize,
656 query_distance: &impl QueryDistance,
657) -> u32 {
658 let mut current_distance = query_distance.distance(current);
659 let mut scores = [0f32; NEIGHBOR_BLOCK];
660 loop {
661 let mut changed = false;
662 for chunk in graph.neighbors(current, level).chunks(NEIGHBOR_BLOCK) {
663 let scored = &mut scores[..chunk.len()];
664 query_distance.distances(chunk, scored);
665 for (&candidate, &distance) in chunk.iter().zip(scored.iter()) {
666 if distance < current_distance
667 || (distance == current_distance && candidate < current)
668 {
669 current = candidate;
670 current_distance = distance;
671 changed = true;
672 }
673 }
674 }
675 if !changed {
676 return current;
677 }
678 }
679}
680
681fn search_graph_layer(
682 links: &[Vec<Vec<u32>>],
683 entry: u32,
684 level: usize,
685 ef: usize,
686 query_distance: &impl QueryDistance,
687 visited: &mut VisitedNodes,
688) -> Vec<GraphCandidate> {
689 search_layer_impl(entry, ef, query_distance, visited, |node| {
690 &links[node as usize][level]
691 })
692}
693
694fn search_compact_layer_reusing(
699 graph: &HnswRoutingGraph,
700 entry: u32,
701 ef: usize,
702 query_distance: &impl QueryDistance,
703 scratch: &mut HnswQueryScratch,
704) {
705 scratch.visited.ensure_nodes(graph.node_levels.len());
706 scratch.visited.reset();
707 scratch.candidates.clear();
708 scratch.best.clear();
709 scratch.ordered.clear();
710 scratch.visited.insert(entry);
711 let first = GraphCandidate {
712 node: entry,
713 distance: query_distance.distance(entry),
714 };
715 scratch.candidates.push(Reverse(first));
716 scratch.best.push(first);
717
718 while let Some(Reverse(current)) = scratch.candidates.pop() {
719 if scratch.best.len() >= ef
720 && scratch
721 .best
722 .peek()
723 .is_some_and(|worst| current.distance > worst.distance)
724 {
725 break;
726 }
727 scratch.pending.clear();
731 for &neighbor in graph.neighbors(current.node, 0) {
732 if scratch.visited.insert(neighbor) {
733 scratch.pending.push(neighbor);
734 }
735 }
736 if scratch.pending.is_empty() {
737 continue;
738 }
739 scratch.pending_distances.clear();
740 scratch.pending_distances.resize(scratch.pending.len(), 0.0);
741 query_distance.distances(&scratch.pending, &mut scratch.pending_distances);
742
743 for (&node, &distance) in scratch.pending.iter().zip(scratch.pending_distances.iter()) {
744 let candidate = GraphCandidate { node, distance };
745 if scratch.best.len() < ef
746 || scratch.best.peek().is_some_and(|worst| candidate < *worst)
747 {
748 scratch.candidates.push(Reverse(candidate));
749 scratch.best.push(candidate);
750 if scratch.best.len() > ef {
751 scratch.best.pop();
752 }
753 }
754 }
755 }
756}
757
758fn order_scratch_candidates(scratch: &mut HnswQueryScratch) {
759 scratch.ordered.extend(scratch.best.drain());
760 scratch.ordered.sort_unstable();
761}
762
763fn search_layer_impl<'a>(
764 entry: u32,
765 ef: usize,
766 query_distance: &impl QueryDistance,
767 visited: &mut VisitedNodes,
768 neighbors: impl Fn(u32) -> &'a [u32],
769) -> Vec<GraphCandidate> {
770 visited.reset();
771 visited.insert(entry);
772 let first = GraphCandidate {
773 node: entry,
774 distance: query_distance.distance(entry),
775 };
776 let mut candidates = BinaryHeap::new();
777 let mut best = BinaryHeap::new();
778 candidates.push(Reverse(first));
779 best.push(first);
780 let mut pending: Vec<u32> = Vec::new();
781 let mut pending_distances: Vec<f32> = Vec::new();
782
783 while let Some(Reverse(current)) = candidates.pop() {
784 if best.len() >= ef
785 && best
786 .peek()
787 .is_some_and(|worst| current.distance > worst.distance)
788 {
789 break;
790 }
791 pending.clear();
792 for &neighbor in neighbors(current.node) {
793 if visited.insert(neighbor) {
794 pending.push(neighbor);
795 }
796 }
797 if pending.is_empty() {
798 continue;
799 }
800 pending_distances.clear();
801 pending_distances.resize(pending.len(), 0.0);
802 query_distance.distances(&pending, &mut pending_distances);
803
804 for (&node, &distance) in pending.iter().zip(pending_distances.iter()) {
805 let candidate = GraphCandidate { node, distance };
806 if best.len() < ef || best.peek().is_some_and(|worst| candidate < *worst) {
807 candidates.push(Reverse(candidate));
808 best.push(candidate);
809 if best.len() > ef {
810 best.pop();
811 }
812 }
813 }
814 }
815 best.into_sorted_vec()
816}
817
818fn select_diverse_neighbors(
819 query_node: u32,
820 mut candidates: Vec<GraphCandidate>,
821 limit: usize,
822 distance: &impl PairDistance,
823) -> Vec<u32> {
824 candidates.sort_unstable();
825 candidates.dedup_by_key(|candidate| candidate.node);
826 let mut selected = Vec::with_capacity(limit);
827 let mut deferred = Vec::new();
828 for candidate in candidates {
829 if candidate.node == query_node {
830 continue;
831 }
832 if selected
833 .iter()
834 .all(|&neighbor| distance.distance(candidate.node, neighbor) > candidate.distance)
835 {
836 selected.push(candidate.node);
837 if selected.len() == limit {
838 return selected;
839 }
840 } else {
841 deferred.push(candidate.node);
842 }
843 }
844 for candidate in deferred {
845 if selected.len() == limit {
846 break;
847 }
848 selected.push(candidate);
849 }
850 selected
851}
852
853fn contiguous_leaf_run(children: &[u32]) -> bool {
854 children
855 .windows(2)
856 .all(|pair| pair[1] == pair[0].saturating_add(1))
857}
858
859#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
863pub struct IvfRoutingTopology {
864 child_offsets: Vec<u32>,
865 leaf_ids: Vec<u32>,
866}
867
868impl IvfRoutingTopology {
869 pub fn from_children(children: &[Vec<u32>]) -> Self {
870 let mut child_offsets = Vec::with_capacity(children.len() + 1);
871 let mut leaf_ids = Vec::new();
872 child_offsets.push(0);
873 for child_list in children {
874 leaf_ids.extend_from_slice(child_list);
875 child_offsets.push(leaf_ids.len() as u32);
876 }
877 Self {
878 child_offsets,
879 leaf_ids,
880 }
881 }
882
883 pub fn parent_count(&self) -> usize {
884 self.child_offsets.len().saturating_sub(1)
885 }
886
887 pub fn children(&self, parent: usize) -> &[u32] {
888 let start = self.child_offsets[parent] as usize;
889 let end = self.child_offsets[parent + 1] as usize;
890 &self.leaf_ids[start..end]
891 }
892
893 pub fn children_run(&self, parent: usize) -> Option<(u32, usize)> {
901 let children = self.children(parent);
902 let first = *children.first()?;
903 contiguous_leaf_run(children).then_some((first, children.len()))
904 }
905
906 pub fn validate(&self, num_leaves: usize) -> bool {
907 if self.parent_count() == 0 {
908 return self.child_offsets.is_empty() && self.leaf_ids.is_empty();
909 }
910 self.child_offsets.first() == Some(&0)
911 && self.child_offsets.last().copied() == Some(self.leaf_ids.len() as u32)
912 && self.child_offsets.windows(2).all(|pair| pair[0] <= pair[1])
913 && self.leaf_ids.len() == num_leaves
914 && self.leaf_ids.iter().all(|&leaf| leaf < num_leaves as u32)
915 && (0..self.parent_count()).all(|parent| contiguous_leaf_run(self.children(parent)))
919 && {
920 let mut leaves = self.leaf_ids.clone();
921 leaves.sort_unstable();
922 leaves.iter().copied().eq(0..num_leaves as u32)
923 }
924 }
925
926 #[cfg(feature = "native")]
927 pub(crate) fn visit_resident_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
928 visit(
929 "two-level child offsets",
930 bytes_of_slice(&self.child_offsets),
931 );
932 visit("two-level leaf IDs", bytes_of_slice(&self.leaf_ids));
933 }
934}
935
936#[cfg(feature = "native")]
939pub(crate) fn bytes_of_slice<T>(slice: &[T]) -> &[u8] {
940 let byte_len = std::mem::size_of_val(slice);
941 if byte_len == 0 {
942 return &[];
943 }
944 unsafe { std::slice::from_raw_parts(slice.as_ptr().cast::<u8>(), byte_len) }
947}
948
949pub fn routing_parent_count(num_leaves: usize) -> usize {
950 if num_leaves <= 1 {
951 return num_leaves;
952 }
953 ((num_leaves as f64).sqrt().ceil() as usize)
954 .clamp(2, 4_096)
955 .min(num_leaves)
956}
957
958pub fn allocate_child_clusters(group_sizes: &[usize], total_clusters: usize) -> Vec<usize> {
961 let total_points: u128 = group_sizes.iter().map(|&size| size as u128).sum();
962 let target = (total_clusters as u128).min(total_points) as usize;
963 if target == 0 {
964 return vec![0; group_sizes.len()];
965 }
966
967 let populated = group_sizes.iter().filter(|&&size| size > 0).count();
968 let guarantee_populated = target >= populated;
969 let mut allocated = vec![0usize; group_sizes.len()];
970 let mut fixed = vec![false; group_sizes.len()];
971 let mut fixed_cells = 0usize;
972
973 if guarantee_populated {
974 loop {
982 let active_weight: u128 = group_sizes
983 .iter()
984 .enumerate()
985 .filter(|(index, size)| **size > 0 && !fixed[*index])
986 .map(|(_, &size)| size as u128)
987 .sum();
988 let active_target = target - fixed_cells;
989 if active_weight == 0 || active_target == 0 {
990 break;
991 }
992 let newly_fixed = group_sizes
993 .iter()
994 .enumerate()
995 .filter_map(|(index, &size)| {
996 (size > 0
997 && !fixed[index]
998 && (size as u128) * (active_target as u128) <= active_weight)
999 .then_some(index)
1000 })
1001 .collect::<Vec<_>>();
1002 if newly_fixed.is_empty() {
1003 break;
1004 }
1005 for index in newly_fixed {
1006 fixed[index] = true;
1007 allocated[index] = 1;
1008 fixed_cells += 1;
1009 }
1010 }
1011 }
1012
1013 let remaining = target - fixed_cells;
1014 if remaining == 0 {
1015 return allocated;
1016 }
1017 let active_weight: u128 = group_sizes
1018 .iter()
1019 .enumerate()
1020 .filter(|(index, size)| **size > 0 && !fixed[*index])
1021 .map(|(_, &size)| size as u128)
1022 .sum();
1023 debug_assert!(active_weight > 0);
1024
1025 let mut remainders = Vec::with_capacity(group_sizes.len());
1026 for (index, &size) in group_sizes.iter().enumerate() {
1027 if size == 0 || fixed[index] {
1028 continue;
1029 }
1030 let numerator = (remaining as u128) * (size as u128);
1031 let whole = (numerator / active_weight) as usize;
1032 allocated[index] = whole;
1033 if whole < size {
1034 remainders.push((index, numerator % active_weight));
1035 }
1036 }
1037
1038 let remainder_cells = target - allocated.iter().sum::<usize>();
1039 remainders.sort_unstable_by(|(left_index, left), (right_index, right)| {
1040 right.cmp(left).then_with(|| left_index.cmp(right_index))
1041 });
1042 debug_assert!(remainder_cells <= remainders.len());
1043 for (index, _) in remainders.into_iter().take(remainder_cells) {
1044 allocated[index] += 1;
1045 }
1046 debug_assert_eq!(allocated.iter().sum::<usize>(), target);
1047 debug_assert!(
1048 allocated
1049 .iter()
1050 .zip(group_sizes)
1051 .all(|(&cells, &size)| cells <= size)
1052 );
1053 allocated
1054}
1055
1056#[derive(Debug, Clone, PartialEq, Eq)]
1058pub struct IvfProbePlan {
1059 pub quantizer_version: u64,
1060 pub request_fingerprint: u64,
1063 pub cluster_ids: Arc<[u32]>,
1064}
1065
1066impl IvfProbePlan {
1067 pub fn new(quantizer_version: u64, request_fingerprint: u64, cluster_ids: Vec<u32>) -> Self {
1068 Self {
1069 quantizer_version,
1070 request_fingerprint,
1071 cluster_ids: cluster_ids.into(),
1072 }
1073 }
1074}
1075
1076fn fingerprint_words(
1077 mode: IvfRoutingMode,
1078 nprobe: usize,
1079 words: impl IntoIterator<Item = u64>,
1080) -> u64 {
1081 let mut hash = 0xcbf2_9ce4_8422_2325u64;
1084 let mode_tag = match mode {
1085 IvfRoutingMode::Auto => 0u64,
1086 IvfRoutingMode::Flat => 1,
1087 IvfRoutingMode::TwoLevel => 2,
1088 IvfRoutingMode::Hnsw => 3,
1089 };
1090 for word in std::iter::once(mode_tag)
1091 .chain(std::iter::once(nprobe as u64))
1092 .chain(words)
1093 {
1094 hash ^= word;
1095 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
1096 }
1097 hash ^= hash >> 33;
1098 hash = hash.wrapping_mul(0xff51_afd7_ed55_8ccd);
1099 hash ^ (hash >> 33)
1100}
1101
1102pub fn float_probe_fingerprint(query: &[f32], nprobe: usize, mode: IvfRoutingMode) -> u64 {
1103 fingerprint_words(
1104 mode,
1105 nprobe,
1106 query.iter().map(|value| value.to_bits() as u64),
1107 )
1108}
1109
1110pub(crate) fn normalize_cosine_in_place(vector: &mut [f32]) {
1111 let norm = crate::structures::simd::dot_product_f32(vector, vector, vector.len()).sqrt();
1112 let inverse_norm = if norm.is_finite() && norm > 0.0 {
1113 1.0 / norm
1114 } else {
1115 0.0
1116 };
1117 vector.iter_mut().for_each(|value| *value *= inverse_norm);
1118}
1119
1120pub(crate) fn normalized_cosine_query(query: &[f32]) -> Vec<f32> {
1121 let mut normalized = query.to_vec();
1122 normalize_cosine_in_place(&mut normalized);
1123 normalized
1124}
1125
1126pub(crate) fn cosine_probe_fingerprint(query: &[f32], nprobe: usize, mode: IvfRoutingMode) -> u64 {
1127 let norm = crate::structures::simd::dot_product_f32(query, query, query.len()).sqrt();
1128 let inverse_norm = if norm.is_finite() && norm > 0.0 {
1129 1.0 / norm
1130 } else {
1131 0.0
1132 };
1133 fingerprint_words(
1134 mode,
1135 nprobe,
1136 query
1137 .iter()
1138 .map(|value| (value * inverse_norm).to_bits() as u64),
1139 )
1140}
1141
1142pub fn binary_probe_fingerprint_with_parent_beam(
1145 query: &[u8],
1146 nprobe: usize,
1147 mode: IvfRoutingMode,
1148 parent_beam_oversample: usize,
1149) -> u64 {
1150 let beam = match mode {
1151 IvfRoutingMode::TwoLevel => {
1152 parent_beam_oversample.clamp(1, MAX_PARENT_BEAM_OVERSAMPLE) as u64
1153 }
1154 IvfRoutingMode::Auto | IvfRoutingMode::Flat | IvfRoutingMode::Hnsw => 0,
1155 };
1156 fingerprint_words(
1157 mode,
1158 nprobe,
1159 std::iter::once(beam).chain(query.iter().map(|&value| value as u64)),
1160 )
1161}
1162
1163#[inline]
1165pub fn effective_routing_mode(mode: IvfRoutingMode, num_leaves: usize) -> IvfRoutingMode {
1166 resolve_auto_routing(mode, num_leaves, HNSW_AUTO_THRESHOLD)
1167}
1168
1169#[inline]
1176pub fn effective_binary_routing_mode(mode: IvfRoutingMode, num_leaves: usize) -> IvfRoutingMode {
1177 resolve_auto_routing(mode, num_leaves, BINARY_HNSW_AUTO_THRESHOLD)
1178}
1179
1180#[inline]
1181fn resolve_auto_routing(
1182 mode: IvfRoutingMode,
1183 num_leaves: usize,
1184 auto_threshold: usize,
1185) -> IvfRoutingMode {
1186 match mode {
1187 IvfRoutingMode::Auto if num_leaves >= auto_threshold => IvfRoutingMode::Hnsw,
1188 IvfRoutingMode::Auto => IvfRoutingMode::Flat,
1189 explicit => explicit,
1190 }
1191}
1192
1193pub fn parent_probe_count(nprobe: usize, num_leaves: usize, num_parents: usize) -> usize {
1195 parent_probe_count_with_oversample(
1196 nprobe,
1197 num_leaves,
1198 num_parents,
1199 DEFAULT_PARENT_BEAM_OVERSAMPLE,
1200 )
1201}
1202
1203pub fn parent_probe_count_with_oversample(
1208 nprobe: usize,
1209 num_leaves: usize,
1210 num_parents: usize,
1211 oversample: usize,
1212) -> usize {
1213 if num_parents == 0 || num_leaves == 0 {
1214 return 0;
1215 }
1216 let leaves_per_parent = num_leaves.div_ceil(num_parents).max(1);
1217 let oversample = oversample.clamp(1, MAX_PARENT_BEAM_OVERSAMPLE);
1218 nprobe
1219 .saturating_mul(oversample)
1220 .div_ceil(leaves_per_parent)
1221 .clamp(1, num_parents)
1222}
1223
1224pub fn select_parent_beam<const HIGHER_IS_BETTER: bool>(
1229 scores: &[f32],
1230 topology: &IvfRoutingTopology,
1231 requested_leaves: usize,
1232) -> Vec<u32> {
1233 select_parent_beam_with_oversample::<HIGHER_IS_BETTER>(
1234 scores,
1235 topology,
1236 requested_leaves,
1237 DEFAULT_PARENT_BEAM_OVERSAMPLE,
1238 )
1239}
1240
1241pub fn select_parent_beam_with_oversample<const HIGHER_IS_BETTER: bool>(
1243 scores: &[f32],
1244 topology: &IvfRoutingTopology,
1245 requested_leaves: usize,
1246 oversample: usize,
1247) -> Vec<u32> {
1248 let parent_count = scores.len().min(topology.parent_count());
1249 let leaf_count = topology.leaf_ids.len();
1250 let requested_leaves = requested_leaves.min(leaf_count);
1251 if parent_count == 0 || requested_leaves == 0 {
1252 return Vec::new();
1253 }
1254
1255 let initial_take = if oversample == DEFAULT_PARENT_BEAM_OVERSAMPLE {
1256 parent_probe_count(requested_leaves, leaf_count, parent_count)
1257 } else {
1258 parent_probe_count_with_oversample(requested_leaves, leaf_count, parent_count, oversample)
1259 }
1260 .min(parent_count);
1261 let scores = &scores[..parent_count];
1262 let initial = select_best::<HIGHER_IS_BETTER>(scores, initial_take);
1263 let initial_coverage: usize = initial
1264 .iter()
1265 .map(|&parent| topology.children(parent as usize).len())
1266 .sum();
1267 if initial_coverage >= requested_leaves || initial_take == parent_count {
1268 return initial;
1269 }
1270
1271 let mut ranked = select_best::<HIGHER_IS_BETTER>(scores, parent_count);
1272 let mut coverage = 0usize;
1273 let mut take = parent_count;
1274 for (index, &parent) in ranked.iter().enumerate() {
1275 coverage = coverage.saturating_add(topology.children(parent as usize).len());
1276 if index + 1 >= initial_take && coverage >= requested_leaves {
1277 take = index + 1;
1278 break;
1279 }
1280 }
1281 ranked.truncate(take);
1282 ranked
1283}
1284
1285pub fn select_parent_beam_for_build<const HIGHER_IS_BETTER: bool>(
1291 scores: &[f32],
1292 topology: &IvfRoutingTopology,
1293 requested_leaves: usize,
1294) -> Vec<u32> {
1295 select_parent_beam_for_build_with_oversample::<HIGHER_IS_BETTER>(
1296 scores,
1297 topology,
1298 requested_leaves,
1299 DEFAULT_PARENT_BEAM_OVERSAMPLE,
1300 MIN_BUILD_PARENT_BEAM,
1301 )
1302}
1303
1304pub fn select_parent_beam_for_build_with_oversample<const HIGHER_IS_BETTER: bool>(
1308 scores: &[f32],
1309 topology: &IvfRoutingTopology,
1310 requested_leaves: usize,
1311 oversample: usize,
1312 minimum_parents: usize,
1313) -> Vec<u32> {
1314 if requested_leaves == 0 {
1315 return Vec::new();
1316 }
1317 let parent_count = scores.len().min(topology.parent_count());
1318 if parent_count == 0 {
1319 return Vec::new();
1320 }
1321
1322 let query_parents = select_parent_beam_with_oversample::<HIGHER_IS_BETTER>(
1323 scores,
1324 topology,
1325 requested_leaves,
1326 oversample,
1327 );
1328 let query_populated = query_parents
1329 .iter()
1330 .filter(|&&parent| !topology.children(parent as usize).is_empty())
1331 .count();
1332
1333 let scores = &scores[..parent_count];
1334 let mut ranked = select_best::<HIGHER_IS_BETTER>(scores, parent_count);
1335 ranked.retain(|&parent| !topology.children(parent as usize).is_empty());
1336 let take = query_populated
1337 .max(minimum_parents.max(1).min(ranked.len()))
1338 .min(ranked.len());
1339 ranked.truncate(take);
1340 ranked
1341}
1342
1343pub fn select_best<const HIGHER_IS_BETTER: bool>(scores: &[f32], take: usize) -> Vec<u32> {
1346 let take = take.min(scores.len());
1347 if take == 0 {
1348 return Vec::new();
1349 }
1350 let compare = |left: &u32, right: &u32| {
1351 let left_score = scores[*left as usize];
1352 let right_score = scores[*right as usize];
1353 let score_order = if HIGHER_IS_BETTER {
1354 right_score.total_cmp(&left_score)
1355 } else {
1356 left_score.total_cmp(&right_score)
1357 };
1358 score_order.then_with(|| left.cmp(right))
1359 };
1360 SELECT_BEST_ORDER.with(|cell| {
1364 let Ok(mut order) = cell.try_borrow_mut() else {
1365 let mut order: Vec<u32> = (0..scores.len() as u32).collect();
1367 return select_best_in(&mut order, take, compare);
1368 };
1369 order.clear();
1370 order.extend(0..scores.len() as u32);
1371 select_best_in(&mut order, take, compare)
1372 })
1373}
1374
1375thread_local! {
1376 static SELECT_BEST_ORDER: std::cell::RefCell<Vec<u32>> =
1377 const { std::cell::RefCell::new(Vec::new()) };
1378}
1379
1380fn select_best_in(
1381 order: &mut Vec<u32>,
1382 take: usize,
1383 compare: impl Fn(&u32, &u32) -> std::cmp::Ordering,
1384) -> Vec<u32> {
1385 if take < order.len() {
1386 order.select_nth_unstable_by(take, &compare);
1387 order.truncate(take);
1388 }
1389 order.sort_unstable_by(&compare);
1390 order.clone()
1391}
1392
1393pub fn select_best_candidates<const HIGHER_IS_BETTER: bool>(
1397 candidates: &mut Vec<(u32, f32)>,
1398 take: usize,
1399) -> Vec<u32> {
1400 let take = take.min(candidates.len());
1401 if take == 0 {
1402 return Vec::new();
1403 }
1404 let compare = |left: &(u32, f32), right: &(u32, f32)| {
1405 let score_order = if HIGHER_IS_BETTER {
1406 right.1.total_cmp(&left.1)
1407 } else {
1408 left.1.total_cmp(&right.1)
1409 };
1410 score_order.then_with(|| left.0.cmp(&right.0))
1411 };
1412 if take < candidates.len() {
1413 candidates.select_nth_unstable_by(take, compare);
1414 candidates.truncate(take);
1415 }
1416 candidates.sort_unstable_by(compare);
1417 candidates
1418 .iter()
1419 .map(|(cluster_id, _)| *cluster_id)
1420 .collect()
1421}
1422
1423#[cfg(test)]
1424mod tests {
1425 use super::*;
1426
1427 #[test]
1432 fn auto_routing_prefers_exact_flat_probing_below_the_measured_crossover() {
1433 for leaves in [1usize, 4_096, 16_384, BINARY_HNSW_AUTO_THRESHOLD - 1] {
1434 assert_eq!(
1435 effective_binary_routing_mode(IvfRoutingMode::Auto, leaves),
1436 IvfRoutingMode::Flat,
1437 "{leaves} binary leaves"
1438 );
1439 }
1440 for leaves in [BINARY_HNSW_AUTO_THRESHOLD, 114_309] {
1441 assert_eq!(
1442 effective_binary_routing_mode(IvfRoutingMode::Auto, leaves),
1443 IvfRoutingMode::Hnsw,
1444 "{leaves} binary leaves"
1445 );
1446 }
1447 assert_eq!(
1449 effective_routing_mode(IvfRoutingMode::Auto, HNSW_AUTO_THRESHOLD),
1450 IvfRoutingMode::Hnsw
1451 );
1452 assert_eq!(
1453 effective_routing_mode(IvfRoutingMode::Auto, HNSW_AUTO_THRESHOLD - 1),
1454 IvfRoutingMode::Flat
1455 );
1456 for leaves in [64usize, 1_000_000] {
1459 assert_eq!(
1460 effective_binary_routing_mode(IvfRoutingMode::Hnsw, leaves),
1461 IvfRoutingMode::Hnsw
1462 );
1463 assert_eq!(
1464 effective_binary_routing_mode(IvfRoutingMode::TwoLevel, leaves),
1465 IvfRoutingMode::TwoLevel
1466 );
1467 }
1468 const {
1469 assert!(HIERARCHICAL_TRAINING_THRESHOLD <= BINARY_HNSW_AUTO_THRESHOLD);
1470 }
1471 }
1472
1473 #[test]
1474 fn deterministic_selection_supports_both_metric_directions() {
1475 let scores = [0.5, 0.9, 0.1, 0.9];
1476 assert_eq!(select_best::<true>(&scores, 2), vec![1, 3]);
1477 assert_eq!(select_best::<false>(&scores, 2), vec![2, 0]);
1478 }
1479
1480 #[test]
1481 fn two_level_beam_is_oversubscribed_but_bounded() {
1482 assert_eq!(parent_probe_count(32, 65_536, 256), 1);
1483 assert_eq!(parent_probe_count(256, 65_536, 256), 4);
1484 assert_eq!(parent_probe_count(65_536, 65_536, 256), 256);
1485 }
1486
1487 #[test]
1488 fn configurable_parent_beam_increases_coverage_with_bounded_work() {
1489 let children: Vec<Vec<u32>> = (0..64)
1490 .map(|parent| {
1491 let first = parent * 64;
1492 (first..first + 64).map(|leaf| leaf as u32).collect()
1493 })
1494 .collect();
1495 let topology = IvfRoutingTopology::from_children(&children);
1496 let scores: Vec<f32> = (0..64).map(|score| score as f32).collect();
1497
1498 let baseline = select_parent_beam_with_oversample::<false>(&scores, &topology, 32, 4);
1499 let recall_oriented =
1500 select_parent_beam_with_oversample::<false>(&scores, &topology, 32, 8);
1501 assert_eq!(baseline.len(), 2);
1502 assert_eq!(recall_oriented.len(), 4);
1503
1504 let covered = recall_oriented
1505 .iter()
1506 .map(|&parent| topology.children(parent as usize).len())
1507 .sum::<usize>();
1508 assert_eq!(covered, 32 * 8);
1509 assert!(covered < topology.leaf_ids.len());
1510
1511 assert_eq!(
1513 parent_probe_count_with_oversample(32, 4_096, 64, usize::MAX),
1514 8
1515 );
1516 }
1517
1518 #[test]
1519 fn two_level_beam_expands_until_skewed_parents_cover_leaf_budget() {
1520 let mut children: Vec<Vec<u32>> = (0..9).map(|leaf| vec![leaf]).collect();
1521 children.push((9..100).collect());
1522 let topology = IvfRoutingTopology::from_children(&children);
1523
1524 let lower_is_better: Vec<f32> = (0..10).map(|score| score as f32).collect();
1528 assert_eq!(
1529 select_parent_beam::<false>(&lower_is_better, &topology, 4),
1530 vec![0, 1, 2, 3]
1531 );
1532
1533 let higher_is_better: Vec<f32> = (0..10).rev().map(|score| score as f32).collect();
1534 assert_eq!(
1535 select_parent_beam::<true>(&higher_is_better, &topology, 4),
1536 vec![0, 1, 2, 3]
1537 );
1538 }
1539
1540 #[test]
1541 fn build_parent_beam_uses_four_populated_parents_when_query_uses_one() {
1542 let children: Vec<Vec<u32>> = (0..4)
1543 .map(|parent| {
1544 let first = parent * 512;
1545 (first..first + 512).map(|leaf| leaf as u32).collect()
1546 })
1547 .collect();
1548 let topology = IvfRoutingTopology::from_children(&children);
1549
1550 let lower_is_better = [0.0, 1.0, 2.0, 3.0];
1551 assert_eq!(
1552 select_parent_beam::<false>(&lower_is_better, &topology, 128),
1553 vec![0]
1554 );
1555 assert_eq!(
1556 select_parent_beam_for_build::<false>(&lower_is_better, &topology, 128),
1557 vec![0, 1, 2, 3]
1558 );
1559
1560 let higher_is_better = [4.0, 3.0, 2.0, 1.0];
1561 assert_eq!(
1562 select_parent_beam::<true>(&higher_is_better, &topology, 128),
1563 vec![0]
1564 );
1565 assert_eq!(
1566 select_parent_beam_for_build::<true>(&higher_is_better, &topology, 128),
1567 vec![0, 1, 2, 3]
1568 );
1569 }
1570
1571 #[test]
1572 fn build_parent_beam_uses_every_available_populated_parent() {
1573 let children = vec![vec![0], vec![], vec![1], vec![], vec![2]];
1574 let topology = IvfRoutingTopology::from_children(&children);
1575 let scores = [1.0, 0.0, 2.0, -1.0, 3.0];
1576
1577 assert_eq!(
1578 select_parent_beam_for_build::<false>(&scores, &topology, 1),
1579 vec![0, 2, 4]
1580 );
1581 }
1582
1583 #[test]
1584 fn child_allocation_uses_largest_remainders_instead_of_largest_parent() {
1585 assert_eq!(allocate_child_clusters(&[100, 90], 4), vec![2, 2]);
1586 assert_eq!(allocate_child_clusters(&[5, 5, 5], 5), vec![2, 2, 1]);
1587 assert_eq!(allocate_child_clusters(&[90, 10], 10), vec![9, 1]);
1588 }
1589
1590 #[test]
1591 fn child_allocation_is_exact_capacity_bounded_and_deterministic() {
1592 assert_eq!(
1593 allocate_child_clusters(&[1, 100, 7, 0], 100),
1594 vec![1, 93, 6, 0]
1595 );
1596 assert_eq!(allocate_child_clusters(&[1, 2, 0], 10), vec![1, 2, 0]);
1597 assert_eq!(allocate_child_clusters(&[10, 9, 8], 2), vec![1, 1, 0]);
1598
1599 for target in 0..=140 {
1600 let sizes = [100, 30, 0, 7];
1601 let allocation = allocate_child_clusters(&sizes, target);
1602 assert_eq!(
1603 allocation.iter().sum::<usize>(),
1604 target.min(sizes.iter().sum())
1605 );
1606 assert!(
1607 allocation
1608 .iter()
1609 .zip(sizes)
1610 .all(|(&cells, size)| cells <= size)
1611 );
1612 if target >= sizes.iter().filter(|&&size| size > 0).count() {
1613 assert!(
1614 allocation
1615 .iter()
1616 .zip(sizes)
1617 .all(|(&cells, size)| size == 0 || cells > 0)
1618 );
1619 }
1620 }
1621 }
1622
1623 #[test]
1624 fn compact_hnsw_routes_without_copying_points() {
1625 let points: Vec<[f32; 2]> = (0..512)
1626 .map(|index| {
1627 let angle = index as f32 * std::f32::consts::TAU / 512.0;
1628 [angle.cos(), angle.sin()]
1629 })
1630 .collect();
1631 let distance = |left: u32, right: u32| {
1632 let [lx, ly] = points[left as usize];
1633 let [rx, ry] = points[right as usize];
1634 (lx - rx).powi(2) + (ly - ry).powi(2)
1635 };
1636 let graph = HnswRoutingGraph::build(points.len(), distance, 42, "test");
1637 assert!(graph.validate(points.len()));
1638 assert!(graph.size_bytes() < points.len() * 512);
1639
1640 let query = [0.37f32, -0.91];
1641 let query_distance_calls = std::cell::Cell::new(0usize);
1642 let routed = graph.search(
1643 |node| {
1644 query_distance_calls.set(query_distance_calls.get() + 1);
1645 let [x, y] = points[node as usize];
1646 (x - query[0]).powi(2) + (y - query[1]).powi(2)
1647 },
1648 10,
1649 );
1650 let mut exact: Vec<u32> = (0..points.len() as u32).collect();
1651 exact.sort_unstable_by(|&left, &right| {
1652 let score = |node: u32| {
1653 let [x, y] = points[node as usize];
1654 (x - query[0]).powi(2) + (y - query[1]).powi(2)
1655 };
1656 score(left)
1657 .total_cmp(&score(right))
1658 .then_with(|| left.cmp(&right))
1659 });
1660 assert_eq!(routed, exact[..10]);
1661
1662 let build_distance_calls = std::cell::Cell::new(0usize);
1663 let build_routed = graph.search_for_build(
1664 |node| {
1665 build_distance_calls.set(build_distance_calls.get() + 1);
1666 let [x, y] = points[node as usize];
1667 (x - query[0]).powi(2) + (y - query[1]).powi(2)
1668 },
1669 10,
1670 );
1671 assert_eq!(build_routed, exact[..10]);
1672 assert_eq!(build_distance_calls.get(), query_distance_calls.get());
1678
1679 let wide_query_calls = std::cell::Cell::new(0usize);
1680 graph.search(
1681 |node| {
1682 wide_query_calls.set(wide_query_calls.get() + 1);
1683 let [x, y] = points[node as usize];
1684 (x - query[0]).powi(2) + (y - query[1]).powi(2)
1685 },
1686 64,
1687 );
1688 let wide_build_calls = std::cell::Cell::new(0usize);
1689 graph.search_for_build(
1690 |node| {
1691 wide_build_calls.set(wide_build_calls.get() + 1);
1692 let [x, y] = points[node as usize];
1693 (x - query[0]).powi(2) + (y - query[1]).powi(2)
1694 },
1695 64,
1696 );
1697 assert!(
1698 wide_build_calls.get() > wide_query_calls.get(),
1699 "multi-candidate construction should spend its wider search budget"
1700 );
1701
1702 let assigned = graph.search_best_for_build(|node| {
1705 let [x, y] = points[node as usize];
1706 (x - query[0]).powi(2) + (y - query[1]).powi(2)
1707 });
1708 assert_eq!(assigned, Some(exact[0]));
1709
1710 let bytes = bincode::serde::encode_to_vec(&graph, bincode::config::standard()).unwrap();
1711 let (decoded, consumed): (HnswRoutingGraph, usize) =
1712 bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
1713 assert_eq!(consumed, bytes.len());
1714 assert!(decoded.validate(points.len()));
1715
1716 let mut corrupted = decoded;
1717 corrupted.node_offsets[1] = u32::MAX;
1718 assert!(!corrupted.validate(points.len()));
1719 }
1720}