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}