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