1use std::sync::Arc;
8use std::{any::Any, collections::HashMap};
9
10pub mod builder;
11pub(crate) mod details;
12pub mod ivf;
13pub mod pq;
14pub mod utils;
15
16#[cfg(test)]
17mod fixture_test;
18
19use self::{ivf::*, pq::PQIndex};
20use arrow_schema::{DataType, Schema};
21use builder::{IvfIndexBuilder, VectorIndexBuildSummary};
22use datafusion::physical_plan::SendableRecordBatchStream;
23use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
24use futures::stream;
25use lance_core::utils::tempfile::TempStdDir;
26use lance_file::previous::reader::FileReader as PreviousFileReader;
27use lance_index::frag_reuse::FragReuseIndex;
28use lance_index::metrics::NoOpMetricsCollector;
29use lance_index::optimize::OptimizeOptions;
30use lance_index::progress::{IndexBuildProgress, noop_progress};
31use lance_index::vector::bq::builder::RabitQuantizer;
32use lance_index::vector::bq::{RQBuildParams, RQRotationType, validate_supported_rq_num_bits};
33use lance_index::vector::flat::index::{FlatBinQuantizer, FlatIndex, FlatQuantizer};
34use lance_index::vector::hnsw::HNSW;
35use lance_index::vector::ivf::builder::recommended_num_partitions;
36use lance_index::vector::ivf::storage::IvfModel;
37use object_store::path::Path;
38
39use lance_arrow::FixedSizeListArrayExt;
40use lance_index::vector::pq::ProductQuantizer;
41use lance_index::vector::quantizer::QuantizationType;
42use lance_index::vector::v3::shuffler::{Shuffler, create_ivf_shuffler};
43use lance_index::vector::v3::subindex::SubIndexType;
44use lance_index::vector::{
45 VectorIndex,
46 hnsw::{
47 builder::HnswBuildParams,
48 index::{HNSWIndex, HNSWIndexOptions},
49 },
50 ivf::IvfBuildParams,
51 pq::PQBuildParams,
52 sq::{ScalarQuantizer, builder::SQBuildParams},
53};
54use lance_index::{INDEX_AUXILIARY_FILE_NAME, INDEX_METADATA_SCHEMA_KEY, IndexType};
55use lance_io::object_store::ObjectStore;
56use lance_io::traits::Reader;
57use lance_linalg::distance::*;
58use lance_table::format::{IndexFile, IndexMetadata};
59use serde::Serialize;
60use tracing::instrument;
61use utils::get_vector_type;
62use uuid::Uuid;
63
64use super::{DatasetIndexExt, DatasetIndexInternalExt, IndexParams, pb};
65use crate::dataset::index::dataset_format_version;
66use crate::dataset::transaction::{Operation, Transaction};
67use crate::{Error, Result, dataset::Dataset, index::pb::vector_index_stage::Stage};
68
69pub const LANCE_VECTOR_INDEX: &str = "__lance_vector_index";
70
71#[derive(Debug)]
73pub struct LogicalVectorIndex {
74 name: String,
75 column: String,
76 segments: Vec<(IndexMetadata, Arc<dyn VectorIndex>)>,
77}
78
79#[derive(Clone, Copy, Debug)]
85pub struct LogicalIvfView<'a> {
86 logical_index: &'a LogicalVectorIndex,
87}
88
89impl LogicalVectorIndex {
90 pub(crate) fn try_new(
91 name: String,
92 column: String,
93 segments: Vec<(IndexMetadata, Arc<dyn VectorIndex>)>,
94 ) -> Result<Self> {
95 if segments.is_empty() {
96 return Err(Error::invalid_input(format!(
97 "LogicalVectorIndex '{}' on column '{}' must contain at least one segment",
98 name, column
99 )));
100 }
101
102 Ok(Self {
103 name,
104 column,
105 segments,
106 })
107 }
108
109 pub fn name(&self) -> &str {
111 &self.name
112 }
113
114 pub fn column(&self) -> &str {
116 &self.column
117 }
118
119 pub fn num_segments(&self) -> usize {
121 self.segments.len()
122 }
123
124 pub fn metadatas(&self) -> impl ExactSizeIterator<Item = &IndexMetadata> + '_ {
126 self.segments.iter().map(|(metadata, _)| metadata)
127 }
128
129 pub fn num_rows_per_segment(&self) -> Vec<(Uuid, u64)> {
131 self.segments
132 .iter()
133 .map(|(metadata, index)| (metadata.uuid, index.num_rows()))
134 .collect()
135 }
136
137 pub fn as_ivf(&self) -> Result<LogicalIvfView<'_>> {
143 Ok(LogicalIvfView {
144 logical_index: self,
145 })
146 }
147
148 pub(crate) fn iter(
149 &self,
150 ) -> impl ExactSizeIterator<Item = (&IndexMetadata, &Arc<dyn VectorIndex>)> + '_ {
151 self.segments
152 .iter()
153 .map(|(metadata, index)| (metadata, index))
154 }
155}
156
157impl<'a> LogicalIvfView<'a> {
158 pub(crate) fn indices(&self) -> impl ExactSizeIterator<Item = &Arc<dyn VectorIndex>> + '_ {
159 self.logical_index.iter().map(|(_, index)| index)
160 }
161
162 pub(crate) fn segments(
163 &self,
164 ) -> impl ExactSizeIterator<Item = (&IndexMetadata, &Arc<dyn VectorIndex>)> + '_ {
165 self.logical_index.iter()
166 }
167
168 pub fn num_partitions_per_segment(&self) -> Vec<(Uuid, usize)> {
170 self.logical_index
171 .iter()
172 .map(|(metadata, index)| (metadata.uuid, index.ivf_model().num_partitions()))
173 .collect()
174 }
175
176 pub fn partition_sizes(&self) -> Vec<(Uuid, Vec<usize>)> {
178 let mut partition_sizes = Vec::with_capacity(self.logical_index.num_segments());
179 for (metadata, index) in self.logical_index.iter() {
180 let num_partitions = index.ivf_model().num_partitions();
181 let mut sizes = Vec::with_capacity(num_partitions);
182 for partition_id in 0..num_partitions {
183 sizes.push(index.partition_size(partition_id));
184 }
185 partition_sizes.push((metadata.uuid, sizes));
186 }
187 partition_sizes
188 }
189
190 pub async fn read_partition(
195 &self,
196 partition_id: usize,
197 with_vector: bool,
198 ) -> Result<SendableRecordBatchStream> {
199 let mut schema: Option<Arc<Schema>> = None;
200 let mut partition_streams = Vec::with_capacity(self.logical_index.num_segments());
201 for index in self.indices() {
202 let stream = index
203 .partition_reader(partition_id, with_vector, &NoOpMetricsCollector)
204 .await?;
205 if schema.is_none() {
206 schema = Some(stream.schema());
207 }
208 partition_streams.push(stream);
209 }
210
211 match schema {
212 Some(schema) => {
213 let merged = stream::select_all(partition_streams);
214 let stream = RecordBatchStreamAdapter::new(schema, merged);
215 Ok(Box::pin(stream))
216 }
217 None => Ok(Box::pin(RecordBatchStreamAdapter::new(
218 Arc::new(Schema::empty()),
219 stream::empty(),
220 ))),
221 }
222 }
223}
224
225#[derive(Debug, Clone)]
227pub enum StageParams {
228 Ivf(IvfBuildParams),
229 Hnsw(HnswBuildParams),
230 PQ(PQBuildParams),
231 SQ(SQBuildParams),
232 RQ(RQBuildParams),
233}
234
235#[derive(Debug, Clone, Serialize)]
239pub enum IndexFileVersion {
240 Legacy,
241 V3,
242}
243
244impl IndexFileVersion {
245 pub fn try_from(version: &str) -> Result<Self> {
246 match version.to_lowercase().as_str() {
247 "legacy" => Ok(Self::Legacy),
248 "v3" => Ok(Self::V3),
249 _ => Err(Error::index(format!(
250 "Invalid index file version: {}",
251 version
252 ))),
253 }
254 }
255}
256
257#[derive(Debug, Clone)]
259pub struct VectorIndexParams {
260 pub stages: Vec<StageParams>,
261
262 pub metric_type: MetricType,
264
265 pub version: IndexFileVersion,
267
268 pub skip_transpose: bool,
270
271 pub runtime_hints: HashMap<String, String>,
275}
276
277impl VectorIndexParams {
278 pub fn version(&mut self, version: IndexFileVersion) -> &mut Self {
279 self.version = version;
280 self
281 }
282
283 pub fn skip_transpose(&mut self, skip_transpose: bool) -> &mut Self {
284 self.skip_transpose = skip_transpose;
285 self
286 }
287
288 pub fn ivf_flat(num_partitions: usize, metric_type: MetricType) -> Self {
289 let ivf_params = IvfBuildParams::new(num_partitions);
290 let stages = vec![StageParams::Ivf(ivf_params)];
291 Self {
292 stages,
293 metric_type,
294 version: IndexFileVersion::V3,
295 skip_transpose: false,
296 runtime_hints: HashMap::new(),
297 }
298 }
299
300 pub fn with_ivf_flat_params(metric_type: MetricType, ivf: IvfBuildParams) -> Self {
301 let stages = vec![StageParams::Ivf(ivf)];
302 Self {
303 stages,
304 metric_type,
305 version: IndexFileVersion::V3,
306 skip_transpose: false,
307 runtime_hints: HashMap::new(),
308 }
309 }
310
311 pub fn ivf_pq(
320 num_partitions: usize,
321 num_bits: u8,
322 num_sub_vectors: usize,
323 metric_type: MetricType,
324 max_iterations: usize,
325 ) -> Self {
326 let mut stages: Vec<StageParams> = vec![];
327 stages.push(StageParams::Ivf(IvfBuildParams::new(num_partitions)));
328
329 let pq_params = PQBuildParams {
330 num_bits: num_bits as usize,
331 num_sub_vectors,
332 max_iters: max_iterations,
333 ..Default::default()
334 };
335 stages.push(StageParams::PQ(pq_params));
336
337 Self {
338 stages,
339 metric_type,
340 version: IndexFileVersion::V3,
341 skip_transpose: false,
342 runtime_hints: HashMap::new(),
343 }
344 }
345
346 pub fn ivf_rq(num_partitions: usize, num_bits: u8, distance_type: DistanceType) -> Self {
347 Self::ivf_rq_with_rotation(
348 num_partitions,
349 num_bits,
350 distance_type,
351 RQRotationType::default(),
352 )
353 }
354
355 pub fn ivf_rq_with_rotation(
356 num_partitions: usize,
357 num_bits: u8,
358 distance_type: DistanceType,
359 rotation_type: RQRotationType,
360 ) -> Self {
361 let ivf = IvfBuildParams::new(num_partitions);
362 let rq = RQBuildParams::with_rotation_type(num_bits, rotation_type);
363 let stages = vec![StageParams::Ivf(ivf), StageParams::RQ(rq)];
364 Self {
365 stages,
366 metric_type: distance_type,
367 version: IndexFileVersion::V3,
368 skip_transpose: false,
369 runtime_hints: HashMap::new(),
370 }
371 }
372
373 pub fn with_ivf_pq_params(
375 metric_type: MetricType,
376 ivf: IvfBuildParams,
377 pq: PQBuildParams,
378 ) -> Self {
379 let stages = vec![StageParams::Ivf(ivf), StageParams::PQ(pq)];
380 Self {
381 stages,
382 metric_type,
383 version: IndexFileVersion::V3,
384 skip_transpose: false,
385 runtime_hints: HashMap::new(),
386 }
387 }
388
389 pub fn with_ivf_sq_params(
390 metric_type: MetricType,
391 ivf: IvfBuildParams,
392 sq: SQBuildParams,
393 ) -> Self {
394 let stages = vec![StageParams::Ivf(ivf), StageParams::SQ(sq)];
395 Self {
396 stages,
397 metric_type,
398 version: IndexFileVersion::V3,
399 skip_transpose: false,
400 runtime_hints: HashMap::new(),
401 }
402 }
403
404 pub fn with_ivf_rq_params(
405 metric_type: MetricType,
406 ivf: IvfBuildParams,
407 rq: RQBuildParams,
408 ) -> Self {
409 let stages = vec![StageParams::Ivf(ivf), StageParams::RQ(rq)];
410 Self {
411 stages,
412 metric_type,
413 version: IndexFileVersion::V3,
414 skip_transpose: false,
415 runtime_hints: HashMap::new(),
416 }
417 }
418
419 pub fn ivf_hnsw(
420 distance_type: DistanceType,
421 ivf: IvfBuildParams,
422 hnsw: HnswBuildParams,
423 ) -> Self {
424 let stages = vec![StageParams::Ivf(ivf), StageParams::Hnsw(hnsw)];
425 Self {
426 stages,
427 metric_type: distance_type,
428 version: IndexFileVersion::V3,
429 skip_transpose: false,
430 runtime_hints: HashMap::new(),
431 }
432 }
433
434 pub fn with_ivf_hnsw_pq_params(
437 metric_type: MetricType,
438 ivf: IvfBuildParams,
439 hnsw: HnswBuildParams,
440 pq: PQBuildParams,
441 ) -> Self {
442 let stages = vec![
443 StageParams::Ivf(ivf),
444 StageParams::Hnsw(hnsw),
445 StageParams::PQ(pq),
446 ];
447 Self {
448 stages,
449 metric_type,
450 version: IndexFileVersion::V3,
451 skip_transpose: false,
452 runtime_hints: HashMap::new(),
453 }
454 }
455
456 pub fn with_ivf_hnsw_sq_params(
459 metric_type: MetricType,
460 ivf: IvfBuildParams,
461 hnsw: HnswBuildParams,
462 sq: SQBuildParams,
463 ) -> Self {
464 let stages = vec![
465 StageParams::Ivf(ivf),
466 StageParams::Hnsw(hnsw),
467 StageParams::SQ(sq),
468 ];
469 Self {
470 stages,
471 metric_type,
472 version: IndexFileVersion::V3,
473 skip_transpose: false,
474 runtime_hints: HashMap::new(),
475 }
476 }
477
478 pub fn index_type(&self) -> IndexType {
479 let len = self.stages.len();
480 match (len, self.stages.get(1), self.stages.last()) {
481 (0, _, _) => IndexType::Vector,
482 (1, _, Some(StageParams::Ivf(_))) => IndexType::IvfFlat,
483 (2, _, Some(StageParams::PQ(_))) => IndexType::IvfPq,
484 (2, _, Some(StageParams::SQ(_))) => IndexType::IvfSq,
485 (2, _, Some(StageParams::RQ(_))) => IndexType::IvfRq,
486 (2, _, Some(StageParams::Hnsw(_))) => IndexType::IvfHnswFlat,
487 (3, Some(StageParams::Hnsw(_)), Some(StageParams::PQ(_))) => IndexType::IvfHnswPq,
488 (3, Some(StageParams::Hnsw(_)), Some(StageParams::SQ(_))) => IndexType::IvfHnswSq,
489 _ => IndexType::Vector,
490 }
491 }
492}
493
494impl IndexParams for VectorIndexParams {
495 fn as_any(&self) -> &dyn Any {
496 self
497 }
498
499 fn index_name(&self) -> &str {
500 LANCE_VECTOR_INDEX
501 }
502}
503
504async fn prepare_vector_segment_build(
511 dataset: &Dataset,
512 column: &str,
513 params: &VectorIndexParams,
514 progress: Arc<dyn IndexBuildProgress>,
515 mode: &str,
516 require_precomputed_ivf: bool,
517) -> Result<(DataType, IndexType, IvfBuildParams, Box<dyn Shuffler>)> {
518 let stages = ¶ms.stages;
519
520 if stages.is_empty() {
521 return Err(Error::index(format!("{mode}: must have at least 1 stage")));
522 }
523
524 let StageParams::Ivf(ivf_params0) = &stages[0] else {
525 return Err(Error::index(format!(
526 "{mode}: invalid stages: {:?}",
527 stages
528 )));
529 };
530
531 if require_precomputed_ivf && ivf_params0.centroids.is_none() {
532 return Err(Error::index(format!(
533 "{mode}: missing precomputed IVF centroids; please provide \
534 IvfBuildParams.centroids for distributed segment build"
535 )));
536 }
537
538 let (vector_type, element_type) = get_vector_type(dataset.schema(), column)?;
539 if let DataType::List(_) = vector_type
540 && params.metric_type != DistanceType::Cosine
541 {
542 return Err(Error::index(format!(
543 "{mode}: multivector type supports only cosine distance"
544 )));
545 }
546
547 let index_type = params.index_type();
548 if index_type == IndexType::IvfRq {
549 let Some(StageParams::RQ(rq_params)) = stages.last() else {
550 return Err(Error::index(format!(
551 "{mode}: invalid stages: {:?}",
552 stages
553 )));
554 };
555 validate_supported_rq_num_bits(rq_params.num_bits)?;
556 }
557
558 let num_rows = dataset.count_rows(None).await?;
559 let num_partitions = ivf_params0.num_partitions.unwrap_or_else(|| {
560 recommended_num_partitions(
561 num_rows,
562 ivf_params0
563 .target_partition_size
564 .unwrap_or(index_type.target_partition_size()),
565 )
566 });
567 let mut ivf_params = ivf_params0.clone();
568 ivf_params.num_partitions = Some(num_partitions);
569
570 let format_version = dataset_format_version(dataset);
571 let temp_dir = TempStdDir::default();
572 let temp_dir_path = Path::from_filesystem_path(&temp_dir)?;
573 let shuffler = create_ivf_shuffler(
574 temp_dir_path,
575 num_partitions,
576 format_version,
577 Some(progress),
578 );
579
580 Ok((element_type, index_type, ivf_params, shuffler))
581}
582
583#[allow(clippy::too_many_arguments)]
585#[instrument(level = "debug", skip(dataset))]
586pub(crate) async fn build_distributed_vector_index(
587 dataset: &Dataset,
588 column: &str,
589 _name: &str,
590 uuid: Uuid,
591 params: &VectorIndexParams,
592 frag_reuse_index: Option<Arc<FragReuseIndex>>,
593 fragment_ids: &[u32],
594 progress: Arc<dyn IndexBuildProgress>,
595) -> Result<(Uuid, Vec<IndexFile>)> {
596 let (element_type, index_type, ivf_params, shuffler) = prepare_vector_segment_build(
597 dataset,
598 column,
599 params,
600 progress.clone(),
601 "Build Distributed Vector Index",
602 true,
603 )
604 .await?;
605 let stages = ¶ms.stages;
606
607 let ivf_centroids = ivf_params
608 .centroids
609 .as_ref()
610 .expect("precomputed IVF centroids required for distributed indexing; checked above")
611 .as_ref()
612 .clone();
613
614 let filtered_dataset = dataset.clone();
615
616 let segment_uuid = uuid;
617 let index_dir = dataset.indices_dir().join(segment_uuid.to_string());
618
619 let fragment_filter = fragment_ids.to_vec();
620
621 let make_ivf_model = || IvfModel::new(ivf_centroids.clone(), None);
622
623 let make_global_pq = |pq_params: &PQBuildParams| -> Result<ProductQuantizer> {
624 if pq_params.codebook.is_none() {
625 return Err(Error::index(
626 "Build Distributed Vector Index: missing precomputed PQ codebook; \
627 please provide PQBuildParams.codebook for distributed indexing"
628 .to_string(),
629 ));
630 }
631
632 let dim = crate::index::vector::utils::get_vector_dim(filtered_dataset.schema(), column)?;
633 let metric_type = params.metric_type;
634
635 let pre_codebook = pq_params
636 .codebook
637 .clone()
638 .expect("checked above that PQ codebook is present");
639 let codebook_fsl =
640 arrow_array::FixedSizeListArray::try_new_from_values(pre_codebook, dim as i32)?;
641
642 Ok(ProductQuantizer::new(
643 pq_params.num_sub_vectors,
644 pq_params.num_bits as u32,
645 dim,
646 codebook_fsl,
647 if metric_type == MetricType::Cosine {
648 MetricType::L2
649 } else {
650 metric_type
651 },
652 ))
653 };
654
655 match index_type {
656 IndexType::IvfFlat => match element_type {
657 DataType::Float16 | DataType::Float32 | DataType::Float64 => {
658 let ivf_model = make_ivf_model();
659
660 let summary = IvfIndexBuilder::<FlatIndex, FlatQuantizer>::new(
661 filtered_dataset,
662 column.to_owned(),
663 index_dir.clone(),
664 params.metric_type,
665 shuffler,
666 Some(ivf_params),
667 Some(()),
668 (),
669 frag_reuse_index,
670 )?
671 .with_ivf(ivf_model)
672 .with_fragment_filter(fragment_filter)
673 .with_progress(progress.clone())
674 .build()
675 .await?;
676 return Ok((segment_uuid, summary.files));
677 }
678 DataType::UInt8 => {
679 let ivf_model = make_ivf_model();
680
681 let summary = IvfIndexBuilder::<FlatIndex, FlatBinQuantizer>::new(
682 filtered_dataset,
683 column.to_owned(),
684 index_dir.clone(),
685 params.metric_type,
686 shuffler,
687 Some(ivf_params),
688 Some(()),
689 (),
690 frag_reuse_index,
691 )?
692 .with_ivf(ivf_model)
693 .with_fragment_filter(fragment_filter)
694 .with_progress(progress.clone())
695 .build()
696 .await?;
697 return Ok((segment_uuid, summary.files));
698 }
699 _ => {
700 return Err(Error::index(format!(
701 "Build Distributed Vector Index: invalid data type: {:?}",
702 element_type
703 )));
704 }
705 },
706
707 IndexType::IvfPq => {
708 let len = stages.len();
709 let StageParams::PQ(pq_params) = &stages[len - 1] else {
710 return Err(Error::index(format!(
711 "Build Distributed Vector Index: invalid stages: {:?}",
712 stages
713 )));
714 };
715
716 match params.version {
717 IndexFileVersion::Legacy => {
718 return Err(Error::index(
719 "Distributed indexing does not support legacy IVF_PQ format".to_string(),
720 ));
721 }
722 IndexFileVersion::V3 => {
723 let ivf_model = make_ivf_model();
724 let global_pq = make_global_pq(pq_params)?;
725
726 let summary = IvfIndexBuilder::<FlatIndex, ProductQuantizer>::new(
727 filtered_dataset,
728 column.to_owned(),
729 index_dir.clone(),
730 params.metric_type,
731 shuffler,
732 Some(ivf_params),
733 Some(pq_params.clone()),
734 (),
735 frag_reuse_index,
736 )?
737 .with_ivf(ivf_model)
738 .with_quantizer(global_pq)
739 .with_transpose(false)
742 .with_fragment_filter(fragment_filter)
743 .with_progress(progress.clone())
744 .build()
745 .await?;
746 return Ok((segment_uuid, summary.files));
747 }
748 }
749 }
750
751 IndexType::IvfSq => {
752 let StageParams::SQ(sq_params) = &stages[1] else {
753 return Err(Error::index(format!(
754 "Build Distributed Vector Index: invalid stages: {:?}",
755 stages
756 )));
757 };
758 let summary = IvfIndexBuilder::<FlatIndex, ScalarQuantizer>::new(
759 filtered_dataset,
760 column.to_owned(),
761 index_dir.clone(),
762 params.metric_type,
763 shuffler,
764 Some(ivf_params),
765 Some(sq_params.clone()),
766 (),
767 frag_reuse_index,
768 )?
769 .with_fragment_filter(fragment_filter)
770 .with_progress(progress.clone())
771 .build()
772 .await?;
773 return Ok((segment_uuid, summary.files));
774 }
775
776 IndexType::IvfHnswFlat => {
777 let StageParams::Hnsw(hnsw_params) = &stages[1] else {
778 return Err(Error::index(format!(
779 "Build Distributed Vector Index: invalid stages: {:?}",
780 stages
781 )));
782 };
783
784 match element_type {
785 DataType::UInt8 => {
786 let summary = IvfIndexBuilder::<HNSW, FlatBinQuantizer>::new(
787 filtered_dataset,
788 column.to_owned(),
789 index_dir.clone(),
790 params.metric_type,
791 shuffler,
792 Some(ivf_params),
793 Some(()),
794 hnsw_params.clone(),
795 frag_reuse_index,
796 )?
797 .with_fragment_filter(fragment_filter)
798 .with_progress(progress.clone())
799 .build()
800 .await?;
801 return Ok((segment_uuid, summary.files));
802 }
803 _ => {
804 let summary = IvfIndexBuilder::<HNSW, FlatQuantizer>::new(
805 filtered_dataset,
806 column.to_owned(),
807 index_dir.clone(),
808 params.metric_type,
809 shuffler,
810 Some(ivf_params),
811 Some(()),
812 hnsw_params.clone(),
813 frag_reuse_index,
814 )?
815 .with_fragment_filter(fragment_filter)
816 .with_progress(progress.clone())
817 .build()
818 .await?;
819 return Ok((segment_uuid, summary.files));
820 }
821 }
822 }
823
824 IndexType::IvfHnswPq => {
825 let StageParams::Hnsw(hnsw_params) = &stages[1] else {
826 return Err(Error::index(format!(
827 "Build Distributed Vector Index: invalid stages: {:?}",
828 stages
829 )));
830 };
831 let StageParams::PQ(pq_params) = &stages[2] else {
832 return Err(Error::index(format!(
833 "Build Distributed Vector Index: invalid stages: {:?}",
834 stages
835 )));
836 };
837
838 let ivf_model = make_ivf_model();
839 let global_pq = make_global_pq(pq_params)?;
840
841 let summary = IvfIndexBuilder::<HNSW, ProductQuantizer>::new(
842 filtered_dataset,
843 column.to_owned(),
844 index_dir.clone(),
845 params.metric_type,
846 shuffler,
847 Some(ivf_params),
848 Some(pq_params.clone()),
849 hnsw_params.clone(),
850 frag_reuse_index,
851 )?
852 .with_ivf(ivf_model)
853 .with_quantizer(global_pq)
854 .with_transpose(false)
857 .with_fragment_filter(fragment_filter)
858 .with_progress(progress.clone())
859 .build()
860 .await?;
861 return Ok((segment_uuid, summary.files));
862 }
863
864 IndexType::IvfHnswSq => {
865 let StageParams::Hnsw(hnsw_params) = &stages[1] else {
866 return Err(Error::index(format!(
867 "Build Distributed Vector Index: invalid stages: {:?}",
868 stages
869 )));
870 };
871 let StageParams::SQ(sq_params) = &stages[2] else {
872 return Err(Error::index(format!(
873 "Build Distributed Vector Index: invalid stages: {:?}",
874 stages
875 )));
876 };
877 let summary = IvfIndexBuilder::<HNSW, ScalarQuantizer>::new(
878 filtered_dataset,
879 column.to_owned(),
880 index_dir.clone(),
881 params.metric_type,
882 shuffler,
883 Some(ivf_params),
884 Some(sq_params.clone()),
885 hnsw_params.clone(),
886 frag_reuse_index,
887 )?
888 .with_fragment_filter(fragment_filter)
889 .with_progress(progress.clone())
890 .build()
891 .await?;
892 return Ok((segment_uuid, summary.files));
893 }
894
895 IndexType::IvfRq => {
896 let StageParams::RQ(rq_params) = &stages[1] else {
897 return Err(Error::index(format!(
898 "Build Distributed Vector Index: invalid stages: {:?}",
899 stages
900 )));
901 };
902
903 let ivf_model = make_ivf_model();
904
905 let summary = IvfIndexBuilder::<FlatIndex, RabitQuantizer>::new(
906 filtered_dataset,
907 column.to_owned(),
908 index_dir.clone(),
909 params.metric_type,
910 shuffler,
911 Some(ivf_params),
912 Some(rq_params.clone()),
913 (),
914 frag_reuse_index,
915 )?
916 .with_ivf(ivf_model)
917 .with_transpose(false)
920 .with_fragment_filter(fragment_filter)
921 .with_progress(progress.clone())
922 .build()
923 .await?;
924 return Ok((segment_uuid, summary.files));
925 }
926
927 _ => {
928 return Err(Error::index(format!(
929 "Build Distributed Vector Index: invalid index type: {:?}",
930 index_type
931 )));
932 }
933 }
934}
935
936#[instrument(level = "debug", skip(dataset))]
938pub(crate) async fn build_vector_index(
939 dataset: &Dataset,
940 column: &str,
941 name: &str,
942 uuid: Uuid,
943 params: &VectorIndexParams,
944 frag_reuse_index: Option<Arc<FragReuseIndex>>,
945 progress: Arc<dyn IndexBuildProgress>,
946) -> Result<Vec<IndexFile>> {
947 let (element_type, index_type, ivf_params, shuffler) = prepare_vector_segment_build(
948 dataset,
949 column,
950 params,
951 progress.clone(),
952 "Build Vector Index",
953 false,
954 )
955 .await?;
956 let stages = ¶ms.stages;
957
958 match index_type {
959 IndexType::IvfFlat => match element_type {
960 DataType::Float16 | DataType::Float32 | DataType::Float64 => {
961 let summary = IvfIndexBuilder::<FlatIndex, FlatQuantizer>::new(
962 dataset.clone(),
963 column.to_owned(),
964 dataset.indices_dir().clone().join(uuid.to_string()),
965 params.metric_type,
966 shuffler,
967 Some(ivf_params),
968 Some(()),
969 (),
970 frag_reuse_index,
971 )?
972 .with_progress(progress.clone())
973 .build()
974 .await?;
975 return Ok(summary.files);
976 }
977 DataType::UInt8 => {
978 let summary = IvfIndexBuilder::<FlatIndex, FlatBinQuantizer>::new(
979 dataset.clone(),
980 column.to_owned(),
981 dataset.indices_dir().clone().join(uuid.to_string()),
982 params.metric_type,
983 shuffler,
984 Some(ivf_params),
985 Some(()),
986 (),
987 frag_reuse_index,
988 )?
989 .with_progress(progress.clone())
990 .build()
991 .await?;
992 return Ok(summary.files);
993 }
994 _ => {
995 return Err(Error::index(format!(
996 "Build Vector Index: invalid data type: {:?}",
997 element_type
998 )));
999 }
1000 },
1001 IndexType::IvfPq => {
1002 let len = stages.len();
1003 let StageParams::PQ(pq_params) = &stages[len - 1] else {
1004 return Err(Error::index(format!(
1005 "Build Vector Index: invalid stages: {:?}",
1006 stages
1007 )));
1008 };
1009
1010 match params.version {
1011 IndexFileVersion::Legacy => {
1012 let files = build_ivf_pq_index(
1013 dataset,
1014 column,
1015 name,
1016 uuid,
1017 params.metric_type,
1018 &ivf_params,
1019 pq_params,
1020 progress.clone(),
1021 )
1022 .await?;
1023 return Ok(files);
1024 }
1025 IndexFileVersion::V3 => {
1026 let mut builder = IvfIndexBuilder::<FlatIndex, ProductQuantizer>::new(
1027 dataset.clone(),
1028 column.to_owned(),
1029 dataset.indices_dir().join(uuid.to_string()),
1030 params.metric_type,
1031 shuffler,
1032 Some(ivf_params),
1033 Some(pq_params.clone()),
1034 (),
1035 frag_reuse_index,
1036 )?;
1037
1038 let summary = builder
1039 .with_transpose(!params.skip_transpose)
1040 .with_progress(progress.clone())
1041 .build()
1042 .await?;
1043 return Ok(summary.files);
1044 }
1045 }
1046 }
1047 IndexType::IvfSq => {
1048 let StageParams::SQ(sq_params) = &stages[1] else {
1049 return Err(Error::index(format!(
1050 "Build Vector Index: invalid stages: {:?}",
1051 stages
1052 )));
1053 };
1054
1055 let summary = IvfIndexBuilder::<FlatIndex, ScalarQuantizer>::new(
1056 dataset.clone(),
1057 column.to_owned(),
1058 dataset.indices_dir().clone().join(uuid.to_string()),
1059 params.metric_type,
1060 shuffler,
1061 Some(ivf_params),
1062 Some(sq_params.clone()),
1063 (),
1064 frag_reuse_index,
1065 )?
1066 .with_progress(progress.clone())
1067 .build()
1068 .await?;
1069 return Ok(summary.files);
1070 }
1071 IndexType::IvfRq => {
1072 let StageParams::RQ(rq_params) = &stages[1] else {
1073 return Err(Error::index(format!(
1074 "Build Vector Index: invalid stages: {:?}",
1075 stages
1076 )));
1077 };
1078
1079 let mut builder = IvfIndexBuilder::<FlatIndex, RabitQuantizer>::new(
1080 dataset.clone(),
1081 column.to_owned(),
1082 dataset.indices_dir().join(uuid.to_string()),
1083 params.metric_type,
1084 shuffler,
1085 Some(ivf_params),
1086 Some(rq_params.clone()),
1087 (),
1088 frag_reuse_index,
1089 )?;
1090
1091 let summary = builder
1092 .with_transpose(!params.skip_transpose)
1093 .with_progress(progress.clone())
1094 .build()
1095 .await?;
1096 return Ok(summary.files);
1097 }
1098 IndexType::IvfHnswFlat => {
1099 let StageParams::Hnsw(hnsw_params) = &stages[1] else {
1100 return Err(Error::index(format!(
1101 "Build Vector Index: invalid stages: {:?}",
1102 stages
1103 )));
1104 };
1105 match element_type {
1106 DataType::UInt8 => {
1107 let summary = IvfIndexBuilder::<HNSW, FlatBinQuantizer>::new(
1108 dataset.clone(),
1109 column.to_owned(),
1110 dataset.indices_dir().clone().join(uuid.to_string()),
1111 params.metric_type,
1112 shuffler,
1113 Some(ivf_params),
1114 Some(()),
1115 hnsw_params.clone(),
1116 frag_reuse_index,
1117 )?
1118 .with_progress(progress.clone())
1119 .build()
1120 .await?;
1121 return Ok(summary.files);
1122 }
1123 _ => {
1124 let summary = IvfIndexBuilder::<HNSW, FlatQuantizer>::new(
1125 dataset.clone(),
1126 column.to_owned(),
1127 dataset.indices_dir().clone().join(uuid.to_string()),
1128 params.metric_type,
1129 shuffler,
1130 Some(ivf_params),
1131 Some(()),
1132 hnsw_params.clone(),
1133 frag_reuse_index,
1134 )?
1135 .with_progress(progress.clone())
1136 .build()
1137 .await?;
1138 return Ok(summary.files);
1139 }
1140 }
1141 }
1142 IndexType::IvfHnswPq => {
1143 let StageParams::Hnsw(hnsw_params) = &stages[1] else {
1144 return Err(Error::index(format!(
1145 "Build Vector Index: invalid stages: {:?}",
1146 stages
1147 )));
1148 };
1149 let StageParams::PQ(pq_params) = &stages[2] else {
1150 return Err(Error::index(format!(
1151 "Build Vector Index: invalid stages: {:?}",
1152 stages
1153 )));
1154 };
1155 let summary = IvfIndexBuilder::<HNSW, ProductQuantizer>::new(
1156 dataset.clone(),
1157 column.to_owned(),
1158 dataset.indices_dir().clone().join(uuid.to_string()),
1159 params.metric_type,
1160 shuffler,
1161 Some(ivf_params),
1162 Some(pq_params.clone()),
1163 hnsw_params.clone(),
1164 frag_reuse_index,
1165 )?
1166 .with_progress(progress.clone())
1167 .build()
1168 .await?;
1169 return Ok(summary.files);
1170 }
1171 IndexType::IvfHnswSq => {
1172 let StageParams::Hnsw(hnsw_params) = &stages[1] else {
1173 return Err(Error::index(format!(
1174 "Build Vector Index: invalid stages: {:?}",
1175 stages
1176 )));
1177 };
1178 let StageParams::SQ(sq_params) = &stages[2] else {
1179 return Err(Error::index(format!(
1180 "Build Vector Index: invalid stages: {:?}",
1181 stages
1182 )));
1183 };
1184 let summary = IvfIndexBuilder::<HNSW, ScalarQuantizer>::new(
1185 dataset.clone(),
1186 column.to_owned(),
1187 dataset.indices_dir().clone().join(uuid.to_string()),
1188 params.metric_type,
1189 shuffler,
1190 Some(ivf_params),
1191 Some(sq_params.clone()),
1192 hnsw_params.clone(),
1193 frag_reuse_index,
1194 )?
1195 .with_progress(progress.clone())
1196 .build()
1197 .await?;
1198 return Ok(summary.files);
1199 }
1200 _ => {
1201 return Err(Error::index(format!(
1202 "Build Vector Index: invalid index type: {:?}",
1203 index_type
1204 )));
1205 }
1206 }
1207}
1208
1209#[instrument(level = "debug", skip(dataset, existing_index, frag_reuse_index))]
1212pub(crate) async fn build_vector_index_incremental(
1213 dataset: &Dataset,
1214 column: &str,
1215 uuid: Uuid,
1216 params: &VectorIndexParams,
1217 existing_index: Arc<dyn VectorIndex>,
1218 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1219 progress: Arc<dyn IndexBuildProgress>,
1220) -> Result<VectorIndexBuildSummary> {
1221 let stages = ¶ms.stages;
1222
1223 if stages.is_empty() {
1224 return Err(Error::index(
1225 "Build Vector Index: must have at least 1 stage".to_string(),
1226 ));
1227 };
1228
1229 let StageParams::Ivf(ivf_params) = &stages[0] else {
1230 return Err(Error::index(format!(
1231 "Build Vector Index: invalid stages: {:?}",
1232 stages
1233 )));
1234 };
1235
1236 let (vector_type, _) = get_vector_type(dataset.schema(), column)?;
1237 if let DataType::List(_) = vector_type
1238 && params.metric_type != DistanceType::Cosine
1239 {
1240 return Err(Error::index(
1241 "Build Vector Index: multivector type supports only cosine distance".to_string(),
1242 ));
1243 }
1244
1245 let ivf_model = existing_index.ivf_model().clone();
1247 let quantizer = existing_index.quantizer();
1248
1249 let expected_partitions = ivf_params
1251 .num_partitions
1252 .unwrap_or(ivf_model.num_partitions());
1253 if ivf_model.num_partitions() != expected_partitions {
1254 return Err(Error::index(format!(
1255 "Number of partitions mismatch: existing index has {} partitions, but params specify {}",
1256 ivf_model.num_partitions(),
1257 expected_partitions
1258 )));
1259 }
1260
1261 let format_version = dataset_format_version(dataset);
1262
1263 let temp_dir = TempStdDir::default();
1264 let temp_dir_path = Path::from_filesystem_path(&temp_dir)?;
1265 let shuffler = create_ivf_shuffler(
1266 temp_dir_path,
1267 ivf_model.num_partitions(),
1268 format_version,
1269 Some(progress.clone()),
1270 );
1271
1272 let index_dir = dataset.indices_dir().join(uuid.to_string());
1273
1274 let (sub_index_type, quantization_type) = existing_index.sub_index_type();
1276
1277 match (sub_index_type, quantization_type) {
1278 (SubIndexType::Flat, QuantizationType::Flat) => {
1280 let summary = IvfIndexBuilder::<FlatIndex, FlatQuantizer>::new_incremental(
1281 dataset.clone(),
1282 column.to_owned(),
1283 index_dir,
1284 params.metric_type,
1285 shuffler,
1286 (),
1287 frag_reuse_index,
1288 OptimizeOptions::append(),
1289 )?
1290 .with_ivf(ivf_model)
1291 .with_quantizer(quantizer.try_into()?)
1292 .with_progress(progress.clone())
1293 .build()
1294 .await?;
1295 return Ok(summary);
1296 }
1297 (SubIndexType::Flat, QuantizationType::FlatBin) => {
1298 let summary = IvfIndexBuilder::<FlatIndex, FlatBinQuantizer>::new_incremental(
1299 dataset.clone(),
1300 column.to_owned(),
1301 index_dir,
1302 params.metric_type,
1303 shuffler,
1304 (),
1305 frag_reuse_index,
1306 OptimizeOptions::append(),
1307 )?
1308 .with_ivf(ivf_model)
1309 .with_quantizer(quantizer.try_into()?)
1310 .with_progress(progress.clone())
1311 .build()
1312 .await?;
1313 return Ok(summary);
1314 }
1315 (SubIndexType::Flat, QuantizationType::Product) => {
1317 let mut builder = IvfIndexBuilder::<FlatIndex, ProductQuantizer>::new_incremental(
1318 dataset.clone(),
1319 column.to_owned(),
1320 index_dir,
1321 params.metric_type,
1322 shuffler,
1323 (),
1324 frag_reuse_index,
1325 OptimizeOptions::append(),
1326 )?;
1327 let summary = builder
1328 .with_ivf(ivf_model)
1329 .with_quantizer(quantizer.try_into()?)
1330 .with_transpose(!params.skip_transpose)
1331 .with_progress(progress.clone())
1332 .build()
1333 .await?;
1334 return Ok(summary);
1335 }
1336 (SubIndexType::Flat, QuantizationType::Scalar) => {
1338 let summary = IvfIndexBuilder::<FlatIndex, ScalarQuantizer>::new_incremental(
1339 dataset.clone(),
1340 column.to_owned(),
1341 index_dir,
1342 params.metric_type,
1343 shuffler,
1344 (),
1345 frag_reuse_index,
1346 OptimizeOptions::append(),
1347 )?
1348 .with_ivf(ivf_model)
1349 .with_quantizer(quantizer.try_into()?)
1350 .with_progress(progress.clone())
1351 .build()
1352 .await?;
1353 return Ok(summary);
1354 }
1355 (SubIndexType::Flat, QuantizationType::Rabit) => {
1357 let mut builder = IvfIndexBuilder::<FlatIndex, RabitQuantizer>::new_incremental(
1358 dataset.clone(),
1359 column.to_owned(),
1360 index_dir,
1361 params.metric_type,
1362 shuffler,
1363 (),
1364 frag_reuse_index,
1365 OptimizeOptions::append(),
1366 )?;
1367 let summary = builder
1368 .with_ivf(ivf_model)
1369 .with_quantizer(quantizer.try_into()?)
1370 .with_transpose(!params.skip_transpose)
1371 .with_progress(progress.clone())
1372 .build()
1373 .await?;
1374 return Ok(summary);
1375 }
1376 (SubIndexType::Hnsw, quantization_type) => {
1378 let StageParams::Hnsw(hnsw_params) = &stages[1] else {
1379 return Err(Error::index(format!(
1380 "Build Vector Index: HNSW index missing HNSW params in stages: {:?}",
1381 stages
1382 )));
1383 };
1384
1385 match quantization_type {
1386 QuantizationType::Flat => {
1387 let summary = IvfIndexBuilder::<HNSW, FlatQuantizer>::new_incremental(
1388 dataset.clone(),
1389 column.to_owned(),
1390 index_dir,
1391 params.metric_type,
1392 shuffler,
1393 hnsw_params.clone(),
1394 frag_reuse_index,
1395 OptimizeOptions::append(),
1396 )?
1397 .with_ivf(ivf_model)
1398 .with_quantizer(quantizer.try_into()?)
1399 .with_progress(progress.clone())
1400 .build()
1401 .await?;
1402 return Ok(summary);
1403 }
1404 QuantizationType::FlatBin => {
1405 let summary = IvfIndexBuilder::<HNSW, FlatBinQuantizer>::new_incremental(
1406 dataset.clone(),
1407 column.to_owned(),
1408 index_dir,
1409 params.metric_type,
1410 shuffler,
1411 hnsw_params.clone(),
1412 frag_reuse_index,
1413 OptimizeOptions::append(),
1414 )?
1415 .with_ivf(ivf_model)
1416 .with_quantizer(quantizer.try_into()?)
1417 .with_progress(progress.clone())
1418 .build()
1419 .await?;
1420 return Ok(summary);
1421 }
1422 QuantizationType::Product => {
1423 let summary = IvfIndexBuilder::<HNSW, ProductQuantizer>::new_incremental(
1424 dataset.clone(),
1425 column.to_owned(),
1426 index_dir,
1427 params.metric_type,
1428 shuffler,
1429 hnsw_params.clone(),
1430 frag_reuse_index,
1431 OptimizeOptions::append(),
1432 )?
1433 .with_ivf(ivf_model)
1434 .with_quantizer(quantizer.try_into()?)
1435 .with_progress(progress.clone())
1436 .build()
1437 .await?;
1438 return Ok(summary);
1439 }
1440 QuantizationType::Scalar => {
1441 let summary = IvfIndexBuilder::<HNSW, ScalarQuantizer>::new_incremental(
1442 dataset.clone(),
1443 column.to_owned(),
1444 index_dir,
1445 params.metric_type,
1446 shuffler,
1447 hnsw_params.clone(),
1448 frag_reuse_index,
1449 OptimizeOptions::append(),
1450 )?
1451 .with_ivf(ivf_model)
1452 .with_quantizer(quantizer.try_into()?)
1453 .with_progress(progress.clone())
1454 .build()
1455 .await?;
1456 return Ok(summary);
1457 }
1458 QuantizationType::Rabit => {
1459 return Err(Error::index(
1460 "Rabit quantization is not supported for HNSW index".to_string(),
1461 ));
1462 }
1463 }
1464 }
1465 }
1466}
1467
1468#[instrument(level = "debug", skip_all)]
1470pub(crate) async fn build_empty_vector_index(
1471 _dataset: &Dataset,
1472 column: &str,
1473 name: &str,
1474 _uuid: Uuid,
1475 _params: &VectorIndexParams,
1476) -> Result<Vec<IndexFile>> {
1477 Err(Error::not_supported_source(
1480 format!(
1481 "Creating empty vector indices with train=False is not yet implemented. \
1482 Index '{}' for column '{}' cannot be created without training.",
1483 name, column
1484 )
1485 .into(),
1486 ))
1487}
1488
1489#[instrument(level = "debug", skip_all, fields(old_uuid = old_uuid.to_string(), new_uuid = new_uuid.to_string(), num_rows = mapping.len()))]
1490pub(crate) async fn remap_vector_index(
1491 dataset: Arc<Dataset>,
1492 column: &str,
1493 old_uuid: &Uuid,
1494 new_uuid: &Uuid,
1495 old_metadata: &IndexMetadata,
1496 mapping: &HashMap<u64, Option<u64>>,
1497) -> Result<Vec<IndexFile>> {
1498 let old_index = dataset
1499 .open_vector_index(column, old_uuid, &NoOpMetricsCollector)
1500 .await?;
1501
1502 if let Some(ivf_index) = old_index.as_any().downcast_ref::<IVFIndex>() {
1503 let file = remap_index_file(
1504 dataset.as_ref(),
1505 old_uuid,
1506 new_uuid,
1507 old_metadata.dataset_version,
1508 ivf_index,
1509 mapping,
1510 old_metadata.name.clone(),
1511 column.to_string(),
1512 vec![],
1516 )
1517 .await?;
1518 Ok(vec![file])
1519 } else {
1520 let files = remap_index_file_v3(
1522 dataset.as_ref(),
1523 new_uuid,
1524 old_index,
1525 mapping,
1526 column.to_string(),
1527 )
1528 .await?;
1529 Ok(files)
1530 }
1531}
1532
1533#[instrument(level = "debug", skip(dataset, vec_idx, reader))]
1535pub(crate) async fn open_vector_index(
1536 dataset: Arc<Dataset>,
1537 uuid: &Uuid,
1538 vec_idx: &lance_index::pb::VectorIndex,
1539 reader: Arc<dyn Reader>,
1540 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1541) -> Result<Arc<dyn VectorIndex>> {
1542 let metric_type = pb::VectorMetricType::try_from(vec_idx.metric_type)?.into();
1543
1544 let mut last_stage: Option<Arc<dyn VectorIndex>> = None;
1545
1546 let frag_reuse_uuid = dataset.frag_reuse_index_uuid().await;
1547
1548 for stg in vec_idx.stages.iter().rev() {
1549 match stg.stage.as_ref() {
1550 #[allow(unused_variables)]
1551 Some(Stage::Transform(tf)) => {
1552 if last_stage.is_none() {
1553 return Err(Error::index(format!(
1554 "Invalid vector index stages: {:?}",
1555 vec_idx.stages
1556 )));
1557 }
1558 }
1559 Some(Stage::Ivf(ivf_pb)) => {
1560 if last_stage.is_none() {
1561 return Err(Error::index(format!(
1562 "Invalid vector index stages: {:?}",
1563 vec_idx.stages
1564 )));
1565 }
1566 let ivf = IvfModel::try_from(ivf_pb.to_owned())?;
1567 last_stage = Some(Arc::new(IVFIndex::try_new(
1568 *uuid,
1569 ivf,
1570 reader.clone(),
1571 last_stage.unwrap(),
1572 metric_type,
1573 dataset
1574 .index_cache
1575 .for_index(uuid, frag_reuse_uuid.as_ref()),
1576 )?));
1577 }
1578 Some(Stage::Pq(pq_proto)) => {
1579 if last_stage.is_some() {
1580 return Err(Error::index(format!(
1581 "Invalid vector index stages: {:?}",
1582 vec_idx.stages
1583 )));
1584 };
1585 let pq = ProductQuantizer::from_proto(pq_proto, metric_type)?;
1586 last_stage = Some(Arc::new(PQIndex::new(
1587 pq,
1588 metric_type,
1589 frag_reuse_index.clone(),
1590 )));
1591 }
1592 Some(Stage::Diskann(_)) => {
1593 return Err(Error::index(
1594 "DiskANN support is removed from Lance.".to_string(),
1595 ));
1596 }
1597 _ => {}
1598 }
1599 }
1600
1601 if last_stage.is_none() {
1602 return Err(Error::index(format!(
1603 "Invalid index stages: {:?}",
1604 vec_idx.stages
1605 )));
1606 }
1607 let idx = last_stage.unwrap();
1608 Ok(idx)
1609}
1610
1611pub(crate) async fn open_index_file(
1618 object_store: &ObjectStore,
1619 path: &Path,
1620 file_name: &str,
1621 file_sizes: &HashMap<String, u64>,
1622) -> Result<Box<dyn Reader>> {
1623 match file_sizes.get(file_name) {
1624 Some(&size) => object_store.open_with_size(path, size as usize).await,
1625 None => object_store.open(path).await,
1626 }
1627}
1628
1629#[instrument(level = "debug", skip(dataset, reader))]
1630pub(crate) async fn open_vector_index_v2(
1631 dataset: Arc<Dataset>,
1632 column: &str,
1633 uuid: &Uuid,
1634 reader: PreviousFileReader,
1635 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1636) -> Result<Arc<dyn VectorIndex>> {
1637 let index_metadata = reader
1638 .schema()
1639 .metadata
1640 .get(INDEX_METADATA_SCHEMA_KEY)
1641 .ok_or(Error::index("Index Metadata not found".to_owned()))?;
1642 let index_metadata: lance_index::IndexMetadata = serde_json::from_str(index_metadata)?;
1643 let distance_type = DistanceType::try_from(index_metadata.distance_type.as_str())?;
1644
1645 let frag_reuse_uuid = dataset.frag_reuse_index_uuid().await;
1646 let index_meta = dataset
1648 .load_index(uuid)
1649 .await?
1650 .ok_or_else(|| Error::index(format!("Index with id {} does not exist", uuid)))?;
1651 let index_dir = dataset.indice_files_dir(&index_meta)?;
1652 let object_store = dataset.object_store_for_index(&index_meta).await?;
1653 let file_sizes = index_meta.file_size_map();
1654
1655 let index: Arc<dyn VectorIndex> = match index_metadata.index_type.as_str() {
1656 "IVF_HNSW_PQ" => {
1657 let aux_path = index_dir
1658 .clone()
1659 .join(uuid.to_string())
1660 .join(INDEX_AUXILIARY_FILE_NAME);
1661 let aux_reader = open_index_file(
1662 object_store.as_ref(),
1663 &aux_path,
1664 INDEX_AUXILIARY_FILE_NAME,
1665 &file_sizes,
1666 )
1667 .await?;
1668
1669 let ivf_data = IvfModel::load(&reader).await?;
1670 let options = HNSWIndexOptions { use_residual: true };
1671 let hnsw = HNSWIndex::<ProductQuantizer>::try_new(
1672 reader.object_reader.clone(),
1673 aux_reader.into(),
1674 options,
1675 )
1676 .await?;
1677 let pb_ivf = pb::Ivf::try_from(&ivf_data)?;
1678 let ivf = IvfModel::try_from(pb_ivf)?;
1679
1680 Arc::new(IVFIndex::try_new(
1681 *uuid,
1682 ivf,
1683 reader.object_reader.clone(),
1684 Arc::new(hnsw),
1685 distance_type,
1686 dataset
1687 .index_cache
1688 .for_index(uuid, frag_reuse_uuid.as_ref()),
1689 )?)
1690 }
1691
1692 "IVF_HNSW_SQ" => {
1693 let aux_path = index_dir
1694 .clone()
1695 .join(uuid.to_string())
1696 .join(INDEX_AUXILIARY_FILE_NAME);
1697 let aux_reader = open_index_file(
1698 object_store.as_ref(),
1699 &aux_path,
1700 INDEX_AUXILIARY_FILE_NAME,
1701 &file_sizes,
1702 )
1703 .await?;
1704
1705 let ivf_data = IvfModel::load(&reader).await?;
1706 let options = HNSWIndexOptions {
1707 use_residual: false,
1708 };
1709
1710 let hnsw = HNSWIndex::<ScalarQuantizer>::try_new(
1711 reader.object_reader.clone(),
1712 aux_reader.into(),
1713 options,
1714 )
1715 .await?;
1716 let pb_ivf = pb::Ivf::try_from(&ivf_data)?;
1717 let ivf = IvfModel::try_from(pb_ivf)?;
1718
1719 Arc::new(IVFIndex::try_new(
1720 *uuid,
1721 ivf,
1722 reader.object_reader.clone(),
1723 Arc::new(hnsw),
1724 distance_type,
1725 dataset
1726 .index_cache
1727 .for_index(uuid, frag_reuse_uuid.as_ref()),
1728 )?)
1729 }
1730
1731 index_type => {
1732 if let Some(ext) = dataset
1733 .session
1734 .index_extensions
1735 .get(&(IndexType::Vector, index_type.to_string()))
1736 {
1737 ext.clone()
1738 .to_vector()
1739 .ok_or(Error::internal(
1740 "unable to cast index extension to vector".to_string(),
1741 ))?
1742 .load_index(dataset.clone(), column, uuid, reader)
1743 .await?
1744 } else {
1745 return Err(Error::index(format!(
1746 "Unsupported index type: {}",
1747 index_metadata.index_type
1748 )));
1749 }
1750 }
1751 };
1752
1753 Ok(index)
1754}
1755
1756pub async fn initialize_vector_index(
1761 target_dataset: &mut Dataset,
1762 source_dataset: &Dataset,
1763 source_index: &IndexMetadata,
1764 field_names: &[&str],
1765) -> Result<()> {
1766 if field_names.is_empty() || field_names.len() > 1 {
1767 return Err(Error::index(format!(
1768 "Unsupported fields for vector index: {:?}",
1769 field_names
1770 )));
1771 }
1772
1773 let column_name = field_names[0];
1775
1776 let source_vector_index = source_dataset
1777 .open_vector_index(column_name, &source_index.uuid, &NoOpMetricsCollector)
1778 .await?;
1779
1780 let metric_type = source_vector_index.metric_type();
1781 let ivf_model = source_vector_index.ivf_model();
1782 let quantizer = source_vector_index.quantizer();
1783 let (sub_index_type, quantization_type) = source_vector_index.sub_index_type();
1784 let ivf_params = derive_ivf_params(ivf_model);
1785
1786 let params = match (sub_index_type, quantization_type) {
1787 (SubIndexType::Flat, QuantizationType::Flat)
1788 | (SubIndexType::Flat, QuantizationType::FlatBin) => {
1789 VectorIndexParams::with_ivf_flat_params(metric_type, ivf_params)
1790 }
1791 (SubIndexType::Flat, QuantizationType::Product) => {
1792 let pq_quantizer: ProductQuantizer = quantizer.try_into()?;
1793 let pq_params = derive_pq_params(&pq_quantizer);
1794 VectorIndexParams::with_ivf_pq_params(metric_type, ivf_params, pq_params)
1795 }
1796 (SubIndexType::Flat, QuantizationType::Scalar) => {
1797 let sq_quantizer: ScalarQuantizer = quantizer.try_into()?;
1798 let sq_params = derive_sq_params(&sq_quantizer);
1799 VectorIndexParams::with_ivf_sq_params(metric_type, ivf_params, sq_params)
1800 }
1801 (SubIndexType::Flat, QuantizationType::Rabit) => {
1802 let rabit_quantizer: RabitQuantizer = quantizer.try_into()?;
1803 let rabit_params = derive_rabit_params(&rabit_quantizer);
1804 VectorIndexParams::with_ivf_rq_params(metric_type, ivf_params, rabit_params)
1805 }
1806 (SubIndexType::Hnsw, quantization_type) => {
1807 let hnsw_params = derive_hnsw_params(source_vector_index.as_ref());
1808 match quantization_type {
1809 QuantizationType::Flat | QuantizationType::FlatBin => {
1810 VectorIndexParams::ivf_hnsw(metric_type, ivf_params, hnsw_params)
1811 }
1812 QuantizationType::Product => {
1813 let pq_quantizer: ProductQuantizer = quantizer.try_into()?;
1814 let pq_params = derive_pq_params(&pq_quantizer);
1815 VectorIndexParams::with_ivf_hnsw_pq_params(
1816 metric_type,
1817 ivf_params,
1818 hnsw_params,
1819 pq_params,
1820 )
1821 }
1822 QuantizationType::Scalar => {
1823 let sq_quantizer: ScalarQuantizer = quantizer.try_into()?;
1824 let sq_params = derive_sq_params(&sq_quantizer);
1825 VectorIndexParams::with_ivf_hnsw_sq_params(
1826 metric_type,
1827 ivf_params,
1828 hnsw_params,
1829 sq_params,
1830 )
1831 }
1832 QuantizationType::Rabit => {
1833 return Err(Error::index(
1834 "Rabit quantization is not supported for HNSW index".to_string(),
1835 ));
1836 }
1837 }
1838 }
1839 };
1840
1841 let new_uuid = Uuid::new_v4();
1842 let frag_reuse_index = target_dataset
1843 .open_frag_reuse_index(&NoOpMetricsCollector)
1844 .await?;
1845
1846 let summary = build_vector_index_incremental(
1847 target_dataset,
1848 column_name,
1849 new_uuid,
1850 ¶ms,
1851 source_vector_index,
1852 frag_reuse_index,
1853 noop_progress(),
1854 )
1855 .await?;
1856
1857 let field = target_dataset.schema().field(column_name).ok_or_else(|| {
1858 Error::index(format!(
1859 "Column '{}' not found in target dataset",
1860 column_name
1861 ))
1862 })?;
1863
1864 let fragment_bitmap = Some(target_dataset.fragment_bitmap.as_ref().clone());
1865
1866 let new_idx = IndexMetadata {
1867 uuid: new_uuid,
1868 name: source_index.name.clone(),
1869 fields: vec![field.id],
1870 dataset_version: target_dataset.manifest.version,
1871 fragment_bitmap,
1872 index_details: source_index.index_details.clone(),
1873 index_version: source_index.index_version,
1874 created_at: Some(chrono::Utc::now()),
1875 base_id: None,
1876 files: Some(summary.files),
1877 };
1878
1879 let transaction = Transaction::new(
1880 target_dataset.manifest.version,
1881 Operation::CreateIndex {
1882 new_indices: vec![new_idx],
1883 removed_indices: vec![],
1884 },
1885 None,
1886 );
1887
1888 target_dataset
1889 .apply_commit(transaction, &Default::default(), &Default::default())
1890 .await?;
1891
1892 Ok(())
1893}
1894
1895fn derive_ivf_params(ivf_model: &IvfModel) -> IvfBuildParams {
1898 IvfBuildParams {
1899 num_partitions: Some(ivf_model.num_partitions()),
1900 target_partition_size: None,
1901 max_iters: 50, centroids: ivf_model.centroids.clone().map(Arc::new),
1903 #[allow(deprecated)]
1904 retrain: false, sample_rate: 256, streaming_sample_rate: None,
1907 streaming_coreset_rate: None,
1908 streaming_refine_passes: 0,
1909 precomputed_partitions_file: None,
1910 precomputed_shuffle_buffers: None,
1911 shuffle_partition_batches: 1024 * 10, shuffle_partition_concurrency: 2, storage_options: None,
1914 }
1915}
1916
1917fn derive_pq_params(pq_quantizer: &ProductQuantizer) -> PQBuildParams {
1920 PQBuildParams {
1921 num_sub_vectors: pq_quantizer.num_sub_vectors,
1922 num_bits: pq_quantizer.num_bits as usize,
1923 max_iters: 50, kmeans_redos: 1, codebook: Some(Arc::new(pq_quantizer.codebook.clone())),
1926 sample_rate: 256, }
1928}
1929
1930fn derive_sq_params(sq_quantizer: &ScalarQuantizer) -> SQBuildParams {
1933 SQBuildParams {
1934 num_bits: sq_quantizer.num_bits(),
1935 sample_rate: 256, }
1937}
1938
1939fn derive_rabit_params(rabit_quantizer: &RabitQuantizer) -> RQBuildParams {
1942 RQBuildParams {
1943 num_bits: rabit_quantizer.num_bits(),
1944 rotation_type: rabit_quantizer.rotation_type(),
1945 rotation: None,
1946 }
1947}
1948
1949fn derive_hnsw_params(source_index: &dyn VectorIndex) -> HnswBuildParams {
1953 let default_params = HnswBuildParams {
1954 max_level: 4,
1955 m: 20,
1956 ef_construction: 100,
1957 prefetch_distance: None,
1958 };
1959
1960 let Ok(stats) = source_index.statistics() else {
1961 return default_params;
1962 };
1963
1964 let Some(sub_index) = stats.get("sub_index") else {
1965 return default_params;
1966 };
1967
1968 if let Some(params) = sub_index.get("params") {
1970 let max_level = params
1971 .get("max_level")
1972 .and_then(|v| v.as_u64())
1973 .map(|v| v as u16)
1974 .unwrap_or(4);
1975 let m = params
1976 .get("m")
1977 .and_then(|v| v.as_u64())
1978 .map(|v| v as usize)
1979 .unwrap_or(20);
1980 let ef_construction = params
1981 .get("ef_construction")
1982 .and_then(|v| v.as_u64())
1983 .map(|v| v as usize)
1984 .unwrap_or(100);
1985
1986 return HnswBuildParams {
1987 max_level,
1988 m,
1989 ef_construction,
1990 prefetch_distance: None,
1991 };
1992 }
1993
1994 default_params
1995}
1996
1997#[cfg(test)]
1998mod tests {
1999 use super::*;
2000 use crate::dataset::Dataset;
2001 use crate::index::DatasetIndexExt;
2002 use arrow_array::Array;
2003 use arrow_array::RecordBatch;
2004 use arrow_array::types::{Float32Type, Int32Type};
2005 use arrow_schema::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
2006 use lance_core::utils::tempfile::TempStrDir;
2007 use lance_datagen::{BatchCount, RowCount, array};
2008 use lance_file::writer::FileWriterOptions;
2009 use lance_index::metrics::NoOpMetricsCollector;
2010 use lance_linalg::distance::MetricType;
2011
2012 #[tokio::test]
2019 async fn test_open_index_file_skips_head_when_size_known() {
2020 use lance_index::INDEX_FILE_NAME;
2021 use lance_io::assert_io_eq;
2022 use lance_io::object_store::{ObjectStoreParams, ObjectStoreRegistry};
2023
2024 let (store, base) = ObjectStore::from_uri_and_params(
2025 Arc::new(ObjectStoreRegistry::default()),
2026 "memory:///",
2027 &ObjectStoreParams::default(),
2028 )
2029 .await
2030 .unwrap();
2031
2032 let path = base.join(INDEX_FILE_NAME);
2033 let data = vec![7u8; 2 * store.block_size()];
2035 store.put(&path, &data).await.unwrap();
2036
2037 let file_sizes = HashMap::from([(INDEX_FILE_NAME.to_string(), data.len() as u64)]);
2038
2039 let _ = store.io_stats_incremental(); let reader = open_index_file(store.as_ref(), &path, INDEX_FILE_NAME, &file_sizes)
2042 .await
2043 .unwrap();
2044 assert_eq!(reader.size().await.unwrap(), data.len());
2045 let stats = store.io_stats_incremental();
2046 assert_io_eq!(
2047 stats,
2048 read_iops,
2049 0,
2050 "a known file size must not trigger a HEAD request"
2051 );
2052
2053 let _ = store.io_stats_incremental(); let reader = open_index_file(store.as_ref(), &path, INDEX_FILE_NAME, &HashMap::new())
2056 .await
2057 .unwrap();
2058 assert_eq!(reader.size().await.unwrap(), data.len());
2059 let stats = store.io_stats_incremental();
2060 assert_io_eq!(
2061 stats,
2062 read_iops,
2063 1,
2064 "an unknown file size must fall back to exactly one HEAD request"
2065 );
2066 }
2067
2068 #[tokio::test]
2073 async fn test_hnsw_index_records_file_sizes() {
2074 use lance_index::{INDEX_AUXILIARY_FILE_NAME, INDEX_FILE_NAME};
2075
2076 let test_dir = TempStrDir::default();
2077 let uri = format!("{}/ds", test_dir.as_str());
2078
2079 let reader = lance_datagen::gen_batch()
2080 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2081 .into_reader_rows(RowCount::from(400), BatchCount::from(1));
2082 let mut dataset = Dataset::write(reader, &uri, None).await.unwrap();
2083
2084 let params = VectorIndexParams::with_ivf_hnsw_pq_params(
2085 MetricType::L2,
2086 IvfBuildParams {
2087 num_partitions: Some(8),
2088 ..Default::default()
2089 },
2090 HnswBuildParams {
2091 max_level: 6,
2092 m: 24,
2093 ef_construction: 120,
2094 prefetch_distance: None,
2095 },
2096 PQBuildParams {
2097 num_sub_vectors: 8,
2098 num_bits: 8,
2099 ..Default::default()
2100 },
2101 );
2102 dataset
2103 .create_index(
2104 &["vector"],
2105 IndexType::Vector,
2106 Some("hnsw".to_string()),
2107 ¶ms,
2108 false,
2109 )
2110 .await
2111 .unwrap();
2112
2113 let indices = dataset.load_indices().await.unwrap();
2114 let index = indices.iter().find(|idx| idx.name == "hnsw").unwrap();
2115 let file_sizes = index.file_size_map();
2116
2117 assert!(
2118 file_sizes.get(INDEX_FILE_NAME).copied().unwrap_or(0) > 0,
2119 "manifest should record a nonzero {INDEX_FILE_NAME} size, got {file_sizes:?}"
2120 );
2121 assert!(
2122 file_sizes
2123 .get(INDEX_AUXILIARY_FILE_NAME)
2124 .copied()
2125 .unwrap_or(0)
2126 > 0,
2127 "manifest should record a nonzero {INDEX_AUXILIARY_FILE_NAME} size, got {file_sizes:?}"
2128 );
2129 }
2130
2131 #[tokio::test]
2132 async fn test_initialize_vector_index_ivf_pq() {
2133 let test_dir = TempStrDir::default();
2134 let source_uri = format!("{}/source", test_dir.as_str());
2135 let target_uri = format!("{}/target", test_dir.as_str());
2136
2137 let source_reader = lance_datagen::gen_batch()
2139 .col("id", array::step::<Int32Type>())
2140 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2141 .into_reader_rows(RowCount::from(300), BatchCount::from(1));
2142 let mut source_dataset = Dataset::write(source_reader, &source_uri, None)
2143 .await
2144 .unwrap();
2145
2146 let params = VectorIndexParams::ivf_pq(10, 8, 16, MetricType::L2, 50);
2148 source_dataset
2149 .create_index(
2150 &["vector"],
2151 IndexType::Vector,
2152 Some("vector_ivf_pq".to_string()),
2153 ¶ms,
2154 false,
2155 )
2156 .await
2157 .unwrap();
2158
2159 let source_dataset = Dataset::open(&source_uri).await.unwrap();
2161 let source_indices = source_dataset.load_indices().await.unwrap();
2162 let source_index = source_indices
2163 .iter()
2164 .find(|idx| idx.name == "vector_ivf_pq")
2165 .unwrap();
2166
2167 let target_reader = lance_datagen::gen_batch()
2169 .col("id", array::step::<Int32Type>())
2170 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2171 .into_reader_rows(RowCount::from(300), BatchCount::from(1));
2172 let mut target_dataset = Dataset::write(target_reader, &target_uri, None)
2173 .await
2174 .unwrap();
2175
2176 initialize_vector_index(
2178 &mut target_dataset,
2179 &source_dataset,
2180 source_index,
2181 &["vector"],
2182 )
2183 .await
2184 .unwrap();
2185
2186 let target_indices = target_dataset.load_indices().await.unwrap();
2188 assert_eq!(target_indices.len(), 1, "Target should have 1 index");
2189 assert_eq!(
2190 target_indices[0].name, "vector_ivf_pq",
2191 "Index name should match"
2192 );
2193 assert_eq!(
2194 target_indices[0].fields,
2195 vec![1],
2196 "Index should be on field 1 (vector)"
2197 );
2198
2199 let target_vector_index = target_dataset
2201 .open_vector_index("vector", &target_indices[0].uuid, &NoOpMetricsCollector)
2202 .await
2203 .unwrap();
2204 let stats = target_vector_index.statistics().unwrap();
2205
2206 assert_eq!(
2208 stats.get("index_type").and_then(|v| v.as_str()),
2209 Some("IVF_PQ"),
2210 "Index type should be IVF_PQ"
2211 );
2212
2213 assert_eq!(
2215 stats.get("metric_type").and_then(|v| v.as_str()),
2216 Some("l2"),
2217 "Metric type should be L2"
2218 );
2219
2220 assert_eq!(
2222 stats.get("num_partitions").and_then(|v| v.as_u64()),
2223 Some(10),
2224 "Should have 10 partitions"
2225 );
2226
2227 let source_vector_index = source_dataset
2229 .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
2230 .await
2231 .unwrap();
2232
2233 let source_ivf_model = source_vector_index.ivf_model();
2235 let target_ivf_model = target_vector_index.ivf_model();
2236
2237 assert_eq!(
2239 source_ivf_model.num_partitions(),
2240 target_ivf_model.num_partitions(),
2241 "Source and target should have same number of partitions"
2242 );
2243
2244 if let (Some(source_centroids), Some(target_centroids)) =
2246 (&source_ivf_model.centroids, &target_ivf_model.centroids)
2247 {
2248 assert_eq!(
2249 source_centroids.len(),
2250 target_centroids.len(),
2251 "Centroids arrays should have same length"
2252 );
2253
2254 for i in 0..source_centroids.len() {
2257 let source_centroid = source_centroids.value(i);
2258 let target_centroid = target_centroids.value(i);
2259
2260 let source_data = source_centroid
2262 .as_any()
2263 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
2264 .expect("Centroid should be Float32Array");
2265 let target_data = target_centroid
2266 .as_any()
2267 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
2268 .expect("Centroid should be Float32Array");
2269
2270 assert_eq!(
2271 source_data.values(),
2272 target_data.values(),
2273 "Centroid {} values should be identical between source and target",
2274 i
2275 );
2276 }
2277 } else {
2278 panic!("Both source and target should have centroids");
2279 }
2280
2281 let source_ivf_params = derive_ivf_params(source_ivf_model);
2283 let target_ivf_params = derive_ivf_params(target_ivf_model);
2284 assert_eq!(
2285 source_ivf_params.num_partitions, target_ivf_params.num_partitions,
2286 "IVF num_partitions should match"
2287 );
2288 assert_eq!(
2289 target_ivf_params.num_partitions,
2290 Some(10),
2291 "Should have 10 partitions as configured"
2292 );
2293
2294 let source_quantizer = source_vector_index.quantizer();
2296 let target_quantizer = target_vector_index.quantizer();
2297 let source_pq: ProductQuantizer = source_quantizer.try_into().unwrap();
2298 let target_pq: ProductQuantizer = target_quantizer.try_into().unwrap();
2299
2300 let source_pq_params = derive_pq_params(&source_pq);
2301 let target_pq_params = derive_pq_params(&target_pq);
2302
2303 assert_eq!(
2304 source_pq_params.num_sub_vectors, target_pq_params.num_sub_vectors,
2305 "PQ num_sub_vectors should match"
2306 );
2307 assert_eq!(
2308 source_pq_params.num_bits, target_pq_params.num_bits,
2309 "PQ num_bits should match"
2310 );
2311 assert_eq!(
2312 target_pq_params.num_sub_vectors, 16,
2313 "PQ should have 16 sub vectors"
2314 );
2315 assert_eq!(target_pq_params.num_bits, 8, "PQ should use 8 bits");
2316
2317 let query_vector = lance_datagen::gen_batch()
2319 .anon_col(array::rand_vec::<Float32Type>(32.into()))
2320 .into_batch_rows(RowCount::from(1))
2321 .unwrap()
2322 .column(0)
2323 .clone();
2324 let query_vector = query_vector
2325 .as_any()
2326 .downcast_ref::<arrow_array::FixedSizeListArray>()
2327 .unwrap();
2328 let results = target_dataset
2329 .scan()
2330 .nearest("vector", &query_vector.value(0), 10)
2331 .unwrap()
2332 .try_into_batch()
2333 .await
2334 .unwrap();
2335 assert_eq!(results.num_rows(), 10, "Should return 10 nearest neighbors");
2336 }
2337
2338 #[tokio::test]
2339 async fn test_initialize_vector_index_ivf_flat() {
2340 let test_dir = TempStrDir::default();
2341 let source_uri = format!("{}/source", test_dir.as_str());
2342 let target_uri = format!("{}/target", test_dir.as_str());
2343
2344 let source_reader = lance_datagen::gen_batch()
2346 .col("id", array::step::<Int32Type>())
2347 .col("vector", array::rand_vec::<Float32Type>(64.into()))
2348 .into_reader_rows(RowCount::from(300), BatchCount::from(1));
2349 let mut source_dataset = Dataset::write(source_reader, &source_uri, None)
2350 .await
2351 .unwrap();
2352
2353 let params = VectorIndexParams::ivf_flat(8, MetricType::Cosine);
2355 source_dataset
2356 .create_index(
2357 &["vector"],
2358 IndexType::Vector,
2359 Some("vector_ivf_flat".to_string()),
2360 ¶ms,
2361 false,
2362 )
2363 .await
2364 .unwrap();
2365
2366 let source_dataset = Dataset::open(&source_uri).await.unwrap();
2368 let source_indices = source_dataset.load_indices().await.unwrap();
2369 let source_index = source_indices
2370 .iter()
2371 .find(|idx| idx.name == "vector_ivf_flat")
2372 .unwrap();
2373
2374 let target_reader = lance_datagen::gen_batch()
2376 .col("id", array::step::<Int32Type>())
2377 .col("vector", array::rand_vec::<Float32Type>(64.into()))
2378 .into_reader_rows(RowCount::from(300), BatchCount::from(1));
2379 let mut target_dataset = Dataset::write(target_reader, &target_uri, None)
2380 .await
2381 .unwrap();
2382
2383 initialize_vector_index(
2385 &mut target_dataset,
2386 &source_dataset,
2387 source_index,
2388 &["vector"],
2389 )
2390 .await
2391 .unwrap();
2392
2393 let target_indices = target_dataset.load_indices().await.unwrap();
2395 assert_eq!(target_indices.len(), 1, "Target should have 1 index");
2396 assert_eq!(
2397 target_indices[0].name, "vector_ivf_flat",
2398 "Index name should match"
2399 );
2400 assert_eq!(
2401 target_indices[0].fields,
2402 vec![1],
2403 "Index should be on field 1 (vector)"
2404 );
2405
2406 let target_vector_index = target_dataset
2408 .open_vector_index("vector", &target_indices[0].uuid, &NoOpMetricsCollector)
2409 .await
2410 .unwrap();
2411 let stats = target_vector_index.statistics().unwrap();
2412
2413 assert_eq!(
2415 stats.get("index_type").and_then(|v| v.as_str()),
2416 Some("IVF_FLAT"),
2417 "Index type should be IVF_FLAT"
2418 );
2419
2420 let metric = stats
2422 .get("metric_type")
2423 .and_then(|v| v.as_str())
2424 .unwrap_or("");
2425 assert!(
2426 metric == "cosine" || metric == "Cosine",
2427 "Metric type should be Cosine, got: {}",
2428 metric
2429 );
2430
2431 assert_eq!(
2433 stats.get("num_partitions").and_then(|v| v.as_u64()),
2434 Some(8),
2435 "Should have 8 partitions"
2436 );
2437
2438 let source_vector_index = source_dataset
2440 .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
2441 .await
2442 .unwrap();
2443
2444 let source_ivf_model = source_vector_index.ivf_model();
2446 let target_ivf_model = target_vector_index.ivf_model();
2447
2448 assert_eq!(
2450 source_ivf_model.num_partitions(),
2451 target_ivf_model.num_partitions(),
2452 "Source and target should have same number of partitions"
2453 );
2454
2455 if let (Some(source_centroids), Some(target_centroids)) =
2457 (&source_ivf_model.centroids, &target_ivf_model.centroids)
2458 {
2459 assert_eq!(
2460 source_centroids.len(),
2461 target_centroids.len(),
2462 "Centroids arrays should have same length"
2463 );
2464
2465 for i in 0..source_centroids.len() {
2468 let source_centroid = source_centroids.value(i);
2469 let target_centroid = target_centroids.value(i);
2470
2471 let source_data = source_centroid
2473 .as_any()
2474 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
2475 .expect("Centroid should be Float32Array");
2476 let target_data = target_centroid
2477 .as_any()
2478 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
2479 .expect("Centroid should be Float32Array");
2480
2481 assert_eq!(
2482 source_data.values(),
2483 target_data.values(),
2484 "Centroid {} values should be identical between source and target",
2485 i
2486 );
2487 }
2488 } else {
2489 panic!("Both source and target should have centroids");
2490 }
2491
2492 let source_ivf_params = derive_ivf_params(source_ivf_model);
2494 let target_ivf_params = derive_ivf_params(target_ivf_model);
2495 assert_eq!(
2496 source_ivf_params.num_partitions, target_ivf_params.num_partitions,
2497 "IVF num_partitions should match"
2498 );
2499 assert_eq!(
2500 target_ivf_params.num_partitions,
2501 Some(8),
2502 "Should have 8 partitions as configured"
2503 );
2504
2505 let query_vector = lance_datagen::gen_batch()
2507 .anon_col(array::rand_vec::<Float32Type>(64.into()))
2508 .into_batch_rows(RowCount::from(1))
2509 .unwrap()
2510 .column(0)
2511 .clone();
2512 let query_vector = query_vector
2513 .as_any()
2514 .downcast_ref::<arrow_array::FixedSizeListArray>()
2515 .unwrap();
2516 let results = target_dataset
2517 .scan()
2518 .nearest("vector", &query_vector.value(0), 5)
2519 .unwrap()
2520 .try_into_batch()
2521 .await
2522 .unwrap();
2523 assert_eq!(results.num_rows(), 5, "Should return 5 nearest neighbors");
2524 }
2525
2526 #[tokio::test]
2527 async fn test_build_distributed_invalid_fragment_ids() {
2528 let test_dir = TempStrDir::default();
2529 let uri = format!("{}/ds", test_dir.as_str());
2530
2531 let reader = lance_datagen::gen_batch()
2532 .col("id", array::step::<Int32Type>())
2533 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2534 .into_reader_rows(RowCount::from(128), BatchCount::from(1));
2535 let dataset = Dataset::write(reader, &uri, None).await.unwrap();
2536
2537 let fragments = dataset.fragments();
2538 assert!(
2539 !fragments.is_empty(),
2540 "Dataset should have at least one fragment"
2541 );
2542 let max_id = fragments.iter().map(|f| f.id as u32).max().unwrap();
2543 let invalid_id = max_id + 1000;
2544
2545 let uuid = Uuid::new_v4();
2547
2548 let mut ivf_params = IvfBuildParams {
2549 num_partitions: Some(4),
2550 ..Default::default()
2551 };
2552 let dim = utils::get_vector_dim(dataset.schema(), "vector").unwrap();
2553 let ivf_model = build_ivf_model(
2554 &dataset,
2555 "vector",
2556 dim,
2557 MetricType::L2,
2558 &ivf_params,
2559 None,
2560 noop_progress(),
2561 )
2562 .await
2563 .unwrap();
2564
2565 ivf_params.centroids = ivf_model.centroids.clone().map(Arc::new);
2567
2568 let params = VectorIndexParams::with_ivf_flat_params(MetricType::L2, ivf_params);
2569
2570 let result = build_distributed_vector_index(
2571 &dataset,
2572 "vector",
2573 "vector_ivf_flat_dist",
2574 uuid,
2575 ¶ms,
2576 None,
2577 &[invalid_id],
2578 noop_progress(),
2579 )
2580 .await;
2581
2582 assert!(
2583 result.is_ok(),
2584 "Expected Ok for invalid fragment ids, got {:?}",
2585 result
2586 );
2587 }
2588
2589 #[tokio::test]
2590 async fn test_build_distributed_empty_fragment_ids() {
2591 let test_dir = TempStrDir::default();
2592 let uri = format!("{}/ds", test_dir.as_str());
2593
2594 let reader = lance_datagen::gen_batch()
2595 .col("id", array::step::<Int32Type>())
2596 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2597 .into_reader_rows(RowCount::from(128), BatchCount::from(1));
2598 let dataset = Dataset::write(reader, &uri, None).await.unwrap();
2599
2600 let uuid = Uuid::new_v4();
2601 let mut ivf_params = IvfBuildParams {
2602 num_partitions: Some(4),
2603 ..Default::default()
2604 };
2605 let dim = utils::get_vector_dim(dataset.schema(), "vector").unwrap();
2606 let ivf_model = build_ivf_model(
2607 &dataset,
2608 "vector",
2609 dim,
2610 MetricType::L2,
2611 &ivf_params,
2612 None,
2613 noop_progress(),
2614 )
2615 .await
2616 .unwrap();
2617
2618 ivf_params.centroids = ivf_model.centroids.clone().map(Arc::new);
2620
2621 let params = VectorIndexParams::with_ivf_flat_params(MetricType::L2, ivf_params);
2622
2623 let result = build_distributed_vector_index(
2624 &dataset,
2625 "vector",
2626 "vector_ivf_flat_dist",
2627 uuid,
2628 ¶ms,
2629 None,
2630 &[],
2631 noop_progress(),
2632 )
2633 .await;
2634
2635 assert!(
2636 result.is_ok(),
2637 "Expected Ok for empty fragment ids, got {:?}",
2638 result
2639 );
2640 }
2641
2642 #[tokio::test]
2643 async fn test_train_ivf_progress_is_emitted_before_completion() {
2644 use std::sync::atomic::{AtomicBool, Ordering};
2645
2646 #[derive(Debug)]
2647 struct RecordingProgress {
2648 train_ivf_complete: AtomicBool,
2649 saw_train_ivf_progress_before_complete: AtomicBool,
2650 saw_train_ivf_progress_after_complete: AtomicBool,
2651 }
2652
2653 #[async_trait::async_trait]
2654 impl IndexBuildProgress for RecordingProgress {
2655 async fn stage_start(&self, _: &str, _: Option<u64>, _: &str) -> Result<()> {
2656 Ok(())
2657 }
2658
2659 async fn stage_progress(&self, stage: &str, _: u64) -> Result<()> {
2660 if stage == "train_ivf" {
2661 if self.train_ivf_complete.load(Ordering::Relaxed) {
2662 self.saw_train_ivf_progress_after_complete
2663 .store(true, Ordering::Relaxed);
2664 } else {
2665 self.saw_train_ivf_progress_before_complete
2666 .store(true, Ordering::Relaxed);
2667 }
2668 }
2669 Ok(())
2670 }
2671
2672 async fn stage_complete(&self, stage: &str) -> Result<()> {
2673 if stage == "train_ivf" {
2674 self.train_ivf_complete.store(true, Ordering::Relaxed);
2675 }
2676 Ok(())
2677 }
2678 }
2679
2680 let test_dir = TempStrDir::default();
2681 let uri = format!("{}/ds", test_dir.as_str());
2682 let reader = lance_datagen::gen_batch()
2683 .col("id", array::step::<Int32Type>())
2684 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2685 .into_reader_rows(RowCount::from(128), BatchCount::from(1));
2686 let dataset = Dataset::write(reader, &uri, None).await.unwrap();
2687
2688 let params = VectorIndexParams::ivf_flat(4, MetricType::L2);
2689 let uuid = Uuid::new_v4();
2690 let progress = Arc::new(RecordingProgress {
2691 train_ivf_complete: AtomicBool::new(false),
2692 saw_train_ivf_progress_before_complete: AtomicBool::new(false),
2693 saw_train_ivf_progress_after_complete: AtomicBool::new(false),
2694 });
2695
2696 build_vector_index(
2697 &dataset,
2698 "vector",
2699 "vector_ivf_flat_progress",
2700 uuid,
2701 ¶ms,
2702 None,
2703 progress.clone(),
2704 )
2705 .await
2706 .unwrap();
2707
2708 assert!(
2709 progress
2710 .saw_train_ivf_progress_before_complete
2711 .load(Ordering::Relaxed),
2712 "expected at least one train_ivf progress event before completion"
2713 );
2714 assert!(
2715 !progress
2716 .saw_train_ivf_progress_after_complete
2717 .load(Ordering::Relaxed),
2718 "found train_ivf progress after completion"
2719 );
2720 }
2721
2722 #[tokio::test]
2723 async fn test_build_distributed_training_metadata_missing() {
2724 let test_dir = TempStrDir::default();
2725 let uri = format!("{}/ds", test_dir.as_str());
2726
2727 let reader = lance_datagen::gen_batch()
2728 .col("id", array::step::<Int32Type>())
2729 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2730 .into_reader_rows(RowCount::from(128), BatchCount::from(1));
2731 let dataset = Dataset::write(reader, &uri, None).await.unwrap();
2732
2733 let params = VectorIndexParams::ivf_flat(4, MetricType::L2);
2734 let uuid = Uuid::new_v4();
2735
2736 let out_base = dataset.indices_dir().join(uuid.to_string());
2739 let training_path = out_base.clone().join("global_training.idx");
2740
2741 let writer = dataset
2742 .object_store
2743 .as_ref()
2744 .create(&training_path)
2745 .await
2746 .unwrap();
2747 let arrow_schema = ArrowSchema::new(vec![Field::new("dummy", ArrowDataType::Int32, true)]);
2748 let mut v2w = lance_file::writer::FileWriter::try_new(
2749 writer,
2750 lance_core::datatypes::Schema::try_from(&arrow_schema).unwrap(),
2751 FileWriterOptions::default(),
2752 )
2753 .unwrap();
2754 let empty_batch = RecordBatch::new_empty(Arc::new(arrow_schema));
2755 v2w.write_batch(&empty_batch).await.unwrap();
2756 v2w.finish().await.unwrap();
2757
2758 let fragments = dataset.fragments();
2759 assert!(
2760 !fragments.is_empty(),
2761 "Dataset should have at least one fragment"
2762 );
2763
2764 let valid_id = fragments[0].id as u32;
2765 let result = build_distributed_vector_index(
2766 &dataset,
2767 "vector",
2768 "vector_ivf_flat_dist",
2769 uuid,
2770 ¶ms,
2771 None,
2772 &[valid_id],
2773 noop_progress(),
2774 )
2775 .await;
2776
2777 match result {
2778 Err(Error::Index { message, .. }) => {
2779 assert!(
2780 message.contains("missing precomputed IVF centroids"),
2781 "Unexpected error message: {}",
2782 message
2783 );
2784 }
2785 Ok(_) => panic!("Expected Error::Index when IVF training metadata is missing, got Ok"),
2786 Err(e) => panic!(
2787 "Expected Error::Index when IVF training metadata is missing, got {:?}",
2788 e
2789 ),
2790 }
2791 }
2792
2793 #[tokio::test]
2794 async fn test_initialize_vector_index_empty_dataset() {
2795 let test_dir = TempStrDir::default();
2796 let source_uri = format!("{}/source", test_dir.as_str());
2797 let target_uri = format!("{}/target", test_dir.as_str());
2798
2799 let source_reader = lance_datagen::gen_batch()
2801 .col("id", array::step::<Int32Type>())
2802 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2803 .into_reader_rows(RowCount::from(300), BatchCount::from(1));
2804 let mut source_dataset = Dataset::write(source_reader, &source_uri, None)
2805 .await
2806 .unwrap();
2807
2808 let params = VectorIndexParams::ivf_pq(10, 8, 16, MetricType::L2, 50);
2810 source_dataset
2811 .create_index(
2812 &["vector"],
2813 IndexType::Vector,
2814 Some("vector_ivf_pq".to_string()),
2815 ¶ms,
2816 false,
2817 )
2818 .await
2819 .unwrap();
2820
2821 let source_dataset = Dataset::open(&source_uri).await.unwrap();
2823 let source_indices = source_dataset.load_indices().await.unwrap();
2824 let source_index = source_indices
2825 .iter()
2826 .find(|idx| idx.name == "vector_ivf_pq")
2827 .unwrap();
2828
2829 let empty_reader = lance_datagen::gen_batch()
2831 .col("id", array::step::<Int32Type>())
2832 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2833 .into_reader_rows(RowCount::from(0), BatchCount::from(1)); let mut target_dataset = Dataset::write(empty_reader, &target_uri, None)
2835 .await
2836 .unwrap();
2837
2838 initialize_vector_index(
2840 &mut target_dataset,
2841 &source_dataset,
2842 source_index,
2843 &["vector"],
2844 )
2845 .await
2846 .unwrap();
2847
2848 let target_indices = target_dataset.load_indices().await.unwrap();
2850 assert_eq!(target_indices.len(), 1, "Empty target should have 1 index");
2851 assert_eq!(
2852 target_indices[0].name, "vector_ivf_pq",
2853 "Index name should match"
2854 );
2855
2856 let source_vector_index = source_dataset
2858 .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
2859 .await
2860 .unwrap();
2861
2862 let target_vector_index = target_dataset
2863 .open_vector_index("vector", &target_indices[0].uuid, &NoOpMetricsCollector)
2864 .await
2865 .unwrap();
2866
2867 let source_ivf_model = source_vector_index.ivf_model();
2869 let target_ivf_model = target_vector_index.ivf_model();
2870
2871 assert_eq!(
2873 source_ivf_model.num_partitions(),
2874 target_ivf_model.num_partitions(),
2875 "Empty dataset should still have same number of partitions as source"
2876 );
2877
2878 if let (Some(source_centroids), Some(target_centroids)) =
2880 (&source_ivf_model.centroids, &target_ivf_model.centroids)
2881 {
2882 assert_eq!(
2883 source_centroids.len(),
2884 target_centroids.len(),
2885 "Centroids arrays should have same length even for empty dataset"
2886 );
2887
2888 for i in 0..source_centroids.len() {
2890 let source_centroid = source_centroids.value(i);
2891 let target_centroid = target_centroids.value(i);
2892
2893 let source_data = source_centroid
2894 .as_any()
2895 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
2896 .expect("Centroid should be Float32Array");
2897 let target_data = target_centroid
2898 .as_any()
2899 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
2900 .expect("Centroid should be Float32Array");
2901
2902 assert_eq!(
2903 source_data.values(),
2904 target_data.values(),
2905 "Empty dataset should have identical centroids from source"
2906 );
2907 }
2908 } else {
2909 panic!("Both source and empty target should have centroids");
2910 }
2911
2912 let new_data_reader = lance_datagen::gen_batch()
2914 .col("id", array::step::<Int32Type>())
2915 .col("vector", array::rand_vec::<Float32Type>(32.into()))
2916 .into_reader_rows(RowCount::from(100), BatchCount::from(1));
2917 target_dataset.append(new_data_reader, None).await.unwrap();
2918
2919 use lance_index::optimize::OptimizeOptions;
2922 target_dataset
2923 .optimize_indices(&OptimizeOptions::merge(10))
2924 .await
2925 .unwrap();
2926
2927 let target_dataset = Dataset::open(&target_uri).await.unwrap();
2929
2930 let index_stats = target_dataset
2932 .index_statistics("vector_ivf_pq")
2933 .await
2934 .unwrap();
2935 let stats_json: serde_json::Value = serde_json::from_str(&index_stats).unwrap();
2936 assert_eq!(
2937 stats_json["num_indices"], 1,
2938 "Should have only 1 merged index after optimize with high num_indices_to_merge"
2939 );
2940 assert_eq!(
2941 stats_json["num_indexed_fragments"], 1,
2942 "Should have indexed the appended fragment (empty dataset has no fragments)"
2943 );
2944 assert_eq!(
2945 stats_json["num_unindexed_fragments"], 0,
2946 "All fragments should be indexed after optimization"
2947 );
2948
2949 let query_vector = lance_datagen::gen_batch()
2951 .anon_col(array::rand_vec::<Float32Type>(32.into()))
2952 .into_batch_rows(RowCount::from(1))
2953 .unwrap()
2954 .column(0)
2955 .clone();
2956 let query_vector = query_vector
2957 .as_any()
2958 .downcast_ref::<arrow_array::FixedSizeListArray>()
2959 .unwrap();
2960
2961 let results = target_dataset
2962 .scan()
2963 .nearest("vector", &query_vector.value(0), 5)
2964 .unwrap()
2965 .try_into_batch()
2966 .await
2967 .unwrap();
2968 assert_eq!(
2969 results.num_rows(),
2970 5,
2971 "Should return 5 nearest neighbors after optimizing index"
2972 );
2973
2974 let target_indices = target_dataset.load_indices().await.unwrap();
2976 let target_vector_index = target_dataset
2977 .open_vector_index("vector", &target_indices[0].uuid, &NoOpMetricsCollector)
2978 .await
2979 .unwrap();
2980
2981 let target_ivf_model = target_vector_index.ivf_model();
2982
2983 if let (Some(source_centroids), Some(target_centroids)) =
2985 (&source_ivf_model.centroids, &target_ivf_model.centroids)
2986 {
2987 for i in 0..source_centroids.len() {
2988 let source_centroid = source_centroids.value(i);
2989 let target_centroid = target_centroids.value(i);
2990
2991 let source_data = source_centroid
2992 .as_any()
2993 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
2994 .expect("Centroid should be Float32Array");
2995 let target_data = target_centroid
2996 .as_any()
2997 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
2998 .expect("Centroid should be Float32Array");
2999
3000 assert_eq!(
3001 source_data.values(),
3002 target_data.values(),
3003 "Centroids should remain identical after optimize_indices"
3004 );
3005 }
3006 }
3007 }
3008
3009 #[tokio::test]
3010 async fn test_initialize_vector_index_ivf_sq() {
3011 let test_dir = TempStrDir::default();
3012 let source_uri = format!("{}/source", test_dir.as_str());
3013 let target_uri = format!("{}/target", test_dir.as_str());
3014
3015 let source_reader = lance_datagen::gen_batch()
3017 .col("id", array::step::<Int32Type>())
3018 .col("vector", array::rand_vec::<Float32Type>(32.into()))
3019 .into_reader_rows(RowCount::from(400), BatchCount::from(1));
3020 let mut source_dataset = Dataset::write(source_reader, &source_uri, None)
3021 .await
3022 .unwrap();
3023
3024 use lance_index::vector::ivf::IvfBuildParams;
3026 use lance_index::vector::sq::builder::SQBuildParams;
3027 let ivf_params = IvfBuildParams::new(6);
3028 let sq_params = SQBuildParams::default();
3029 let params = VectorIndexParams::with_ivf_sq_params(MetricType::Dot, ivf_params, sq_params);
3030 source_dataset
3031 .create_index(
3032 &["vector"],
3033 IndexType::Vector,
3034 Some("vector_ivf_sq".to_string()),
3035 ¶ms,
3036 false,
3037 )
3038 .await
3039 .unwrap();
3040
3041 let source_dataset = Dataset::open(&source_uri).await.unwrap();
3043 let source_indices = source_dataset.load_indices().await.unwrap();
3044 let source_index = source_indices
3045 .iter()
3046 .find(|idx| idx.name == "vector_ivf_sq")
3047 .unwrap();
3048
3049 let target_reader = lance_datagen::gen_batch()
3051 .col("id", array::step::<Int32Type>())
3052 .col("vector", array::rand_vec::<Float32Type>(32.into()))
3053 .into_reader_rows(RowCount::from(400), BatchCount::from(1));
3054 let mut target_dataset = Dataset::write(target_reader, &target_uri, None)
3055 .await
3056 .unwrap();
3057
3058 initialize_vector_index(
3060 &mut target_dataset,
3061 &source_dataset,
3062 source_index,
3063 &["vector"],
3064 )
3065 .await
3066 .unwrap();
3067
3068 let target_indices = target_dataset.load_indices().await.unwrap();
3070 assert_eq!(target_indices.len(), 1, "Target should have 1 index");
3071 assert_eq!(
3072 target_indices[0].name, "vector_ivf_sq",
3073 "Index name should match"
3074 );
3075 assert_eq!(
3076 target_indices[0].fields,
3077 vec![1],
3078 "Index should be on field 1 (vector)"
3079 );
3080
3081 let target_vector_index = target_dataset
3083 .open_vector_index("vector", &target_indices[0].uuid, &NoOpMetricsCollector)
3084 .await
3085 .unwrap();
3086 let stats = target_vector_index.statistics().unwrap();
3087
3088 assert_eq!(
3090 stats.get("index_type").and_then(|v| v.as_str()),
3091 Some("IVF_SQ"),
3092 "Index type should be IVF_SQ"
3093 );
3094
3095 let metric = stats
3097 .get("metric_type")
3098 .and_then(|v| v.as_str())
3099 .unwrap_or("");
3100 assert!(
3101 metric == "dot" || metric == "Dot",
3102 "Metric type should be Dot, got: {}",
3103 metric
3104 );
3105
3106 assert_eq!(
3108 stats.get("num_partitions").and_then(|v| v.as_u64()),
3109 Some(6),
3110 "Should have 6 partitions"
3111 );
3112
3113 let source_vector_index = source_dataset
3115 .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
3116 .await
3117 .unwrap();
3118
3119 let source_ivf_model = source_vector_index.ivf_model();
3121 let target_ivf_model = target_vector_index.ivf_model();
3122
3123 assert_eq!(
3125 source_ivf_model.num_partitions(),
3126 target_ivf_model.num_partitions(),
3127 "Source and target should have same number of partitions"
3128 );
3129
3130 if let (Some(source_centroids), Some(target_centroids)) =
3132 (&source_ivf_model.centroids, &target_ivf_model.centroids)
3133 {
3134 assert_eq!(
3135 source_centroids.len(),
3136 target_centroids.len(),
3137 "Centroids arrays should have same length"
3138 );
3139
3140 for i in 0..source_centroids.len() {
3143 let source_centroid = source_centroids.value(i);
3144 let target_centroid = target_centroids.value(i);
3145
3146 let source_data = source_centroid
3148 .as_any()
3149 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
3150 .expect("Centroid should be Float32Array");
3151 let target_data = target_centroid
3152 .as_any()
3153 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
3154 .expect("Centroid should be Float32Array");
3155
3156 assert_eq!(
3157 source_data.values(),
3158 target_data.values(),
3159 "Centroid {} values should be identical between source and target",
3160 i
3161 );
3162 }
3163 } else {
3164 panic!("Both source and target should have centroids");
3165 }
3166
3167 let source_ivf_params = derive_ivf_params(source_ivf_model);
3169 let target_ivf_params = derive_ivf_params(target_ivf_model);
3170 assert_eq!(
3171 source_ivf_params.num_partitions, target_ivf_params.num_partitions,
3172 "IVF num_partitions should match"
3173 );
3174 assert_eq!(
3175 target_ivf_params.num_partitions,
3176 Some(6),
3177 "Should have 6 partitions as configured"
3178 );
3179
3180 let source_quantizer = source_vector_index.quantizer();
3182 let target_quantizer = target_vector_index.quantizer();
3183 let source_sq: ScalarQuantizer = source_quantizer.try_into().unwrap();
3184 let target_sq: ScalarQuantizer = target_quantizer.try_into().unwrap();
3185
3186 let source_sq_params = derive_sq_params(&source_sq);
3187 let target_sq_params = derive_sq_params(&target_sq);
3188
3189 assert_eq!(
3190 source_sq_params.num_bits, target_sq_params.num_bits,
3191 "SQ num_bits should match"
3192 );
3193
3194 let query_vector = lance_datagen::gen_batch()
3196 .anon_col(array::rand_vec::<Float32Type>(32.into()))
3197 .into_batch_rows(RowCount::from(1))
3198 .unwrap()
3199 .column(0)
3200 .clone();
3201 let query_vector = query_vector
3202 .as_any()
3203 .downcast_ref::<arrow_array::FixedSizeListArray>()
3204 .unwrap();
3205 let results = target_dataset
3206 .scan()
3207 .nearest("vector", &query_vector.value(0), 15)
3208 .unwrap()
3209 .try_into_batch()
3210 .await
3211 .unwrap();
3212 assert_eq!(results.num_rows(), 15, "Should return 15 nearest neighbors");
3213 }
3214
3215 #[tokio::test]
3216 async fn test_initialize_vector_index_ivf_hnsw_pq() {
3217 let test_dir = TempStrDir::default();
3218 let source_uri = format!("{}/source", test_dir.as_str());
3219 let target_uri = format!("{}/target", test_dir.as_str());
3220
3221 let source_reader = lance_datagen::gen_batch()
3223 .col("id", array::step::<Int32Type>())
3224 .col("vector", array::rand_vec::<Float32Type>(32.into()))
3225 .into_reader_rows(RowCount::from(400), BatchCount::from(1));
3226 let mut source_dataset = Dataset::write(source_reader, &source_uri, None)
3227 .await
3228 .unwrap();
3229
3230 let ivf_params = IvfBuildParams {
3232 num_partitions: Some(8),
3233 ..Default::default()
3234 };
3235 let hnsw_params = HnswBuildParams {
3236 max_level: 6,
3237 m: 24,
3238 ef_construction: 120,
3239 prefetch_distance: None,
3240 };
3241 let pq_params = PQBuildParams {
3242 num_sub_vectors: 8,
3243 num_bits: 8,
3244 ..Default::default()
3245 };
3246 let params = VectorIndexParams::with_ivf_hnsw_pq_params(
3247 MetricType::L2,
3248 ivf_params,
3249 hnsw_params,
3250 pq_params,
3251 );
3252
3253 source_dataset
3254 .create_index(
3255 &["vector"],
3256 IndexType::Vector,
3257 Some("vector_ivf_hnsw_pq".to_string()),
3258 ¶ms,
3259 false,
3260 )
3261 .await
3262 .unwrap();
3263
3264 let source_dataset = Dataset::open(&source_uri).await.unwrap();
3266 let source_indices = source_dataset.load_indices().await.unwrap();
3267 let source_index = source_indices
3268 .iter()
3269 .find(|idx| idx.name == "vector_ivf_hnsw_pq")
3270 .unwrap();
3271
3272 let target_reader = lance_datagen::gen_batch()
3274 .col("id", array::step::<Int32Type>())
3275 .col("vector", array::rand_vec::<Float32Type>(32.into()))
3276 .into_reader_rows(RowCount::from(100), BatchCount::from(1));
3277 let mut target_dataset = Dataset::write(target_reader, &target_uri, None)
3278 .await
3279 .unwrap();
3280
3281 initialize_vector_index(
3283 &mut target_dataset,
3284 &source_dataset,
3285 source_index,
3286 &["vector"],
3287 )
3288 .await
3289 .unwrap();
3290
3291 let target_indices = target_dataset.load_indices().await.unwrap();
3293 assert_eq!(target_indices.len(), 1, "Target should have 1 index");
3294 assert_eq!(
3295 target_indices[0].name, "vector_ivf_hnsw_pq",
3296 "Index name should match"
3297 );
3298
3299 let target_vector_index = target_dataset
3301 .open_vector_index("vector", &target_indices[0].uuid, &NoOpMetricsCollector)
3302 .await
3303 .unwrap();
3304 let stats = target_vector_index.statistics().unwrap();
3305
3306 assert_eq!(
3308 stats.get("index_type").and_then(|v| v.as_str()),
3309 Some("IVF_HNSW_PQ"),
3310 "Index type should be IVF_HNSW_PQ"
3311 );
3312
3313 assert_eq!(
3315 stats.get("metric_type").and_then(|v| v.as_str()),
3316 Some("l2"),
3317 "Metric type should be L2"
3318 );
3319
3320 assert_eq!(
3322 stats.get("num_partitions").and_then(|v| v.as_u64()),
3323 Some(8),
3324 "Should have 8 partitions"
3325 );
3326
3327 let source_vector_index = source_dataset
3329 .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
3330 .await
3331 .unwrap();
3332
3333 let source_ivf_model = source_vector_index.ivf_model();
3335 let target_ivf_model = target_vector_index.ivf_model();
3336
3337 assert_eq!(
3339 source_ivf_model.num_partitions(),
3340 target_ivf_model.num_partitions(),
3341 "Source and target should have same number of partitions"
3342 );
3343
3344 if let (Some(source_centroids), Some(target_centroids)) =
3346 (&source_ivf_model.centroids, &target_ivf_model.centroids)
3347 {
3348 assert_eq!(
3349 source_centroids.len(),
3350 target_centroids.len(),
3351 "Centroids arrays should have same length"
3352 );
3353
3354 let source_centroid = source_centroids.value(0);
3356 let target_centroid = target_centroids.value(0);
3357
3358 let source_data = source_centroid
3359 .as_any()
3360 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
3361 .expect("Centroid should be Float32Array");
3362 let target_data = target_centroid
3363 .as_any()
3364 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
3365 .expect("Centroid should be Float32Array");
3366
3367 assert_eq!(
3368 source_data.values(),
3369 target_data.values(),
3370 "Centroid values should be identical between source and target"
3371 );
3372 } else {
3373 panic!("Both source and target should have centroids");
3374 }
3375
3376 let sub_index = stats
3378 .get("sub_index")
3379 .and_then(|v| v.as_object())
3380 .expect("IVF_HNSW_PQ index should have sub_index");
3381
3382 assert_eq!(
3384 sub_index.get("nbits").and_then(|v| v.as_u64()),
3385 Some(8),
3386 "PQ should use 8 bits"
3387 );
3388 assert_eq!(
3389 sub_index.get("num_sub_vectors").and_then(|v| v.as_u64()),
3390 Some(8),
3391 "PQ should have 8 sub vectors"
3392 );
3393
3394 let source_ivf_params = derive_ivf_params(source_ivf_model);
3396 let target_ivf_params = derive_ivf_params(target_ivf_model);
3397 assert_eq!(
3398 source_ivf_params.num_partitions, target_ivf_params.num_partitions,
3399 "IVF num_partitions should match"
3400 );
3401 assert_eq!(
3402 target_ivf_params.num_partitions,
3403 Some(8),
3404 "Should have 8 partitions as configured"
3405 );
3406
3407 let source_quantizer = source_vector_index.quantizer();
3409 let target_quantizer = target_vector_index.quantizer();
3410 let source_pq: ProductQuantizer = source_quantizer.try_into().unwrap();
3411 let target_pq: ProductQuantizer = target_quantizer.try_into().unwrap();
3412
3413 let source_pq_params = derive_pq_params(&source_pq);
3414 let target_pq_params = derive_pq_params(&target_pq);
3415
3416 assert_eq!(
3417 source_pq_params.num_sub_vectors, target_pq_params.num_sub_vectors,
3418 "PQ num_sub_vectors should match"
3419 );
3420 assert_eq!(
3421 source_pq_params.num_bits, target_pq_params.num_bits,
3422 "PQ num_bits should match"
3423 );
3424 assert_eq!(
3425 target_pq_params.num_sub_vectors, 8,
3426 "PQ should have 8 sub vectors"
3427 );
3428 assert_eq!(target_pq_params.num_bits, 8, "PQ should use 8 bits");
3429
3430 let derived_hnsw_params = derive_hnsw_params(target_vector_index.as_ref());
3432 assert_eq!(
3433 derived_hnsw_params.max_level, 6,
3434 "HNSW max_level should be extracted as 6 from source index"
3435 );
3436 assert_eq!(
3437 derived_hnsw_params.m, 24,
3438 "HNSW m should be extracted as 24 from source index"
3439 );
3440 assert_eq!(
3441 derived_hnsw_params.ef_construction, 120,
3442 "HNSW ef_construction should be extracted as 120 from source index"
3443 );
3444
3445 let query_vector = lance_datagen::gen_batch()
3447 .anon_col(array::rand_vec::<Float32Type>(32.into()))
3448 .into_batch_rows(RowCount::from(1))
3449 .unwrap()
3450 .column(0)
3451 .clone();
3452 let query_vector = query_vector
3453 .as_any()
3454 .downcast_ref::<arrow_array::FixedSizeListArray>()
3455 .unwrap();
3456 let results = target_dataset
3457 .scan()
3458 .nearest("vector", &query_vector.value(0), 5)
3459 .unwrap()
3460 .try_into_batch()
3461 .await
3462 .unwrap();
3463 assert_eq!(results.num_rows(), 5, "Should return 5 nearest neighbors");
3464 }
3465
3466 #[tokio::test]
3467 async fn test_initialize_vector_index_ivf_hnsw_sq() {
3468 let test_dir = TempStrDir::default();
3469 let source_uri = format!("{}/source", test_dir.as_str());
3470 let target_uri = format!("{}/target", test_dir.as_str());
3471
3472 let source_reader = lance_datagen::gen_batch()
3474 .col("id", array::step::<Int32Type>())
3475 .col("vector", array::rand_vec::<Float32Type>(32.into()))
3476 .into_reader_rows(RowCount::from(300), BatchCount::from(1));
3477 let mut source_dataset = Dataset::write(source_reader, &source_uri, None)
3478 .await
3479 .unwrap();
3480
3481 let ivf_params = IvfBuildParams {
3483 num_partitions: Some(6),
3484 ..Default::default()
3485 };
3486 let hnsw_params = HnswBuildParams {
3487 max_level: 5,
3488 m: 16,
3489 ef_construction: 80,
3490 prefetch_distance: None,
3491 };
3492 let sq_params = SQBuildParams {
3493 num_bits: 8,
3494 ..Default::default()
3495 };
3496 let params = VectorIndexParams::with_ivf_hnsw_sq_params(
3497 MetricType::Cosine,
3498 ivf_params,
3499 hnsw_params,
3500 sq_params,
3501 );
3502
3503 source_dataset
3504 .create_index(
3505 &["vector"],
3506 IndexType::Vector,
3507 Some("vector_ivf_hnsw_sq".to_string()),
3508 ¶ms,
3509 false,
3510 )
3511 .await
3512 .unwrap();
3513
3514 let source_dataset = Dataset::open(&source_uri).await.unwrap();
3516 let source_indices = source_dataset.load_indices().await.unwrap();
3517 let source_index = source_indices
3518 .iter()
3519 .find(|idx| idx.name == "vector_ivf_hnsw_sq")
3520 .unwrap();
3521
3522 let target_reader = lance_datagen::gen_batch()
3524 .col("id", array::step::<Int32Type>())
3525 .col("vector", array::rand_vec::<Float32Type>(32.into()))
3526 .into_reader_rows(RowCount::from(100), BatchCount::from(1));
3527 let mut target_dataset = Dataset::write(target_reader, &target_uri, None)
3528 .await
3529 .unwrap();
3530
3531 initialize_vector_index(
3533 &mut target_dataset,
3534 &source_dataset,
3535 source_index,
3536 &["vector"],
3537 )
3538 .await
3539 .unwrap();
3540
3541 let target_indices = target_dataset.load_indices().await.unwrap();
3543 assert_eq!(target_indices.len(), 1, "Target should have 1 index");
3544 assert_eq!(
3545 target_indices[0].name, "vector_ivf_hnsw_sq",
3546 "Index name should match"
3547 );
3548
3549 let target_vector_index = target_dataset
3551 .open_vector_index("vector", &target_indices[0].uuid, &NoOpMetricsCollector)
3552 .await
3553 .unwrap();
3554 let stats = target_vector_index.statistics().unwrap();
3555
3556 assert_eq!(
3558 stats.get("index_type").and_then(|v| v.as_str()),
3559 Some("IVF_HNSW_SQ"),
3560 "Index type should be IVF_HNSW_SQ"
3561 );
3562
3563 assert_eq!(
3565 stats.get("metric_type").and_then(|v| v.as_str()),
3566 Some("cosine"),
3567 "Metric type should be cosine"
3568 );
3569
3570 assert_eq!(
3572 stats.get("num_partitions").and_then(|v| v.as_u64()),
3573 Some(6),
3574 "Should have 6 partitions"
3575 );
3576
3577 let source_vector_index = source_dataset
3579 .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
3580 .await
3581 .unwrap();
3582
3583 let source_ivf_model = source_vector_index.ivf_model();
3585 let target_ivf_model = target_vector_index.ivf_model();
3586
3587 assert_eq!(
3589 source_ivf_model.num_partitions(),
3590 target_ivf_model.num_partitions(),
3591 "Source and target should have same number of partitions"
3592 );
3593
3594 let sub_index = stats
3596 .get("sub_index")
3597 .and_then(|v| v.as_object())
3598 .expect("IVF_HNSW_SQ index should have sub_index");
3599 assert_eq!(
3601 sub_index.get("num_bits").and_then(|v| v.as_u64()),
3602 Some(8),
3603 "SQ should use 8 bits"
3604 );
3605
3606 if let (Some(source_centroids), Some(target_centroids)) =
3608 (&source_ivf_model.centroids, &target_ivf_model.centroids)
3609 {
3610 assert_eq!(
3611 source_centroids.len(),
3612 target_centroids.len(),
3613 "Centroids arrays should have same length"
3614 );
3615
3616 for i in 0..source_centroids.len() {
3619 let source_centroid = source_centroids.value(i);
3620 let target_centroid = target_centroids.value(i);
3621
3622 let source_data = source_centroid
3624 .as_any()
3625 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
3626 .expect("Centroid should be Float32Array");
3627 let target_data = target_centroid
3628 .as_any()
3629 .downcast_ref::<arrow_array::PrimitiveArray<arrow_array::types::Float32Type>>()
3630 .expect("Centroid should be Float32Array");
3631
3632 assert_eq!(
3633 source_data.values(),
3634 target_data.values(),
3635 "Centroid {} values should be identical between source and target",
3636 i
3637 );
3638 }
3639 } else {
3640 panic!("Both source and target should have centroids");
3641 }
3642
3643 let source_ivf_params = derive_ivf_params(source_ivf_model);
3645 let target_ivf_params = derive_ivf_params(target_ivf_model);
3646 assert_eq!(
3647 source_ivf_params.num_partitions, target_ivf_params.num_partitions,
3648 "IVF num_partitions should match"
3649 );
3650 assert_eq!(
3651 target_ivf_params.num_partitions,
3652 Some(6),
3653 "Should have 6 partitions as configured"
3654 );
3655
3656 let source_quantizer = source_vector_index.quantizer();
3658 let target_quantizer = target_vector_index.quantizer();
3659 let source_sq: ScalarQuantizer = source_quantizer.try_into().unwrap();
3660 let target_sq: ScalarQuantizer = target_quantizer.try_into().unwrap();
3661
3662 let source_sq_params = derive_sq_params(&source_sq);
3663 let target_sq_params = derive_sq_params(&target_sq);
3664
3665 assert_eq!(
3666 source_sq_params.num_bits, target_sq_params.num_bits,
3667 "SQ num_bits should match"
3668 );
3669 assert_eq!(target_sq_params.num_bits, 8, "SQ should use 8 bits");
3670
3671 let derived_hnsw_params = derive_hnsw_params(target_vector_index.as_ref());
3673 assert_eq!(
3674 derived_hnsw_params.max_level, 5,
3675 "HNSW max_level should be extracted as 5 from source index"
3676 );
3677 assert_eq!(
3678 derived_hnsw_params.m, 16,
3679 "HNSW m should be extracted as 16 from source index"
3680 );
3681 assert_eq!(
3682 derived_hnsw_params.ef_construction, 80,
3683 "HNSW ef_construction should be extracted as 80 from source index"
3684 );
3685
3686 let query_vector = lance_datagen::gen_batch()
3688 .anon_col(array::rand_vec::<Float32Type>(32.into()))
3689 .into_batch_rows(RowCount::from(1))
3690 .unwrap()
3691 .column(0)
3692 .clone();
3693 let query_vector = query_vector
3694 .as_any()
3695 .downcast_ref::<arrow_array::FixedSizeListArray>()
3696 .unwrap();
3697 let results = target_dataset
3698 .scan()
3699 .nearest("vector", &query_vector.value(0), 5)
3700 .unwrap()
3701 .try_into_batch()
3702 .await
3703 .unwrap();
3704 assert_eq!(results.num_rows(), 5, "Should return 5 nearest neighbors");
3705 }
3706}