Skip to main content

lance_index/vector/flat/
index.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Flat Vector Index.
5//!
6
7use std::collections::{BinaryHeap, HashMap};
8use std::sync::Arc;
9
10use arrow::array::AsArray;
11use arrow_array::{Array, ArrayRef, Float32Array, RecordBatch, UInt64Array};
12use arrow_schema::{DataType, Field, Schema, SchemaRef};
13use lance_core::deepsize::DeepSizeOf;
14use lance_core::{Error, ROW_ID_FIELD, Result};
15use lance_file::previous::reader::FileReader as PreviousFileReader;
16use lance_linalg::distance::DistanceType;
17use serde::{Deserialize, Serialize};
18
19use crate::{
20    metrics::MetricsCollector,
21    prefilter::PreFilter,
22    vector::{
23        ApproxMode, DIST_COL, Query,
24        graph::{OrderedFloat, OrderedNode},
25        quantizer::{Quantization, QuantizationType, Quantizer, QuantizerMetadata},
26        storage::{
27            DistCalculator, DistanceCalculatorOptions, QueryResidual, QueryScratch, VectorStore,
28        },
29        v3::subindex::IvfSubIndex,
30    },
31};
32
33use super::storage::{FLAT_COLUMN, FlatBinStorage, FlatFloatStorage};
34
35#[inline(always)]
36fn push_candidate_local(
37    res: &mut BinaryHeap<OrderedNode<u64>>,
38    k: usize,
39    row_id: u64,
40    dist: OrderedFloat,
41) {
42    if k == 0 {
43        return;
44    }
45    if res.len() < k {
46        res.push(OrderedNode::new(row_id, dist));
47    } else if res.peek().is_some_and(|node| node.dist > dist) {
48        res.pop();
49        res.push(OrderedNode::new(row_id, dist));
50    }
51}
52
53/// A Flat index is any index that stores no metadata, and
54/// during query, it simply scans over the storage and returns the top k results
55#[derive(Debug, Clone, Default, DeepSizeOf)]
56pub struct FlatIndex {}
57
58use std::sync::LazyLock;
59
60static ANN_SEARCH_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(|| {
61    Schema::new(vec![
62        Field::new(DIST_COL, DataType::Float32, true),
63        ROW_ID_FIELD.clone(),
64    ])
65    .into()
66});
67
68#[derive(Default)]
69pub struct FlatQueryParams {
70    lower_bound: Option<f32>,
71    upper_bound: Option<f32>,
72    dist_q_c: f32,
73    approx_mode: ApproxMode,
74}
75
76impl From<&Query> for FlatQueryParams {
77    fn from(q: &Query) -> Self {
78        Self {
79            lower_bound: q.lower_bound,
80            upper_bound: q.upper_bound,
81            dist_q_c: q.dist_q_c,
82            approx_mode: q.approx_mode,
83        }
84    }
85}
86
87impl IvfSubIndex for FlatIndex {
88    type QueryParams = FlatQueryParams;
89    type BuildParams = ();
90
91    fn name() -> &'static str {
92        "FLAT"
93    }
94
95    fn metadata_key() -> &'static str {
96        "lance:flat"
97    }
98
99    fn schema() -> arrow_schema::SchemaRef {
100        Schema::new(vec![Field::new("__flat_marker", DataType::UInt64, false)]).into()
101    }
102
103    fn search(
104        &self,
105        query: ArrayRef,
106        k: usize,
107        params: Self::QueryParams,
108        storage: &impl VectorStore,
109        prefilter: Arc<dyn PreFilter>,
110        metrics: &dyn MetricsCollector,
111    ) -> Result<RecordBatch> {
112        let mut scratch = QueryScratch::new();
113        self.search_with_scratch(
114            query,
115            k,
116            params,
117            storage,
118            prefilter,
119            metrics,
120            None,
121            &mut scratch,
122        )
123    }
124
125    fn search_with_scratch(
126        &self,
127        query: ArrayRef,
128        k: usize,
129        params: Self::QueryParams,
130        storage: &impl VectorStore,
131        prefilter: Arc<dyn PreFilter>,
132        metrics: &dyn MetricsCollector,
133        residual: Option<QueryResidual<'_>>,
134        scratch: &mut QueryScratch,
135    ) -> Result<RecordBatch> {
136        let is_range_query = params.lower_bound.is_some() || params.upper_bound.is_some();
137        let row_ids = storage.row_ids();
138        let dist_calc = storage.dist_calculator_with_scratch(
139            query,
140            params.dist_q_c,
141            residual,
142            &mut scratch.query_f32,
143            DistanceCalculatorOptions {
144                approx_mode: params.approx_mode,
145            },
146        );
147        let mut res = BinaryHeap::with_capacity(k);
148        metrics.record_comparisons(storage.len());
149
150        match prefilter.is_empty() {
151            true => {
152                dist_calc.distance_all_with_scratch(
153                    k,
154                    &mut scratch.distances,
155                    &mut scratch.u16,
156                    &mut scratch.u8,
157                    &mut scratch.u32,
158                );
159                let dists = scratch.distances.iter().copied();
160
161                if is_range_query {
162                    let lower_bound = params.lower_bound.unwrap_or(f32::MIN).into();
163                    let upper_bound = params.upper_bound.unwrap_or(f32::MAX).into();
164
165                    for (&row_id, dist) in row_ids.zip(dists) {
166                        let dist = dist.into();
167                        if dist < lower_bound || dist >= upper_bound {
168                            continue;
169                        }
170                        push_candidate_local(&mut res, k, row_id, dist);
171                    }
172                } else {
173                    for (&row_id, dist) in row_ids.zip(dists) {
174                        let dist = dist.into();
175                        push_candidate_local(&mut res, k, row_id, dist);
176                    }
177                }
178            }
179            false => {
180                let row_addr_mask = prefilter.mask();
181                if is_range_query {
182                    let lower_bound = params.lower_bound.unwrap_or(f32::MIN).into();
183                    let upper_bound = params.upper_bound.unwrap_or(f32::MAX).into();
184                    for (id, &row_addr) in row_ids.enumerate() {
185                        if !row_addr_mask.selected(row_addr) {
186                            continue;
187                        }
188                        let dist = dist_calc.distance(id as u32).into();
189                        if dist < lower_bound || dist >= upper_bound {
190                            continue;
191                        }
192
193                        push_candidate_local(&mut res, k, row_addr, dist);
194                    }
195                } else {
196                    for (id, &row_addr) in row_ids.enumerate() {
197                        if !row_addr_mask.selected(row_addr) {
198                            continue;
199                        }
200
201                        let dist = dist_calc.distance(id as u32).into();
202                        push_candidate_local(&mut res, k, row_addr, dist);
203                    }
204                }
205            }
206        };
207
208        // we don't need to sort the results by distances here
209        // because there's a SortExec node in the query plan which sorts the results from all partitions
210        let (row_ids, dists): (Vec<_>, Vec<_>) = res.into_iter().map(|r| (r.id, r.dist.0)).unzip();
211        let (row_ids, dists) = (UInt64Array::from(row_ids), Float32Array::from(dists));
212
213        Ok(RecordBatch::try_new(
214            ANN_SEARCH_SCHEMA.clone(),
215            vec![Arc::new(dists), Arc::new(row_ids)],
216        )?)
217    }
218
219    fn supports_global_topk_heap() -> bool {
220        true
221    }
222
223    fn accumulate_topk(
224        &self,
225        query: ArrayRef,
226        k: usize,
227        params: Self::QueryParams,
228        storage: &impl VectorStore,
229        prefilter: Arc<dyn PreFilter>,
230        res: &mut BinaryHeap<OrderedNode<u64>>,
231        metrics: &dyn MetricsCollector,
232    ) -> Result<()> {
233        let mut scratch = QueryScratch::new();
234        self.accumulate_topk_with_scratch(
235            query,
236            k,
237            params,
238            storage,
239            prefilter,
240            res,
241            None,
242            &mut scratch,
243            metrics,
244        )
245    }
246
247    fn accumulate_topk_with_scratch(
248        &self,
249        query: ArrayRef,
250        k: usize,
251        params: Self::QueryParams,
252        storage: &impl VectorStore,
253        prefilter: Arc<dyn PreFilter>,
254        res: &mut BinaryHeap<OrderedNode<u64>>,
255        residual: Option<QueryResidual<'_>>,
256        scratch: &mut QueryScratch,
257        metrics: &dyn MetricsCollector,
258    ) -> Result<()> {
259        let row_ids = storage.row_ids();
260        let dist_calc = storage.dist_calculator_with_scratch(
261            query,
262            params.dist_q_c,
263            residual,
264            &mut scratch.query_f32,
265            DistanceCalculatorOptions {
266                approx_mode: params.approx_mode,
267            },
268        );
269        metrics.record_comparisons(storage.len());
270
271        match prefilter.is_empty() {
272            true => {
273                dist_calc.accumulate_topk_with_scratch(
274                    k,
275                    params.lower_bound,
276                    params.upper_bound,
277                    |id| storage.row_id(id),
278                    res,
279                    &mut scratch.distances,
280                    &mut scratch.u16,
281                    &mut scratch.u8,
282                    &mut scratch.u32,
283                );
284            }
285            false => {
286                let row_addr_mask = prefilter.mask();
287                dist_calc.accumulate_filtered_topk_with_scratch(
288                    k,
289                    params.lower_bound,
290                    params.upper_bound,
291                    row_ids.enumerate().map(|(id, &row_id)| (id as u32, row_id)),
292                    |row_id| row_addr_mask.selected(row_id),
293                    res,
294                    &mut scratch.distances,
295                    &mut scratch.u16,
296                    &mut scratch.u8,
297                    &mut scratch.u32,
298                );
299            }
300        };
301        Ok(())
302    }
303
304    fn load(_: RecordBatch) -> Result<Self> {
305        Ok(Self {})
306    }
307
308    fn index_vectors(_: &impl VectorStore, _: Self::BuildParams) -> Result<Self>
309    where
310        Self: Sized,
311    {
312        Ok(Self {})
313    }
314
315    fn remap(&self, _: &HashMap<u64, Option<u64>>, _: &impl VectorStore) -> Result<Self> {
316        Ok(self.clone())
317    }
318
319    fn to_batch(&self) -> Result<RecordBatch> {
320        Ok(RecordBatch::new_empty(Schema::empty().into()))
321    }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, DeepSizeOf)]
325pub struct FlatMetadata {
326    pub dim: usize,
327}
328
329#[async_trait::async_trait]
330impl QuantizerMetadata for FlatMetadata {
331    async fn load(_: &PreviousFileReader) -> Result<Self> {
332        unimplemented!("Flat will be used in new index builder which doesn't require this")
333    }
334}
335
336#[derive(Debug, Clone, DeepSizeOf)]
337pub struct FlatQuantizer {
338    dim: usize,
339    distance_type: DistanceType,
340}
341
342impl FlatQuantizer {
343    pub fn new(dim: usize, distance_type: DistanceType) -> Self {
344        Self { dim, distance_type }
345    }
346}
347
348impl Quantization for FlatQuantizer {
349    type BuildParams = ();
350    type Metadata = FlatMetadata;
351    type Storage = FlatFloatStorage;
352
353    fn build(data: &dyn Array, distance_type: DistanceType, _: &Self::BuildParams) -> Result<Self> {
354        let dim = data.as_fixed_size_list().value_length();
355        Ok(Self::new(dim as usize, distance_type))
356    }
357
358    fn retrain(&mut self, _: &dyn Array) -> Result<()> {
359        Ok(())
360    }
361
362    fn code_dim(&self) -> usize {
363        self.dim
364    }
365
366    fn column(&self) -> &'static str {
367        FLAT_COLUMN
368    }
369
370    fn from_metadata(metadata: &Self::Metadata, distance_type: DistanceType) -> Result<Quantizer> {
371        Ok(Quantizer::Flat(Self {
372            dim: metadata.dim,
373            distance_type,
374        }))
375    }
376
377    fn metadata(&self, _: Option<crate::vector::quantizer::QuantizationMetadata>) -> FlatMetadata {
378        FlatMetadata { dim: self.dim }
379    }
380
381    fn metadata_key() -> &'static str {
382        "flat"
383    }
384
385    fn quantization_type() -> QuantizationType {
386        QuantizationType::Flat
387    }
388
389    fn quantize(&self, vectors: &dyn Array) -> Result<ArrayRef> {
390        Ok(vectors.slice(0, vectors.len()))
391    }
392
393    fn field(&self) -> Field {
394        Field::new(
395            FLAT_COLUMN,
396            DataType::FixedSizeList(
397                Arc::new(Field::new("item", DataType::Float32, true)),
398                self.dim as i32,
399            ),
400            true,
401        )
402    }
403}
404
405impl From<FlatQuantizer> for Quantizer {
406    fn from(value: FlatQuantizer) -> Self {
407        Self::Flat(value)
408    }
409}
410
411impl TryFrom<Quantizer> for FlatQuantizer {
412    type Error = Error;
413
414    fn try_from(value: Quantizer) -> Result<Self> {
415        match value {
416            Quantizer::Flat(quantizer) => Ok(quantizer),
417            _ => Err(Error::invalid_input("quantizer is not FlatQuantizer")),
418        }
419    }
420}
421
422#[derive(Debug, Clone, DeepSizeOf)]
423pub struct FlatBinQuantizer {
424    dim: usize,
425    distance_type: DistanceType,
426}
427
428impl FlatBinQuantizer {
429    pub fn new(dim: usize, distance_type: DistanceType) -> Self {
430        Self { dim, distance_type }
431    }
432}
433
434impl Quantization for FlatBinQuantizer {
435    type BuildParams = ();
436    type Metadata = FlatMetadata;
437    type Storage = FlatBinStorage;
438
439    fn build(data: &dyn Array, distance_type: DistanceType, _: &Self::BuildParams) -> Result<Self> {
440        let dim = data.as_fixed_size_list().value_length();
441        Ok(Self::new(dim as usize, distance_type))
442    }
443
444    fn retrain(&mut self, _: &dyn Array) -> Result<()> {
445        Ok(())
446    }
447
448    fn code_dim(&self) -> usize {
449        self.dim
450    }
451
452    fn column(&self) -> &'static str {
453        FLAT_COLUMN
454    }
455
456    fn from_metadata(metadata: &Self::Metadata, distance_type: DistanceType) -> Result<Quantizer> {
457        Ok(Quantizer::FlatBin(Self {
458            dim: metadata.dim,
459            distance_type,
460        }))
461    }
462
463    fn metadata(&self, _: Option<crate::vector::quantizer::QuantizationMetadata>) -> FlatMetadata {
464        FlatMetadata { dim: self.dim }
465    }
466
467    fn metadata_key() -> &'static str {
468        "flat"
469    }
470
471    fn quantization_type() -> QuantizationType {
472        QuantizationType::FlatBin
473    }
474
475    fn quantize(&self, vectors: &dyn Array) -> Result<ArrayRef> {
476        Ok(vectors.slice(0, vectors.len()))
477    }
478
479    fn field(&self) -> Field {
480        Field::new(
481            FLAT_COLUMN,
482            DataType::FixedSizeList(
483                Arc::new(Field::new("item", DataType::UInt8, true)),
484                self.dim as i32,
485            ),
486            true,
487        )
488    }
489}
490
491impl From<FlatBinQuantizer> for Quantizer {
492    fn from(value: FlatBinQuantizer) -> Self {
493        Self::FlatBin(value)
494    }
495}
496
497impl TryFrom<Quantizer> for FlatBinQuantizer {
498    type Error = Error;
499
500    fn try_from(value: Quantizer) -> Result<Self> {
501        match value {
502            Quantizer::FlatBin(quantizer) => Ok(quantizer),
503            _ => Err(Error::invalid_input("quantizer is not FlatBinQuantizer")),
504        }
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511
512    use arrow_array::FixedSizeListArray;
513    use async_trait::async_trait;
514    use lance_arrow::FixedSizeListArrayExt;
515    use lance_select::{RowAddrMask, RowAddrTreeMap};
516
517    use crate::metrics::NoOpMetricsCollector;
518    use crate::prefilter::NoFilter;
519
520    struct MaskPreFilter {
521        mask: Arc<RowAddrMask>,
522    }
523
524    #[async_trait]
525    impl PreFilter for MaskPreFilter {
526        async fn wait_for_ready(&self) -> Result<()> {
527            Ok(())
528        }
529
530        fn is_empty(&self) -> bool {
531            false
532        }
533
534        fn mask(&self) -> Arc<RowAddrMask> {
535            self.mask.clone()
536        }
537
538        fn filter_row_ids<'a>(&self, row_ids: Box<dyn Iterator<Item = &'a u64> + 'a>) -> Vec<u64> {
539            self.mask.selected_indices(row_ids)
540        }
541    }
542
543    fn test_storage() -> FlatFloatStorage {
544        let values = Float32Array::from(vec![
545            0.0, 0.0, // row 0
546            1.0, 0.0, // row 1
547            1.0, 1.0, // row 2
548            3.0, 3.0, // row 3
549            4.0, 4.0, // row 4
550        ]);
551        let vectors = FixedSizeListArray::try_new_from_values(values, 2).unwrap();
552        FlatFloatStorage::new(vectors, DistanceType::L2)
553    }
554
555    fn query() -> ArrayRef {
556        Arc::new(Float32Array::from(vec![1.0, 1.0]))
557    }
558
559    fn batch_results(batch: RecordBatch) -> Vec<(u64, f32)> {
560        let dists = batch
561            .column(0)
562            .as_primitive::<arrow_array::types::Float32Type>();
563        let row_ids = batch
564            .column(1)
565            .as_primitive::<arrow_array::types::UInt64Type>();
566        let mut results = row_ids
567            .values()
568            .iter()
569            .zip(dists.values().iter())
570            .map(|(row_id, dist)| (*row_id, *dist))
571            .collect::<Vec<_>>();
572        results.sort_by(|left, right| left.0.cmp(&right.0));
573        results
574    }
575
576    fn heap_results(heap: BinaryHeap<OrderedNode<u64>>) -> Vec<(u64, f32)> {
577        let mut results = heap
578            .into_iter()
579            .map(|node| (node.id, node.dist.0))
580            .collect::<Vec<_>>();
581        results.sort_by(|left, right| left.0.cmp(&right.0));
582        results
583    }
584
585    #[test]
586    fn test_flat_search_matches_accumulate_topk_without_prefilter() {
587        let index = FlatIndex::default();
588        let storage = test_storage();
589        let k = 3;
590        let search_results = batch_results(
591            index
592                .search(
593                    query(),
594                    k,
595                    FlatQueryParams::default(),
596                    &storage,
597                    Arc::new(NoFilter),
598                    &NoOpMetricsCollector,
599                )
600                .unwrap(),
601        );
602
603        let mut heap = BinaryHeap::with_capacity(k);
604        index
605            .accumulate_topk(
606                query(),
607                k,
608                FlatQueryParams::default(),
609                &storage,
610                Arc::new(NoFilter),
611                &mut heap,
612                &NoOpMetricsCollector,
613            )
614            .unwrap();
615
616        assert_eq!(search_results, heap_results(heap));
617    }
618
619    #[test]
620    fn test_flat_search_matches_accumulate_topk_with_prefilter() {
621        let index = FlatIndex::default();
622        let storage = test_storage();
623        let k = 2;
624        let filter = Arc::new(MaskPreFilter {
625            mask: Arc::new(RowAddrMask::from_allowed(RowAddrTreeMap::from_iter([
626                0_u64, 3, 4,
627            ]))),
628        });
629        let search_results = batch_results(
630            index
631                .search(
632                    query(),
633                    k,
634                    FlatQueryParams::default(),
635                    &storage,
636                    filter.clone(),
637                    &NoOpMetricsCollector,
638                )
639                .unwrap(),
640        );
641
642        let mut heap = BinaryHeap::with_capacity(k);
643        index
644            .accumulate_topk(
645                query(),
646                k,
647                FlatQueryParams::default(),
648                &storage,
649                filter,
650                &mut heap,
651                &NoOpMetricsCollector,
652            )
653            .unwrap();
654
655        assert_eq!(search_results, heap_results(heap));
656        assert_eq!(
657            search_results.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
658            vec![0, 3]
659        );
660    }
661}