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
5//!
6
7use std::any::Any;
8use std::fmt::Debug;
9use std::{collections::HashMap, sync::Arc};
10
11use arrow_array::{ArrayRef, Float32Array, RecordBatch, UInt32Array};
12use arrow_schema::Field;
13use async_trait::async_trait;
14use datafusion::execution::SendableRecordBatchStream;
15use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
16use futures::stream;
17use ivf::storage::IvfModel;
18use lance_core::deepsize::DeepSizeOf;
19use lance_core::{Error, ROW_ID_FIELD, Result};
20use lance_io::traits::Reader;
21use lance_linalg::distance::DistanceType;
22use quantizer::{QuantizationType, Quantizer};
23use std::sync::LazyLock;
24use v3::subindex::SubIndexType;
25
26pub mod bq;
27pub mod distributed;
28pub mod flat;
29pub mod graph;
30pub mod hnsw;
31pub mod ivf;
32pub mod kmeans;
33pub mod pq;
34pub mod quantizer;
35pub mod residual;
36pub mod shared;
37pub mod sq;
38pub mod storage;
39pub mod transform;
40pub mod utils;
41pub mod v3;
42
43use super::pb;
44use crate::metrics::MetricsCollector;
45use crate::{Index, prefilter::PreFilter};
46
47// TODO: Make these crate private once the migration from lance to lance-index is done.
48pub const DIST_COL: &str = "_distance";
49pub const DISTANCE_TYPE_KEY: &str = "distance_type";
50pub const INDEX_UUID_COLUMN: &str = "__index_uuid";
51pub const PART_ID_COLUMN: &str = "__ivf_part_id";
52pub const DIST_Q_C_COLUMN: &str = "__dist_q_c";
53// dist from vector to centroid
54pub const CENTROID_DIST_COLUMN: &str = "__centroid_dist";
55pub const PQ_CODE_COLUMN: &str = "__pq_code";
56pub const SQ_CODE_COLUMN: &str = "__sq_code";
57pub const LOSS_METADATA_KEY: &str = "_loss";
58
59pub type PreparedPartitionSearchHandle = Box<dyn Any + Send>;
60
61/// Controls when a multi-partition search should stop producing more partition results.
62pub trait PartitionSearchControl: Send + Sync {
63    fn should_stop(&self) -> bool;
64
65    fn record_batch(&self, _batch: &RecordBatch) {}
66}
67
68pub static VECTOR_RESULT_SCHEMA: LazyLock<arrow_schema::SchemaRef> = LazyLock::new(|| {
69    arrow_schema::SchemaRef::new(arrow_schema::Schema::new(vec![
70        Field::new(DIST_COL, arrow_schema::DataType::Float32, true),
71        ROW_ID_FIELD.clone(),
72    ]))
73});
74
75pub static PART_ID_FIELD: LazyLock<arrow_schema::Field> = LazyLock::new(|| {
76    arrow_schema::Field::new(PART_ID_COLUMN, arrow_schema::DataType::UInt32, true)
77});
78
79pub static CENTROID_DIST_FIELD: LazyLock<arrow_schema::Field> = LazyLock::new(|| {
80    arrow_schema::Field::new(CENTROID_DIST_COLUMN, arrow_schema::DataType::Float32, true)
81});
82
83pub const DEFAULT_QUERY_PARALLELISM: i32 = 0;
84
85/// Controls the speed / accuracy tradeoff for approximate vector search.
86///
87/// This currently only affects RQ-quantized vector indexes, such as IVF_RQ.
88/// Other index types ignore this setting.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
90pub enum ApproxMode {
91    /// Prefer lower query latency, which can reduce recall.
92    Fast,
93
94    /// Use the default balance between query latency and recall.
95    #[default]
96    Normal,
97
98    /// Prefer higher recall, which can increase query latency.
99    Accurate,
100}
101
102/// Query parameters for the vector indices
103
104#[derive(Debug, Clone)]
105pub struct Query {
106    /// The column to be searched.
107    pub column: String,
108
109    /// The vector to be searched.
110    pub key: ArrayRef,
111
112    /// Top k results to return.
113    pub k: usize,
114
115    /// The lower bound (inclusive) of the distance to be searched.
116    pub lower_bound: Option<f32>,
117
118    /// The upper bound (exclusive) of the distance to be searched.
119    pub upper_bound: Option<f32>,
120
121    /// The minimum number of probes to load and search.  More partitions
122    /// will only be loaded if we have not found k results, or the algorithm
123    /// determines more partitions are needed to satisfy recall requirements.
124    ///
125    /// The planner will always search at least this many partitions. Defaults to 1.
126    pub minimum_nprobes: usize,
127
128    /// The maximum number of probes to load and search.  If not set then
129    /// ALL partitions will be searched, if needed, to satisfy k results.
130    pub maximum_nprobes: Option<usize>,
131
132    /// The number of candidates to reserve while searching.
133    /// this is an optional parameter for HNSW related index types.
134    pub ef: Option<usize>,
135
136    /// If presented, apply a refine step.
137    /// TODO: should we support fraction / float number here?
138    pub refine_factor: Option<u32>,
139
140    /// Distance metric type. If None, uses the index's metric (if available)
141    /// or the default for the data type.
142    pub metric_type: Option<DistanceType>,
143
144    /// Whether to use an ANN index if available
145    pub use_index: bool,
146
147    /// Maximum partition-search concurrency for a single vector query.
148    ///
149    /// The default is 0.
150    /// Value 0 selects the automatic policy; today this resolves to 1 for the
151    /// sequential fast path unless an index implementation overrides it.
152    /// Value -1 uses the CPU pool size.
153    /// Value 1 uses the single-worker sequential partition search path.
154    /// Values >= 2 use the partition-parallel path and are clamped to the CPU
155    /// pool size by the execution layer.
156    pub query_parallelism: i32,
157
158    /// the distance between the query and the centroid
159    /// this is only used for IVF index with Rabit quantization
160    pub dist_q_c: f32,
161
162    /// Controls the speed / accuracy tradeoff for approximate vector search.
163    ///
164    /// This currently only affects RQ-quantized vector indexes, such as IVF_RQ.
165    /// Other index types ignore this setting.
166    pub approx_mode: ApproxMode,
167}
168
169impl From<pb::VectorMetricType> for DistanceType {
170    fn from(proto: pb::VectorMetricType) -> Self {
171        match proto {
172            pb::VectorMetricType::L2 => Self::L2,
173            pb::VectorMetricType::Cosine => Self::Cosine,
174            pb::VectorMetricType::Dot => Self::Dot,
175            pb::VectorMetricType::Hamming => Self::Hamming,
176        }
177    }
178}
179
180impl From<DistanceType> for pb::VectorMetricType {
181    fn from(mt: DistanceType) -> Self {
182        match mt {
183            DistanceType::L2 => Self::L2,
184            DistanceType::Cosine => Self::Cosine,
185            DistanceType::Dot => Self::Dot,
186            DistanceType::Hamming => Self::Hamming,
187        }
188    }
189}
190
191/// Vector Index for (Approximate) Nearest Neighbor (ANN) Search.
192///
193/// Vector indices are often built as a chain of indices.  For example, IVF -> PQ
194/// or IVF -> HNSW -> SQ.
195///
196/// We use one trait for both the top-level and the sub-indices.  Typically the top-level
197/// search is a partition-aware search and all sub-indices are whole-index searches.
198#[async_trait]
199#[allow(clippy::redundant_pub_crate)]
200pub trait VectorIndex: Send + Sync + std::fmt::Debug + Index {
201    /// Search entire index for k nearest neighbors.
202    ///
203    /// It returns a [RecordBatch] with Schema of:
204    ///
205    /// ```
206    /// use arrow_schema::{Schema, Field, DataType};
207    ///
208    /// Schema::new(vec![
209    ///   Field::new("_rowid", DataType::UInt64, true),
210    ///   Field::new("_distance", DataType::Float32, true),
211    /// ]);
212    /// ```
213    ///
214    /// The `pre_filter` argument is used to filter out row ids that we know are
215    /// not relevant to the query. For example, it removes deleted rows or rows that
216    /// do not match a user-provided filter.
217    async fn search(
218        &self,
219        query: &Query,
220        pre_filter: Arc<dyn PreFilter>,
221        metrics: &dyn MetricsCollector,
222    ) -> Result<RecordBatch>;
223
224    /// Find partitions that may contain nearest neighbors.
225    ///
226    /// If maximum_nprobes is set then this method will return the partitions
227    /// that are most likely to contain the nearest neighbors (e.g. the closest
228    /// partitions to the query vector).
229    ///
230    /// Return the partition ids and the distances between the query and the centroids,
231    /// the results should be in sorted order from closest to farthest.
232    fn find_partitions(&self, query: &Query) -> Result<(UInt32Array, Float32Array)>;
233
234    /// Get the total number of partitions in the index.
235    fn total_partitions(&self) -> usize;
236
237    /// Search a single partition for nearest neighbors.
238    ///
239    /// This method should return the same results as [`VectorIndex::search`] method except
240    /// that it will only search a single partition.
241    async fn search_in_partition(
242        &self,
243        partition_id: usize,
244        query: &Query,
245        pre_filter: Arc<dyn PreFilter>,
246        metrics: &dyn MetricsCollector,
247    ) -> Result<RecordBatch>;
248
249    /// Asynchronously prepare a single-partition search so the CPU-heavy portion
250    /// can be executed separately.
251    async fn prepare_partition_search(
252        &self,
253        _partition_id: usize,
254        _query: &Query,
255        _pre_filter: Arc<dyn PreFilter>,
256        _metrics: &dyn MetricsCollector,
257    ) -> Result<PreparedPartitionSearchHandle> {
258        unimplemented!("prepared partition search is not supported for this index")
259    }
260
261    /// Execute the synchronous portion of a previously prepared partition search.
262    fn search_prepared_partition(
263        &self,
264        _prepared: PreparedPartitionSearchHandle,
265        _metrics: &dyn MetricsCollector,
266    ) -> Result<RecordBatch> {
267        unimplemented!("prepared partition search is not supported for this index")
268    }
269
270    /// Return true if the index supports splitting partition search into async
271    /// prepare and sync execute phases.
272    fn supports_prepared_partition_search(&self) -> bool {
273        false
274    }
275
276    /// Choose partition search concurrency for `query_parallelism = 0`.
277    ///
278    /// The default keeps the single-worker sequential path. Index
279    /// implementations can override this when their sub-index search work does
280    /// not benefit from the sequential fast path.
281    fn auto_query_parallelism(&self, _cpu_pool_size: usize) -> usize {
282        1
283    }
284
285    /// Search a range of partitions and return a stream of per-partition result batches.
286    ///
287    /// The default implementation searches each partition sequentially with
288    /// [`VectorIndex::search_in_partition`]. Implementations can override this
289    /// to use a more efficient execution strategy.
290    #[allow(clippy::too_many_arguments)]
291    async fn search_partitions(
292        self: Arc<Self>,
293        query: Query,
294        partitions: Arc<UInt32Array>,
295        q_c_dists: Arc<Float32Array>,
296        start_idx: usize,
297        end_idx: usize,
298        pre_filter: Arc<dyn PreFilter>,
299        control: Option<Arc<dyn PartitionSearchControl>>,
300        metrics: Arc<dyn MetricsCollector>,
301    ) -> Result<SendableRecordBatchStream>
302    where
303        Self: 'static,
304    {
305        if partitions.len() != q_c_dists.len() {
306            return Err(Error::invalid_input(format!(
307                "partition count {} does not match centroid distance count {}",
308                partitions.len(),
309                q_c_dists.len()
310            )));
311        }
312        if start_idx > end_idx || end_idx > partitions.len() {
313            return Err(Error::invalid_input(format!(
314                "invalid partition search range [{start_idx}, {end_idx}) for {} partitions",
315                partitions.len()
316            )));
317        }
318
319        let stream = stream::try_unfold(start_idx, move |idx| {
320            let index = self.clone();
321            let partitions = partitions.clone();
322            let q_c_dists = q_c_dists.clone();
323            let query = query.clone();
324            let pre_filter = pre_filter.clone();
325            let control = control.clone();
326            let metrics = metrics.clone();
327            async move {
328                if idx >= end_idx
329                    || control
330                        .as_ref()
331                        .is_some_and(|control| control.should_stop())
332                {
333                    return Ok(None);
334                }
335                let part_id = partitions.value(idx);
336                let mut query = query;
337                query.dist_q_c = q_c_dists.value(idx);
338                index
339                    .search_in_partition(part_id as usize, &query, pre_filter, metrics.as_ref())
340                    .await
341                    .map(|batch| {
342                        if let Some(control) = control.as_ref() {
343                            control.record_batch(&batch);
344                        }
345                        Some((batch, idx + 1))
346                    })
347                    .map_err(Into::into)
348            }
349        });
350        Ok(Box::pin(RecordBatchStreamAdapter::new(
351            VECTOR_RESULT_SCHEMA.clone(),
352            stream,
353        )))
354    }
355
356    /// If the index is loadable by IVF, so it can be a sub-index that
357    /// is loaded on demand by IVF.
358    fn is_loadable(&self) -> bool;
359
360    /// Use residual vector to search.
361    fn use_residual(&self) -> bool;
362
363    // async fn append(&self, batches: Vec<RecordBatch>) -> Result<()>;
364    // async fn merge(&self, indices: Vec<Arc<dyn VectorIndex>>) -> Result<()>;
365
366    /// Load the index from the reader on-demand.
367    async fn load(
368        &self,
369        reader: Arc<dyn Reader>,
370        offset: usize,
371        length: usize,
372    ) -> Result<Box<dyn VectorIndex>>;
373
374    /// Load the partition from the reader on-demand.
375    async fn load_partition(
376        &self,
377        reader: Arc<dyn Reader>,
378        offset: usize,
379        length: usize,
380        _partition_id: usize,
381    ) -> Result<Box<dyn VectorIndex>> {
382        self.load(reader, offset, length).await
383    }
384
385    // for IVF only
386    async fn partition_reader(
387        &self,
388        _partition_id: usize,
389        _with_vector: bool,
390        _metrics: &dyn MetricsCollector,
391    ) -> Result<SendableRecordBatchStream> {
392        unimplemented!("only for IVF")
393    }
394
395    // for SubIndex only
396    async fn to_batch_stream(&self, with_vector: bool) -> Result<SendableRecordBatchStream>;
397
398    fn num_rows(&self) -> u64;
399
400    /// Return the IDs of rows in the index.
401    fn row_ids(&self) -> Box<dyn Iterator<Item = &'_ u64> + '_>;
402
403    /// Remap the index according to mapping
404    ///
405    /// Each item in mapping describes an old row id -> new row id
406    /// pair.  If old row id -> None then that row id has been
407    /// deleted and can be removed from the index.
408    ///
409    /// If an old row id is not in the mapping then it should be
410    /// left alone.
411    async fn remap(&mut self, mapping: &HashMap<u64, Option<u64>>) -> Result<()>;
412
413    /// The metric type of this vector index.
414    fn metric_type(&self) -> DistanceType;
415
416    fn ivf_model(&self) -> &IvfModel;
417    fn quantizer(&self) -> Quantizer;
418    fn partition_size(&self, part_id: usize) -> usize;
419
420    /// the index type of this vector index.
421    fn sub_index_type(&self) -> (SubIndexType, QuantizationType);
422
423    /// The cumulative I/O performed while opening this index (file footers, IVF
424    /// centroids, quantization metadata).  This is a one-time cost; it is
425    /// reported once, on the query that actually opens the index, and is `None`
426    /// for index implementations that do not track it.
427    fn open_io_stats(&self) -> Option<lance_io::scheduler::ScanStats> {
428        None
429    }
430}
431
432// it can be an IVF index or a partition of IVF index
433pub trait VectorIndexCacheEntry: Debug + Send + Sync + DeepSizeOf {
434    fn as_any(&self) -> &dyn Any;
435}