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