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;
20
21const PARENT_BEAM_OVERSAMPLE: usize = 4;
25const MIN_BUILD_PARENT_BEAM: usize = 4;
28
29const HNSW_M: usize = 32;
30const HNSW_EF_CONSTRUCTION: usize = 200;
31const HNSW_QUERY_OVERSAMPLE: usize = 4;
32const HNSW_MIN_EF_SEARCH: usize = 128;
33const HNSW_BUILD_OVERSAMPLE: usize = 8;
38const HNSW_MIN_EF_BUILD: usize = 512;
39
40#[derive(Clone, Copy, Debug)]
41struct GraphCandidate {
42 node: u32,
43 distance: f32,
44}
45
46impl PartialEq for GraphCandidate {
47 fn eq(&self, other: &Self) -> bool {
48 self.node == other.node && self.distance.to_bits() == other.distance.to_bits()
49 }
50}
51
52impl Eq for GraphCandidate {}
53
54impl PartialOrd for GraphCandidate {
55 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
56 Some(self.cmp(other))
57 }
58}
59
60impl Ord for GraphCandidate {
61 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
62 self.distance
63 .total_cmp(&other.distance)
64 .then_with(|| self.node.cmp(&other.node))
65 }
66}
67
68struct VisitedNodes {
69 epochs: Vec<u32>,
70 current: u32,
71}
72
73impl VisitedNodes {
74 fn new(nodes: usize) -> Self {
75 Self {
76 epochs: vec![0; nodes],
77 current: 0,
78 }
79 }
80
81 fn reset(&mut self) {
82 self.current = self.current.wrapping_add(1);
83 if self.current == 0 {
84 self.epochs.fill(0);
85 self.current = 1;
86 }
87 }
88
89 fn ensure_nodes(&mut self, nodes: usize) {
90 if self.epochs.len() < nodes {
91 self.epochs.resize(nodes, 0);
92 }
93 }
94
95 fn insert(&mut self, node: u32) -> bool {
96 let slot = &mut self.epochs[node as usize];
97 if *slot == self.current {
98 false
99 } else {
100 *slot = self.current;
101 true
102 }
103 }
104}
105
106struct HnswQueryScratch {
107 visited: VisitedNodes,
108 candidates: BinaryHeap<Reverse<GraphCandidate>>,
109 best: BinaryHeap<GraphCandidate>,
110 ordered: Vec<GraphCandidate>,
111}
112
113impl HnswQueryScratch {
114 fn new() -> Self {
115 Self {
116 visited: VisitedNodes::new(0),
117 candidates: BinaryHeap::new(),
118 best: BinaryHeap::new(),
119 ordered: Vec::new(),
120 }
121 }
122}
123
124thread_local! {
125 static HNSW_QUERY_SCRATCH: std::cell::RefCell<HnswQueryScratch> =
129 std::cell::RefCell::new(HnswQueryScratch::new());
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct HnswRoutingGraph {
137 m: u16,
138 ef_construction: u32,
139 entry_point: u32,
140 max_level: u8,
141 node_levels: Vec<u8>,
142 node_offsets: Vec<u32>,
145 level_offsets: Vec<u32>,
146 neighbors: Vec<u32>,
147}
148
149impl HnswRoutingGraph {
150 pub fn build(node_count: usize, distance: impl Fn(u32, u32) -> f32, seed: u64) -> Self {
151 assert!(node_count > 0 && node_count <= u32::MAX as usize);
152 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
153 let level_multiplier = 1.0 / (HNSW_M as f64).ln();
154 let node_levels: Vec<u8> = (0..node_count)
155 .map(|_| {
156 let uniform = rng.random::<f64>().clamp(f64::MIN_POSITIVE, 1.0);
157 (-uniform.ln() * level_multiplier).floor().min(31.0) as u8
158 })
159 .collect();
160 let mut insertion_order: Vec<u32> = (0..node_count as u32).collect();
161 insertion_order.shuffle(&mut rng);
162 let mut links: Vec<Vec<Vec<u32>>> = node_levels
163 .iter()
164 .map(|&level| vec![Vec::new(); level as usize + 1])
165 .collect();
166 let mut visited = VisitedNodes::new(node_count);
167 let mut entry_point = insertion_order[0];
168 let mut max_level = node_levels[entry_point as usize];
169
170 for &node in insertion_order.iter().skip(1) {
171 let node_level = node_levels[node as usize];
172 let mut entry = entry_point;
173 let node_distance = |candidate| distance(node, candidate);
174
175 for level in ((node_level as usize + 1)..=max_level as usize).rev() {
176 entry = greedy_search_level(&links, entry, level, &node_distance);
177 }
178
179 for level in (0..=usize::min(node_level as usize, max_level as usize)).rev() {
180 let candidates = search_graph_layer(
181 &links,
182 entry,
183 level,
184 HNSW_EF_CONSTRUCTION,
185 &node_distance,
186 &mut visited,
187 );
188 if let Some(best) = candidates.first() {
189 entry = best.node;
190 }
191 let max_connections = if level == 0 { HNSW_M * 2 } else { HNSW_M };
192 let selected =
193 select_diverse_neighbors(node, candidates, max_connections, &distance);
194 links[node as usize][level] = selected.clone();
195 for neighbor in selected {
196 let adjacency = &mut links[neighbor as usize][level];
197 if !adjacency.contains(&node) {
198 adjacency.push(node);
199 }
200 if adjacency.len() > max_connections {
201 let candidates = adjacency
202 .iter()
203 .copied()
204 .map(|candidate| GraphCandidate {
205 node: candidate,
206 distance: distance(neighbor, candidate),
207 })
208 .collect();
209 *adjacency = select_diverse_neighbors(
210 neighbor,
211 candidates,
212 max_connections,
213 &distance,
214 );
215 }
216 }
217 }
218
219 if node_level > max_level {
220 entry_point = node;
221 max_level = node_level;
222 }
223 }
224
225 Self::compact(
226 HNSW_M,
227 HNSW_EF_CONSTRUCTION,
228 entry_point,
229 max_level,
230 node_levels,
231 links,
232 )
233 }
234
235 fn compact(
236 m: usize,
237 ef_construction: usize,
238 entry_point: u32,
239 max_level: u8,
240 node_levels: Vec<u8>,
241 links: Vec<Vec<Vec<u32>>>,
242 ) -> Self {
243 let mut node_offsets = Vec::with_capacity(links.len() + 1);
244 let level_count: usize = links.iter().map(|levels| levels.len() + 1).sum();
245 let neighbor_count: usize = links
246 .iter()
247 .flat_map(|levels| levels.iter())
248 .map(Vec::len)
249 .sum();
250 let mut level_offsets = Vec::with_capacity(level_count);
251 let mut neighbors = Vec::with_capacity(neighbor_count);
252 for levels in links {
253 node_offsets.push(level_offsets.len() as u32);
254 for mut adjacency in levels {
255 adjacency.sort_unstable();
256 adjacency.dedup();
257 level_offsets.push(neighbors.len() as u32);
258 neighbors.extend(adjacency);
259 }
260 level_offsets.push(neighbors.len() as u32);
261 }
262 node_offsets.push(level_offsets.len() as u32);
263 Self {
264 m: m as u16,
265 ef_construction: ef_construction as u32,
266 entry_point,
267 max_level,
268 node_levels,
269 node_offsets,
270 level_offsets,
271 neighbors,
272 }
273 }
274
275 #[inline]
276 pub fn neighbors(&self, node: u32, level: usize) -> &[u32] {
277 if (self.node_levels[node as usize] as usize) < level {
278 return &[];
279 }
280 let offset_index = self.node_offsets[node as usize] as usize + level;
281 let start = self.level_offsets[offset_index] as usize;
282 let end = self.level_offsets[offset_index + 1] as usize;
283 &self.neighbors[start..end]
284 }
285
286 pub fn search(&self, query_distance: impl Fn(u32) -> f32, take: usize) -> Vec<u32> {
287 let take = take.min(self.node_levels.len());
288 if take == 0 {
289 return Vec::new();
290 }
291 let ef_search = take
292 .saturating_mul(HNSW_QUERY_OVERSAMPLE)
293 .max(HNSW_MIN_EF_SEARCH)
294 .min(self.node_levels.len());
295 self.search_with_budget(query_distance, take, ef_search)
296 }
297
298 pub(crate) fn search_for_build(
300 &self,
301 query_distance: impl Fn(u32) -> f32,
302 take: usize,
303 ) -> Vec<u32> {
304 let take = take.min(self.node_levels.len());
305 if take == 0 {
306 return Vec::new();
307 }
308 let ef_search = take
309 .saturating_mul(HNSW_BUILD_OVERSAMPLE)
310 .max(HNSW_MIN_EF_BUILD)
311 .min(self.node_levels.len());
312 self.search_with_budget(query_distance, take, ef_search)
313 }
314
315 fn search_with_budget(
316 &self,
317 query_distance: impl Fn(u32) -> f32,
318 take: usize,
319 ef_search: usize,
320 ) -> Vec<u32> {
321 let mut entry = self.entry_point;
322 for level in (1..=self.max_level as usize).rev() {
323 entry = greedy_search_compact(self, entry, level, &query_distance);
324 }
325 HNSW_QUERY_SCRATCH.with(|scratch| {
326 let mut scratch = scratch.borrow_mut();
327 search_compact_layer_reusing(self, entry, ef_search, &query_distance, &mut scratch);
328 scratch
329 .ordered
330 .iter()
331 .take(take)
332 .map(|candidate| candidate.node)
333 .collect()
334 })
335 }
336
337 pub fn validate(&self, expected_nodes: usize) -> bool {
338 if self.m as usize != HNSW_M
339 || self.ef_construction as usize != HNSW_EF_CONSTRUCTION
340 || expected_nodes == 0
341 || self.node_levels.len() != expected_nodes
342 || self.node_offsets.len() != expected_nodes + 1
343 || self.node_offsets.first() != Some(&0)
344 || self.node_offsets.last().copied() != Some(self.level_offsets.len() as u32)
345 || self.node_offsets.windows(2).any(|pair| pair[0] > pair[1])
346 || self
347 .node_offsets
348 .iter()
349 .any(|&offset| offset as usize > self.level_offsets.len())
350 || self.entry_point as usize >= expected_nodes
351 || self.node_levels[self.entry_point as usize] != self.max_level
352 || self.node_levels.iter().copied().max() != Some(self.max_level)
353 || self.level_offsets.last().copied() != Some(self.neighbors.len() as u32)
354 || self.level_offsets.windows(2).any(|pair| pair[0] > pair[1])
355 || self
356 .neighbors
357 .iter()
358 .any(|&node| node as usize >= expected_nodes)
359 {
360 return false;
361 }
362 for node in 0..expected_nodes {
363 let start = self.node_offsets[node] as usize;
364 let end = self.node_offsets[node + 1] as usize;
365 if end.saturating_sub(start) != self.node_levels[node] as usize + 2 {
366 return false;
367 }
368 for level in 0..=self.node_levels[node] as usize {
369 let adjacency = self.neighbors(node as u32, level);
370 let max_connections = if level == 0 { HNSW_M * 2 } else { HNSW_M };
371 if adjacency.len() > max_connections
372 || adjacency.contains(&(node as u32))
373 || adjacency.windows(2).any(|pair| pair[0] >= pair[1])
374 {
375 return false;
376 }
377 }
378 }
379 true
380 }
381
382 pub fn size_bytes(&self) -> usize {
383 self.node_levels.len()
384 + self.node_offsets.len() * size_of::<u32>()
385 + self.level_offsets.len() * size_of::<u32>()
386 + self.neighbors.len() * size_of::<u32>()
387 + 32
388 }
389
390 #[cfg(feature = "native")]
393 pub(crate) fn visit_resident_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
394 visit("HNSW node levels", bytes_of_slice(&self.node_levels));
395 visit("HNSW node offsets", bytes_of_slice(&self.node_offsets));
396 visit("HNSW level offsets", bytes_of_slice(&self.level_offsets));
397 visit("HNSW neighbors", bytes_of_slice(&self.neighbors));
398 }
399}
400
401fn greedy_search_level(
402 links: &[Vec<Vec<u32>>],
403 mut current: u32,
404 level: usize,
405 query_distance: &impl Fn(u32) -> f32,
406) -> u32 {
407 let mut current_distance = query_distance(current);
408 loop {
409 let mut changed = false;
410 for &candidate in &links[current as usize][level] {
411 let distance = query_distance(candidate);
412 if distance < current_distance || (distance == current_distance && candidate < current)
413 {
414 current = candidate;
415 current_distance = distance;
416 changed = true;
417 }
418 }
419 if !changed {
420 return current;
421 }
422 }
423}
424
425fn greedy_search_compact(
426 graph: &HnswRoutingGraph,
427 mut current: u32,
428 level: usize,
429 query_distance: &impl Fn(u32) -> f32,
430) -> u32 {
431 let mut current_distance = query_distance(current);
432 loop {
433 let mut changed = false;
434 for &candidate in graph.neighbors(current, level) {
435 let distance = query_distance(candidate);
436 if distance < current_distance || (distance == current_distance && candidate < current)
437 {
438 current = candidate;
439 current_distance = distance;
440 changed = true;
441 }
442 }
443 if !changed {
444 return current;
445 }
446 }
447}
448
449fn search_graph_layer(
450 links: &[Vec<Vec<u32>>],
451 entry: u32,
452 level: usize,
453 ef: usize,
454 query_distance: &impl Fn(u32) -> f32,
455 visited: &mut VisitedNodes,
456) -> Vec<GraphCandidate> {
457 search_layer_impl(entry, ef, query_distance, visited, |node| {
458 &links[node as usize][level]
459 })
460}
461
462fn search_compact_layer_reusing(
463 graph: &HnswRoutingGraph,
464 entry: u32,
465 ef: usize,
466 query_distance: &impl Fn(u32) -> f32,
467 scratch: &mut HnswQueryScratch,
468) {
469 scratch.visited.ensure_nodes(graph.node_levels.len());
470 scratch.visited.reset();
471 scratch.candidates.clear();
472 scratch.best.clear();
473 scratch.ordered.clear();
474 scratch.visited.insert(entry);
475 let first = GraphCandidate {
476 node: entry,
477 distance: query_distance(entry),
478 };
479 scratch.candidates.push(Reverse(first));
480 scratch.best.push(first);
481
482 while let Some(Reverse(current)) = scratch.candidates.pop() {
483 if scratch.best.len() >= ef
484 && scratch
485 .best
486 .peek()
487 .is_some_and(|worst| current.distance > worst.distance)
488 {
489 break;
490 }
491 for &neighbor in graph.neighbors(current.node, 0) {
492 if !scratch.visited.insert(neighbor) {
493 continue;
494 }
495 let candidate = GraphCandidate {
496 node: neighbor,
497 distance: query_distance(neighbor),
498 };
499 if scratch.best.len() < ef
500 || scratch.best.peek().is_some_and(|worst| candidate < *worst)
501 {
502 scratch.candidates.push(Reverse(candidate));
503 scratch.best.push(candidate);
504 if scratch.best.len() > ef {
505 scratch.best.pop();
506 }
507 }
508 }
509 }
510 scratch.ordered.extend(scratch.best.drain());
511 scratch.ordered.sort_unstable();
512}
513
514fn search_layer_impl<'a>(
515 entry: u32,
516 ef: usize,
517 query_distance: &impl Fn(u32) -> f32,
518 visited: &mut VisitedNodes,
519 neighbors: impl Fn(u32) -> &'a [u32],
520) -> Vec<GraphCandidate> {
521 visited.reset();
522 visited.insert(entry);
523 let first = GraphCandidate {
524 node: entry,
525 distance: query_distance(entry),
526 };
527 let mut candidates = BinaryHeap::new();
528 let mut best = BinaryHeap::new();
529 candidates.push(Reverse(first));
530 best.push(first);
531
532 while let Some(Reverse(current)) = candidates.pop() {
533 if best.len() >= ef
534 && best
535 .peek()
536 .is_some_and(|worst| current.distance > worst.distance)
537 {
538 break;
539 }
540 for &neighbor in neighbors(current.node) {
541 if !visited.insert(neighbor) {
542 continue;
543 }
544 let candidate = GraphCandidate {
545 node: neighbor,
546 distance: query_distance(neighbor),
547 };
548 if best.len() < ef || best.peek().is_some_and(|worst| candidate < *worst) {
549 candidates.push(Reverse(candidate));
550 best.push(candidate);
551 if best.len() > ef {
552 best.pop();
553 }
554 }
555 }
556 }
557 best.into_sorted_vec()
558}
559
560fn select_diverse_neighbors(
561 query_node: u32,
562 mut candidates: Vec<GraphCandidate>,
563 limit: usize,
564 distance: &impl Fn(u32, u32) -> f32,
565) -> Vec<u32> {
566 candidates.sort_unstable();
567 candidates.dedup_by_key(|candidate| candidate.node);
568 let mut selected = Vec::with_capacity(limit);
569 let mut deferred = Vec::new();
570 for candidate in candidates {
571 if candidate.node == query_node {
572 continue;
573 }
574 if selected
575 .iter()
576 .all(|&neighbor| distance(candidate.node, neighbor) > candidate.distance)
577 {
578 selected.push(candidate.node);
579 if selected.len() == limit {
580 return selected;
581 }
582 } else {
583 deferred.push(candidate.node);
584 }
585 }
586 for candidate in deferred {
587 if selected.len() == limit {
588 break;
589 }
590 selected.push(candidate);
591 }
592 selected
593}
594
595#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
599pub struct IvfRoutingTopology {
600 child_offsets: Vec<u32>,
601 leaf_ids: Vec<u32>,
602}
603
604impl IvfRoutingTopology {
605 pub fn from_children(children: &[Vec<u32>]) -> Self {
606 let mut child_offsets = Vec::with_capacity(children.len() + 1);
607 let mut leaf_ids = Vec::new();
608 child_offsets.push(0);
609 for child_list in children {
610 leaf_ids.extend_from_slice(child_list);
611 child_offsets.push(leaf_ids.len() as u32);
612 }
613 Self {
614 child_offsets,
615 leaf_ids,
616 }
617 }
618
619 pub fn parent_count(&self) -> usize {
620 self.child_offsets.len().saturating_sub(1)
621 }
622
623 pub fn children(&self, parent: usize) -> &[u32] {
624 let start = self.child_offsets[parent] as usize;
625 let end = self.child_offsets[parent + 1] as usize;
626 &self.leaf_ids[start..end]
627 }
628
629 pub fn validate(&self, num_leaves: usize) -> bool {
630 if self.parent_count() == 0 {
631 return self.child_offsets.is_empty() && self.leaf_ids.is_empty();
632 }
633 self.child_offsets.first() == Some(&0)
634 && self.child_offsets.last().copied() == Some(self.leaf_ids.len() as u32)
635 && self.child_offsets.windows(2).all(|pair| pair[0] <= pair[1])
636 && self.leaf_ids.len() == num_leaves
637 && self.leaf_ids.iter().all(|&leaf| leaf < num_leaves as u32)
638 && {
639 let mut leaves = self.leaf_ids.clone();
640 leaves.sort_unstable();
641 leaves.iter().copied().eq(0..num_leaves as u32)
642 }
643 }
644
645 #[cfg(feature = "native")]
646 pub(crate) fn visit_resident_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
647 visit(
648 "two-level child offsets",
649 bytes_of_slice(&self.child_offsets),
650 );
651 visit("two-level leaf IDs", bytes_of_slice(&self.leaf_ids));
652 }
653}
654
655#[cfg(feature = "native")]
658pub(crate) fn bytes_of_slice<T>(slice: &[T]) -> &[u8] {
659 let byte_len = std::mem::size_of_val(slice);
660 if byte_len == 0 {
661 return &[];
662 }
663 unsafe { std::slice::from_raw_parts(slice.as_ptr().cast::<u8>(), byte_len) }
666}
667
668pub fn routing_parent_count(num_leaves: usize) -> usize {
669 if num_leaves <= 1 {
670 return num_leaves;
671 }
672 ((num_leaves as f64).sqrt().ceil() as usize)
673 .clamp(2, 4_096)
674 .min(num_leaves)
675}
676
677pub fn allocate_child_clusters(group_sizes: &[usize], total_clusters: usize) -> Vec<usize> {
680 let total_points: u128 = group_sizes.iter().map(|&size| size as u128).sum();
681 let target = (total_clusters as u128).min(total_points) as usize;
682 if target == 0 {
683 return vec![0; group_sizes.len()];
684 }
685
686 let populated = group_sizes.iter().filter(|&&size| size > 0).count();
687 let guarantee_populated = target >= populated;
688 let mut allocated = vec![0usize; group_sizes.len()];
689 let mut fixed = vec![false; group_sizes.len()];
690 let mut fixed_cells = 0usize;
691
692 if guarantee_populated {
693 loop {
701 let active_weight: u128 = group_sizes
702 .iter()
703 .enumerate()
704 .filter(|(index, size)| **size > 0 && !fixed[*index])
705 .map(|(_, &size)| size as u128)
706 .sum();
707 let active_target = target - fixed_cells;
708 if active_weight == 0 || active_target == 0 {
709 break;
710 }
711 let newly_fixed = group_sizes
712 .iter()
713 .enumerate()
714 .filter_map(|(index, &size)| {
715 (size > 0
716 && !fixed[index]
717 && (size as u128) * (active_target as u128) <= active_weight)
718 .then_some(index)
719 })
720 .collect::<Vec<_>>();
721 if newly_fixed.is_empty() {
722 break;
723 }
724 for index in newly_fixed {
725 fixed[index] = true;
726 allocated[index] = 1;
727 fixed_cells += 1;
728 }
729 }
730 }
731
732 let remaining = target - fixed_cells;
733 if remaining == 0 {
734 return allocated;
735 }
736 let active_weight: u128 = group_sizes
737 .iter()
738 .enumerate()
739 .filter(|(index, size)| **size > 0 && !fixed[*index])
740 .map(|(_, &size)| size as u128)
741 .sum();
742 debug_assert!(active_weight > 0);
743
744 let mut remainders = Vec::with_capacity(group_sizes.len());
745 for (index, &size) in group_sizes.iter().enumerate() {
746 if size == 0 || fixed[index] {
747 continue;
748 }
749 let numerator = (remaining as u128) * (size as u128);
750 let whole = (numerator / active_weight) as usize;
751 allocated[index] = whole;
752 if whole < size {
753 remainders.push((index, numerator % active_weight));
754 }
755 }
756
757 let remainder_cells = target - allocated.iter().sum::<usize>();
758 remainders.sort_unstable_by(|(left_index, left), (right_index, right)| {
759 right.cmp(left).then_with(|| left_index.cmp(right_index))
760 });
761 debug_assert!(remainder_cells <= remainders.len());
762 for (index, _) in remainders.into_iter().take(remainder_cells) {
763 allocated[index] += 1;
764 }
765 debug_assert_eq!(allocated.iter().sum::<usize>(), target);
766 debug_assert!(
767 allocated
768 .iter()
769 .zip(group_sizes)
770 .all(|(&cells, &size)| cells <= size)
771 );
772 allocated
773}
774
775#[derive(Debug, Clone, PartialEq, Eq)]
777pub struct IvfProbePlan {
778 pub quantizer_version: u64,
779 pub request_fingerprint: u64,
782 pub cluster_ids: Arc<[u32]>,
783}
784
785impl IvfProbePlan {
786 pub fn new(quantizer_version: u64, request_fingerprint: u64, cluster_ids: Vec<u32>) -> Self {
787 Self {
788 quantizer_version,
789 request_fingerprint,
790 cluster_ids: cluster_ids.into(),
791 }
792 }
793}
794
795fn fingerprint_words(
796 mode: IvfRoutingMode,
797 nprobe: usize,
798 words: impl IntoIterator<Item = u64>,
799) -> u64 {
800 let mut hash = 0xcbf2_9ce4_8422_2325u64;
803 let mode_tag = match mode {
804 IvfRoutingMode::Auto => 0u64,
805 IvfRoutingMode::Flat => 1,
806 IvfRoutingMode::TwoLevel => 2,
807 IvfRoutingMode::Hnsw => 3,
808 };
809 for word in std::iter::once(mode_tag)
810 .chain(std::iter::once(nprobe as u64))
811 .chain(words)
812 {
813 hash ^= word;
814 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
815 }
816 hash ^= hash >> 33;
817 hash = hash.wrapping_mul(0xff51_afd7_ed55_8ccd);
818 hash ^ (hash >> 33)
819}
820
821pub fn float_probe_fingerprint(query: &[f32], nprobe: usize, mode: IvfRoutingMode) -> u64 {
822 fingerprint_words(
823 mode,
824 nprobe,
825 query.iter().map(|value| value.to_bits() as u64),
826 )
827}
828
829pub(crate) fn normalize_cosine_in_place(vector: &mut [f32]) {
830 let norm = crate::structures::simd::dot_product_f32(vector, vector, vector.len()).sqrt();
831 let inverse_norm = if norm.is_finite() && norm > 0.0 {
832 1.0 / norm
833 } else {
834 0.0
835 };
836 vector.iter_mut().for_each(|value| *value *= inverse_norm);
837}
838
839pub(crate) fn normalized_cosine_query(query: &[f32]) -> Vec<f32> {
840 let mut normalized = query.to_vec();
841 normalize_cosine_in_place(&mut normalized);
842 normalized
843}
844
845pub(crate) fn cosine_probe_fingerprint(query: &[f32], nprobe: usize, mode: IvfRoutingMode) -> u64 {
846 let norm = crate::structures::simd::dot_product_f32(query, query, query.len()).sqrt();
847 let inverse_norm = if norm.is_finite() && norm > 0.0 {
848 1.0 / norm
849 } else {
850 0.0
851 };
852 fingerprint_words(
853 mode,
854 nprobe,
855 query
856 .iter()
857 .map(|value| (value * inverse_norm).to_bits() as u64),
858 )
859}
860
861pub fn binary_probe_fingerprint(query: &[u8], nprobe: usize, mode: IvfRoutingMode) -> u64 {
862 fingerprint_words(mode, nprobe, query.iter().map(|&value| value as u64))
863}
864
865#[inline]
866pub fn effective_routing_mode(mode: IvfRoutingMode, num_leaves: usize) -> IvfRoutingMode {
867 match mode {
868 IvfRoutingMode::Auto if num_leaves >= HNSW_AUTO_THRESHOLD => IvfRoutingMode::Hnsw,
869 IvfRoutingMode::Auto => IvfRoutingMode::Flat,
870 explicit => explicit,
871 }
872}
873
874pub fn parent_probe_count(nprobe: usize, num_leaves: usize, num_parents: usize) -> usize {
876 if num_parents == 0 || num_leaves == 0 {
877 return 0;
878 }
879 let leaves_per_parent = num_leaves.div_ceil(num_parents).max(1);
880 nprobe
881 .saturating_mul(PARENT_BEAM_OVERSAMPLE)
882 .div_ceil(leaves_per_parent)
883 .clamp(1, num_parents)
884}
885
886pub fn select_parent_beam<const HIGHER_IS_BETTER: bool>(
891 scores: &[f32],
892 topology: &IvfRoutingTopology,
893 requested_leaves: usize,
894) -> Vec<u32> {
895 let parent_count = scores.len().min(topology.parent_count());
896 let leaf_count = topology.leaf_ids.len();
897 let requested_leaves = requested_leaves.min(leaf_count);
898 if parent_count == 0 || requested_leaves == 0 {
899 return Vec::new();
900 }
901
902 let initial_take =
903 parent_probe_count(requested_leaves, leaf_count, parent_count).min(parent_count);
904 let scores = &scores[..parent_count];
905 let initial = select_best::<HIGHER_IS_BETTER>(scores, initial_take);
906 let initial_coverage: usize = initial
907 .iter()
908 .map(|&parent| topology.children(parent as usize).len())
909 .sum();
910 if initial_coverage >= requested_leaves || initial_take == parent_count {
911 return initial;
912 }
913
914 let mut ranked = select_best::<HIGHER_IS_BETTER>(scores, parent_count);
915 let mut coverage = 0usize;
916 let mut take = parent_count;
917 for (index, &parent) in ranked.iter().enumerate() {
918 coverage = coverage.saturating_add(topology.children(parent as usize).len());
919 if index + 1 >= initial_take && coverage >= requested_leaves {
920 take = index + 1;
921 break;
922 }
923 }
924 ranked.truncate(take);
925 ranked
926}
927
928pub fn select_parent_beam_for_build<const HIGHER_IS_BETTER: bool>(
934 scores: &[f32],
935 topology: &IvfRoutingTopology,
936 requested_leaves: usize,
937) -> Vec<u32> {
938 if requested_leaves == 0 {
939 return Vec::new();
940 }
941 let parent_count = scores.len().min(topology.parent_count());
942 if parent_count == 0 {
943 return Vec::new();
944 }
945
946 let query_parents = select_parent_beam::<HIGHER_IS_BETTER>(scores, topology, requested_leaves);
947 let query_populated = query_parents
948 .iter()
949 .filter(|&&parent| !topology.children(parent as usize).is_empty())
950 .count();
951
952 let scores = &scores[..parent_count];
953 let mut ranked = select_best::<HIGHER_IS_BETTER>(scores, parent_count);
954 ranked.retain(|&parent| !topology.children(parent as usize).is_empty());
955 let take = query_populated
956 .max(MIN_BUILD_PARENT_BEAM.min(ranked.len()))
957 .min(ranked.len());
958 ranked.truncate(take);
959 ranked
960}
961
962pub fn select_best<const HIGHER_IS_BETTER: bool>(scores: &[f32], take: usize) -> Vec<u32> {
965 let take = take.min(scores.len());
966 if take == 0 {
967 return Vec::new();
968 }
969 let mut order: Vec<u32> = (0..scores.len() as u32).collect();
970 let compare = |left: &u32, right: &u32| {
971 let left_score = scores[*left as usize];
972 let right_score = scores[*right as usize];
973 let score_order = if HIGHER_IS_BETTER {
974 right_score.total_cmp(&left_score)
975 } else {
976 left_score.total_cmp(&right_score)
977 };
978 score_order.then_with(|| left.cmp(right))
979 };
980 if take < order.len() {
981 order.select_nth_unstable_by(take, compare);
982 order.truncate(take);
983 }
984 order.sort_unstable_by(compare);
985 order
986}
987
988pub fn select_best_candidates<const HIGHER_IS_BETTER: bool>(
992 candidates: &mut Vec<(u32, f32)>,
993 take: usize,
994) -> Vec<u32> {
995 let take = take.min(candidates.len());
996 if take == 0 {
997 return Vec::new();
998 }
999 let compare = |left: &(u32, f32), right: &(u32, f32)| {
1000 let score_order = if HIGHER_IS_BETTER {
1001 right.1.total_cmp(&left.1)
1002 } else {
1003 left.1.total_cmp(&right.1)
1004 };
1005 score_order.then_with(|| left.0.cmp(&right.0))
1006 };
1007 if take < candidates.len() {
1008 candidates.select_nth_unstable_by(take, compare);
1009 candidates.truncate(take);
1010 }
1011 candidates.sort_unstable_by(compare);
1012 candidates
1013 .iter()
1014 .map(|(cluster_id, _)| *cluster_id)
1015 .collect()
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021
1022 #[test]
1023 fn deterministic_selection_supports_both_metric_directions() {
1024 let scores = [0.5, 0.9, 0.1, 0.9];
1025 assert_eq!(select_best::<true>(&scores, 2), vec![1, 3]);
1026 assert_eq!(select_best::<false>(&scores, 2), vec![2, 0]);
1027 }
1028
1029 #[test]
1030 fn two_level_beam_is_oversubscribed_but_bounded() {
1031 assert_eq!(parent_probe_count(32, 65_536, 256), 1);
1032 assert_eq!(parent_probe_count(256, 65_536, 256), 4);
1033 assert_eq!(parent_probe_count(65_536, 65_536, 256), 256);
1034 }
1035
1036 #[test]
1037 fn two_level_beam_expands_until_skewed_parents_cover_leaf_budget() {
1038 let mut children: Vec<Vec<u32>> = (0..9).map(|leaf| vec![leaf]).collect();
1039 children.push((9..100).collect());
1040 let topology = IvfRoutingTopology::from_children(&children);
1041
1042 let lower_is_better: Vec<f32> = (0..10).map(|score| score as f32).collect();
1046 assert_eq!(
1047 select_parent_beam::<false>(&lower_is_better, &topology, 4),
1048 vec![0, 1, 2, 3]
1049 );
1050
1051 let higher_is_better: Vec<f32> = (0..10).rev().map(|score| score as f32).collect();
1052 assert_eq!(
1053 select_parent_beam::<true>(&higher_is_better, &topology, 4),
1054 vec![0, 1, 2, 3]
1055 );
1056 }
1057
1058 #[test]
1059 fn build_parent_beam_uses_four_populated_parents_when_query_uses_one() {
1060 let children: Vec<Vec<u32>> = (0..4)
1061 .map(|parent| {
1062 let first = parent * 512;
1063 (first..first + 512).map(|leaf| leaf as u32).collect()
1064 })
1065 .collect();
1066 let topology = IvfRoutingTopology::from_children(&children);
1067
1068 let lower_is_better = [0.0, 1.0, 2.0, 3.0];
1069 assert_eq!(
1070 select_parent_beam::<false>(&lower_is_better, &topology, 128),
1071 vec![0]
1072 );
1073 assert_eq!(
1074 select_parent_beam_for_build::<false>(&lower_is_better, &topology, 128),
1075 vec![0, 1, 2, 3]
1076 );
1077
1078 let higher_is_better = [4.0, 3.0, 2.0, 1.0];
1079 assert_eq!(
1080 select_parent_beam::<true>(&higher_is_better, &topology, 128),
1081 vec![0]
1082 );
1083 assert_eq!(
1084 select_parent_beam_for_build::<true>(&higher_is_better, &topology, 128),
1085 vec![0, 1, 2, 3]
1086 );
1087 }
1088
1089 #[test]
1090 fn build_parent_beam_uses_every_available_populated_parent() {
1091 let children = vec![vec![0], vec![], vec![1], vec![], vec![2]];
1092 let topology = IvfRoutingTopology::from_children(&children);
1093 let scores = [1.0, 0.0, 2.0, -1.0, 3.0];
1094
1095 assert_eq!(
1096 select_parent_beam_for_build::<false>(&scores, &topology, 1),
1097 vec![0, 2, 4]
1098 );
1099 }
1100
1101 #[test]
1102 fn child_allocation_uses_largest_remainders_instead_of_largest_parent() {
1103 assert_eq!(allocate_child_clusters(&[100, 90], 4), vec![2, 2]);
1104 assert_eq!(allocate_child_clusters(&[5, 5, 5], 5), vec![2, 2, 1]);
1105 assert_eq!(allocate_child_clusters(&[90, 10], 10), vec![9, 1]);
1106 }
1107
1108 #[test]
1109 fn child_allocation_is_exact_capacity_bounded_and_deterministic() {
1110 assert_eq!(
1111 allocate_child_clusters(&[1, 100, 7, 0], 100),
1112 vec![1, 93, 6, 0]
1113 );
1114 assert_eq!(allocate_child_clusters(&[1, 2, 0], 10), vec![1, 2, 0]);
1115 assert_eq!(allocate_child_clusters(&[10, 9, 8], 2), vec![1, 1, 0]);
1116
1117 for target in 0..=140 {
1118 let sizes = [100, 30, 0, 7];
1119 let allocation = allocate_child_clusters(&sizes, target);
1120 assert_eq!(
1121 allocation.iter().sum::<usize>(),
1122 target.min(sizes.iter().sum())
1123 );
1124 assert!(
1125 allocation
1126 .iter()
1127 .zip(sizes)
1128 .all(|(&cells, size)| cells <= size)
1129 );
1130 if target >= sizes.iter().filter(|&&size| size > 0).count() {
1131 assert!(
1132 allocation
1133 .iter()
1134 .zip(sizes)
1135 .all(|(&cells, size)| size == 0 || cells > 0)
1136 );
1137 }
1138 }
1139 }
1140
1141 #[test]
1142 fn compact_hnsw_routes_without_copying_points() {
1143 let points: Vec<[f32; 2]> = (0..512)
1144 .map(|index| {
1145 let angle = index as f32 * std::f32::consts::TAU / 512.0;
1146 [angle.cos(), angle.sin()]
1147 })
1148 .collect();
1149 let distance = |left: u32, right: u32| {
1150 let [lx, ly] = points[left as usize];
1151 let [rx, ry] = points[right as usize];
1152 (lx - rx).powi(2) + (ly - ry).powi(2)
1153 };
1154 let graph = HnswRoutingGraph::build(points.len(), distance, 42);
1155 assert!(graph.validate(points.len()));
1156 assert!(graph.size_bytes() < points.len() * 512);
1157
1158 let query = [0.37f32, -0.91];
1159 let query_distance_calls = std::cell::Cell::new(0usize);
1160 let routed = graph.search(
1161 |node| {
1162 query_distance_calls.set(query_distance_calls.get() + 1);
1163 let [x, y] = points[node as usize];
1164 (x - query[0]).powi(2) + (y - query[1]).powi(2)
1165 },
1166 10,
1167 );
1168 let mut exact: Vec<u32> = (0..points.len() as u32).collect();
1169 exact.sort_unstable_by(|&left, &right| {
1170 let score = |node: u32| {
1171 let [x, y] = points[node as usize];
1172 (x - query[0]).powi(2) + (y - query[1]).powi(2)
1173 };
1174 score(left)
1175 .total_cmp(&score(right))
1176 .then_with(|| left.cmp(&right))
1177 });
1178 assert_eq!(routed, exact[..10]);
1179
1180 let build_distance_calls = std::cell::Cell::new(0usize);
1181 let build_routed = graph.search_for_build(
1182 |node| {
1183 build_distance_calls.set(build_distance_calls.get() + 1);
1184 let [x, y] = points[node as usize];
1185 (x - query[0]).powi(2) + (y - query[1]).powi(2)
1186 },
1187 10,
1188 );
1189 assert_eq!(build_routed, exact[..10]);
1190 assert!(
1191 build_distance_calls.get() > query_distance_calls.get(),
1192 "offline construction should spend its wider search budget"
1193 );
1194
1195 let bytes = bincode::serde::encode_to_vec(&graph, bincode::config::standard()).unwrap();
1196 let (decoded, consumed): (HnswRoutingGraph, usize) =
1197 bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
1198 assert_eq!(consumed, bytes.len());
1199 assert!(decoded.validate(points.len()));
1200
1201 let mut corrupted = decoded;
1202 corrupted.node_offsets[1] = u32::MAX;
1203 assert!(!corrupted.validate(points.len()));
1204 }
1205}