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