Skip to main content

hermes_core/structures/vector/ivf/
routing.rs

1//! Metric-agnostic IVF routing primitives.
2//!
3//! Quantizers provide metric-specific centroid scores. This module owns the
4//! topology-independent parts: flat/two-level policy, bounded beam sizing,
5//! deterministic top selection, and the versioned probe plan shared by every
6//! segment participating in one query.
7
8use std::cmp::Reverse;
9use std::collections::BinaryHeap;
10use std::sync::Arc;
11
12use crate::dsl::IvfRoutingMode;
13use rand::prelude::*;
14use serde::{Deserialize, Serialize};
15
16/// Automatic routing switches to a centroid index at this leaf count. Below
17/// it, a SIMD-friendly flat pass is normally cheaper than another level of
18/// indirection.
19pub const HNSW_AUTO_THRESHOLD: usize = 4_096;
20
21/// Extra leaf coverage requested from the parent level. A beam of four times
22/// the minimum parent count avoids the recall cliff of greedy one-parent
23/// hierarchical routing while keeping parent/leaf scoring sublinear.
24const PARENT_BEAM_OVERSAMPLE: usize = 4;
25
26const HNSW_M: usize = 32;
27const HNSW_EF_CONSTRUCTION: usize = 200;
28const HNSW_QUERY_OVERSAMPLE: usize = 4;
29const HNSW_MIN_EF_SEARCH: usize = 128;
30
31#[derive(Clone, Copy, Debug)]
32struct GraphCandidate {
33    node: u32,
34    distance: f32,
35}
36
37impl PartialEq for GraphCandidate {
38    fn eq(&self, other: &Self) -> bool {
39        self.node == other.node && self.distance.to_bits() == other.distance.to_bits()
40    }
41}
42
43impl Eq for GraphCandidate {}
44
45impl PartialOrd for GraphCandidate {
46    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
47        Some(self.cmp(other))
48    }
49}
50
51impl Ord for GraphCandidate {
52    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
53        self.distance
54            .total_cmp(&other.distance)
55            .then_with(|| self.node.cmp(&other.node))
56    }
57}
58
59struct VisitedNodes {
60    epochs: Vec<u32>,
61    current: u32,
62}
63
64impl VisitedNodes {
65    fn new(nodes: usize) -> Self {
66        Self {
67            epochs: vec![0; nodes],
68            current: 0,
69        }
70    }
71
72    fn reset(&mut self) {
73        self.current = self.current.wrapping_add(1);
74        if self.current == 0 {
75            self.epochs.fill(0);
76            self.current = 1;
77        }
78    }
79
80    fn ensure_nodes(&mut self, nodes: usize) {
81        if self.epochs.len() < nodes {
82            self.epochs.resize(nodes, 0);
83        }
84    }
85
86    fn insert(&mut self, node: u32) -> bool {
87        let slot = &mut self.epochs[node as usize];
88        if *slot == self.current {
89            false
90        } else {
91            *slot = self.current;
92            true
93        }
94    }
95}
96
97struct HnswQueryScratch {
98    visited: VisitedNodes,
99    candidates: BinaryHeap<Reverse<GraphCandidate>>,
100    best: BinaryHeap<GraphCandidate>,
101    ordered: Vec<GraphCandidate>,
102}
103
104impl HnswQueryScratch {
105    fn new() -> Self {
106        Self {
107            visited: VisitedNodes::new(0),
108            candidates: BinaryHeap::new(),
109            best: BinaryHeap::new(),
110            ordered: Vec::new(),
111        }
112    }
113}
114
115thread_local! {
116    /// Segment construction routes millions of vectors through the same graph.
117    /// Retaining scratch per worker avoids zeroing the visited bitmap and
118    /// reallocating both heaps for every assignment.
119    static HNSW_QUERY_SCRATCH: std::cell::RefCell<HnswQueryScratch> =
120        std::cell::RefCell::new(HnswQueryScratch::new());
121}
122
123/// Compact, centroid-free HNSW topology. Node IDs are global leaf IDs, so the
124/// graph shares the quantizer's existing centroid matrix rather than storing a
125/// second copy of every vector.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct HnswRoutingGraph {
128    m: u16,
129    ef_construction: u32,
130    entry_point: u32,
131    max_level: u8,
132    node_levels: Vec<u8>,
133    /// Per-node ranges into `level_offsets`; each node owns level_count + 1
134    /// offsets so every adjacency is a direct pair of indexed loads.
135    node_offsets: Vec<u32>,
136    level_offsets: Vec<u32>,
137    neighbors: Vec<u32>,
138}
139
140impl HnswRoutingGraph {
141    pub fn build(node_count: usize, distance: impl Fn(u32, u32) -> f32, seed: u64) -> Self {
142        assert!(node_count > 0 && node_count <= u32::MAX as usize);
143        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
144        let level_multiplier = 1.0 / (HNSW_M as f64).ln();
145        let node_levels: Vec<u8> = (0..node_count)
146            .map(|_| {
147                let uniform = rng.random::<f64>().clamp(f64::MIN_POSITIVE, 1.0);
148                (-uniform.ln() * level_multiplier).floor().min(31.0) as u8
149            })
150            .collect();
151        let mut insertion_order: Vec<u32> = (0..node_count as u32).collect();
152        insertion_order.shuffle(&mut rng);
153        let mut links: Vec<Vec<Vec<u32>>> = node_levels
154            .iter()
155            .map(|&level| vec![Vec::new(); level as usize + 1])
156            .collect();
157        let mut visited = VisitedNodes::new(node_count);
158        let mut entry_point = insertion_order[0];
159        let mut max_level = node_levels[entry_point as usize];
160
161        for &node in insertion_order.iter().skip(1) {
162            let node_level = node_levels[node as usize];
163            let mut entry = entry_point;
164            let node_distance = |candidate| distance(node, candidate);
165
166            for level in ((node_level as usize + 1)..=max_level as usize).rev() {
167                entry = greedy_search_level(&links, entry, level, &node_distance);
168            }
169
170            for level in (0..=usize::min(node_level as usize, max_level as usize)).rev() {
171                let candidates = search_graph_layer(
172                    &links,
173                    entry,
174                    level,
175                    HNSW_EF_CONSTRUCTION,
176                    &node_distance,
177                    &mut visited,
178                );
179                if let Some(best) = candidates.first() {
180                    entry = best.node;
181                }
182                let max_connections = if level == 0 { HNSW_M * 2 } else { HNSW_M };
183                let selected =
184                    select_diverse_neighbors(node, candidates, max_connections, &distance);
185                links[node as usize][level] = selected.clone();
186                for neighbor in selected {
187                    let adjacency = &mut links[neighbor as usize][level];
188                    if !adjacency.contains(&node) {
189                        adjacency.push(node);
190                    }
191                    if adjacency.len() > max_connections {
192                        let candidates = adjacency
193                            .iter()
194                            .copied()
195                            .map(|candidate| GraphCandidate {
196                                node: candidate,
197                                distance: distance(neighbor, candidate),
198                            })
199                            .collect();
200                        *adjacency = select_diverse_neighbors(
201                            neighbor,
202                            candidates,
203                            max_connections,
204                            &distance,
205                        );
206                    }
207                }
208            }
209
210            if node_level > max_level {
211                entry_point = node;
212                max_level = node_level;
213            }
214        }
215
216        Self::compact(
217            HNSW_M,
218            HNSW_EF_CONSTRUCTION,
219            entry_point,
220            max_level,
221            node_levels,
222            links,
223        )
224    }
225
226    fn compact(
227        m: usize,
228        ef_construction: usize,
229        entry_point: u32,
230        max_level: u8,
231        node_levels: Vec<u8>,
232        links: Vec<Vec<Vec<u32>>>,
233    ) -> Self {
234        let mut node_offsets = Vec::with_capacity(links.len() + 1);
235        let level_count: usize = links.iter().map(|levels| levels.len() + 1).sum();
236        let neighbor_count: usize = links
237            .iter()
238            .flat_map(|levels| levels.iter())
239            .map(Vec::len)
240            .sum();
241        let mut level_offsets = Vec::with_capacity(level_count);
242        let mut neighbors = Vec::with_capacity(neighbor_count);
243        for levels in links {
244            node_offsets.push(level_offsets.len() as u32);
245            for mut adjacency in levels {
246                adjacency.sort_unstable();
247                adjacency.dedup();
248                level_offsets.push(neighbors.len() as u32);
249                neighbors.extend(adjacency);
250            }
251            level_offsets.push(neighbors.len() as u32);
252        }
253        node_offsets.push(level_offsets.len() as u32);
254        Self {
255            m: m as u16,
256            ef_construction: ef_construction as u32,
257            entry_point,
258            max_level,
259            node_levels,
260            node_offsets,
261            level_offsets,
262            neighbors,
263        }
264    }
265
266    #[inline]
267    pub fn neighbors(&self, node: u32, level: usize) -> &[u32] {
268        if (self.node_levels[node as usize] as usize) < level {
269            return &[];
270        }
271        let offset_index = self.node_offsets[node as usize] as usize + level;
272        let start = self.level_offsets[offset_index] as usize;
273        let end = self.level_offsets[offset_index + 1] as usize;
274        &self.neighbors[start..end]
275    }
276
277    pub fn search(&self, query_distance: impl Fn(u32) -> f32, take: usize) -> Vec<u32> {
278        let take = take.min(self.node_levels.len());
279        if take == 0 {
280            return Vec::new();
281        }
282        let mut entry = self.entry_point;
283        for level in (1..=self.max_level as usize).rev() {
284            entry = greedy_search_compact(self, entry, level, &query_distance);
285        }
286        let ef_search = take
287            .saturating_mul(HNSW_QUERY_OVERSAMPLE)
288            .max(HNSW_MIN_EF_SEARCH)
289            .min(self.node_levels.len());
290        HNSW_QUERY_SCRATCH.with(|scratch| {
291            let mut scratch = scratch.borrow_mut();
292            search_compact_layer_reusing(self, entry, ef_search, &query_distance, &mut scratch);
293            scratch
294                .ordered
295                .iter()
296                .take(take)
297                .map(|candidate| candidate.node)
298                .collect()
299        })
300    }
301
302    pub fn search_one(&self, query_distance: impl Fn(u32) -> f32) -> u32 {
303        let mut entry = self.entry_point;
304        for level in (1..=self.max_level as usize).rev() {
305            entry = greedy_search_compact(self, entry, level, &query_distance);
306        }
307        let ef_search = HNSW_MIN_EF_SEARCH.min(self.node_levels.len());
308        HNSW_QUERY_SCRATCH.with(|scratch| {
309            let mut scratch = scratch.borrow_mut();
310            search_compact_layer_reusing(self, entry, ef_search, &query_distance, &mut scratch);
311            scratch
312                .ordered
313                .first()
314                .map_or(entry, |candidate| candidate.node)
315        })
316    }
317
318    pub fn validate(&self, expected_nodes: usize) -> bool {
319        if self.m as usize != HNSW_M
320            || self.ef_construction as usize != HNSW_EF_CONSTRUCTION
321            || expected_nodes == 0
322            || self.node_levels.len() != expected_nodes
323            || self.node_offsets.len() != expected_nodes + 1
324            || self.node_offsets.first() != Some(&0)
325            || self.node_offsets.last().copied() != Some(self.level_offsets.len() as u32)
326            || self.node_offsets.windows(2).any(|pair| pair[0] > pair[1])
327            || self
328                .node_offsets
329                .iter()
330                .any(|&offset| offset as usize > self.level_offsets.len())
331            || self.entry_point as usize >= expected_nodes
332            || self.node_levels[self.entry_point as usize] != self.max_level
333            || self.node_levels.iter().copied().max() != Some(self.max_level)
334            || self.level_offsets.last().copied() != Some(self.neighbors.len() as u32)
335            || self.level_offsets.windows(2).any(|pair| pair[0] > pair[1])
336            || self
337                .neighbors
338                .iter()
339                .any(|&node| node as usize >= expected_nodes)
340        {
341            return false;
342        }
343        for node in 0..expected_nodes {
344            let start = self.node_offsets[node] as usize;
345            let end = self.node_offsets[node + 1] as usize;
346            if end.saturating_sub(start) != self.node_levels[node] as usize + 2 {
347                return false;
348            }
349            for level in 0..=self.node_levels[node] as usize {
350                let adjacency = self.neighbors(node as u32, level);
351                let max_connections = if level == 0 { HNSW_M * 2 } else { HNSW_M };
352                if adjacency.len() > max_connections
353                    || adjacency.contains(&(node as u32))
354                    || adjacency.windows(2).any(|pair| pair[0] >= pair[1])
355                {
356                    return false;
357                }
358            }
359        }
360        true
361    }
362
363    pub fn size_bytes(&self) -> usize {
364        self.node_levels.len()
365            + self.node_offsets.len() * size_of::<u32>()
366            + self.level_offsets.len() * size_of::<u32>()
367            + self.neighbors.len() * size_of::<u32>()
368            + 32
369    }
370
371    /// Visit the compact, immutable arrays touched by every HNSW route.
372    /// Query scratch is thread-local and intentionally excluded.
373    #[cfg(feature = "native")]
374    pub(crate) fn visit_resident_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
375        visit("HNSW node levels", bytes_of_slice(&self.node_levels));
376        visit("HNSW node offsets", bytes_of_slice(&self.node_offsets));
377        visit("HNSW level offsets", bytes_of_slice(&self.level_offsets));
378        visit("HNSW neighbors", bytes_of_slice(&self.neighbors));
379    }
380}
381
382fn greedy_search_level(
383    links: &[Vec<Vec<u32>>],
384    mut current: u32,
385    level: usize,
386    query_distance: &impl Fn(u32) -> f32,
387) -> u32 {
388    let mut current_distance = query_distance(current);
389    loop {
390        let mut changed = false;
391        for &candidate in &links[current as usize][level] {
392            let distance = query_distance(candidate);
393            if distance < current_distance || (distance == current_distance && candidate < current)
394            {
395                current = candidate;
396                current_distance = distance;
397                changed = true;
398            }
399        }
400        if !changed {
401            return current;
402        }
403    }
404}
405
406fn greedy_search_compact(
407    graph: &HnswRoutingGraph,
408    mut current: u32,
409    level: usize,
410    query_distance: &impl Fn(u32) -> f32,
411) -> u32 {
412    let mut current_distance = query_distance(current);
413    loop {
414        let mut changed = false;
415        for &candidate in graph.neighbors(current, level) {
416            let distance = query_distance(candidate);
417            if distance < current_distance || (distance == current_distance && candidate < current)
418            {
419                current = candidate;
420                current_distance = distance;
421                changed = true;
422            }
423        }
424        if !changed {
425            return current;
426        }
427    }
428}
429
430fn search_graph_layer(
431    links: &[Vec<Vec<u32>>],
432    entry: u32,
433    level: usize,
434    ef: usize,
435    query_distance: &impl Fn(u32) -> f32,
436    visited: &mut VisitedNodes,
437) -> Vec<GraphCandidate> {
438    search_layer_impl(entry, ef, query_distance, visited, |node| {
439        &links[node as usize][level]
440    })
441}
442
443fn search_compact_layer_reusing(
444    graph: &HnswRoutingGraph,
445    entry: u32,
446    ef: usize,
447    query_distance: &impl Fn(u32) -> f32,
448    scratch: &mut HnswQueryScratch,
449) {
450    scratch.visited.ensure_nodes(graph.node_levels.len());
451    scratch.visited.reset();
452    scratch.candidates.clear();
453    scratch.best.clear();
454    scratch.ordered.clear();
455    scratch.visited.insert(entry);
456    let first = GraphCandidate {
457        node: entry,
458        distance: query_distance(entry),
459    };
460    scratch.candidates.push(Reverse(first));
461    scratch.best.push(first);
462
463    while let Some(Reverse(current)) = scratch.candidates.pop() {
464        if scratch.best.len() >= ef
465            && scratch
466                .best
467                .peek()
468                .is_some_and(|worst| current.distance > worst.distance)
469        {
470            break;
471        }
472        for &neighbor in graph.neighbors(current.node, 0) {
473            if !scratch.visited.insert(neighbor) {
474                continue;
475            }
476            let candidate = GraphCandidate {
477                node: neighbor,
478                distance: query_distance(neighbor),
479            };
480            if scratch.best.len() < ef
481                || scratch.best.peek().is_some_and(|worst| candidate < *worst)
482            {
483                scratch.candidates.push(Reverse(candidate));
484                scratch.best.push(candidate);
485                if scratch.best.len() > ef {
486                    scratch.best.pop();
487                }
488            }
489        }
490    }
491    scratch.ordered.extend(scratch.best.drain());
492    scratch.ordered.sort_unstable();
493}
494
495fn search_layer_impl<'a>(
496    entry: u32,
497    ef: usize,
498    query_distance: &impl Fn(u32) -> f32,
499    visited: &mut VisitedNodes,
500    neighbors: impl Fn(u32) -> &'a [u32],
501) -> Vec<GraphCandidate> {
502    visited.reset();
503    visited.insert(entry);
504    let first = GraphCandidate {
505        node: entry,
506        distance: query_distance(entry),
507    };
508    let mut candidates = BinaryHeap::new();
509    let mut best = BinaryHeap::new();
510    candidates.push(Reverse(first));
511    best.push(first);
512
513    while let Some(Reverse(current)) = candidates.pop() {
514        if best.len() >= ef
515            && best
516                .peek()
517                .is_some_and(|worst| current.distance > worst.distance)
518        {
519            break;
520        }
521        for &neighbor in neighbors(current.node) {
522            if !visited.insert(neighbor) {
523                continue;
524            }
525            let candidate = GraphCandidate {
526                node: neighbor,
527                distance: query_distance(neighbor),
528            };
529            if best.len() < ef || best.peek().is_some_and(|worst| candidate < *worst) {
530                candidates.push(Reverse(candidate));
531                best.push(candidate);
532                if best.len() > ef {
533                    best.pop();
534                }
535            }
536        }
537    }
538    best.into_sorted_vec()
539}
540
541fn select_diverse_neighbors(
542    query_node: u32,
543    mut candidates: Vec<GraphCandidate>,
544    limit: usize,
545    distance: &impl Fn(u32, u32) -> f32,
546) -> Vec<u32> {
547    candidates.sort_unstable();
548    candidates.dedup_by_key(|candidate| candidate.node);
549    let mut selected = Vec::with_capacity(limit);
550    let mut deferred = Vec::new();
551    for candidate in candidates {
552        if candidate.node == query_node {
553            continue;
554        }
555        if selected
556            .iter()
557            .all(|&neighbor| distance(candidate.node, neighbor) > candidate.distance)
558        {
559            selected.push(candidate.node);
560            if selected.len() == limit {
561                return selected;
562            }
563        } else {
564            deferred.push(candidate.node);
565        }
566    }
567    for candidate in deferred {
568        if selected.len() == limit {
569            break;
570        }
571        selected.push(candidate);
572    }
573    selected
574}
575
576/// Compact parent-to-leaf adjacency shared by float and binary quantizers.
577/// Offsets avoid one heap allocation per parent and serialize as two flat
578/// arrays in the single index-level quantizer artifact.
579#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
580pub struct IvfRoutingTopology {
581    child_offsets: Vec<u32>,
582    leaf_ids: Vec<u32>,
583}
584
585impl IvfRoutingTopology {
586    pub fn from_children(children: &[Vec<u32>]) -> Self {
587        let mut child_offsets = Vec::with_capacity(children.len() + 1);
588        let mut leaf_ids = Vec::new();
589        child_offsets.push(0);
590        for child_list in children {
591            leaf_ids.extend_from_slice(child_list);
592            child_offsets.push(leaf_ids.len() as u32);
593        }
594        Self {
595            child_offsets,
596            leaf_ids,
597        }
598    }
599
600    pub fn parent_count(&self) -> usize {
601        self.child_offsets.len().saturating_sub(1)
602    }
603
604    pub fn children(&self, parent: usize) -> &[u32] {
605        let start = self.child_offsets[parent] as usize;
606        let end = self.child_offsets[parent + 1] as usize;
607        &self.leaf_ids[start..end]
608    }
609
610    pub fn validate(&self, num_leaves: usize) -> bool {
611        if self.parent_count() == 0 {
612            return self.child_offsets.is_empty() && self.leaf_ids.is_empty();
613        }
614        self.child_offsets.first() == Some(&0)
615            && self.child_offsets.last().copied() == Some(self.leaf_ids.len() as u32)
616            && self.child_offsets.windows(2).all(|pair| pair[0] <= pair[1])
617            && self.leaf_ids.len() == num_leaves
618            && self.leaf_ids.iter().all(|&leaf| leaf < num_leaves as u32)
619            && {
620                let mut leaves = self.leaf_ids.clone();
621                leaves.sort_unstable();
622                leaves.iter().copied().eq(0..num_leaves as u32)
623            }
624    }
625
626    #[cfg(feature = "native")]
627    pub(crate) fn visit_resident_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
628        visit(
629            "two-level child offsets",
630            bytes_of_slice(&self.child_offsets),
631        );
632        visit("two-level leaf IDs", bytes_of_slice(&self.leaf_ids));
633    }
634}
635
636/// View an initialized plain-data slice as bytes for residency operations.
637/// The returned slice cannot outlive the source and is never mutated.
638#[cfg(feature = "native")]
639pub(crate) fn bytes_of_slice<T>(slice: &[T]) -> &[u8] {
640    let byte_len = std::mem::size_of_val(slice);
641    if byte_len == 0 {
642        return &[];
643    }
644    // SAFETY: every byte in an initialized `T` allocation may be read as u8;
645    // the lifetime remains tied to `slice`, and callers receive no mutation.
646    unsafe { std::slice::from_raw_parts(slice.as_ptr().cast::<u8>(), byte_len) }
647}
648
649pub fn routing_parent_count(num_leaves: usize) -> usize {
650    if num_leaves <= 1 {
651        return num_leaves;
652    }
653    ((num_leaves as f64).sqrt().ceil() as usize)
654        .clamp(2, 4_096)
655        .min(num_leaves)
656}
657
658/// Allocate exactly `total_clusters` child cells proportionally to populated
659/// parent groups, without assigning more cells than training points.
660pub fn allocate_child_clusters(group_sizes: &[usize], total_clusters: usize) -> Vec<usize> {
661    let mut allocated: Vec<usize> = group_sizes
662        .iter()
663        .map(|&size| usize::from(size > 0))
664        .collect();
665    let mut remaining = total_clusters.saturating_sub(allocated.iter().sum());
666    let total_points: usize = group_sizes.iter().sum();
667    if remaining == 0 || total_points == 0 {
668        return allocated;
669    }
670    for (allocation, &size) in allocated.iter_mut().zip(group_sizes) {
671        let capacity = size.saturating_sub(*allocation);
672        let share = remaining
673            .saturating_mul(size)
674            .checked_div(total_points)
675            .unwrap_or(0)
676            .min(capacity);
677        *allocation += share;
678    }
679    remaining = total_clusters.saturating_sub(allocated.iter().sum());
680    while remaining > 0 {
681        let Some((index, _)) = group_sizes
682            .iter()
683            .enumerate()
684            .filter(|(index, size)| allocated[*index] < **size)
685            .max_by_key(|(index, size)| (**size, std::cmp::Reverse(allocated[*index])))
686        else {
687            break;
688        };
689        allocated[index] += 1;
690        remaining -= 1;
691    }
692    allocated
693}
694
695/// A centroid selection computed once and reused by every segment.
696#[derive(Debug, Clone, PartialEq, Eq)]
697pub struct IvfProbePlan {
698    pub quantizer_version: u64,
699    /// Hash of the query, routing mode, and requested leaf count. This keeps a
700    /// reused mutable query object from accidentally reusing an older route.
701    pub request_fingerprint: u64,
702    pub cluster_ids: Arc<[u32]>,
703}
704
705impl IvfProbePlan {
706    pub fn new(quantizer_version: u64, request_fingerprint: u64, cluster_ids: Vec<u32>) -> Self {
707        Self {
708            quantizer_version,
709            request_fingerprint,
710            cluster_ids: cluster_ids.into(),
711        }
712    }
713}
714
715fn fingerprint_words(
716    mode: IvfRoutingMode,
717    nprobe: usize,
718    words: impl IntoIterator<Item = u64>,
719) -> u64 {
720    // FNV-1a with an extra avalanche. This is a cache key, not a persisted
721    // identity or an adversarial hash table key.
722    let mut hash = 0xcbf2_9ce4_8422_2325u64;
723    let mode_tag = match mode {
724        IvfRoutingMode::Auto => 0u64,
725        IvfRoutingMode::Flat => 1,
726        IvfRoutingMode::TwoLevel => 2,
727        IvfRoutingMode::Hnsw => 3,
728    };
729    for word in std::iter::once(mode_tag)
730        .chain(std::iter::once(nprobe as u64))
731        .chain(words)
732    {
733        hash ^= word;
734        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
735    }
736    hash ^= hash >> 33;
737    hash = hash.wrapping_mul(0xff51_afd7_ed55_8ccd);
738    hash ^ (hash >> 33)
739}
740
741pub fn float_probe_fingerprint(query: &[f32], nprobe: usize, mode: IvfRoutingMode) -> u64 {
742    fingerprint_words(
743        mode,
744        nprobe,
745        query.iter().map(|value| value.to_bits() as u64),
746    )
747}
748
749pub fn binary_probe_fingerprint(query: &[u8], nprobe: usize, mode: IvfRoutingMode) -> u64 {
750    fingerprint_words(mode, nprobe, query.iter().map(|&value| value as u64))
751}
752
753#[inline]
754pub fn effective_routing_mode(mode: IvfRoutingMode, num_leaves: usize) -> IvfRoutingMode {
755    match mode {
756        IvfRoutingMode::Auto if num_leaves >= HNSW_AUTO_THRESHOLD => IvfRoutingMode::Hnsw,
757        IvfRoutingMode::Auto => IvfRoutingMode::Flat,
758        explicit => explicit,
759    }
760}
761
762/// Number of parent cells to put in the routing beam.
763pub fn parent_probe_count(nprobe: usize, num_leaves: usize, num_parents: usize) -> usize {
764    if num_parents == 0 || num_leaves == 0 {
765        return 0;
766    }
767    let leaves_per_parent = num_leaves.div_ceil(num_parents).max(1);
768    nprobe
769        .saturating_mul(PARENT_BEAM_OVERSAMPLE)
770        .div_ceil(leaves_per_parent)
771        .clamp(1, num_parents)
772}
773
774/// Deterministically select the best score indexes without fully sorting the
775/// input. `HIGHER_IS_BETTER` covers Hamming similarity; `false` covers L2.
776pub fn select_best<const HIGHER_IS_BETTER: bool>(scores: &[f32], take: usize) -> Vec<u32> {
777    let take = take.min(scores.len());
778    if take == 0 {
779        return Vec::new();
780    }
781    let mut order: Vec<u32> = (0..scores.len() as u32).collect();
782    let compare = |left: &u32, right: &u32| {
783        let left_score = scores[*left as usize];
784        let right_score = scores[*right as usize];
785        let score_order = if HIGHER_IS_BETTER {
786            right_score.total_cmp(&left_score)
787        } else {
788            left_score.total_cmp(&right_score)
789        };
790        score_order.then_with(|| left.cmp(right))
791    };
792    if take < order.len() {
793        order.select_nth_unstable_by(take, compare);
794        order.truncate(take);
795    }
796    order.sort_unstable_by(compare);
797    order
798}
799
800/// Select leaf IDs from a scored candidate set. Candidate IDs need not be
801/// contiguous, which lets both metrics share the exact same two-level beam
802/// implementation.
803pub fn select_best_candidates<const HIGHER_IS_BETTER: bool>(
804    candidates: &mut Vec<(u32, f32)>,
805    take: usize,
806) -> Vec<u32> {
807    let take = take.min(candidates.len());
808    if take == 0 {
809        return Vec::new();
810    }
811    let compare = |left: &(u32, f32), right: &(u32, f32)| {
812        let score_order = if HIGHER_IS_BETTER {
813            right.1.total_cmp(&left.1)
814        } else {
815            left.1.total_cmp(&right.1)
816        };
817        score_order.then_with(|| left.0.cmp(&right.0))
818    };
819    if take < candidates.len() {
820        candidates.select_nth_unstable_by(take, compare);
821        candidates.truncate(take);
822    }
823    candidates.sort_unstable_by(compare);
824    candidates
825        .iter()
826        .map(|(cluster_id, _)| *cluster_id)
827        .collect()
828}
829
830#[cfg(test)]
831mod tests {
832    use super::*;
833
834    #[test]
835    fn deterministic_selection_supports_both_metric_directions() {
836        let scores = [0.5, 0.9, 0.1, 0.9];
837        assert_eq!(select_best::<true>(&scores, 2), vec![1, 3]);
838        assert_eq!(select_best::<false>(&scores, 2), vec![2, 0]);
839    }
840
841    #[test]
842    fn two_level_beam_is_oversubscribed_but_bounded() {
843        assert_eq!(parent_probe_count(32, 65_536, 256), 1);
844        assert_eq!(parent_probe_count(256, 65_536, 256), 4);
845        assert_eq!(parent_probe_count(65_536, 65_536, 256), 256);
846    }
847
848    #[test]
849    fn compact_hnsw_routes_without_copying_points() {
850        let points: Vec<[f32; 2]> = (0..512)
851            .map(|index| {
852                let angle = index as f32 * std::f32::consts::TAU / 512.0;
853                [angle.cos(), angle.sin()]
854            })
855            .collect();
856        let distance = |left: u32, right: u32| {
857            let [lx, ly] = points[left as usize];
858            let [rx, ry] = points[right as usize];
859            (lx - rx).powi(2) + (ly - ry).powi(2)
860        };
861        let graph = HnswRoutingGraph::build(points.len(), distance, 42);
862        assert!(graph.validate(points.len()));
863        assert!(graph.size_bytes() < points.len() * 512);
864
865        let query = [0.37f32, -0.91];
866        let routed = graph.search(
867            |node| {
868                let [x, y] = points[node as usize];
869                (x - query[0]).powi(2) + (y - query[1]).powi(2)
870            },
871            10,
872        );
873        let mut exact: Vec<u32> = (0..points.len() as u32).collect();
874        exact.sort_unstable_by(|&left, &right| {
875            let score = |node: u32| {
876                let [x, y] = points[node as usize];
877                (x - query[0]).powi(2) + (y - query[1]).powi(2)
878            };
879            score(left)
880                .total_cmp(&score(right))
881                .then_with(|| left.cmp(&right))
882        });
883        assert_eq!(routed, exact[..10]);
884
885        let bytes = bincode::serde::encode_to_vec(&graph, bincode::config::standard()).unwrap();
886        let (decoded, consumed): (HnswRoutingGraph, usize) =
887            bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
888        assert_eq!(consumed, bytes.len());
889        assert!(decoded.validate(points.len()));
890
891        let mut corrupted = decoded;
892        corrupted.node_offsets[1] = u32::MAX;
893        assert!(!corrupted.validate(points.len()));
894    }
895}