Skip to main content

recern_vector/
hnsw.rs

1//! Hierarchical Navigable Small World graph (Malkov & Yashunin, 2016).
2//!
3//! The graph only stores links. Vectors live in [`Vectors`] and are passed in
4//! by the owning collection, which also decides which nodes are acceptable
5//! results (deleted and filtered-out nodes are still traversed as waypoints).
6//!
7//! Link access goes through [`ReadLinks`] / [`Links`] so the same insertion
8//! code runs on a plain `&mut` graph (single inserts, queries) and on a graph
9//! with one mutex per node (parallel batch build, as in hnswlib).
10
11use std::cell::RefCell;
12use std::cmp::{Ordering, Reverse};
13use std::collections::BinaryHeap;
14use std::ops::Range;
15use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering};
16use std::sync::{Mutex, MutexGuard, PoisonError};
17
18use crate::error::{Error, Result};
19use crate::metric::Metric;
20use crate::rng::SplitMix64;
21
22/// Highest layer a node can be assigned to.
23pub(crate) const MAX_LEVEL: usize = 16;
24
25/// HNSW index parameters.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct HnswParams {
28    /// Maximum links per node on upper layers. Layer 0 allows `2 * m`.
29    pub m: usize,
30    /// Candidate list size while building the graph. Higher builds a better
31    /// graph more slowly.
32    pub ef_construction: usize,
33    /// Default candidate list size while searching. Higher improves recall at
34    /// the cost of latency.
35    pub ef_search: usize,
36}
37
38impl Default for HnswParams {
39    fn default() -> Self {
40        Self {
41            m: 16,
42            ef_construction: 200,
43            ef_search: 64,
44        }
45    }
46}
47
48impl HnswParams {
49    pub(crate) fn validate(&self) -> Result<()> {
50        if !(2..=256).contains(&self.m) {
51            return Err(Error::InvalidArgument("m must be between 2 and 256".into()));
52        }
53        if self.ef_construction == 0 || self.ef_search == 0 {
54            return Err(Error::InvalidArgument(
55                "ef values must be greater than zero".into(),
56            ));
57        }
58        Ok(())
59    }
60
61    pub(crate) fn max_links(&self, layer: usize) -> usize {
62        if layer == 0 { self.m * 2 } else { self.m }
63    }
64}
65
66/// Contiguous encoded vectors, without retaining a duplicate f32 copy.
67pub(crate) struct Vectors {
68    pub(crate) dim: usize,
69    pub(crate) data: Vec<f32>,
70    pub(crate) codes: Vec<i8>,
71    pub(crate) scales: Vec<f32>,
72    pub(crate) encoding: crate::Quantization,
73    metric: Metric,
74}
75impl Vectors {
76    #[cfg(test)]
77    pub(crate) fn new(dim: usize) -> Self {
78        Self::with_encoding(dim, crate::Quantization::F32, Metric::L2)
79    }
80    pub(crate) fn with_encoding(dim: usize, encoding: crate::Quantization, metric: Metric) -> Self {
81        Self {
82            dim,
83            data: Vec::new(),
84            codes: Vec::new(),
85            scales: Vec::new(),
86            encoding,
87            metric,
88        }
89    }
90    pub(crate) fn len(&self) -> usize {
91        if self.encoding == crate::Quantization::F32 {
92            self.data.len() / self.dim.max(1)
93        } else {
94            self.scales.len()
95        }
96    }
97    pub(crate) fn bytes(&self) -> usize {
98        self.data.len() * 4 + self.codes.len() + self.scales.len() * 4
99    }
100    pub(crate) fn truncate(&mut self, nodes: usize) {
101        self.data.truncate(nodes * self.dim);
102        self.codes.truncate(nodes * self.dim);
103        self.scales.truncate(nodes);
104    }
105    #[inline]
106    pub(crate) fn get(&self, node: u32) -> std::borrow::Cow<'_, [f32]> {
107        let start = node as usize * self.dim;
108        if self.encoding == crate::Quantization::F32 {
109            return (&self.data[start..start + self.dim]).into();
110        }
111        self.codes[start..start + self.dim]
112            .iter()
113            .map(|&v| v as f32 * self.scales[node as usize])
114            .collect::<Vec<_>>()
115            .into()
116    }
117    #[inline]
118    pub(crate) fn distance(&self, query: &[f32], node: u32, metric: Metric) -> f32 {
119        if self.encoding == crate::Quantization::F32 {
120            return metric.distance(query, &self.get(node));
121        }
122        let start = node as usize * self.dim;
123        let scale = self.scales[node as usize];
124        let mut sum = 0.0;
125        for (&a, &b) in query.iter().zip(&self.codes[start..start + self.dim]) {
126            let b = b as f32 * scale;
127            sum += if metric == Metric::L2 {
128                (a - b) * (a - b)
129            } else {
130                a * b
131            };
132        }
133        match metric {
134            Metric::L2 => sum,
135            Metric::Cosine => 1.0 - sum,
136            Metric::Dot => -sum,
137        }
138    }
139    pub(crate) fn prefetch(&self, node: u32) {
140        if self.encoding == crate::Quantization::F32 {
141            prefetch(&self.get(node));
142        }
143    }
144    pub(crate) fn push(&mut self, vector: &[f32]) {
145        debug_assert_eq!(vector.len(), self.dim);
146        if self.encoding == crate::Quantization::F32 {
147            self.data.extend_from_slice(vector);
148            return;
149        }
150        let max = vector.iter().fold(0.0f32, |m, v| m.max(v.abs()));
151        let mut scale = if max == 0.0 {
152            1.0
153        } else {
154            (max / 127.0).max(f32::from_bits(1))
155        };
156        let start = self.codes.len();
157        self.codes.extend(
158            vector
159                .iter()
160                .map(|&v| (v / scale).round().clamp(-127.0, 127.0) as i8),
161        );
162        if self.metric == Metric::Cosine {
163            let norm = self.codes[start..]
164                .iter()
165                .map(|&v| (v as f32).powi(2))
166                .sum::<f32>()
167                .sqrt();
168            scale = 1.0 / norm;
169        }
170        self.scales.push(scale);
171    }
172}
173
174#[derive(Clone, Copy, Debug)]
175pub(crate) struct Candidate {
176    pub(crate) dist: f32,
177    pub(crate) id: u32,
178}
179
180impl PartialEq for Candidate {
181    fn eq(&self, other: &Self) -> bool {
182        self.cmp(other) == Ordering::Equal
183    }
184}
185
186impl Eq for Candidate {}
187
188impl PartialOrd for Candidate {
189    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
190        Some(self.cmp(other))
191    }
192}
193
194impl Ord for Candidate {
195    fn cmp(&self, other: &Self) -> Ordering {
196        self.dist
197            .total_cmp(&other.dist)
198            .then(self.id.cmp(&other.id))
199    }
200}
201
202/// Work done by a search, reported by `explain`.
203#[derive(Clone, Copy, Debug, Default)]
204pub(crate) struct Counters {
205    pub(crate) visited: usize,
206    pub(crate) distance_computations: usize,
207}
208
209/// Set of visited nodes: a bitset (one bit per node keeps it cache-resident
210/// on large graphs) that is reset by clearing only the words a search
211/// touched. Reused across searches on the same thread.
212#[derive(Default)]
213pub(crate) struct Visited {
214    bits: Vec<u64>,
215    touched: Vec<u32>,
216    /// Scratch buffer for the unvisited neighbors of the node being expanded.
217    fresh: Vec<u32>,
218}
219
220impl Visited {
221    fn reset(&mut self, len: usize) {
222        for word in self.touched.drain(..) {
223            self.bits[word as usize] = 0;
224        }
225        let words = len.div_ceil(64);
226        if self.bits.len() < words {
227            self.bits.resize(words, 0);
228        }
229    }
230
231    /// Marks `node` as visited and returns whether it was new.
232    #[inline]
233    fn insert(&mut self, node: u32) -> bool {
234        let (word, bit) = (node as usize / 64, 1u64 << (node % 64));
235        let bits = &mut self.bits[word];
236        if *bits & bit != 0 {
237            return false;
238        }
239        if *bits == 0 {
240            self.touched.push(word as u32);
241        }
242        *bits |= bit;
243        true
244    }
245}
246
247thread_local! {
248    static VISITED: RefCell<Visited> = RefCell::new(Visited::default());
249}
250
251/// Read access to neighbor lists.
252pub(crate) trait ReadLinks {
253    fn with_neighbors<R>(&self, node: u32, layer: usize, f: impl FnOnce(&[u32]) -> R) -> R;
254
255    /// Whether `node` is linked on every layer. Only nodes another thread is
256    /// still inserting are not.
257    fn is_ready(&self, _node: u32) -> bool {
258        true
259    }
260}
261
262/// Write access to neighbor lists.
263pub(crate) trait Links: ReadLinks {
264    /// Writes the links chosen for a newly inserted node. Links other inserts
265    /// added to it meanwhile are kept, pruned together to `max`.
266    fn set_neighbors(
267        &mut self,
268        node: u32,
269        layer: usize,
270        neighbors: Vec<u32>,
271        max: usize,
272        vectors: &Vectors,
273        metric: Metric,
274    );
275
276    /// Adds a link `node -> new`, pruning the list back to `max` links.
277    fn add_link(
278        &mut self,
279        node: u32,
280        layer: usize,
281        new: u32,
282        max: usize,
283        vectors: &Vectors,
284        metric: Metric,
285    );
286}
287
288impl ReadLinks for [Vec<Vec<u32>>] {
289    #[inline]
290    fn with_neighbors<R>(&self, node: u32, layer: usize, f: impl FnOnce(&[u32]) -> R) -> R {
291        f(&self[node as usize][layer])
292    }
293}
294
295/// Exclusive access for sequential inserts.
296struct Exclusive<'a>(&'a mut [Vec<Vec<u32>>]);
297
298impl ReadLinks for Exclusive<'_> {
299    fn with_neighbors<R>(&self, node: u32, layer: usize, f: impl FnOnce(&[u32]) -> R) -> R {
300        self.0.with_neighbors(node, layer, f)
301    }
302}
303
304impl Links for Exclusive<'_> {
305    fn set_neighbors(
306        &mut self,
307        node: u32,
308        layer: usize,
309        neighbors: Vec<u32>,
310        _: usize,
311        _: &Vectors,
312        _: Metric,
313    ) {
314        // Sequential inserts: nothing can have linked to `node` yet.
315        self.0[node as usize][layer] = neighbors;
316    }
317
318    fn add_link(
319        &mut self,
320        node: u32,
321        layer: usize,
322        new: u32,
323        max: usize,
324        vectors: &Vectors,
325        metric: Metric,
326    ) {
327        push_and_prune(
328            &mut self.0[node as usize][layer],
329            node,
330            new,
331            max,
332            vectors,
333            metric,
334        );
335    }
336}
337
338/// Shared access for parallel inserts: one lock per node.
339#[derive(Clone, Copy)]
340struct Shared<'a> {
341    links: &'a [Mutex<Vec<Vec<u32>>>],
342    ready: &'a [AtomicBool],
343}
344
345impl Shared<'_> {
346    fn lock(&self, node: u32) -> MutexGuard<'_, Vec<Vec<u32>>> {
347        self.links[node as usize]
348            .lock()
349            .unwrap_or_else(PoisonError::into_inner)
350    }
351}
352
353impl ReadLinks for Shared<'_> {
354    fn with_neighbors<R>(&self, node: u32, layer: usize, f: impl FnOnce(&[u32]) -> R) -> R {
355        f(&self.lock(node)[layer])
356    }
357
358    fn is_ready(&self, node: u32) -> bool {
359        self.ready[node as usize].load(AtomicOrdering::Acquire)
360    }
361}
362
363impl Links for Shared<'_> {
364    fn set_neighbors(
365        &mut self,
366        node: u32,
367        layer: usize,
368        neighbors: Vec<u32>,
369        max: usize,
370        vectors: &Vectors,
371        metric: Metric,
372    ) {
373        let mut links = self.lock(node);
374        let list = &mut links[layer];
375        if list.is_empty() {
376            *list = neighbors;
377            return;
378        }
379        for neighbor in neighbors {
380            if !list.contains(&neighbor) {
381                list.push(neighbor);
382            }
383        }
384        prune(list, node, max, vectors, metric);
385    }
386
387    fn add_link(
388        &mut self,
389        node: u32,
390        layer: usize,
391        new: u32,
392        max: usize,
393        vectors: &Vectors,
394        metric: Metric,
395    ) {
396        push_and_prune(&mut self.lock(node)[layer], node, new, max, vectors, metric);
397    }
398}
399
400pub(crate) struct Graph {
401    pub(crate) params: HnswParams,
402    /// `links[node][layer]` lists the neighbors of `node` on `layer`.
403    pub(crate) links: Vec<Vec<Vec<u32>>>,
404    pub(crate) entry: Option<u32>,
405    pub(crate) max_level: usize,
406    pub(crate) rng: SplitMix64,
407}
408
409impl Graph {
410    pub(crate) fn new(params: HnswParams, seed: u64) -> Self {
411        Self {
412            params,
413            links: Vec::new(),
414            entry: None,
415            max_level: 0,
416            rng: SplitMix64::new(seed),
417        }
418    }
419
420    pub(crate) fn len(&self) -> usize {
421        self.links.len()
422    }
423
424    fn random_level(&mut self) -> usize {
425        let ml = 1.0 / (self.params.m as f64).ln();
426        let level = (-self.rng.next_f64().ln() * ml).floor() as usize;
427        level.min(MAX_LEVEL)
428    }
429
430    /// Links node `id`, which must be the next node and whose vector must
431    /// already be in `vectors`.
432    pub(crate) fn insert(&mut self, id: u32, vectors: &Vectors, metric: Metric) {
433        debug_assert_eq!(id as usize, self.links.len());
434        self.insert_batch(id..id + 1, vectors, metric, 1);
435    }
436
437    /// Links nodes `nodes`, which must directly follow the existing nodes and
438    /// whose vectors must already be in `vectors`, using up to `threads`
439    /// threads. Levels are drawn in node order, so a single-threaded batch
440    /// builds exactly the same graph as inserting the nodes one by one.
441    pub(crate) fn insert_batch(
442        &mut self,
443        nodes: Range<u32>,
444        vectors: &Vectors,
445        metric: Metric,
446        threads: usize,
447    ) {
448        debug_assert_eq!(nodes.start as usize, self.links.len());
449        let levels: Vec<usize> = nodes.clone().map(|_| self.random_level()).collect();
450        for &level in &levels {
451            self.links.push(vec![Vec::new(); level + 1]);
452        }
453        let mut rest = nodes.clone();
454        if self.entry.is_none() {
455            let Some(first) = rest.next() else { return };
456            self.entry = Some(first);
457            self.max_level = levels[0];
458        }
459        let level_of = |node: u32| levels[(node - nodes.start) as usize];
460        let params = self.params;
461
462        // Small batches are not worth the lock overhead.
463        if threads <= 1 || rest.len() < 1000 {
464            VISITED.with_borrow_mut(|visited| {
465                for node in rest {
466                    let level = level_of(node);
467                    let entry = self.entry.expect("entry is set above");
468                    let mut links = Exclusive(&mut self.links);
469                    link_node(
470                        &mut links,
471                        &params,
472                        node,
473                        level,
474                        entry,
475                        self.max_level,
476                        vectors,
477                        metric,
478                        visited,
479                    );
480                    if level > self.max_level {
481                        self.max_level = level;
482                        self.entry = Some(node);
483                    }
484                }
485            });
486            return;
487        }
488
489        let locked: Vec<Mutex<Vec<Vec<u32>>>> = std::mem::take(&mut self.links)
490            .into_iter()
491            .map(Mutex::new)
492            .collect();
493        let top = Mutex::new((self.entry.expect("entry is set above"), self.max_level));
494        let next = AtomicUsize::new(rest.start as usize);
495        let ready: Vec<AtomicBool> = (0..locked.len())
496            .map(|node| AtomicBool::new(node < rest.start as usize))
497            .collect();
498        std::thread::scope(|scope| {
499            for _ in 0..threads {
500                scope.spawn(|| {
501                    let mut links = Shared {
502                        links: &locked,
503                        ready: &ready,
504                    };
505                    let mut visited = Visited::default();
506                    loop {
507                        let node = next.fetch_add(1, AtomicOrdering::Relaxed);
508                        if node >= rest.end as usize {
509                            break;
510                        }
511                        let (node, level) = (node as u32, level_of(node as u32));
512                        let mut top = top.lock().unwrap_or_else(PoisonError::into_inner);
513                        let (entry, max_level) = *top;
514                        if level > max_level {
515                            // Rare: the new node becomes the entry point, so other
516                            // inserts wait until it is fully linked (as in hnswlib).
517                            link_node(
518                                &mut links,
519                                &params,
520                                node,
521                                level,
522                                entry,
523                                max_level,
524                                vectors,
525                                metric,
526                                &mut visited,
527                            );
528                            *top = (node, level);
529                        } else {
530                            drop(top);
531                            link_node(
532                                &mut links,
533                                &params,
534                                node,
535                                level,
536                                entry,
537                                max_level,
538                                vectors,
539                                metric,
540                                &mut visited,
541                            );
542                        }
543                        ready[node as usize].store(true, AtomicOrdering::Release);
544                    }
545                });
546            }
547        });
548        self.links = locked
549            .into_iter()
550            .map(|m| m.into_inner().unwrap_or_else(PoisonError::into_inner))
551            .collect();
552        let (entry, max_level) = top.into_inner().unwrap_or_else(PoisonError::into_inner);
553        self.entry = Some(entry);
554        self.max_level = max_level;
555        self.repair_unreachable(vectors, metric);
556    }
557
558    /// Concurrent pruning can, rarely, remove every incoming link of a node,
559    /// making it invisible to search. Re-links each such node from its
560    /// nearest reachable neighbors on layer 0. Returns how many were found.
561    pub(crate) fn repair_unreachable(&mut self, vectors: &Vectors, metric: Metric) -> usize {
562        let reachable = self.reachable();
563        let orphans: Vec<u32> = (0..self.links.len() as u32)
564            .filter(|&node| !reachable[node as usize])
565            .collect();
566        let Some(entry) = self.entry else { return 0 };
567        let max = self.params.max_links(0);
568        VISITED.with_borrow_mut(|visited| {
569            for &orphan in &orphans {
570                let query = vectors.get(orphan);
571                let found = {
572                    let links: &[Vec<Vec<u32>>] = &self.links;
573                    let mut counters = Counters::default();
574                    let mut ep = Candidate {
575                        dist: vectors.distance(&query, entry, metric),
576                        id: entry,
577                    };
578                    for layer in (1..=self.max_level).rev() {
579                        ep = greedy(links, &query, ep, layer, vectors, metric, &mut counters);
580                    }
581                    let ef = self.params.ef_construction;
582                    let accept = |n: u32| n != orphan;
583                    search_layer(
584                        links,
585                        &query,
586                        &[ep],
587                        ef,
588                        0,
589                        vectors,
590                        metric,
591                        &accept,
592                        &mut counters,
593                        visited,
594                    )
595                };
596                let mut linked = false;
597                for candidate in found.iter().take(self.params.m) {
598                    let list = &mut self.links[candidate.id as usize][0];
599                    push_and_prune(list, candidate.id, orphan, max, vectors, metric);
600                    linked |= list.contains(&orphan);
601                }
602                if !linked && let Some(nearest) = found.first() {
603                    // The heuristic rejected every link; keep one anyway.
604                    self.links[nearest.id as usize][0].push(orphan);
605                }
606            }
607        });
608        orphans.len()
609    }
610
611    /// Returns up to `k` accepted nodes closest to `query`, nearest first.
612    #[allow(clippy::too_many_arguments)]
613    pub(crate) fn search(
614        &self,
615        query: &[f32],
616        k: usize,
617        ef: usize,
618        vectors: &Vectors,
619        metric: Metric,
620        accept: &dyn Fn(u32) -> bool,
621        counters: &mut Counters,
622    ) -> Vec<Candidate> {
623        let Some(entry) = self.entry else {
624            return Vec::new();
625        };
626        let links: &[Vec<Vec<u32>>] = &self.links;
627        counters.distance_computations += 1;
628        let mut ep = Candidate {
629            dist: vectors.distance(query, entry, metric),
630            id: entry,
631        };
632        for layer in (1..=self.max_level).rev() {
633            ep = greedy(links, query, ep, layer, vectors, metric, counters);
634        }
635        let mut found = VISITED.with_borrow_mut(|visited| {
636            search_layer(
637                links,
638                query,
639                &[ep],
640                ef.max(k),
641                0,
642                vectors,
643                metric,
644                accept,
645                counters,
646                visited,
647            )
648        });
649        found.truncate(k);
650        found
651    }
652
653    /// Number of nodes present on each layer, bottom first.
654    pub(crate) fn nodes_per_layer(&self) -> Vec<usize> {
655        if self.links.is_empty() {
656            return Vec::new();
657        }
658        let mut counts = vec![0; self.max_level + 1];
659        for layers in &self.links {
660            for count in counts.iter_mut().take(layers.len()) {
661                *count += 1;
662            }
663        }
664        counts
665    }
666
667    /// Marks every node reachable from the entry point on layer 0.
668    pub(crate) fn reachable(&self) -> Vec<bool> {
669        let mut seen = vec![false; self.links.len()];
670        let Some(entry) = self.entry else { return seen };
671        let mut stack = vec![entry];
672        seen[entry as usize] = true;
673        while let Some(node) = stack.pop() {
674            for &neighbor in &self.links[node as usize][0] {
675                if !seen[neighbor as usize] {
676                    seen[neighbor as usize] = true;
677                    stack.push(neighbor);
678                }
679            }
680        }
681        seen
682    }
683}
684
685/// Inserts `node` into the graph starting from a snapshot of the entry point.
686/// The node's own links are written before any reverse link, so concurrent
687/// searches never reach a node without neighbors on a fully linked layer.
688#[allow(clippy::too_many_arguments)]
689fn link_node<L: Links>(
690    links: &mut L,
691    params: &HnswParams,
692    node: u32,
693    level: usize,
694    entry: u32,
695    max_level: usize,
696    vectors: &Vectors,
697    metric: Metric,
698    visited: &mut Visited,
699) {
700    let query = vectors.get(node);
701    let mut counters = Counters::default();
702    let mut ep = Candidate {
703        dist: vectors.distance(&query, entry, metric),
704        id: entry,
705    };
706    for layer in (level + 1..=max_level).rev() {
707        ep = greedy(&*links, &query, ep, layer, vectors, metric, &mut counters);
708    }
709
710    let mut entry_points = vec![ep];
711    for layer in (0..=level.min(max_level)).rev() {
712        let found = search_layer(
713            &*links,
714            &query,
715            &entry_points,
716            params.ef_construction,
717            layer,
718            vectors,
719            metric,
720            &|n| n != node,
721            &mut counters,
722            visited,
723        );
724        let max = params.max_links(layer);
725        let neighbors = select_neighbors(&found, max, vectors, metric);
726        links.set_neighbors(node, layer, neighbors.clone(), max, vectors, metric);
727        for neighbor in neighbors {
728            links.add_link(neighbor, layer, node, max, vectors, metric);
729        }
730        entry_points = found;
731    }
732}
733
734/// Hints the CPU to start loading `vector` into cache, one prefetch per
735/// 64-byte line.
736#[inline(always)]
737fn prefetch(vector: &[f32]) {
738    for line in vector.chunks(16) {
739        let ptr = line.as_ptr();
740        #[cfg(target_arch = "aarch64")]
741        // SAFETY: a prefetch is only a hint; it never faults and has no
742        // architectural side effects.
743        unsafe {
744            std::arch::asm!("prfm pldl1keep, [{0}]", in(reg) ptr, options(nostack, preserves_flags, readonly));
745        }
746        #[cfg(target_arch = "x86_64")]
747        // SAFETY: as above; `_mm_prefetch` is available on every x86_64 CPU.
748        unsafe {
749            std::arch::x86_64::_mm_prefetch(ptr.cast::<i8>(), std::arch::x86_64::_MM_HINT_T0);
750        }
751        #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
752        let _ = ptr;
753    }
754}
755
756fn greedy<R: ReadLinks + ?Sized>(
757    links: &R,
758    query: &[f32],
759    mut best: Candidate,
760    layer: usize,
761    vectors: &Vectors,
762    metric: Metric,
763    counters: &mut Counters,
764) -> Candidate {
765    loop {
766        let current = best.id;
767        links.with_neighbors(current, layer, |neighbors| {
768            for &neighbor in neighbors {
769                vectors.prefetch(neighbor);
770            }
771            for &neighbor in neighbors {
772                counters.distance_computations += 1;
773                let dist = vectors.distance(query, neighbor, metric);
774                // A node still being inserted may have empty lower layers;
775                // descending into it would leave the search stranded.
776                if dist < best.dist && links.is_ready(neighbor) {
777                    best = Candidate { dist, id: neighbor };
778                }
779            }
780        });
781        if best.id == current {
782            return best;
783        }
784    }
785}
786
787/// Beam search on one layer. Every reachable node is traversed, but only
788/// nodes passing `accept` enter the result set, so a restrictive filter
789/// widens the search instead of returning fewer results.
790#[allow(clippy::too_many_arguments)]
791fn search_layer<R: ReadLinks + ?Sized>(
792    links: &R,
793    query: &[f32],
794    entry_points: &[Candidate],
795    ef: usize,
796    layer: usize,
797    vectors: &Vectors,
798    metric: Metric,
799    accept: &dyn Fn(u32) -> bool,
800    counters: &mut Counters,
801    visited: &mut Visited,
802) -> Vec<Candidate> {
803    visited.reset(vectors.len());
804    let mut candidates = BinaryHeap::new();
805    let mut results: BinaryHeap<Candidate> = BinaryHeap::new();
806
807    for &ep in entry_points {
808        if !visited.insert(ep.id) {
809            continue;
810        }
811        counters.visited += 1;
812        candidates.push(Reverse(ep));
813        if accept(ep.id) {
814            results.push(ep);
815            if results.len() > ef {
816                results.pop();
817            }
818        }
819    }
820
821    while let Some(Reverse(current)) = candidates.pop() {
822        if results.len() >= ef && results.peek().is_some_and(|w| current.dist > w.dist) {
823            break;
824        }
825        // Collect unvisited neighbors first and prefetch their vectors, so
826        // the memory loads overlap instead of stalling one distance at a time.
827        let mut fresh = std::mem::take(&mut visited.fresh);
828        fresh.clear();
829        links.with_neighbors(current.id, layer, |neighbors| {
830            for &neighbor in neighbors {
831                if visited.insert(neighbor) {
832                    fresh.push(neighbor);
833                }
834            }
835        });
836        for &neighbor in &fresh {
837            vectors.prefetch(neighbor);
838        }
839        for &neighbor in &fresh {
840            counters.visited += 1;
841            counters.distance_computations += 1;
842            let dist = vectors.distance(query, neighbor, metric);
843            let worst = results.peek().map_or(f32::INFINITY, |w| w.dist);
844            if results.len() < ef || dist < worst {
845                let candidate = Candidate { dist, id: neighbor };
846                candidates.push(Reverse(candidate));
847                if accept(neighbor) {
848                    results.push(candidate);
849                    if results.len() > ef {
850                        results.pop();
851                    }
852                }
853            }
854        }
855        visited.fresh = fresh;
856    }
857
858    let mut out = results.into_vec();
859    out.sort();
860    out
861}
862
863/// Neighbor selection heuristic: prefer candidates that are closer to the
864/// base node than to any already selected neighbor, which keeps links spread
865/// across directions. Remaining slots are filled with the nearest pruned
866/// candidates. `candidates` must be sorted nearest first.
867fn select_neighbors(
868    candidates: &[Candidate],
869    max: usize,
870    vectors: &Vectors,
871    metric: Metric,
872) -> Vec<u32> {
873    let mut selected: Vec<Candidate> = Vec::with_capacity(max);
874    let mut pruned = Vec::new();
875    for &candidate in candidates {
876        if selected.len() >= max {
877            break;
878        }
879        let vector = vectors.get(candidate.id);
880        let diverse = selected
881            .iter()
882            .all(|s| vectors.distance(&vector, s.id, metric) > candidate.dist);
883        if diverse {
884            selected.push(candidate);
885        } else {
886            pruned.push(candidate);
887        }
888    }
889    for candidate in pruned {
890        if selected.len() >= max {
891            break;
892        }
893        selected.push(candidate);
894    }
895    selected.into_iter().map(|c| c.id).collect()
896}
897
898fn push_and_prune(
899    list: &mut Vec<u32>,
900    node: u32,
901    new: u32,
902    max: usize,
903    vectors: &Vectors,
904    metric: Metric,
905) {
906    list.push(new);
907    prune(list, node, max, vectors, metric);
908}
909
910/// Shrinks `list` back to `max` links with the selection heuristic.
911fn prune(list: &mut Vec<u32>, node: u32, max: usize, vectors: &Vectors, metric: Metric) {
912    if list.len() <= max {
913        return;
914    }
915    let base = vectors.get(node);
916    let mut candidates: Vec<Candidate> = list
917        .iter()
918        .map(|&n| Candidate {
919            dist: vectors.distance(&base, n, metric),
920            id: n,
921        })
922        .collect();
923    candidates.sort();
924    *list = select_neighbors(&candidates, max, vectors, metric);
925}
926
927#[cfg(test)]
928mod tests {
929    use super::*;
930
931    fn random_graph(n: usize, dim: usize) -> (Graph, Vectors) {
932        let mut rng = SplitMix64::new(9);
933        let mut vectors = Vectors::new(dim);
934        for _ in 0..n {
935            let v: Vec<f32> = (0..dim).map(|_| rng.next_f64() as f32).collect();
936            vectors.push(&v);
937        }
938        let mut graph = Graph::new(HnswParams::default(), 1);
939        graph.insert_batch(0..n as u32, &vectors, Metric::L2, 1);
940        (graph, vectors)
941    }
942
943    #[test]
944    fn repair_relinks_orphaned_nodes() {
945        let (mut graph, vectors) = random_graph(500, 8);
946        let orphan = (0..500u32).find(|&n| Some(n) != graph.entry).unwrap();
947        for layers in &mut graph.links {
948            for list in layers.iter_mut() {
949                list.retain(|&n| n != orphan);
950            }
951        }
952        assert!(!graph.reachable()[orphan as usize]);
953        assert_eq!(graph.repair_unreachable(&vectors, Metric::L2), 1);
954        assert!(graph.reachable().iter().all(|&r| r));
955        assert_eq!(graph.repair_unreachable(&vectors, Metric::L2), 0);
956    }
957
958    #[test]
959    fn visited_set_resets_between_searches() {
960        let mut visited = Visited::default();
961        visited.reset(200);
962        assert!(visited.insert(3) && visited.insert(130));
963        assert!(!visited.insert(3));
964        visited.reset(200);
965        assert!(visited.insert(3) && visited.insert(130));
966    }
967}