Skip to main content

lance/index/
vector.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Vector Index for Fast Approximate Nearest Neighbor (ANN) Search
5//!
6
7use 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/// A materialized snapshot of one logical vector index and all of its segments.
72#[derive(Debug)]
73pub struct LogicalVectorIndex {
74    name: String,
75    column: String,
76    segments: Vec<(IndexMetadata, Arc<dyn VectorIndex>)>,
77}
78
79/// An IVF-specific inspection and maintenance view over a [`LogicalVectorIndex`].
80///
81/// Callers must explicitly opt into this view before accessing partition-level
82/// APIs so that IVF semantics stay separated from the generic logical vector
83/// index abstraction.
84#[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    /// Returns the logical index name.
110    pub fn name(&self) -> &str {
111        &self.name
112    }
113
114    /// Returns the vector column that this logical index is built on.
115    pub fn column(&self) -> &str {
116        &self.column
117    }
118
119    /// Returns the number of physical segments in this logical index.
120    pub fn num_segments(&self) -> usize {
121        self.segments.len()
122    }
123
124    /// Returns the committed metadata for all physical segments.
125    pub fn metadatas(&self) -> impl ExactSizeIterator<Item = &IndexMetadata> + '_ {
126        self.segments.iter().map(|(metadata, _)| metadata)
127    }
128
129    /// Returns the indexed row count for each segment.
130    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    /// Returns an IVF view over this logical index.
138    ///
139    /// This is currently fallible only to leave room for future non-IVF vector
140    /// index families. All vector indices currently opened through this path are
141    /// IVF-backed.
142    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    /// Returns the partition count for each segment in this IVF index.
169    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    /// Returns the partition sizes for each segment in this IVF index.
177    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    /// Reads one IVF partition across all segments in this logical index.
191    ///
192    /// The returned stream preserves segment boundaries only implicitly through
193    /// concatenation order; callers should treat it as a merged partition view.
194    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/// Parameters of each index stage.
226#[derive(Debug, Clone)]
227pub enum StageParams {
228    Ivf(IvfBuildParams),
229    Hnsw(HnswBuildParams),
230    PQ(PQBuildParams),
231    SQ(SQBuildParams),
232    RQ(RQBuildParams),
233}
234
235// The version of the index file.
236// `Legacy` is used for only IVF_PQ index, and is the default value.
237// The other index types are using `V3`.
238#[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/// The parameters to build vector index.
258#[derive(Debug, Clone)]
259pub struct VectorIndexParams {
260    pub stages: Vec<StageParams>,
261
262    /// Vector distance metrics type.
263    pub metric_type: MetricType,
264
265    /// The version of the index file.
266    pub version: IndexFileVersion,
267
268    /// Skip transpose / packing for PQ and RQ storage.
269    pub skip_transpose: bool,
270
271    /// Runtime hints: optional build preferences stored in the index manifest.
272    /// Keys use reverse-DNS namespacing (e.g., "lance.ivf.max_iters").
273    /// Populated by the build path and merged into VectorIndexDetails at creation time.
274    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    /// Create index parameters for `IVF_PQ` index.
312    ///
313    /// Parameters
314    ///
315    ///  - `num_partitions`: the number of IVF partitions.
316    ///  - `num_bits`: the number of bits to present the centroids used in PQ. Can only be `8` for now.
317    ///  - `num_sub_vectors`: the number of sub vectors used in PQ.
318    ///  - `metric_type`: how to compute distance, i.e., `L2` or `Cosine`.
319    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    /// Create index parameters with `IVF` and `PQ` parameters, respectively.
374    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    /// Create index parameters with `IVF`, `PQ` and `HNSW` parameters, respectively.
435    /// This is used for `IVF_HNSW_PQ` index.
436    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    /// Create index parameters with `IVF`, `HNSW` and `SQ` parameters, respectively.
457    /// This is used for `IVF_HNSW_SQ` index.
458    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
504/// Prepare the shared build inputs used by both direct local builds and
505/// staged shard builds.
506///
507/// These paths emit different file layouts, but they follow the same rules for
508/// validating the vector column, deriving the effective index type, sizing IVF
509/// partitions, and constructing the shuffler.
510async 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 = &params.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/// Build a Distributed Vector Index for specific fragments
584#[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 = &params.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                    // For distributed shards, keep PQ codes in row-major layout.
740                    // A single transpose is performed in the distributed merge stage.
741                    .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            // For distributed shards, keep PQ codes in row-major layout.
855            // A single transpose is performed in the distributed merge stage.
856            .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            // For distributed shards, keep RQ codes in row-major layout.
918            // A single packing pass is performed in the distributed merge stage.
919            .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/// Build a Vector Index
937#[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 = &params.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/// Build a Vector Index incrementally using an existing index's IVF model and quantizer
1210/// This creates a delta index that shares centroids with the source index
1211#[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 = &params.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    // Extract IVF model and quantizer from existing index
1246    let ivf_model = existing_index.ivf_model().clone();
1247    let quantizer = existing_index.quantizer();
1248
1249    // Ensure the number of partitions matches
1250    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    // Determine the index type and build incrementally
1275    let (sub_index_type, quantization_type) = existing_index.sub_index_type();
1276
1277    match (sub_index_type, quantization_type) {
1278        // IVF_FLAT
1279        (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        // IVF_PQ
1316        (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        // IVF_SQ
1337        (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        // IVF_RQ
1356        (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        // IVF_HNSW variants
1377        (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/// Build an empty vector index without training on data
1469#[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    // For now, return a NotImplementedError to indicate this functionality
1478    // is still being developed
1479    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            // We can safely assume there are no transforms today.  We assert above that the
1513            // top stage is IVF and IVF does not support transforms between IVF and PQ.  This
1514            // will be fixed in the future.
1515            vec![],
1516        )
1517        .await?;
1518        Ok(vec![file])
1519    } else {
1520        // it's v3 index
1521        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/// Open the Vector index on dataset, specified by the `uuid`.
1534#[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
1611/// Open an index file without a HEAD request when the size is already known.
1612///
1613/// `file_sizes` maps a file name to its size in bytes (see
1614/// `IndexMetadata::file_size_map`). If `file_name` is missing, which is the case
1615/// for older indices that did not record sizes, this falls back to `open`, which
1616/// issues a HEAD to learn the size.
1617pub(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    // Load the index metadata to get the correct index directory
1647    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
1756/// Initialize a vector index from a source dataset
1757/// This will reuse the centroids from the source dataset,
1758/// making the new indices basically a "delta index" of the source dataset,
1759/// until the new dataset fully retains the index.
1760pub 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    // Vector indices currently support only single fields, use the first one
1774    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        &params,
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
1895/// Create IVF build parameters for delta index creation from an existing IVF model
1896/// TODO: support deriving all the original parameters
1897fn 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, // Default
1902        centroids: ivf_model.centroids.clone().map(Arc::new),
1903        #[allow(deprecated)]
1904        retrain: false, // Don't retrain since we have centroids
1905        sample_rate: 256, // Default
1906        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, // Default
1912        shuffle_partition_concurrency: 2,     // Default
1913        storage_options: None,
1914    }
1915}
1916
1917/// Create PQ build parameters from a ProductQuantizer
1918/// TODO: support consistently deriving all the original parameters
1919fn 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,   // Default
1924        kmeans_redos: 1, // Default
1925        codebook: Some(Arc::new(pq_quantizer.codebook.clone())),
1926        sample_rate: 256, // Default
1927    }
1928}
1929
1930/// Create SQ build parameters from a ScalarQuantizer
1931/// TODO: support consistently deriving all the original parameters
1932fn derive_sq_params(sq_quantizer: &ScalarQuantizer) -> SQBuildParams {
1933    SQBuildParams {
1934        num_bits: sq_quantizer.num_bits(),
1935        sample_rate: 256, // Default
1936    }
1937}
1938
1939/// Create Rabit build parameters from a RabitQuantizer
1940/// TODO: support consistently deriving all the original parameters
1941fn 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
1949/// Extract HNSW build parameters from the source vector index statistics.
1950/// Returns default parameters if extraction fails.
1951/// TODO: support consistently deriving all the original parameters
1952fn 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    // Extract HNSW parameters from sub_index.params
1969    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    /// `open_index_file` skips the HEAD when the size is known and still falls
2013    /// back to a HEAD for older indices that did not record sizes. A HEAD is
2014    /// issued as a `get_opts` call with `head = true`, so a proxy store counts
2015    /// those against the index file.
2016    ///
2017    /// Regression test for <https://github.com/lance-format/lance/issues/6944>.
2018    #[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        // Larger than the block size so size discovery needs a separate HEAD.
2034        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        // Size recorded in the manifest, so reading the size issues no HEAD.
2040        let _ = store.io_stats_incremental(); // reset
2041        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        // Size unknown, as in an older index, so it falls back to a HEAD.
2054        let _ = store.io_stats_incremental(); // reset
2055        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    /// `open_index_file` looks up sizes in `IndexMetadata::file_size_map()` by
2069    /// bare file name. This pins that a freshly created HNSW index records both
2070    /// the main and auxiliary files under those exact names with nonzero sizes,
2071    /// which is what lets the open path skip the HEAD.
2072    #[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                &params,
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        // Create source dataset with vector column (need at least 256 rows for PQ training)
2138        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        // Create IVF_PQ index on source
2147        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                &params,
2154                false,
2155            )
2156            .await
2157            .unwrap();
2158
2159        // Reload to get updated metadata
2160        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        // Create target dataset with same schema
2168        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 IVF_PQ index on target
2177        initialize_vector_index(
2178            &mut target_dataset,
2179            &source_dataset,
2180            source_index,
2181            &["vector"],
2182        )
2183        .await
2184        .unwrap();
2185
2186        // Verify index was created
2187        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        // Verify the index type and parameters match
2200        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        // Check basic index type
2207        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        // Check metric type
2214        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        // Check number of partitions
2221        assert_eq!(
2222            stats.get("num_partitions").and_then(|v| v.as_u64()),
2223            Some(10),
2224            "Should have 10 partitions"
2225        );
2226
2227        // Verify centroids are shared between source and target indices
2228        let source_vector_index = source_dataset
2229            .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
2230            .await
2231            .unwrap();
2232
2233        // Get IVF models from both indices to compare centroids
2234        let source_ivf_model = source_vector_index.ivf_model();
2235        let target_ivf_model = target_vector_index.ivf_model();
2236
2237        // Verify they have the same number of partitions
2238        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        // Verify the centroids are exactly the same (key verification for delta indices)
2245        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            // Compare actual centroid values
2255            // Since value() returns Arc<dyn Array>, we need to compare the data directly
2256            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                // Convert to the same type for comparison
2261                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        // Verify IVF parameters are correctly derived
2282        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        // Verify PQ parameters are correctly derived
2295        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        // Verify the index is functional by performing a search
2318        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        // Create source dataset with vector column
2345        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        // Create IVF_FLAT index on source
2354        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                &params,
2361                false,
2362            )
2363            .await
2364            .unwrap();
2365
2366        // Reload to get updated metadata
2367        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        // Create target dataset with same schema
2375        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 IVF_FLAT index on target
2384        initialize_vector_index(
2385            &mut target_dataset,
2386            &source_dataset,
2387            source_index,
2388            &["vector"],
2389        )
2390        .await
2391        .unwrap();
2392
2393        // Verify index was created
2394        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        // Verify the index type and parameters match
2407        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        // Check basic index type
2414        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        // Check metric type (Cosine might be stored as "cosine")
2421        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        // Check number of partitions
2432        assert_eq!(
2433            stats.get("num_partitions").and_then(|v| v.as_u64()),
2434            Some(8),
2435            "Should have 8 partitions"
2436        );
2437
2438        // Verify centroids are shared between source and target indices
2439        let source_vector_index = source_dataset
2440            .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
2441            .await
2442            .unwrap();
2443
2444        // Get IVF models from both indices to compare centroids
2445        let source_ivf_model = source_vector_index.ivf_model();
2446        let target_ivf_model = target_vector_index.ivf_model();
2447
2448        // Verify they have the same number of partitions
2449        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        // Verify the centroids are exactly the same (key verification for delta indices)
2456        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            // Compare actual centroid values
2466            // Since value() returns Arc<dyn Array>, we need to compare the data directly
2467            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                // Convert to the same type for comparison
2472                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        // Verify IVF parameters are correctly derived
2493        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        // Verify the index is functional
2506        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 params = VectorIndexParams::ivf_flat(4, MetricType::L2);
2546        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        // Attach precomputed global centroids to ivf_params for distributed build.
2566        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            &params,
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        // Attach precomputed global centroids to ivf_params for distributed build.
2619        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            &params,
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            &params,
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        // Pre-create a malformed global training file that is missing the
2737        // `lance:global_ivf_centroids` metadata key.
2738        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            &params,
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        // Create source dataset with vector column
2800        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        // Create IVF_PQ index on source
2809        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                &params,
2816                false,
2817            )
2818            .await
2819            .unwrap();
2820
2821        // Reload to get updated metadata
2822        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        // Create EMPTY target dataset with same schema
2830        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)); // Empty dataset
2834        let mut target_dataset = Dataset::write(empty_reader, &target_uri, None)
2835            .await
2836            .unwrap();
2837
2838        // Initialize IVF_PQ index on empty target
2839        initialize_vector_index(
2840            &mut target_dataset,
2841            &source_dataset,
2842            source_index,
2843            &["vector"],
2844        )
2845        .await
2846        .unwrap();
2847
2848        // Verify index was created even though dataset is empty
2849        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        // Open both indices to compare centroids
2857        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        // Get IVF models from both indices
2868        let source_ivf_model = source_vector_index.ivf_model();
2869        let target_ivf_model = target_vector_index.ivf_model();
2870
2871        // Verify they have the same number of partitions
2872        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        // Verify the centroids are exactly the same even for empty dataset
2879        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            // Compare actual centroid values
2889            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        // Now add data to the target dataset
2913        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        // Run optimize_indices to index the newly added data and merge indices
2920        // We set num_indices_to_merge to a high value to force merging all indices into one
2921        use lance_index::optimize::OptimizeOptions;
2922        target_dataset
2923            .optimize_indices(&OptimizeOptions::merge(10))
2924            .await
2925            .unwrap();
2926
2927        // Reload dataset to get updated index metadata
2928        let target_dataset = Dataset::open(&target_uri).await.unwrap();
2929
2930        // Verify we have only one merged index after optimization
2931        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        // The index should now work with the new data
2950        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        // Verify that the optimized index still shares centroids with the source
2975        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        // Verify centroids are still the same after optimization
2984        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        // Create source dataset with vector column
3016        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        // Create IVF_SQ index on source
3025        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                &params,
3036                false,
3037            )
3038            .await
3039            .unwrap();
3040
3041        // Reload to get updated metadata
3042        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        // Create target dataset with same schema
3050        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 IVF_SQ index on target
3059        initialize_vector_index(
3060            &mut target_dataset,
3061            &source_dataset,
3062            source_index,
3063            &["vector"],
3064        )
3065        .await
3066        .unwrap();
3067
3068        // Verify index was created
3069        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        // Verify the index type and parameters match
3082        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        // Check basic index type
3089        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        // Check metric type (Dot might be stored as "dot")
3096        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        // Check number of partitions
3107        assert_eq!(
3108            stats.get("num_partitions").and_then(|v| v.as_u64()),
3109            Some(6),
3110            "Should have 6 partitions"
3111        );
3112
3113        // Verify centroids are shared between source and target indices
3114        let source_vector_index = source_dataset
3115            .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
3116            .await
3117            .unwrap();
3118
3119        // Get IVF models from both indices to compare centroids
3120        let source_ivf_model = source_vector_index.ivf_model();
3121        let target_ivf_model = target_vector_index.ivf_model();
3122
3123        // Verify they have the same number of partitions
3124        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        // Verify the centroids are exactly the same (key verification for delta indices)
3131        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            // Compare actual centroid values
3141            // Since value() returns Arc<dyn Array>, we need to compare the data directly
3142            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                // Convert to the same type for comparison
3147                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        // Verify IVF parameters are correctly derived
3168        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        // Verify SQ parameters are correctly derived
3181        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        // Verify the index is functional by performing a search
3195        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        // Create source dataset with vector column (need at least 256 rows for PQ training)
3222        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        // Create IVF_HNSW_PQ index on source with custom HNSW parameters
3231        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                &params,
3259                false,
3260            )
3261            .await
3262            .unwrap();
3263
3264        // Reload to get updated metadata
3265        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        // Create target dataset with same schema
3273        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 IVF_HNSW_PQ index on target
3282        initialize_vector_index(
3283            &mut target_dataset,
3284            &source_dataset,
3285            source_index,
3286            &["vector"],
3287        )
3288        .await
3289        .unwrap();
3290
3291        // Verify index was created
3292        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        // Verify the index type and parameters match
3300        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        // Check basic index type
3307        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        // Check metric type
3314        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        // Check number of partitions
3321        assert_eq!(
3322            stats.get("num_partitions").and_then(|v| v.as_u64()),
3323            Some(8),
3324            "Should have 8 partitions"
3325        );
3326
3327        // Verify centroids are shared between source and target indices
3328        let source_vector_index = source_dataset
3329            .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
3330            .await
3331            .unwrap();
3332
3333        // Get IVF models from both indices to compare centroids
3334        let source_ivf_model = source_vector_index.ivf_model();
3335        let target_ivf_model = target_vector_index.ivf_model();
3336
3337        // Verify they have the same number of partitions
3338        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        // Verify the centroids are exactly the same (key verification for delta indices)
3345        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            // Compare first centroid to verify they're identical
3355            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        // Check sub_index contains HNSW and PQ information
3377        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        // Verify PQ parameters
3383        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        // Verify IVF parameters are correctly derived
3395        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        // Verify PQ parameters are correctly derived
3408        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        // Verify HNSW parameters are extracted and used correctly
3431        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        // Verify the index is functional
3446        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        // Create source dataset with vector column
3473        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        // Create IVF_HNSW_SQ index on source with custom HNSW parameters
3482        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                &params,
3509                false,
3510            )
3511            .await
3512            .unwrap();
3513
3514        // Reload to get updated metadata
3515        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        // Create target dataset with same schema
3523        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 IVF_HNSW_SQ index on target
3532        initialize_vector_index(
3533            &mut target_dataset,
3534            &source_dataset,
3535            source_index,
3536            &["vector"],
3537        )
3538        .await
3539        .unwrap();
3540
3541        // Verify index was created
3542        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        // Verify the index type and parameters match
3550        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        // Check basic index type
3557        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        // Check metric type
3564        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        // Check number of partitions
3571        assert_eq!(
3572            stats.get("num_partitions").and_then(|v| v.as_u64()),
3573            Some(6),
3574            "Should have 6 partitions"
3575        );
3576
3577        // Verify centroids are shared between source and target indices
3578        let source_vector_index = source_dataset
3579            .open_vector_index("vector", &source_index.uuid, &NoOpMetricsCollector)
3580            .await
3581            .unwrap();
3582
3583        // Get IVF models from both indices to compare centroids
3584        let source_ivf_model = source_vector_index.ivf_model();
3585        let target_ivf_model = target_vector_index.ivf_model();
3586
3587        // Verify they have the same number of partitions
3588        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        // Check sub_index contains SQ information
3595        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        // Verify SQ parameters
3600        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        // Verify the centroids are exactly the same (key verification for delta indices)
3607        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            // Compare actual centroid values
3617            // Since value() returns Arc<dyn Array>, we need to compare the data directly
3618            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                // Convert to the same type for comparison
3623                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        // Verify IVF parameters are correctly derived
3644        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        // Verify SQ parameters are correctly derived
3657        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        // Verify HNSW parameters are extracted and used correctly
3672        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        // Verify the index is functional by performing a search
3687        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}