1use arrow::array::{AsArray, ListBuilder, UInt32Builder};
7use arrow::compute::concat_batches;
8use arrow::datatypes::{DataType, UInt32Type};
9use arrow_array::{ArrayRef, Float32Array, ListArray, RecordBatch, UInt64Array};
10use crossbeam_queue::ArrayQueue;
11use itertools::Itertools;
12use lance_core::deepsize::DeepSizeOf;
13use lance_core::utils::row_addr_remap::RowAddrRemap;
14
15use lance_core::utils::tokio::get_num_compute_intensive_cpus;
16use lance_linalg::distance::DistanceType;
17use rayon::prelude::*;
18use std::cmp::min;
19use std::collections::{BinaryHeap, HashMap, VecDeque};
20use std::fmt::Debug;
21use std::iter;
22use std::sync::Arc;
23use std::sync::RwLock;
24use std::sync::atomic::{AtomicUsize, Ordering};
25use tracing::instrument;
26
27use lance_core::{Error, Result};
28use rand::{Rng, SeedableRng, rngs::SmallRng};
29use serde::{Deserialize, Serialize};
30
31use super::super::graph::beam_search;
32use super::{
33 HNSW_TYPE, HnswMetadata, VECTOR_ID_COL, VECTOR_ID_FIELD, select_neighbors_heuristic_owned,
34};
35use crate::metrics::MetricsCollector;
36use crate::prefilter::PreFilter;
37use crate::vector::flat::storage::{FlatBinStorage, FlatFloatStorage};
38use crate::vector::graph::builder::GraphBuilderNode;
39use crate::vector::graph::{
40 BorrowingGraph, DISTS_FIELD, Graph, NEIGHBORS_COL, NEIGHBORS_FIELD, OrderedFloat, OrderedNode,
41 VisitedGenerator,
42};
43use crate::vector::graph::{Visited, beam_search_borrowed, greedy_search, greedy_search_borrowed};
44use crate::vector::storage::{DistCalculator, VectorStore};
45use crate::vector::v3::subindex::IvfSubIndex;
46use crate::vector::{Query, VECTOR_RESULT_SCHEMA};
47
48pub const HNSW_METADATA_KEY: &str = "lance:hnsw";
49
50pub(crate) const HNSW_LEVEL_RNG_SEED: u64 = 42;
58
59#[derive(Debug, Clone, Serialize, Deserialize, DeepSizeOf)]
61pub struct HnswBuildParams {
62 pub max_level: u16,
64
65 pub m: usize,
67
68 pub ef_construction: usize,
70
71 pub prefetch_distance: Option<usize>,
73}
74
75impl From<&HnswBuildParams> for crate::pb::HnswParameters {
76 fn from(params: &HnswBuildParams) -> Self {
77 Self {
78 max_connections: params.m as u32,
79 construction_ef: params.ef_construction as u32,
80 max_level: params.max_level as u32,
81 }
82 }
83}
84
85impl Default for HnswBuildParams {
86 fn default() -> Self {
87 Self {
88 max_level: 7,
89 m: 20,
90 ef_construction: 150,
91 prefetch_distance: Some(2),
92 }
93 }
94}
95
96impl HnswBuildParams {
97 pub fn max_level(mut self, max_level: u16) -> Self {
100 self.max_level = max_level;
101 self
102 }
103
104 pub fn num_edges(mut self, m: usize) -> Self {
107 self.m = m;
108 self
109 }
110
111 pub fn ef_construction(mut self, ef_construction: usize) -> Self {
116 self.ef_construction = ef_construction;
117 self
118 }
119
120 pub async fn build(self, data: ArrayRef, distance_type: DistanceType) -> Result<HNSW> {
126 let vectors = data.as_fixed_size_list().clone();
127 match (vectors.value_type(), distance_type) {
128 (DataType::UInt8, DistanceType::Hamming) => {
129 let vec_store = Arc::new(FlatBinStorage::new(vectors, distance_type));
130 HNSW::index_vectors(vec_store.as_ref(), self)
131 }
132 (DataType::UInt8, _) => Err(Error::invalid_input(format!(
133 "HNSW only supports hamming distance for UInt8 vectors, got {}",
134 distance_type
135 ))),
136 (_, DistanceType::Hamming) => Err(Error::invalid_input(format!(
137 "HNSW hamming distance only supports UInt8 vectors, got {}",
138 vectors.value_type()
139 ))),
140 _ => {
141 let vec_store = Arc::new(FlatFloatStorage::new(vectors, distance_type));
142 HNSW::index_vectors(vec_store.as_ref(), self)
143 }
144 }
145 }
146}
147
148#[derive(Clone, DeepSizeOf)]
156pub struct HNSW {
157 inner: Arc<HnswCore>,
158}
159
160struct HnswCore {
161 params: HnswBuildParams,
162 graph: HnswGraph,
163 level_count: Vec<usize>,
164 entry_point: u32,
165 visited_generator_queue: Arc<ArrayQueue<VisitedGenerator>>,
166}
167
168impl DeepSizeOf for HnswCore {
169 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
170 self.params.deep_size_of_children(context)
171 + self.graph.deep_size_of_children(context)
172 + self.level_count.deep_size_of_children(context)
173 }
175}
176
177impl HnswCore {
178 fn max_level(&self) -> u16 {
179 self.params.max_level
180 }
181
182 fn num_nodes(&self, level: usize) -> usize {
183 self.level_count[level]
184 }
185}
186
187impl Debug for HNSW {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 write!(f, "HNSW(max_layers: {})", self.inner.max_level() as usize,)
190 }
191}
192
193impl HNSW {
194 pub(crate) fn from_parts(
197 params: HnswBuildParams,
198 nodes: Vec<GraphBuilderNode>,
199 level_count: Vec<usize>,
200 entry_point: u32,
201 ) -> Self {
202 let queue_size = get_num_compute_intensive_cpus().max(1) * 2;
203 let visited_generator_queue = Arc::new(ArrayQueue::new(queue_size));
204 for _ in 0..queue_size {
205 let _ = visited_generator_queue.push(VisitedGenerator::new(0));
206 }
207 Self {
208 inner: Arc::new(HnswCore {
209 params,
210 graph: HnswGraph::Built(Arc::new(nodes)),
211 level_count,
212 entry_point,
213 visited_generator_queue,
214 }),
215 }
216 }
217
218 pub fn empty() -> Self {
219 Self {
220 inner: Arc::new(HnswCore {
221 params: HnswBuildParams::default(),
222 graph: HnswGraph::Built(Arc::new(Vec::new())),
223 level_count: Vec::new(),
224 entry_point: 0,
225 visited_generator_queue: Arc::new(ArrayQueue::new(1)),
226 }),
227 }
228 }
229
230 pub fn len(&self) -> usize {
231 match &self.inner.graph {
232 HnswGraph::Built(nodes) => nodes.len(),
233 HnswGraph::Loaded(graph) => graph.level_count[0],
235 }
236 }
237
238 pub fn is_empty(&self) -> bool {
239 self.len() == 0
240 }
241
242 pub fn max_level(&self) -> u16 {
243 self.inner.max_level()
244 }
245
246 pub fn num_nodes(&self, level: usize) -> usize {
247 self.inner.num_nodes(level)
248 }
249
250 pub fn nodes(&self) -> Option<Arc<Vec<GraphBuilderNode>>> {
255 match &self.inner.graph {
256 HnswGraph::Built(nodes) => Some(nodes.clone()),
257 HnswGraph::Loaded(_) => None,
258 }
259 }
260
261 #[allow(clippy::too_many_arguments)]
262 pub fn search_inner(
263 &self,
264 query: ArrayRef,
265 k: usize,
266 params: &HnswQueryParams,
267 bitset: Option<Visited>,
268 visited_generator: &mut VisitedGenerator,
269 storage: &impl VectorStore,
270 prefetch_distance: Option<usize>,
271 ) -> Result<Vec<OrderedNode>> {
272 let dist_calc = storage.dist_calculator(query, params.dist_q_c);
273 let entry = self.inner.entry_point;
274 let ep = OrderedNode::new(entry, dist_calc.distance(entry).into());
275
276 let result = match &self.inner.graph {
282 HnswGraph::Built(nodes) => {
283 let nodes = nodes.as_slice();
284 self.run_search(
285 ep,
286 k,
287 params,
288 bitset.as_ref(),
289 visited_generator,
290 storage.len(),
291 prefetch_distance,
292 &dist_calc,
293 |level| ImmutableHnswLevelView::new(level, nodes),
294 ImmutableHnswBottomView::new(nodes),
295 )
296 }
297 HnswGraph::Loaded(graph) => {
298 let graph = graph.as_ref();
299 self.run_search(
300 ep,
301 k,
302 params,
303 bitset.as_ref(),
304 visited_generator,
305 storage.len(),
306 prefetch_distance,
307 &dist_calc,
308 |level| LoadedHnswLevelView::new(level, graph),
309 LoadedHnswBottomView::new(graph),
310 )
311 }
312 };
313 Ok(result)
314 }
315
316 #[allow(clippy::too_many_arguments)]
324 fn run_search<L, B>(
325 &self,
326 ep: OrderedNode,
327 k: usize,
328 params: &HnswQueryParams,
329 bitset: Option<&Visited>,
330 visited_generator: &mut VisitedGenerator,
331 storage_len: usize,
332 prefetch_distance: Option<usize>,
333 dist_calc: &impl DistCalculator,
334 make_level: impl Fn(u16) -> L,
335 bottom: B,
336 ) -> Vec<OrderedNode>
337 where
338 L: BorrowingGraph,
339 B: BorrowingGraph,
340 {
341 let mut ep = ep;
342 for level in (0..self.max_level()).rev() {
343 let cur_level = make_level(level);
344 ep = greedy_search_borrowed(
345 &cur_level,
346 ep,
347 dist_calc,
348 self.inner.params.prefetch_distance,
349 );
350 }
351 let mut visited = visited_generator.generate(storage_len);
352 beam_search_borrowed(
353 &bottom,
354 &ep,
355 params,
356 dist_calc,
357 bitset,
358 prefetch_distance,
359 &mut visited,
360 )
361 .into_iter()
362 .take(k)
363 .collect::<Vec<OrderedNode>>()
364 }
365
366 #[instrument(level = "debug", skip(self, query, bitset, storage))]
367 pub fn search_basic(
368 &self,
369 query: ArrayRef,
370 k: usize,
371 params: &HnswQueryParams,
372 bitset: Option<Visited>,
373 storage: &impl VectorStore,
374 ) -> Result<Vec<OrderedNode>> {
375 let mut visited_generator = self
376 .inner
377 .visited_generator_queue
378 .pop()
379 .unwrap_or_else(|| VisitedGenerator::new(storage.len()));
380 let result = self.search_inner(
381 query,
382 k,
383 params,
384 bitset,
385 &mut visited_generator,
386 storage,
387 Some(2),
388 );
389
390 match self.inner.visited_generator_queue.push(visited_generator) {
391 Ok(_) => {}
392 Err(_) => {
393 log::warn!("visited_generator_queue is full");
394 }
395 }
396
397 result
398 }
399
400 #[instrument(level = "debug", skip(self, storage, query, prefilter_bitset))]
401 fn flat_search(
402 &self,
403 storage: &impl VectorStore,
404 query: ArrayRef,
405 k: usize,
406 prefilter_bitset: Visited,
407 params: &HnswQueryParams,
408 ) -> Vec<OrderedNode> {
409 let lower_bound: OrderedFloat = params.lower_bound.unwrap_or(f32::MIN).into();
410 let upper_bound: OrderedFloat = params.upper_bound.unwrap_or(f32::MAX).into();
411
412 let dist_calc = storage.dist_calculator(query, params.dist_q_c);
413 let mut heap = BinaryHeap::<OrderedNode>::with_capacity(k);
414
415 match self.inner.params.prefetch_distance {
416 Some(ahead) if ahead > 0 => {
417 let mut ids_iter = prefilter_bitset.iter_ones().map(|i| i as u32);
418 let mut buffer = VecDeque::with_capacity(ahead + 1);
419 for _ in 0..=ahead {
420 if let Some(id) = ids_iter.next() {
421 buffer.push_back(id);
422 } else {
423 break;
424 }
425 }
426
427 while let Some(node_id) = buffer.pop_front() {
428 if let Some(&prefetch_id) = buffer.get(ahead - 1) {
429 dist_calc.prefetch(prefetch_id);
430 }
431 if let Some(next) = ids_iter.next() {
432 buffer.push_back(next);
433 }
434
435 let dist: OrderedFloat = dist_calc.distance(node_id).into();
436 if dist <= lower_bound || dist > upper_bound {
437 continue;
438 }
439 if heap.len() < k {
440 heap.push((dist, node_id).into());
441 } else if dist < heap.peek().unwrap().dist {
442 heap.pop();
443 heap.push((dist, node_id).into());
444 }
445 }
446 }
447 _ => {
448 for node_id in prefilter_bitset.iter_ones().map(|i| i as u32) {
449 let dist: OrderedFloat = dist_calc.distance(node_id).into();
450 if dist <= lower_bound || dist > upper_bound {
451 continue;
452 }
453 if heap.len() < k {
454 heap.push((dist, node_id).into());
455 } else if dist < heap.peek().unwrap().dist {
456 heap.pop();
457 heap.push((dist, node_id).into());
458 }
459 }
460 }
461 };
462 heap.into_sorted_vec()
463 }
464
465 pub fn metadata(&self) -> HnswMetadata {
467 let level_offsets = self
470 .inner
471 .level_count
472 .iter()
473 .chain(iter::once(&0))
474 .scan(0, |state, x| {
475 let start = *state;
476 *state += *x;
477 Some(start)
478 })
479 .collect();
480
481 HnswMetadata {
482 entry_point: self.inner.entry_point,
483 params: self.inner.params.clone(),
484 level_offsets,
485 }
486 }
487}
488
489struct HnswBuilder {
490 params: HnswBuildParams,
491
492 nodes: Arc<Vec<RwLock<GraphBuilderNode>>>,
493 level_count: Vec<AtomicUsize>,
494
495 entry_point: u32,
496
497 visited_generator_queue: Arc<ArrayQueue<VisitedGenerator>>,
498}
499
500impl DeepSizeOf for HnswBuilder {
501 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
502 self.params.deep_size_of_children(context)
503 + self.nodes.deep_size_of_children(context)
504 + self.level_count.deep_size_of_children(context)
505 }
507}
508
509impl HnswBuilder {
510 fn finish(self) -> HNSW {
511 let nodes = match Arc::try_unwrap(self.nodes) {
512 Ok(nodes) => nodes
513 .into_iter()
514 .map(|node| node.into_inner().expect("builder lock poisoned"))
515 .collect(),
516 Err(nodes) => nodes
517 .iter()
518 .map(|node| node.read().expect("builder lock poisoned").clone())
519 .collect(),
520 };
521
522 let level_count = self
523 .level_count
524 .into_iter()
525 .map(|count| count.load(Ordering::Relaxed))
526 .collect();
527
528 HNSW {
529 inner: Arc::new(HnswCore {
530 params: self.params,
531 graph: HnswGraph::Built(Arc::new(nodes)),
532 level_count,
533 entry_point: self.entry_point,
534 visited_generator_queue: self.visited_generator_queue,
535 }),
536 }
537 }
538
539 pub fn with_params(params: HnswBuildParams, storage: &impl VectorStore) -> Self {
541 let len = storage.len();
542 let max_level = params.max_level;
543
544 let level_count = (0..max_level)
545 .map(|_| AtomicUsize::new(0))
546 .collect::<Vec<_>>();
547
548 let visited_generator_queue = Arc::new(ArrayQueue::new(get_num_compute_intensive_cpus()));
549 for _ in 0..get_num_compute_intensive_cpus() {
550 visited_generator_queue
551 .push(VisitedGenerator::new(0))
552 .unwrap();
553 }
554 let mut builder = Self {
555 params,
556 nodes: Arc::new(Vec::new()),
557 level_count,
558 entry_point: 0,
559 visited_generator_queue,
560 };
561
562 if storage.is_empty() {
563 return builder;
564 }
565
566 let mut nodes = Vec::with_capacity(len);
567 {
568 if len > 0 {
569 nodes.push(RwLock::new(GraphBuilderNode::new(0, max_level as usize)));
570 }
571 let mut level_rng = SmallRng::seed_from_u64(HNSW_LEVEL_RNG_SEED);
572 for i in 1..len {
573 nodes.push(RwLock::new(GraphBuilderNode::new(
574 i as u32,
575 builder.random_level(&mut level_rng) as usize + 1,
576 )));
577 }
578 }
579 builder.nodes = Arc::new(nodes);
580
581 builder
582 }
583
584 fn random_level<R: Rng + ?Sized>(&self, rng: &mut R) -> u16 {
588 let ml = 1.0 / (self.params.m as f32).ln();
589 min(
590 (-rng.random::<f32>().ln() * ml) as u16,
591 self.params.max_level - 1,
592 )
593 }
594
595 fn insert(
597 &self,
598 node: u32,
599 visited_generator: &mut VisitedGenerator,
600 storage: &impl VectorStore,
601 ) {
602 let nodes = &self.nodes;
603 let target_level = nodes[node as usize].read().unwrap().level_neighbors.len() as u16 - 1;
604 let dist_calc = storage.dist_calculator_from_id(node);
605 let mut ep = OrderedNode::new(
606 self.entry_point,
607 dist_calc.distance(self.entry_point).into(),
608 );
609
610 for level in (target_level + 1..self.params.max_level).rev() {
619 let cur_level = HnswLevelView::new(level, nodes);
620 ep = greedy_search(&cur_level, ep, &dist_calc, self.params.prefetch_distance);
621 }
622
623 let mut pruned_neighbors_per_level: Vec<Vec<_>> =
624 vec![Vec::new(); (target_level + 1) as usize];
625 {
626 let mut current_node = nodes[node as usize].write().unwrap();
627 for level in (0..=target_level).rev() {
628 self.level_count[level as usize].fetch_add(1, Ordering::Relaxed);
629
630 let neighbors = self.search_level(&ep, level, &dist_calc, nodes, visited_generator);
631 for neighbor in &neighbors {
632 current_node.add_neighbor(neighbor.id, neighbor.dist, level);
633 }
634 self.prune(storage, &mut current_node, level);
635 pruned_neighbors_per_level[level as usize]
636 .clone_from(¤t_node.level_neighbors_ranked[level as usize]);
637
638 ep = neighbors[0].clone();
639 }
640 }
641 for (level, pruned_neighbors) in pruned_neighbors_per_level.iter().enumerate() {
642 for unpruned_edge in pruned_neighbors {
643 let level = level as u16;
644 let m_max = match level {
645 0 => self.params.m * 2,
646 _ => self.params.m,
647 };
648 if unpruned_edge.dist
649 < nodes[unpruned_edge.id as usize]
650 .read()
651 .unwrap()
652 .cutoff(level, m_max)
653 {
654 let mut chosen_node = nodes[unpruned_edge.id as usize].write().unwrap();
655 chosen_node.add_neighbor(node, unpruned_edge.dist, level);
656 self.prune(storage, &mut chosen_node, level);
657 }
658 }
659 }
660 }
661
662 fn search_level(
663 &self,
664 ep: &OrderedNode,
665 level: u16,
666 dist_calc: &impl DistCalculator,
667 nodes: &[RwLock<GraphBuilderNode>],
668 visited_generator: &mut VisitedGenerator,
669 ) -> Vec<OrderedNode> {
670 let cur_level = HnswLevelView::new(level, nodes);
671 let mut visited = visited_generator.generate(nodes.len());
672 beam_search(
673 &cur_level,
674 ep,
675 &HnswQueryParams {
676 ef: self.params.ef_construction,
677 lower_bound: None,
678 upper_bound: None,
679 dist_q_c: 0.0,
680 },
681 dist_calc,
682 None,
683 self.params.prefetch_distance,
684 &mut visited,
685 )
686 }
687
688 fn prune(&self, storage: &impl VectorStore, builder_node: &mut GraphBuilderNode, level: u16) {
689 let m_max = match level {
690 0 => self.params.m * 2,
691 _ => self.params.m,
692 };
693
694 let neighbors_ranked = &mut builder_node.level_neighbors_ranked[level as usize];
695 if neighbors_ranked.len() <= m_max {
696 builder_node.update_from_ranked_neighbors(level);
697 return;
698 }
699
700 let level_neighbors = std::mem::take(neighbors_ranked);
701 *neighbors_ranked = select_neighbors_heuristic_owned(storage, level_neighbors, m_max);
702 builder_node.update_from_ranked_neighbors(level);
703 }
704}
705
706pub(crate) struct HnswLevelView<'a> {
709 level: u16,
710 nodes: &'a [RwLock<GraphBuilderNode>],
711}
712
713impl<'a> HnswLevelView<'a> {
714 pub fn new(level: u16, nodes: &'a [RwLock<GraphBuilderNode>]) -> Self {
715 Self { level, nodes }
716 }
717}
718
719impl Graph for HnswLevelView<'_> {
720 fn len(&self) -> usize {
721 self.nodes.len()
722 }
723
724 fn neighbors(&self, key: u32) -> Arc<Vec<u32>> {
725 let node = &self.nodes[key as usize];
726 node.read().unwrap().level_neighbors[self.level as usize].clone()
727 }
728}
729
730pub(crate) struct ImmutableHnswLevelView<'a> {
731 level: u16,
732 nodes: &'a [GraphBuilderNode],
733}
734
735impl<'a> ImmutableHnswLevelView<'a> {
736 pub fn new(level: u16, nodes: &'a [GraphBuilderNode]) -> Self {
737 Self { level, nodes }
738 }
739}
740
741impl Graph for ImmutableHnswLevelView<'_> {
742 fn len(&self) -> usize {
743 self.nodes.len()
744 }
745
746 fn neighbors(&self, key: u32) -> Arc<Vec<u32>> {
747 self.nodes[key as usize].level_neighbors[self.level as usize].clone()
748 }
749}
750
751impl BorrowingGraph for ImmutableHnswLevelView<'_> {
752 fn len(&self) -> usize {
753 self.nodes.len()
754 }
755
756 fn neighbors(&self, key: u32) -> &[u32] {
757 self.nodes[key as usize].level_neighbors[self.level as usize].as_slice()
758 }
759}
760
761pub(crate) struct ImmutableHnswBottomView<'a> {
762 nodes: &'a [GraphBuilderNode],
763}
764
765impl<'a> ImmutableHnswBottomView<'a> {
766 pub fn new(nodes: &'a [GraphBuilderNode]) -> Self {
767 Self { nodes }
768 }
769}
770
771impl Graph for ImmutableHnswBottomView<'_> {
772 fn len(&self) -> usize {
773 self.nodes.len()
774 }
775
776 fn neighbors(&self, key: u32) -> Arc<Vec<u32>> {
777 self.nodes[key as usize].bottom_neighbors.clone()
778 }
779}
780
781impl BorrowingGraph for ImmutableHnswBottomView<'_> {
782 fn len(&self) -> usize {
783 self.nodes.len()
784 }
785
786 fn neighbors(&self, key: u32) -> &[u32] {
787 self.nodes[key as usize].bottom_neighbors.as_slice()
788 }
789}
790
791enum LevelLookup {
793 Dense,
797 Sparse(HashMap<u32, u32>),
808}
809
810struct LoadedHnswGraph {
822 batch: RecordBatch,
825 level_neighbors: Vec<ListArray>,
827 level_lookup: Vec<LevelLookup>,
829 level_count: Vec<usize>,
831}
832
833impl DeepSizeOf for LoadedHnswGraph {
834 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
835 self.batch.get_array_memory_size()
841 }
842}
843
844impl LoadedHnswGraph {
845 #[inline]
848 fn neighbors_at(&self, level: usize, key: u32) -> &[u32] {
849 let row = match &self.level_lookup[level] {
850 LevelLookup::Dense => key as usize,
851 LevelLookup::Sparse(id_to_row) => match id_to_row.get(&key) {
852 Some(&row) => row as usize,
853 None => return &[],
859 },
860 };
861 let list = &self.level_neighbors[level];
862 let offsets = list.value_offsets();
863 let start = offsets[row] as usize;
864 let end = offsets[row + 1] as usize;
865 let values = list.values().as_primitive::<UInt32Type>();
869 &values.values()[start..end]
870 }
871}
872
873pub(crate) struct LoadedHnswLevelView<'a> {
875 level: usize,
876 graph: &'a LoadedHnswGraph,
877}
878
879impl<'a> LoadedHnswLevelView<'a> {
880 fn new(level: u16, graph: &'a LoadedHnswGraph) -> Self {
881 Self {
882 level: level as usize,
883 graph,
884 }
885 }
886}
887
888impl Graph for LoadedHnswLevelView<'_> {
889 fn len(&self) -> usize {
890 self.graph.level_count[0]
892 }
893
894 fn neighbors(&self, key: u32) -> Arc<Vec<u32>> {
895 Arc::new(self.graph.neighbors_at(self.level, key).to_vec())
899 }
900}
901
902impl BorrowingGraph for LoadedHnswLevelView<'_> {
903 fn len(&self) -> usize {
904 self.graph.level_count[0]
905 }
906
907 fn neighbors(&self, key: u32) -> &[u32] {
908 self.graph.neighbors_at(self.level, key)
909 }
910}
911
912pub(crate) struct LoadedHnswBottomView<'a> {
914 graph: &'a LoadedHnswGraph,
915}
916
917impl<'a> LoadedHnswBottomView<'a> {
918 fn new(graph: &'a LoadedHnswGraph) -> Self {
919 Self { graph }
920 }
921}
922
923impl Graph for LoadedHnswBottomView<'_> {
924 fn len(&self) -> usize {
925 self.graph.level_count[0]
926 }
927
928 fn neighbors(&self, key: u32) -> Arc<Vec<u32>> {
929 Arc::new(self.graph.neighbors_at(0, key).to_vec())
930 }
931}
932
933impl BorrowingGraph for LoadedHnswBottomView<'_> {
934 fn len(&self) -> usize {
935 self.graph.level_count[0]
936 }
937
938 fn neighbors(&self, key: u32) -> &[u32] {
939 self.graph.neighbors_at(0, key)
940 }
941}
942
943enum HnswGraph {
945 Built(Arc<Vec<GraphBuilderNode>>),
949 Loaded(Arc<LoadedHnswGraph>),
951}
952
953impl DeepSizeOf for HnswGraph {
954 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
955 match self {
956 Self::Built(nodes) => nodes.deep_size_of_children(context),
957 Self::Loaded(graph) => graph.deep_size_of_children(context),
958 }
959 }
960}
961
962#[derive(Debug, Clone, Copy)]
963pub struct HnswQueryParams {
964 pub ef: usize,
965 pub lower_bound: Option<f32>,
966 pub upper_bound: Option<f32>,
967 pub dist_q_c: f32,
968}
969
970impl From<&Query> for HnswQueryParams {
971 fn from(query: &Query) -> Self {
972 let k = query.k * query.refine_factor.unwrap_or(1) as usize;
973 Self {
974 ef: query.ef.unwrap_or(k + k / 2),
975 lower_bound: query.lower_bound,
976 upper_bound: query.upper_bound,
977 dist_q_c: query.dist_q_c,
978 }
979 }
980}
981
982impl IvfSubIndex for HNSW {
983 type BuildParams = HnswBuildParams;
984 type QueryParams = HnswQueryParams;
985
986 fn load(data: RecordBatch) -> Result<Self>
987 where
988 Self: Sized,
989 {
990 if data.num_rows() == 0 {
991 return Ok(Self::empty());
992 }
993
994 let hnsw_metadata = data
995 .schema_ref()
996 .metadata()
997 .get(HNSW_METADATA_KEY)
998 .ok_or(Error::index(format!("{} not found", HNSW_METADATA_KEY)))?;
999 let hnsw_metadata: HnswMetadata = serde_json::from_str(hnsw_metadata).map_err(|e| {
1000 Error::index(format!(
1001 "Failed to decode HNSW metadata: {}, json: {}",
1002 e, hnsw_metadata
1003 ))
1004 })?;
1005
1006 let level_batches: Vec<RecordBatch> = hnsw_metadata
1008 .level_offsets
1009 .iter()
1010 .tuple_windows()
1011 .map(|(start, end)| data.slice(*start, end - start))
1012 .collect();
1013
1014 let level_count = level_batches
1015 .iter()
1016 .map(|b| b.num_rows())
1017 .collect::<Vec<_>>();
1018
1019 let mut level_neighbors = Vec::with_capacity(level_batches.len());
1024 let mut level_lookup = Vec::with_capacity(level_batches.len());
1025 for (level, batch) in level_batches.iter().enumerate() {
1026 let neighbors = batch[NEIGHBORS_COL].as_list::<i32>().clone();
1029 let ids = batch[VECTOR_ID_COL].as_primitive::<UInt32Type>();
1030 if level == 0 {
1031 if let Some((row, id)) = ids
1040 .values()
1041 .iter()
1042 .enumerate()
1043 .find(|&(row, id)| *id != row as u32)
1044 {
1045 return Err(Error::index(format!(
1046 "HNSW level-0 __vector_id must equal the row index, but \
1047 row {row} has __vector_id {id}; the on-disk batch is \
1048 malformed or was written by an incompatible version"
1049 )));
1050 }
1051 level_lookup.push(LevelLookup::Dense);
1052 } else {
1053 let id_to_row: HashMap<u32, u32> = ids
1059 .values()
1060 .iter()
1061 .enumerate()
1062 .map(|(row, id)| (*id, row as u32))
1063 .collect();
1064 level_lookup.push(LevelLookup::Sparse(id_to_row));
1065 }
1066 level_neighbors.push(neighbors);
1067 }
1068
1069 let num_nodes = level_count[0];
1074 if hnsw_metadata.entry_point as usize >= num_nodes {
1075 return Err(Error::index(format!(
1076 "HNSW entry_point {} is out of range for a graph with {num_nodes} \
1077 nodes; the on-disk batch is malformed or was written by an \
1078 incompatible version",
1079 hnsw_metadata.entry_point
1080 )));
1081 }
1082
1083 let visited_generator_queue =
1084 Arc::new(ArrayQueue::new(get_num_compute_intensive_cpus() * 2));
1085 for _ in 0..get_num_compute_intensive_cpus() * 2 {
1086 visited_generator_queue
1087 .push(VisitedGenerator::new(0))
1088 .unwrap();
1089 }
1090
1091 let graph = LoadedHnswGraph {
1092 batch: data,
1093 level_neighbors,
1094 level_lookup,
1095 level_count: level_count.clone(),
1096 };
1097 let inner = HnswCore {
1098 params: hnsw_metadata.params,
1099 graph: HnswGraph::Loaded(Arc::new(graph)),
1100 level_count,
1101 entry_point: hnsw_metadata.entry_point,
1102 visited_generator_queue,
1103 };
1104
1105 Ok(Self {
1106 inner: Arc::new(inner),
1107 })
1108 }
1109
1110 fn name() -> &'static str {
1111 HNSW_TYPE
1112 }
1113
1114 fn metadata_key() -> &'static str {
1115 "lance:hnsw"
1116 }
1117
1118 fn schema() -> arrow_schema::SchemaRef {
1120 arrow_schema::Schema::new(vec![
1121 VECTOR_ID_FIELD.clone(),
1122 NEIGHBORS_FIELD.clone(),
1123 DISTS_FIELD.clone(),
1124 ])
1125 .into()
1126 }
1127
1128 #[instrument(level = "debug", skip(self, query, storage, prefilter, _metrics))]
1129 fn search(
1130 &self,
1131 query: ArrayRef,
1132 k: usize,
1133 params: Self::QueryParams,
1134 storage: &impl VectorStore,
1135 prefilter: Arc<dyn PreFilter>,
1136 _metrics: &dyn MetricsCollector,
1137 ) -> Result<RecordBatch> {
1138 if params.ef < k {
1139 return Err(Error::index(
1140 "ef must be greater than or equal to k".to_string(),
1141 ));
1142 }
1143
1144 let schema = VECTOR_RESULT_SCHEMA.clone();
1145 if self.is_empty() {
1146 return Ok(RecordBatch::new_empty(schema));
1147 }
1148
1149 let mut prefilter_generator = self
1150 .inner
1151 .visited_generator_queue
1152 .pop()
1153 .unwrap_or_else(|| VisitedGenerator::new(storage.len()));
1154 let prefilter_bitset = if prefilter.is_empty() {
1155 None
1156 } else {
1157 let indices = prefilter.filter_row_ids(Box::new(storage.row_ids()));
1158 let mut bitset = prefilter_generator.generate(storage.len());
1159 for indices in indices {
1160 bitset.insert(indices as u32);
1161 }
1162 Some(bitset)
1163 };
1164
1165 let remained = prefilter_bitset
1166 .as_ref()
1167 .map(|b| b.count_ones())
1168 .unwrap_or(storage.len());
1169 let results = if remained < self.len() * 10 / 100 {
1170 let prefilter_bitset =
1171 prefilter_bitset.expect("the prefilter bitset must be set for flat search");
1172 self.flat_search(storage, query, k, prefilter_bitset, ¶ms)
1173 } else {
1174 self.search_basic(query, k, ¶ms, prefilter_bitset, storage)?
1175 };
1176 let _ = self.inner.visited_generator_queue.push(prefilter_generator);
1178
1179 let (row_ids, dists): (Vec<_>, Vec<_>) = results
1181 .into_iter()
1182 .map(|r| (storage.row_id(r.id), r.dist.0))
1183 .unique_by(|r| r.0)
1184 .unzip();
1185 let row_ids = Arc::new(UInt64Array::from(row_ids));
1186 let distances = Arc::new(Float32Array::from(dists));
1187
1188 Ok(RecordBatch::try_new(schema, vec![distances, row_ids])?)
1189 }
1190
1191 fn index_vectors(storage: &impl VectorStore, params: Self::BuildParams) -> Result<Self>
1193 where
1194 Self: Sized,
1195 {
1196 let builder = HnswBuilder::with_params(params, storage);
1197
1198 log::debug!(
1199 "Building HNSW graph: num={}, max_levels={}, m={}, ef_construction={}, distance_type:{}",
1200 storage.len(),
1201 builder.params.max_level,
1202 builder.params.m,
1203 builder.params.ef_construction,
1204 storage.distance_type(),
1205 );
1206
1207 if storage.is_empty() {
1208 return Ok(builder.finish());
1209 }
1210
1211 let len = storage.len();
1212 builder.level_count[0].fetch_add(1, Ordering::Relaxed);
1213 (1..len).into_par_iter().for_each_init(
1214 || VisitedGenerator::new(len),
1215 |visited_generator, node| {
1216 builder.insert(node as u32, visited_generator, storage);
1217 },
1218 );
1219
1220 assert_eq!(builder.level_count[0].load(Ordering::Relaxed), len);
1221 Ok(builder.finish())
1222 }
1223
1224 fn remap(
1225 &self,
1226 _mapping: &RowAddrRemap, store: &impl VectorStore,
1228 ) -> Result<Self> {
1229 Self::index_vectors(store, self.inner.params.clone())
1232 }
1233
1234 fn to_batch(&self) -> Result<RecordBatch> {
1236 let nodes = match &self.inner.graph {
1237 HnswGraph::Built(nodes) => nodes,
1238 HnswGraph::Loaded(graph) => {
1239 let metadata = serde_json::to_string(&self.metadata())?;
1252 let mut schema_metadata = graph.batch.schema_ref().metadata().clone();
1253 schema_metadata.insert(HNSW_METADATA_KEY.to_string(), metadata);
1254 let schema = graph
1255 .batch
1256 .schema()
1257 .as_ref()
1258 .clone()
1259 .with_metadata(schema_metadata);
1260 return Ok(graph.batch.clone().with_schema(Arc::new(schema))?);
1261 }
1262 };
1263
1264 let mut vector_id_builder = UInt32Builder::with_capacity(self.len());
1265 let mut neighbors_builder = ListBuilder::with_capacity(UInt32Builder::new(), self.len());
1266 let mut distances_builder =
1267 ListBuilder::with_capacity(arrow_array::builder::Float32Builder::new(), self.len());
1268 let mut batches = Vec::with_capacity(self.max_level() as usize);
1269 for level in 0..self.max_level() {
1270 let level = level as usize;
1271 for (id, node) in nodes.iter().enumerate() {
1272 if level >= node.level_neighbors.len() {
1273 continue;
1274 }
1275 let neighbors = node.level_neighbors[level].iter().map(|n| Some(*n));
1276 let distances = node.level_neighbors_ranked[level]
1277 .iter()
1278 .map(|n| Some(n.dist.0));
1279 vector_id_builder.append_value(id as u32);
1280 neighbors_builder.append_value(neighbors);
1281 distances_builder.append_value(distances);
1282 }
1283
1284 let batch = RecordBatch::try_new(
1285 Self::schema(),
1286 vec![
1287 Arc::new(vector_id_builder.finish()),
1288 Arc::new(neighbors_builder.finish()),
1289 Arc::new(distances_builder.finish()),
1290 ],
1291 )?;
1292 batches.push(batch);
1293 }
1294
1295 let metadata = self.metadata();
1296 let metadata = serde_json::to_string(&metadata)?;
1297 let schema = Self::schema()
1298 .as_ref()
1299 .clone()
1300 .with_metadata(HashMap::from_iter(vec![(
1301 HNSW_METADATA_KEY.to_string(),
1302 metadata,
1303 )]));
1304 let batch = concat_batches(&Self::schema(), batches.iter())?;
1305 let batch = batch.with_schema(Arc::new(schema))?;
1306 Ok(batch)
1307 }
1308}
1309
1310#[cfg(test)]
1311mod tests {
1312 use std::sync::Arc;
1313
1314 use arrow_array::{ArrayRef, FixedSizeListArray, RecordBatch, UInt8Array, UInt32Array};
1315 use arrow_schema::Schema;
1316 use lance_arrow::FixedSizeListArrayExt;
1317 use lance_core::deepsize::DeepSizeOf;
1318 use lance_file::previous::{
1319 reader::FileReader as PreviousFileReader,
1320 writer::{
1321 FileWriter as PreviousFileWriter, FileWriterOptions as PreviousFileWriterOptions,
1322 },
1323 };
1324 use lance_io::object_store::ObjectStore;
1325 use lance_linalg::distance::DistanceType;
1326 use lance_table::format::SelfDescribingFileReader;
1327 use lance_table::io::manifest::ManifestDescribing;
1328 use lance_testing::datagen::generate_random_array;
1329 use object_store::path::Path;
1330 use rstest::rstest;
1331
1332 use super::HnswGraph;
1333 use crate::vector::storage::{DistCalculator, VectorStore};
1334 use crate::vector::v3::subindex::IvfSubIndex;
1335 use crate::vector::{
1336 flat::storage::{FlatBinStorage, FlatFloatStorage},
1337 graph::{DISTS_FIELD, NEIGHBORS_FIELD},
1338 hnsw::{
1339 HNSW, VECTOR_ID_FIELD,
1340 builder::{HnswBuildParams, HnswQueryParams},
1341 },
1342 };
1343
1344 #[tokio::test]
1345 async fn test_builder_write_load() {
1346 const DIM: usize = 32;
1347 const TOTAL: usize = 2048;
1348 const NUM_EDGES: usize = 20;
1349 let data = generate_random_array(TOTAL * DIM);
1350 let fsl = FixedSizeListArray::try_new_from_values(data, DIM as i32).unwrap();
1351 let store = Arc::new(FlatFloatStorage::new(fsl.clone(), DistanceType::L2));
1352 let builder = HNSW::index_vectors(
1353 store.as_ref(),
1354 HnswBuildParams::default()
1355 .num_edges(NUM_EDGES)
1356 .ef_construction(50),
1357 )
1358 .unwrap();
1359
1360 let object_store = ObjectStore::memory();
1361 let path = Path::from("test_builder_write_load");
1362 let writer = object_store.create(&path).await.unwrap();
1363 let schema = Schema::new(vec![
1364 VECTOR_ID_FIELD.clone(),
1365 NEIGHBORS_FIELD.clone(),
1366 DISTS_FIELD.clone(),
1367 ]);
1368 let schema = lance_core::datatypes::Schema::try_from(&schema).unwrap();
1369 let mut writer = PreviousFileWriter::<ManifestDescribing>::with_object_writer(
1370 writer,
1371 schema,
1372 &PreviousFileWriterOptions::default(),
1373 )
1374 .unwrap();
1375 let batch = builder.to_batch().unwrap();
1376 let metadata = batch.schema_ref().metadata().clone();
1377 writer.write(&[batch]).await.unwrap();
1378 writer.finish_with_metadata(&metadata).await.unwrap();
1379
1380 let reader = PreviousFileReader::try_new_self_described(&object_store, &path, None)
1381 .await
1382 .unwrap();
1383 let batch = reader
1384 .read_range(0..reader.len(), reader.schema())
1385 .await
1386 .unwrap();
1387 let loaded_hnsw = HNSW::load(batch).unwrap();
1388
1389 let query = fsl.value(0);
1390 let k = 10;
1391 let params = HnswQueryParams {
1392 ef: 50,
1393 lower_bound: None,
1394 upper_bound: None,
1395 dist_q_c: 0.0,
1396 };
1397 let builder_results = builder
1398 .search_basic(query.clone(), k, ¶ms, None, store.as_ref())
1399 .unwrap();
1400 let loaded_results = loaded_hnsw
1401 .search_basic(query, k, ¶ms, None, store.as_ref())
1402 .unwrap();
1403 assert_eq!(builder_results, loaded_results);
1404 }
1405
1406 #[tokio::test]
1407 async fn test_builder_write_load_binary_hamming() {
1408 const DIM: usize = 8;
1409 const TOTAL: usize = 256;
1410 const NUM_EDGES: usize = 20;
1411 let data = UInt8Array::from_iter_values((0..TOTAL * DIM).map(|v| (v % 16) as u8));
1412 let fsl = FixedSizeListArray::try_new_from_values(data, DIM as i32).unwrap();
1413 let store = Arc::new(FlatBinStorage::new(fsl.clone(), DistanceType::Hamming));
1414 let builder = HnswBuildParams::default()
1415 .num_edges(NUM_EDGES)
1416 .ef_construction(50)
1417 .build(Arc::new(fsl.clone()), DistanceType::Hamming)
1418 .await
1419 .unwrap();
1420
1421 let object_store = ObjectStore::memory();
1422 let path = Path::from("test_builder_write_load_binary_hamming");
1423 let writer = object_store.create(&path).await.unwrap();
1424 let schema = Schema::new(vec![
1425 VECTOR_ID_FIELD.clone(),
1426 NEIGHBORS_FIELD.clone(),
1427 DISTS_FIELD.clone(),
1428 ]);
1429 let schema = lance_core::datatypes::Schema::try_from(&schema).unwrap();
1430 let mut writer = PreviousFileWriter::<ManifestDescribing>::with_object_writer(
1431 writer,
1432 schema,
1433 &PreviousFileWriterOptions::default(),
1434 )
1435 .unwrap();
1436 let batch = builder.to_batch().unwrap();
1437 let metadata = batch.schema_ref().metadata().clone();
1438 writer.write(&[batch]).await.unwrap();
1439 writer.finish_with_metadata(&metadata).await.unwrap();
1440
1441 let reader = PreviousFileReader::try_new_self_described(&object_store, &path, None)
1442 .await
1443 .unwrap();
1444 let batch = reader
1445 .read_range(0..reader.len(), reader.schema())
1446 .await
1447 .unwrap();
1448 let loaded_hnsw = HNSW::load(batch).unwrap();
1449
1450 let query = fsl.value(0);
1451 let k = 10;
1452 let params = HnswQueryParams {
1453 ef: 50,
1454 lower_bound: None,
1455 upper_bound: None,
1456 dist_q_c: 0.0,
1457 };
1458 let builder_results = builder
1459 .search_basic(query.clone(), k, ¶ms, None, store.as_ref())
1460 .unwrap();
1461 let loaded_results = loaded_hnsw
1462 .search_basic(query, k, ¶ms, None, store.as_ref())
1463 .unwrap();
1464 assert_eq!(builder_results, loaded_results);
1465 }
1466
1467 fn brute_force_topk(store: &FlatFloatStorage, query: ArrayRef, k: usize) -> Vec<u32> {
1469 let dist_calc = store.dist_calculator(query, 0.0);
1470 let mut all: Vec<(f32, u32)> = (0..store.len() as u32)
1471 .map(|id| (dist_calc.distance(id), id))
1472 .collect();
1473 all.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
1474 all.into_iter().take(k).map(|(_, id)| id).collect()
1475 }
1476
1477 #[rstest]
1482 #[case::l2_single(DistanceType::L2, 1)]
1483 #[case::l2_pair(DistanceType::L2, 2)]
1484 #[case::l2_multi_level(DistanceType::L2, 2048)]
1485 #[case::dot_multi_level(DistanceType::Dot, 2048)]
1486 #[tokio::test]
1487 async fn test_loaded_search_parity_and_recall(
1488 #[case] distance_type: DistanceType,
1489 #[case] total: usize,
1490 ) {
1491 const DIM: usize = 32;
1492 let fsl =
1493 FixedSizeListArray::try_new_from_values(generate_random_array(total * DIM), DIM as i32)
1494 .unwrap();
1495 let store = Arc::new(FlatFloatStorage::new(fsl.clone(), distance_type));
1496 let builder = HNSW::index_vectors(
1497 store.as_ref(),
1498 HnswBuildParams::default().num_edges(20).ef_construction(50),
1499 )
1500 .unwrap();
1501 assert!(!matches!(builder.inner.graph, HnswGraph::Loaded(_)));
1502
1503 let loaded = HNSW::load(builder.to_batch().unwrap()).unwrap();
1504 assert!(matches!(loaded.inner.graph, HnswGraph::Loaded(_)));
1505 assert_eq!(loaded.len(), total);
1506
1507 let k = total.min(10);
1508 let params = HnswQueryParams {
1509 ef: 50,
1510 lower_bound: None,
1511 upper_bound: None,
1512 dist_q_c: 0.0,
1513 };
1514 let query = fsl.value(0);
1515
1516 let builder_results = builder
1517 .search_basic(query.clone(), k, ¶ms, None, store.as_ref())
1518 .unwrap();
1519 let loaded_results = loaded
1520 .search_basic(query.clone(), k, ¶ms, None, store.as_ref())
1521 .unwrap();
1522 assert_eq!(builder_results, loaded_results);
1523
1524 let truth: std::collections::HashSet<u32> = brute_force_topk(store.as_ref(), query, k)
1526 .into_iter()
1527 .collect();
1528 let hits = loaded_results
1529 .iter()
1530 .filter(|n| truth.contains(&n.id))
1531 .count();
1532 let recall = hits as f32 / k as f32;
1533 assert!(recall >= 0.5, "recall {recall} below 0.5 (k={k})");
1534 }
1535
1536 #[tokio::test]
1546 async fn test_loaded_level_offsets_misalignment_invariant() {
1547 use arrow::array::AsArray;
1548 use arrow::datatypes::UInt32Type;
1549
1550 const DIM: usize = 32;
1551 const TOTAL: usize = 2048;
1552 let fsl =
1553 FixedSizeListArray::try_new_from_values(generate_random_array(TOTAL * DIM), DIM as i32)
1554 .unwrap();
1555 let store = Arc::new(FlatFloatStorage::new(fsl.clone(), DistanceType::L2));
1556 let builder = HNSW::index_vectors(
1557 store.as_ref(),
1558 HnswBuildParams::default().num_edges(20).ef_construction(50),
1559 )
1560 .unwrap();
1561
1562 assert!(
1564 builder.max_level() >= 2,
1565 "expected a multi-level graph (got max_level {})",
1566 builder.max_level()
1567 );
1568
1569 let batch = builder.to_batch().unwrap();
1570 let md = builder.metadata();
1571 let total_counted = *md.level_offsets.last().unwrap();
1572
1573 assert!(
1577 batch.num_rows() > total_counted,
1578 "expected serialized rows ({}) to exceed sum(level_count) ({}) -- \
1579 entry point should be written at every level",
1580 batch.num_rows(),
1581 total_counted,
1582 );
1583
1584 let n = md.level_offsets[1];
1587 assert_eq!(n, TOTAL);
1588 let level0 = batch.slice(0, n);
1589 let ids = level0.column(0).as_primitive::<UInt32Type>();
1590 assert!(
1591 ids.values()
1592 .iter()
1593 .enumerate()
1594 .all(|(row, id)| *id == row as u32),
1595 "level-0 __vector_id must equal the row index",
1596 );
1597
1598 let loaded = HNSW::load(batch).unwrap();
1602 assert!(matches!(loaded.inner.graph, HnswGraph::Loaded(_)));
1603 let params = HnswQueryParams {
1604 ef: 50,
1605 lower_bound: None,
1606 upper_bound: None,
1607 dist_q_c: 0.0,
1608 };
1609 let query = fsl.value(0);
1610 let builder_results = builder
1611 .search_basic(query.clone(), 10, ¶ms, None, store.as_ref())
1612 .unwrap();
1613 let loaded_results = loaded
1614 .search_basic(query, 10, ¶ms, None, store.as_ref())
1615 .unwrap();
1616 assert_eq!(builder_results, loaded_results);
1617 }
1618
1619 #[tokio::test]
1625 async fn test_load_rejects_misaligned_level0_id() {
1626 use arrow::array::AsArray;
1627 use arrow::datatypes::UInt32Type;
1628
1629 const DIM: usize = 16;
1630 const TOTAL: usize = 256;
1631 let fsl =
1632 FixedSizeListArray::try_new_from_values(generate_random_array(TOTAL * DIM), DIM as i32)
1633 .unwrap();
1634 let store = Arc::new(FlatFloatStorage::new(fsl, DistanceType::L2));
1635 let builder = HNSW::index_vectors(
1636 store.as_ref(),
1637 HnswBuildParams::default().num_edges(20).ef_construction(50),
1638 )
1639 .unwrap();
1640
1641 let batch = builder.to_batch().unwrap();
1642 let mut ids = batch
1645 .column(0)
1646 .as_primitive::<UInt32Type>()
1647 .values()
1648 .to_vec();
1649 ids[0] = ids.len() as u32;
1650 let mut columns = batch.columns().to_vec();
1651 columns[0] = Arc::new(UInt32Array::from(ids));
1652 let corrupted = RecordBatch::try_new(batch.schema(), columns).unwrap();
1653
1654 assert!(
1655 HNSW::load(corrupted).is_err(),
1656 "load() must reject a misaligned level-0 __vector_id"
1657 );
1658 }
1659
1660 #[tokio::test]
1665 async fn test_load_rejects_out_of_range_entry_point() {
1666 use super::{HNSW_METADATA_KEY, HnswMetadata};
1667
1668 const DIM: usize = 16;
1669 const TOTAL: usize = 256;
1670 let fsl =
1671 FixedSizeListArray::try_new_from_values(generate_random_array(TOTAL * DIM), DIM as i32)
1672 .unwrap();
1673 let store = Arc::new(FlatFloatStorage::new(fsl, DistanceType::L2));
1674 let builder = HNSW::index_vectors(
1675 store.as_ref(),
1676 HnswBuildParams::default().num_edges(20).ef_construction(50),
1677 )
1678 .unwrap();
1679
1680 let batch = builder.to_batch().unwrap();
1681 let mut metadata = batch.schema_ref().metadata().clone();
1682 let mut md: HnswMetadata =
1683 serde_json::from_str(metadata.get(HNSW_METADATA_KEY).unwrap()).unwrap();
1684 let n = md.level_offsets[1];
1686 md.entry_point = n as u32;
1687 metadata.insert(
1688 HNSW_METADATA_KEY.to_string(),
1689 serde_json::to_string(&md).unwrap(),
1690 );
1691 let schema = batch.schema().as_ref().clone().with_metadata(metadata);
1695 let corrupted = RecordBatch::try_new(Arc::new(schema), batch.columns().to_vec()).unwrap();
1696
1697 assert!(
1698 HNSW::load(corrupted).is_err(),
1699 "load() must reject an out-of-range entry_point"
1700 );
1701 }
1702
1703 #[tokio::test]
1705 async fn test_loaded_empty_index() {
1706 const DIM: usize = 16;
1707 let fsl =
1708 FixedSizeListArray::try_new_from_values(generate_random_array(0), DIM as i32).unwrap();
1709 let store = Arc::new(FlatFloatStorage::new(fsl, DistanceType::L2));
1710 let builder = HNSW::index_vectors(store.as_ref(), HnswBuildParams::default()).unwrap();
1711 assert!(builder.is_empty());
1712
1713 let batch = builder.to_batch().unwrap();
1714 assert_eq!(batch.num_rows(), 0);
1715
1716 let loaded = HNSW::load(batch).unwrap();
1717 assert!(loaded.is_empty());
1718 assert_eq!(loaded.len(), 0);
1719 assert!(!matches!(loaded.inner.graph, HnswGraph::Loaded(_)));
1721 assert_eq!(loaded.to_batch().unwrap().num_rows(), 0);
1722 }
1723
1724 #[tokio::test]
1729 async fn test_to_batch_roundtrip_loaded() {
1730 const DIM: usize = 24;
1731 const TOTAL: usize = 1500;
1732 let fsl =
1733 FixedSizeListArray::try_new_from_values(generate_random_array(TOTAL * DIM), DIM as i32)
1734 .unwrap();
1735 let store = Arc::new(FlatFloatStorage::new(fsl.clone(), DistanceType::L2));
1736 let builder = HNSW::index_vectors(
1737 store.as_ref(),
1738 HnswBuildParams::default().num_edges(16).ef_construction(50),
1739 )
1740 .unwrap();
1741
1742 let b1 = builder.to_batch().unwrap();
1743 let loaded = HNSW::load(b1.clone()).unwrap();
1744 assert!(matches!(loaded.inner.graph, HnswGraph::Loaded(_)));
1745 let b2 = loaded.to_batch().unwrap();
1746 assert_eq!(b1, b2);
1747
1748 let reloaded = HNSW::load(b2).unwrap();
1749 let params = HnswQueryParams {
1750 ef: 50,
1751 lower_bound: None,
1752 upper_bound: None,
1753 dist_q_c: 0.0,
1754 };
1755 let query = fsl.value(7);
1756 let a = builder
1757 .search_basic(query.clone(), 10, ¶ms, None, store.as_ref())
1758 .unwrap();
1759 let b = reloaded
1760 .search_basic(query, 10, ¶ms, None, store.as_ref())
1761 .unwrap();
1762 assert_eq!(a, b);
1763 }
1764
1765 #[tokio::test]
1773 async fn test_to_batch_loaded_preserves_extra_schema_metadata() {
1774 use super::HNSW_METADATA_KEY;
1775
1776 const DIM: usize = 24;
1777 const TOTAL: usize = 512;
1778 let fsl =
1779 FixedSizeListArray::try_new_from_values(generate_random_array(TOTAL * DIM), DIM as i32)
1780 .unwrap();
1781 let store = Arc::new(FlatFloatStorage::new(fsl, DistanceType::L2));
1782 let builder = HNSW::index_vectors(
1783 store.as_ref(),
1784 HnswBuildParams::default().num_edges(16).ef_construction(50),
1785 )
1786 .unwrap();
1787
1788 let built_batch = builder.to_batch().unwrap();
1792 let mut metadata = built_batch.schema_ref().metadata().clone();
1793 metadata.insert(
1794 "lance:index_metadata".to_string(),
1795 "{\"distance_type\":\"l2\"}".to_string(),
1796 );
1797 metadata.insert("lance:ivf".to_string(), "42".to_string());
1798 let schema = built_batch
1799 .schema()
1800 .as_ref()
1801 .clone()
1802 .with_metadata(metadata);
1803 let batch_with_extra =
1804 RecordBatch::try_new(Arc::new(schema), built_batch.columns().to_vec()).unwrap();
1805
1806 let loaded = HNSW::load(batch_with_extra).unwrap();
1807 assert!(matches!(loaded.inner.graph, HnswGraph::Loaded(_)));
1808
1809 let out = loaded.to_batch().unwrap();
1812 let out_metadata = out.schema_ref().metadata();
1813 assert!(out_metadata.contains_key(HNSW_METADATA_KEY));
1814 assert_eq!(
1815 out_metadata.get("lance:index_metadata").map(String::as_str),
1816 Some("{\"distance_type\":\"l2\"}"),
1817 );
1818 assert_eq!(
1819 out_metadata.get("lance:ivf").map(String::as_str),
1820 Some("42")
1821 );
1822
1823 let reloaded = HNSW::load(out).unwrap();
1825 assert_eq!(reloaded.len(), loaded.len());
1826 }
1827
1828 #[tokio::test]
1832 async fn test_loaded_graph_is_arrow_backed() {
1833 const DIM: usize = 32;
1834 const TOTAL: usize = 2048;
1835 let fsl =
1836 FixedSizeListArray::try_new_from_values(generate_random_array(TOTAL * DIM), DIM as i32)
1837 .unwrap();
1838 let store = Arc::new(FlatFloatStorage::new(fsl, DistanceType::L2));
1839 let builder = HNSW::index_vectors(
1840 store.as_ref(),
1841 HnswBuildParams::default().num_edges(20).ef_construction(50),
1842 )
1843 .unwrap();
1844 assert!(!matches!(builder.inner.graph, HnswGraph::Loaded(_)));
1845
1846 let loaded = HNSW::load(builder.to_batch().unwrap()).unwrap();
1847 assert!(matches!(loaded.inner.graph, HnswGraph::Loaded(_)));
1848 assert!(
1849 loaded.deep_size_of() < builder.deep_size_of(),
1850 "loaded graph ({}) should be lighter than built ({})",
1851 loaded.deep_size_of(),
1852 builder.deep_size_of(),
1853 );
1854 }
1855}