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}