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}