Skip to main content

lance_index/vector/
graph.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Generic Graph implementation.
5//!
6
7use std::cmp::Reverse;
8use std::collections::{BinaryHeap, VecDeque};
9use std::sync::Arc;
10
11use arrow_schema::{DataType, Field};
12use lance_core::deepsize::DeepSizeOf;
13
14use crate::vector::hnsw::builder::HnswQueryParams;
15
16pub mod builder;
17
18use crate::vector::DIST_COL;
19
20use crate::vector::storage::DistCalculator;
21
22pub(crate) const NEIGHBORS_COL: &str = "__neighbors";
23
24use std::sync::LazyLock;
25
26/// NEIGHBORS field.
27pub static NEIGHBORS_FIELD: LazyLock<Field> = LazyLock::new(|| {
28    Field::new(
29        NEIGHBORS_COL,
30        DataType::List(Field::new_list_field(DataType::UInt32, true).into()),
31        true,
32    )
33});
34pub static DISTS_FIELD: LazyLock<Field> = LazyLock::new(|| {
35    Field::new(
36        DIST_COL,
37        DataType::List(Field::new_list_field(DataType::Float32, true).into()),
38        true,
39    )
40});
41
42pub struct GraphNode<I = u32> {
43    pub id: I,
44    pub neighbors: Vec<I>,
45}
46
47impl<I> GraphNode<I> {
48    pub fn new(id: I, neighbors: Vec<I>) -> Self {
49        Self { id, neighbors }
50    }
51}
52
53impl<I> From<I> for GraphNode<I> {
54    fn from(id: I) -> Self {
55        Self {
56            id,
57            neighbors: vec![],
58        }
59    }
60}
61
62/// A wrapper for f32 to make it ordered, so that we can put it into
63/// a BTree or Heap
64#[derive(Debug, PartialEq, Clone, Copy, DeepSizeOf)]
65pub struct OrderedFloat(pub f32);
66
67impl PartialOrd for OrderedFloat {
68    #[inline(always)]
69    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
70        Some(self.cmp(other))
71    }
72}
73
74impl Eq for OrderedFloat {}
75
76impl Ord for OrderedFloat {
77    #[inline(always)]
78    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
79        self.0.total_cmp(&other.0)
80    }
81}
82
83impl From<f32> for OrderedFloat {
84    fn from(f: f32) -> Self {
85        Self(f)
86    }
87}
88
89impl From<OrderedFloat> for f32 {
90    fn from(f: OrderedFloat) -> Self {
91        f.0
92    }
93}
94
95#[derive(Debug, Eq, PartialEq, Clone, DeepSizeOf)]
96pub struct OrderedNode<T = u32>
97where
98    T: PartialEq + Eq,
99{
100    pub id: T,
101    pub dist: OrderedFloat,
102}
103
104impl<T: PartialEq + Eq> OrderedNode<T> {
105    pub fn new(id: T, dist: OrderedFloat) -> Self {
106        Self { id, dist }
107    }
108}
109
110impl<T: PartialEq + Eq> PartialOrd for OrderedNode<T> {
111    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
112        Some(self.cmp(other))
113    }
114}
115
116impl<T: PartialEq + Eq> Ord for OrderedNode<T> {
117    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
118        self.dist.cmp(&other.dist)
119    }
120}
121
122impl<T: PartialEq + Eq> From<(OrderedFloat, T)> for OrderedNode<T> {
123    fn from((dist, id): (OrderedFloat, T)) -> Self {
124        Self { id, dist }
125    }
126}
127
128impl<T: PartialEq + Eq> From<OrderedNode<T>> for (OrderedFloat, T) {
129    fn from(node: OrderedNode<T>) -> Self {
130        (node.dist, node.id)
131    }
132}
133
134/// Distance calculator.
135///
136/// This trait is used to calculate a query vector to a stream of vector IDs.
137///
138pub trait DistanceCalculator {
139    /// Compute distances between one query vector to all the vectors in the
140    /// list of IDs.
141    fn compute_distances(&self, ids: &[u32]) -> Box<dyn Iterator<Item = f32>>;
142}
143
144/// Graph trait.
145///
146/// Type parameters
147/// ---------------
148/// K: Vertex Index type
149/// T: the data type of vector, i.e., ``f32`` or ``f16``.
150pub trait Graph {
151    /// Get the number of nodes in the graph.
152    fn len(&self) -> usize;
153
154    /// Returns true if the graph is empty.
155    fn is_empty(&self) -> bool {
156        self.len() == 0
157    }
158
159    /// Get the neighbors of a graph node, identifyied by the index.
160    fn neighbors(&self, key: u32) -> Arc<Vec<u32>>;
161}
162
163pub trait BorrowingGraph {
164    /// Get the number of nodes in the graph.
165    fn len(&self) -> usize;
166
167    /// Returns true if the graph is empty.
168    fn is_empty(&self) -> bool {
169        self.len() == 0
170    }
171
172    /// Borrow the neighbors of a graph node, identified by the index.
173    fn neighbors(&self, key: u32) -> &[u32];
174}
175
176const WORD_BITS: usize = usize::BITS as usize;
177
178/// Compact visited list for graph traversals.
179pub struct Visited<'a> {
180    visited: &'a mut Vec<usize>,
181    recently_visited: &'a mut Vec<u32>,
182}
183
184impl Visited<'_> {
185    pub fn insert(&mut self, node_id: u32) {
186        let node_id_usize = node_id as usize;
187        let word_index = node_id_usize / WORD_BITS;
188        let mask = 1usize << (node_id_usize % WORD_BITS);
189        if self.visited[word_index] & mask == 0 {
190            self.visited[word_index] |= mask;
191            self.recently_visited.push(node_id);
192        }
193    }
194
195    pub fn contains(&self, node_id: u32) -> bool {
196        let node_id_usize = node_id as usize;
197        let word_index = node_id_usize / WORD_BITS;
198        let mask = 1usize << (node_id_usize % WORD_BITS);
199        self.visited[word_index] & mask != 0
200    }
201
202    #[inline(always)]
203    pub fn iter_ones(&self) -> impl Iterator<Item = usize> + '_ {
204        self.recently_visited
205            .iter()
206            .map(|node_id| *node_id as usize)
207    }
208
209    pub fn count_ones(&self) -> usize {
210        self.recently_visited.len()
211    }
212}
213
214impl Drop for Visited<'_> {
215    fn drop(&mut self) {
216        for node_id in self.recently_visited.iter().copied() {
217            let node_id_usize = node_id as usize;
218            let word_index = node_id_usize / WORD_BITS;
219            let mask = 1usize << (node_id_usize % WORD_BITS);
220            self.visited[word_index] &= !mask;
221        }
222        self.recently_visited.clear();
223    }
224}
225
226#[derive(Debug, Clone)]
227pub struct VisitedGenerator {
228    visited: Vec<usize>,
229    recently_visited: Vec<u32>,
230    capacity: usize,
231}
232
233impl VisitedGenerator {
234    pub fn new(capacity: usize) -> Self {
235        Self {
236            visited: vec![0; capacity.div_ceil(WORD_BITS)],
237            recently_visited: Vec::new(),
238            capacity,
239        }
240    }
241
242    pub fn generate(&mut self, node_count: usize) -> Visited<'_> {
243        if node_count > self.capacity {
244            let new_capacity = self.capacity.max(node_count).next_power_of_two();
245            self.visited.resize(new_capacity.div_ceil(WORD_BITS), 0);
246            self.capacity = new_capacity;
247        }
248        Visited {
249            visited: &mut self.visited,
250            recently_visited: &mut self.recently_visited,
251        }
252    }
253}
254
255fn process_neighbors_with_look_ahead<F>(
256    neighbors: &[u32],
257    mut process_neighbor: F,
258    look_ahead: Option<usize>,
259    dist_calc: &impl DistCalculator,
260) where
261    F: FnMut(u32),
262{
263    match look_ahead {
264        Some(look_ahead) => {
265            for i in 0..neighbors.len().saturating_sub(look_ahead) {
266                dist_calc.prefetch(neighbors[i + look_ahead]);
267                process_neighbor(neighbors[i]);
268            }
269            for neighbor in &neighbors[neighbors.len().saturating_sub(look_ahead)..] {
270                process_neighbor(*neighbor);
271            }
272        }
273        None => {
274            for neighbor in neighbors.iter() {
275                process_neighbor(*neighbor);
276            }
277        }
278    }
279}
280
281#[inline]
282fn furthest_distance(results: &BinaryHeap<OrderedNode>) -> OrderedFloat {
283    results
284        .peek()
285        .map(|node| node.dist)
286        .unwrap_or(OrderedFloat(f32::INFINITY))
287}
288
289#[inline]
290fn push_result(results: &mut BinaryHeap<OrderedNode>, candidate: OrderedNode, k: usize) {
291    if results.len() < k {
292        results.push(candidate);
293    } else if candidate.dist < results.peek().unwrap().dist {
294        results.pop();
295        results.push(candidate);
296    }
297}
298
299macro_rules! beam_search_loop {
300    (
301        $candidates:ident,
302        $results:ident,
303        $visited:ident,
304        $k:expr,
305        $dist_calc:expr,
306        $prefetch_distance:expr,
307        $accepts_result:expr,
308        |$current:ident, $process_neighbor:ident| $visit_neighbors:block
309    ) => {{
310        while !$candidates.is_empty() {
311            let $current = $candidates.pop().expect("candidates is empty").0;
312            let furthest = furthest_distance(&$results);
313
314            if $current.dist > furthest && $results.len() == $k {
315                break;
316            }
317
318            let $process_neighbor = |neighbor: u32| {
319                if $visited.contains(neighbor) {
320                    return;
321                }
322                $visited.insert(neighbor);
323                let dist: OrderedFloat = $dist_calc.distance(neighbor).into();
324                if dist <= furthest || $results.len() < $k {
325                    if $accepts_result(neighbor, dist) {
326                        push_result(&mut $results, (dist, neighbor).into(), $k);
327                    }
328                    $candidates.push(Reverse((dist, neighbor).into()));
329                }
330            };
331            $visit_neighbors
332        }
333    }};
334}
335
336macro_rules! greedy_search_loop {
337    (
338        $current:ident,
339        $closest_dist:ident,
340        $dist_calc:expr,
341        $prefetch_distance:expr,
342        |$process_neighbor:ident| $visit_neighbors:block
343    ) => {{
344        loop {
345            let mut next = None;
346            let $process_neighbor = |neighbor: u32| {
347                let dist = $dist_calc.distance(neighbor);
348                if dist < $closest_dist {
349                    $closest_dist = dist;
350                    next = Some(neighbor);
351                }
352            };
353            $visit_neighbors
354
355            if let Some(next) = next {
356                $current = next;
357            } else {
358                break;
359            }
360        }
361    }};
362}
363
364/// Beam search over a graph
365///
366/// This is the same as ``search-layer`` in HNSW.
367///
368/// Parameters
369/// ----------
370/// graph : Graph
371///  The graph to search.
372/// start : &[OrderedNode]
373///  The starting point.
374/// query : &[f32]
375///  The query vector.
376/// k : usize
377///  The number of results to return.
378/// bitset : Option<&RoaringBitmap>
379///  The bitset of node IDs to filter the results, bit 1 for the node to keep, and bit 0 for the node to discard.
380///
381/// Returns
382/// -------
383/// A descending sorted list of ``(dist, node_id)`` pairs.
384///
385/// WARNING: Internal API,  API stability is not guaranteed
386///
387/// TODO: This isn't actually beam search, function should probably be renamed
388pub fn beam_search(
389    graph: &dyn Graph,
390    ep: &OrderedNode,
391    params: &HnswQueryParams,
392    dist_calc: &impl DistCalculator,
393    bitset: Option<&Visited>,
394    prefetch_distance: Option<usize>,
395    visited: &mut Visited,
396) -> Vec<OrderedNode> {
397    let k = params.ef;
398    let mut candidates = BinaryHeap::with_capacity(k);
399    visited.insert(ep.id);
400    candidates.push(Reverse(ep.clone()));
401
402    let mut results = BinaryHeap::with_capacity(k);
403    let no_filter =
404        bitset.is_none() && params.lower_bound.is_none() && params.upper_bound.is_none();
405
406    if no_filter {
407        results.push(ep.clone());
408        let accepts_result = |_: u32, _: OrderedFloat| true;
409        beam_search_loop!(
410            candidates,
411            results,
412            visited,
413            k,
414            dist_calc,
415            prefetch_distance,
416            accepts_result,
417            |current, process_neighbor| {
418                let neighbors = graph.neighbors(current.id);
419                process_neighbors_with_look_ahead(
420                    &neighbors,
421                    process_neighbor,
422                    prefetch_distance,
423                    dist_calc,
424                );
425            }
426        );
427        return results.into_sorted_vec();
428    }
429
430    // add range search support
431    let lower_bound: OrderedFloat = params.lower_bound.unwrap_or(f32::MIN).into();
432    let upper_bound: OrderedFloat = params.upper_bound.unwrap_or(f32::MAX).into();
433
434    if bitset.map(|bitset| bitset.contains(ep.id)).unwrap_or(true)
435        && ep.dist >= lower_bound
436        && ep.dist < upper_bound
437    {
438        results.push(ep.clone());
439    }
440
441    let accepts_result = |node_id: u32, dist: OrderedFloat| {
442        bitset
443            .map(|bitset| bitset.contains(node_id))
444            .unwrap_or(true)
445            && dist >= lower_bound
446            && dist < upper_bound
447    };
448    beam_search_loop!(
449        candidates,
450        results,
451        visited,
452        k,
453        dist_calc,
454        prefetch_distance,
455        accepts_result,
456        |current, process_neighbor| {
457            let neighbors = graph.neighbors(current.id);
458            process_neighbors_with_look_ahead(
459                &neighbors,
460                process_neighbor,
461                prefetch_distance,
462                dist_calc,
463            );
464        }
465    );
466    results.into_sorted_vec()
467}
468
469pub fn beam_search_borrowed(
470    graph: &impl BorrowingGraph,
471    ep: &OrderedNode,
472    params: &HnswQueryParams,
473    dist_calc: &impl DistCalculator,
474    bitset: Option<&Visited>,
475    prefetch_distance: Option<usize>,
476    visited: &mut Visited,
477) -> Vec<OrderedNode> {
478    let k = params.ef;
479    let mut candidates = BinaryHeap::with_capacity(k);
480    visited.insert(ep.id);
481    candidates.push(Reverse(ep.clone()));
482
483    let mut results = BinaryHeap::with_capacity(k);
484    let no_filter =
485        bitset.is_none() && params.lower_bound.is_none() && params.upper_bound.is_none();
486
487    if no_filter {
488        results.push(ep.clone());
489        let accepts_result = |_: u32, _: OrderedFloat| true;
490        beam_search_loop!(
491            candidates,
492            results,
493            visited,
494            k,
495            dist_calc,
496            prefetch_distance,
497            accepts_result,
498            |current, process_neighbor| {
499                let neighbors = graph.neighbors(current.id);
500                process_neighbors_with_look_ahead(
501                    neighbors,
502                    process_neighbor,
503                    prefetch_distance,
504                    dist_calc,
505                );
506            }
507        );
508        return results.into_sorted_vec();
509    }
510
511    let lower_bound: OrderedFloat = params.lower_bound.unwrap_or(f32::MIN).into();
512    let upper_bound: OrderedFloat = params.upper_bound.unwrap_or(f32::MAX).into();
513
514    if bitset.map(|bitset| bitset.contains(ep.id)).unwrap_or(true)
515        && ep.dist >= lower_bound
516        && ep.dist < upper_bound
517    {
518        results.push(ep.clone());
519    }
520
521    let accepts_result = |node_id: u32, dist: OrderedFloat| {
522        bitset
523            .map(|bitset| bitset.contains(node_id))
524            .unwrap_or(true)
525            && dist >= lower_bound
526            && dist < upper_bound
527    };
528    beam_search_loop!(
529        candidates,
530        results,
531        visited,
532        k,
533        dist_calc,
534        prefetch_distance,
535        accepts_result,
536        |current, process_neighbor| {
537            let neighbors = graph.neighbors(current.id);
538            process_neighbors_with_look_ahead(
539                neighbors,
540                process_neighbor,
541                prefetch_distance,
542                dist_calc,
543            );
544        }
545    );
546    results.into_sorted_vec()
547}
548
549/// Number of mask-passing nodes used to seed [beam_search_acorn]'s frontier.
550const ACORN_SEED_COUNT: usize = 16;
551
552/// Cap on starved-frontier waypoint expansions in [beam_search_acorn],
553/// as a multiple of `ef`.
554const ACORN_BRIDGE_BUDGET_FACTOR: usize = 4;
555
556/// Beam search over the mask-passing subgraph (ACORN-1).
557///
558/// Only nodes in `bitset` get distances. A filtered-out neighbor contributes
559/// its own neighbors instead, expanded once via `expanded`. Deeper masked
560/// chains are crossed through unscored waypoints under a budget. The frontier
561/// starts from the entry point plus mask-sampled seeds. May return fewer than
562/// `min(ef, passing)` results if the budget runs out, so callers needing a
563/// guarantee must check the count.
564#[allow(clippy::too_many_arguments)]
565pub fn beam_search_acorn(
566    graph: &impl BorrowingGraph,
567    ep: &OrderedNode,
568    params: &HnswQueryParams,
569    dist_calc: &impl DistCalculator,
570    bitset: &Visited,
571    prefetch_distance: Option<usize>,
572    visited: &mut Visited,
573    expanded: &mut Visited,
574) -> Vec<OrderedNode> {
575    let ef = params.ef;
576    let lower_bound: OrderedFloat = params.lower_bound.unwrap_or(f32::MIN).into();
577    let upper_bound: OrderedFloat = params.upper_bound.unwrap_or(f32::MAX).into();
578    let passing_total = bitset.count_ones();
579    let mut candidates = BinaryHeap::with_capacity(ef);
580    let mut results = BinaryHeap::with_capacity(ef);
581    // collected per node before scoring so prefetch targets are the ids
582    // that actually get distances
583    let mut passing: Vec<u32> = Vec::with_capacity(64);
584    // masked nodes seen two hops out, expandable if the frontier starves,
585    // deduped against `expanded` at pop rather than at push
586    let mut waypoints: VecDeque<u32> = VecDeque::new();
587    let mut bridge_budget = ACORN_BRIDGE_BUDGET_FACTOR * ef;
588
589    // the entry point seeds the traversal even if it fails the mask
590    visited.insert(ep.id);
591    candidates.push(Reverse(ep.clone()));
592    if bitset.contains(ep.id) && ep.dist >= lower_bound && ep.dist < upper_bound {
593        results.push(ep.clone());
594    }
595
596    let stride = (passing_total / ACORN_SEED_COUNT).max(1);
597    for seed in bitset.iter_ones().step_by(stride).take(ACORN_SEED_COUNT) {
598        let seed = seed as u32;
599        if visited.contains(seed) {
600            continue;
601        }
602        visited.insert(seed);
603        let dist: OrderedFloat = dist_calc.distance(seed).into();
604        if dist >= lower_bound && dist < upper_bound {
605            push_result(&mut results, (dist, seed).into(), ef);
606        }
607        candidates.push(Reverse((dist, seed).into()));
608    }
609
610    loop {
611        let Some(Reverse(current)) = candidates.pop() else {
612            // frontier starved: burn bridge budget through masked waypoints
613            // until a new passing node is found
614            if results.len() >= ef.min(passing_total) {
615                break;
616            }
617            let mut found = false;
618            while let Some(waypoint) = waypoints.pop_front() {
619                if bridge_budget == 0 {
620                    break;
621                }
622                if expanded.contains(waypoint) {
623                    continue;
624                }
625                expanded.insert(waypoint);
626                bridge_budget -= 1;
627                for &neighbor in graph.neighbors(waypoint) {
628                    if bitset.contains(neighbor) {
629                        if !visited.contains(neighbor) {
630                            visited.insert(neighbor);
631                            let dist: OrderedFloat = dist_calc.distance(neighbor).into();
632                            if dist >= lower_bound && dist < upper_bound {
633                                push_result(&mut results, (dist, neighbor).into(), ef);
634                            }
635                            candidates.push(Reverse((dist, neighbor).into()));
636                            found = true;
637                        }
638                    } else if !expanded.contains(neighbor) {
639                        waypoints.push_back(neighbor);
640                    }
641                }
642                if found {
643                    break;
644                }
645            }
646            if !found {
647                break;
648            }
649            continue;
650        };
651        if current.dist > furthest_distance(&results) && results.len() == ef {
652            break;
653        }
654
655        passing.clear();
656        for &neighbor in graph.neighbors(current.id) {
657            if bitset.contains(neighbor) {
658                if !visited.contains(neighbor) {
659                    visited.insert(neighbor);
660                    passing.push(neighbor);
661                }
662            } else if !expanded.contains(neighbor) {
663                expanded.insert(neighbor);
664                for &second_hop in graph.neighbors(neighbor) {
665                    if bitset.contains(second_hop) {
666                        if !visited.contains(second_hop) {
667                            visited.insert(second_hop);
668                            passing.push(second_hop);
669                        }
670                    } else if !expanded.contains(second_hop) {
671                        waypoints.push_back(second_hop);
672                    }
673                }
674            }
675        }
676
677        process_neighbors_with_look_ahead(
678            &passing,
679            |node| {
680                let dist: OrderedFloat = dist_calc.distance(node).into();
681                if dist <= furthest_distance(&results) || results.len() < ef {
682                    if dist >= lower_bound && dist < upper_bound {
683                        push_result(&mut results, (dist, node).into(), ef);
684                    }
685                    candidates.push(Reverse((dist, node).into()));
686                }
687            },
688            prefetch_distance,
689            dist_calc,
690        );
691    }
692    results.into_sorted_vec()
693}
694
695/// Greedy search over a graph
696///
697/// This searches for only one result, only used for finding the entry point
698///
699/// Parameters
700/// ----------
701/// graph : Graph
702///    The graph to search.
703/// start : u32
704///   The index starting point.
705/// query : &[f32]
706///   The query vector.
707///
708/// Returns
709/// -------
710/// A ``(dist, node_id)`` pair.
711///
712/// WARNING: Internal API,  API stability is not guaranteed
713pub fn greedy_search(
714    graph: &dyn Graph,
715    start: OrderedNode,
716    dist_calc: &impl DistCalculator,
717    prefetch_distance: Option<usize>,
718) -> OrderedNode {
719    let mut current = start.id;
720    let mut closest_dist = start.dist.0;
721    greedy_search_loop!(
722        current,
723        closest_dist,
724        dist_calc,
725        prefetch_distance,
726        |process_neighbor| {
727            let neighbors = graph.neighbors(current);
728            process_neighbors_with_look_ahead(
729                &neighbors,
730                process_neighbor,
731                prefetch_distance,
732                dist_calc,
733            );
734        }
735    );
736    OrderedNode::new(current, closest_dist.into())
737}
738
739pub fn greedy_search_borrowed(
740    graph: &impl BorrowingGraph,
741    start: OrderedNode,
742    dist_calc: &impl DistCalculator,
743    prefetch_distance: Option<usize>,
744) -> OrderedNode {
745    let mut current = start.id;
746    let mut closest_dist = start.dist.0;
747    greedy_search_loop!(
748        current,
749        closest_dist,
750        dist_calc,
751        prefetch_distance,
752        |process_neighbor| {
753            let neighbors = graph.neighbors(current);
754            process_neighbors_with_look_ahead(
755                neighbors,
756                process_neighbor,
757                prefetch_distance,
758                dist_calc,
759            );
760        }
761    );
762    OrderedNode::new(current, closest_dist.into())
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768
769    struct ChainGraph {
770        neighbors: Vec<Vec<u32>>,
771    }
772
773    impl BorrowingGraph for ChainGraph {
774        fn len(&self) -> usize {
775            self.neighbors.len()
776        }
777
778        fn neighbors(&self, key: u32) -> &[u32] {
779            &self.neighbors[key as usize]
780        }
781    }
782
783    struct ZeroDistance;
784
785    impl DistCalculator for ZeroDistance {
786        fn distance(&self, _id: u32) -> f32 {
787            0.0
788        }
789
790        fn distance_all(&self, _k_hint: usize) -> Vec<f32> {
791            Vec::new()
792        }
793    }
794
795    /// Passing components joined only through chains of two masked nodes
796    /// must still all be found (from review: without waypoint expansion
797    /// only the seeded nodes return).
798    #[test]
799    fn test_acorn_reaches_across_masked_chains() {
800        const PASSING_COUNT: usize = 20;
801        const FAILING_COUNT: usize = (PASSING_COUNT - 1) * 2;
802        let mut neighbors = vec![Vec::new(); PASSING_COUNT + FAILING_COUNT];
803        for index in 0..PASSING_COUNT - 1 {
804            let left = index as u32;
805            let first_failing = (PASSING_COUNT + index * 2) as u32;
806            let second_failing = first_failing + 1;
807            let right = left + 1;
808
809            neighbors[left as usize].push(first_failing);
810            neighbors[first_failing as usize].extend([left, second_failing]);
811            neighbors[second_failing as usize].extend([first_failing, right]);
812            neighbors[right as usize].push(second_failing);
813        }
814        let graph = ChainGraph { neighbors };
815        let params = HnswQueryParams {
816            ef: 30,
817            lower_bound: None,
818            upper_bound: None,
819            dist_q_c: 0.0,
820            use_acorn: false,
821        };
822        let entry = OrderedNode::new(0, 0.0.into());
823
824        let mut mask_generator = VisitedGenerator::new(graph.len());
825        let mut mask = mask_generator.generate(graph.len());
826        for id in 0..PASSING_COUNT as u32 {
827            mask.insert(id);
828        }
829
830        let mut acorn_visited_generator = VisitedGenerator::new(graph.len());
831        let mut acorn_expanded_generator = VisitedGenerator::new(graph.len());
832        let acorn_results = beam_search_acorn(
833            &graph,
834            &entry,
835            &params,
836            &ZeroDistance,
837            &mask,
838            None,
839            &mut acorn_visited_generator.generate(graph.len()),
840            &mut acorn_expanded_generator.generate(graph.len()),
841        );
842
843        let mut basic_visited_generator = VisitedGenerator::new(graph.len());
844        let basic_results = beam_search_borrowed(
845            &graph,
846            &entry,
847            &params,
848            &ZeroDistance,
849            Some(&mask),
850            None,
851            &mut basic_visited_generator.generate(graph.len()),
852        );
853
854        assert_eq!(basic_results.len(), PASSING_COUNT);
855        assert_eq!(acorn_results.len(), PASSING_COUNT);
856        assert!(acorn_results.iter().all(|node| mask.contains(node.id)));
857    }
858}