Skip to main content

lance_index/vector/sq/
storage.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::ops::Range;
5
6use arrow::datatypes::Float64Type;
7use arrow::{compute::concat_batches, datatypes::Float16Type};
8use arrow_array::{
9    ArrayRef, RecordBatch, UInt8Array, UInt64Array,
10    cast::AsArray,
11    types::{Float32Type, UInt8Type, UInt64Type},
12};
13use arrow_schema::{DataType, SchemaRef};
14use async_trait::async_trait;
15use lance_core::deepsize::DeepSizeOf;
16use lance_core::{Error, ROW_ID, Result};
17use lance_file::previous::reader::FileReader as PreviousFileReader;
18use lance_io::object_store::ObjectStore;
19use lance_linalg::distance::{DistanceType, dot_distance, l2_u8::l2_u8};
20use lance_table::format::SelfDescribingFileReader;
21use object_store::path::Path;
22use serde::{Deserialize, Serialize};
23use std::sync::Arc;
24
25use super::{ScalarQuantizer, scale_to_u8};
26use crate::frag_reuse::FragReuseIndex;
27use crate::{
28    INDEX_METADATA_SCHEMA_KEY, IndexMetadata,
29    vector::{
30        SQ_CODE_COLUMN,
31        quantizer::{QuantizerMetadata, QuantizerStorage},
32        storage::{DistCalculator, VectorStore},
33        transform::Transformer,
34    },
35};
36
37pub const SQ_METADATA_KEY: &str = "lance:sq";
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct ScalarQuantizationMetadata {
41    pub dim: usize,
42    pub num_bits: u16,
43    pub bounds: Range<f64>,
44}
45
46impl DeepSizeOf for ScalarQuantizationMetadata {
47    fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
48        0
49    }
50}
51
52#[async_trait]
53impl QuantizerMetadata for ScalarQuantizationMetadata {
54    async fn load(reader: &PreviousFileReader) -> Result<Self> {
55        let metadata_str = reader
56            .schema()
57            .metadata
58            .get(SQ_METADATA_KEY)
59            .ok_or(Error::index(format!(
60                "Reading SQ metadata: metadata key {} not found",
61                SQ_METADATA_KEY
62            )))?;
63        serde_json::from_str(metadata_str)
64            .map_err(|_| Error::index(format!("Failed to parse index metadata: {}", metadata_str)))
65    }
66}
67
68/// An immutable chunk of ScalarQuantizationStorage.
69#[derive(Debug, Clone)]
70struct SQStorageChunk {
71    batch: RecordBatch,
72
73    dim: usize,
74
75    // Helper fields, references to the batch
76    // These fields share the `Arc` pointer to the columns in batch,
77    // so it does not take more memory.
78    row_ids: UInt64Array,
79    sq_codes: UInt8Array,
80}
81
82impl SQStorageChunk {
83    // Create a new chunk from a RecordBatch.
84    fn new(batch: RecordBatch) -> Result<Self> {
85        let row_ids = batch
86            .column_by_name(ROW_ID)
87            .ok_or(Error::index(
88                "Row ID column not found in the batch".to_owned(),
89            ))?
90            .as_primitive::<UInt64Type>()
91            .clone();
92        let fsl = batch
93            .column_by_name(SQ_CODE_COLUMN)
94            .ok_or(Error::index(
95                "SQ code column not found in the batch".to_owned(),
96            ))?
97            .as_fixed_size_list();
98        let dim = fsl.value_length() as usize;
99        let sq_codes = fsl
100            .values()
101            .as_primitive_opt::<UInt8Type>()
102            .ok_or(Error::index(
103                "SQ code column is not FixedSizeList<u8>".to_owned(),
104            ))?
105            .clone();
106        Ok(Self {
107            batch,
108            dim,
109            row_ids,
110            sq_codes,
111        })
112    }
113
114    /// Returns vector dimension
115    fn dim(&self) -> usize {
116        self.dim
117    }
118
119    fn len(&self) -> usize {
120        self.row_ids.len()
121    }
122
123    fn schema(&self) -> &SchemaRef {
124        self.batch.schema_ref()
125    }
126
127    #[inline]
128    fn row_id(&self, id: u32) -> u64 {
129        self.row_ids.value(id as usize)
130    }
131
132    /// Get a slice of SQ code for id
133    #[inline]
134    fn sq_code_slice(&self, id: u32) -> &[u8] {
135        // assert!(id < self.len() as u32);
136        &self.sq_codes.values()[id as usize * self.dim..(id + 1) as usize * self.dim]
137    }
138}
139
140impl DeepSizeOf for SQStorageChunk {
141    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
142        self.batch.deep_size_of_children(context)
143    }
144}
145
146#[derive(Debug, Clone)]
147pub struct ScalarQuantizationStorage {
148    quantizer: ScalarQuantizer,
149
150    distance_type: DistanceType,
151
152    /// Chunks of storage
153    offsets: Vec<u32>,
154    chunks: Vec<SQStorageChunk>,
155}
156
157impl DeepSizeOf for ScalarQuantizationStorage {
158    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
159        self.chunks
160            .iter()
161            .map(|c| c.deep_size_of_children(context))
162            .sum()
163    }
164}
165
166const SQ_CHUNK_CAPACITY: usize = 1024;
167
168impl ScalarQuantizationStorage {
169    pub fn try_new(
170        num_bits: u16,
171        distance_type: DistanceType,
172        bounds: Range<f64>,
173        batches: impl IntoIterator<Item = RecordBatch>,
174        frag_reuse_index: Option<Arc<FragReuseIndex>>,
175    ) -> Result<Self> {
176        let mut chunks = Vec::with_capacity(SQ_CHUNK_CAPACITY);
177        let mut offsets = Vec::with_capacity(SQ_CHUNK_CAPACITY + 1);
178        offsets.push(0);
179        for mut batch in batches.into_iter() {
180            if let Some(frag_reuse_index_ref) = frag_reuse_index.as_ref() {
181                batch = frag_reuse_index_ref.remap_row_ids_record_batch(batch, 0)?
182            }
183            offsets.push(offsets.last().unwrap() + batch.num_rows() as u32);
184            let chunk = SQStorageChunk::new(batch)?;
185            chunks.push(chunk);
186        }
187        let quantizer = ScalarQuantizer::with_bounds(num_bits, chunks[0].dim(), bounds);
188
189        Ok(Self {
190            quantizer,
191            distance_type,
192            offsets,
193            chunks,
194        })
195    }
196
197    /// Get the chunk that covers the id.
198    ///
199    /// Returns:
200    /// `(offset, chunk)`
201    ///
202    /// We did not check out of range in this call. But the out of range will
203    /// panic once you access the data in the last [SQStorageChunk].
204    fn chunk(&self, id: u32) -> (u32, &SQStorageChunk) {
205        match self.offsets.binary_search(&id) {
206            Ok(o) => (self.offsets[o], &self.chunks[o]),
207            Err(o) => (self.offsets[o - 1], &self.chunks[o - 1]),
208        }
209    }
210
211    pub async fn load(
212        object_store: &ObjectStore,
213        path: &Path,
214        frag_reuse_index: Option<Arc<FragReuseIndex>>,
215    ) -> Result<Self> {
216        let reader = PreviousFileReader::try_new_self_described(object_store, path, None).await?;
217        let schema = reader.schema();
218
219        let metadata_str = schema
220            .metadata
221            .get(INDEX_METADATA_SCHEMA_KEY)
222            .ok_or(Error::index(format!(
223                "Reading SQ storage: index key {} not found",
224                INDEX_METADATA_SCHEMA_KEY
225            )))?;
226        let index_metadata: IndexMetadata = serde_json::from_str(metadata_str).map_err(|_| {
227            Error::index(format!("Failed to parse index metadata: {}", metadata_str))
228        })?;
229        let distance_type = DistanceType::try_from(index_metadata.distance_type.as_str())?;
230        let metadata = ScalarQuantizationMetadata::load(&reader).await?;
231
232        Self::load_partition(
233            &reader,
234            0..reader.len(),
235            distance_type,
236            &metadata,
237            frag_reuse_index,
238        )
239        .await
240    }
241
242    fn optimize(self) -> Result<Self> {
243        if self.len() <= SQ_CHUNK_CAPACITY {
244            Ok(self)
245        } else {
246            let mut new = self.clone();
247            let batch = concat_batches(
248                self.chunks[0].schema(),
249                self.chunks.iter().map(|c| &c.batch),
250            )?;
251            new.offsets = vec![0, batch.num_rows() as u32];
252            new.chunks = vec![SQStorageChunk::new(batch)?];
253            Ok(new)
254        }
255    }
256}
257
258#[async_trait]
259impl QuantizerStorage for ScalarQuantizationStorage {
260    type Metadata = ScalarQuantizationMetadata;
261
262    fn try_from_batch(
263        batch: RecordBatch,
264        metadata: &Self::Metadata,
265        distance_type: DistanceType,
266        frag_reuse_index: Option<Arc<FragReuseIndex>>,
267    ) -> Result<Self>
268    where
269        Self: Sized,
270    {
271        Self::try_new(
272            metadata.num_bits,
273            distance_type,
274            metadata.bounds.clone(),
275            [batch],
276            frag_reuse_index,
277        )
278    }
279
280    fn metadata(&self) -> &Self::Metadata {
281        &self.quantizer.metadata
282    }
283
284    /// Load a partition of SQ storage from disk.
285    ///
286    /// Parameters
287    /// ----------
288    /// - *reader: file reader
289    /// - *range: row range of the partition
290    /// - *metric_type: metric type of the vectors
291    /// - *metadata: scalar quantization metadata
292    async fn load_partition(
293        reader: &PreviousFileReader,
294        range: std::ops::Range<usize>,
295        distance_type: DistanceType,
296        metadata: &Self::Metadata,
297        frag_reuse_index: Option<Arc<FragReuseIndex>>,
298    ) -> Result<Self> {
299        let schema = reader.schema();
300        let batch = reader.read_range(range, schema).await?;
301
302        Self::try_new(
303            metadata.num_bits,
304            distance_type,
305            metadata.bounds.clone(),
306            [batch],
307            frag_reuse_index,
308        )
309    }
310}
311
312impl VectorStore for ScalarQuantizationStorage {
313    type DistanceCalculator<'a> = SQDistCalculator<'a>;
314
315    fn to_batches(&self) -> Result<impl Iterator<Item = RecordBatch>> {
316        Ok(self.chunks.iter().map(|c| c.batch.clone()))
317    }
318
319    fn append_batch(&self, batch: RecordBatch, vector_column: &str) -> Result<Self> {
320        // TODO: use chunked storage
321        let transformer = super::transform::SQTransformer::new(
322            self.quantizer.clone(),
323            vector_column.to_string(),
324            SQ_CODE_COLUMN.to_string(),
325        );
326
327        let new_batch = transformer.transform(&batch)?;
328
329        // self.quantizer.transform(data)
330        let mut storage = self.clone();
331        let offset = self.len() as u32;
332        let new_chunk = SQStorageChunk::new(new_batch)?;
333        storage.offsets.push(offset + new_chunk.len() as u32);
334        storage.chunks.push(new_chunk);
335
336        storage.optimize()
337    }
338
339    fn schema(&self) -> &SchemaRef {
340        self.chunks[0].schema()
341    }
342
343    fn as_any(&self) -> &dyn std::any::Any {
344        self
345    }
346
347    fn len(&self) -> usize {
348        *self.offsets.last().unwrap() as usize
349    }
350
351    /// Return the [DistanceType] of the vectors.
352    fn distance_type(&self) -> DistanceType {
353        self.distance_type
354    }
355
356    fn row_id(&self, id: u32) -> u64 {
357        let (offset, chunk) = self.chunk(id);
358        chunk.row_id(id - offset)
359    }
360
361    fn row_ids(&self) -> impl Iterator<Item = &u64> {
362        self.chunks.iter().flat_map(|c| c.row_ids.values())
363    }
364
365    /// Create a [DistCalculator] to compute the distance between the query.
366    ///
367    /// Using dist calculator can be more efficient as it can pre-compute some
368    /// values.
369    fn dist_calculator(&self, query: ArrayRef, _dist_q_c: f32) -> Self::DistanceCalculator<'_> {
370        SQDistCalculator::new(query, self, self.quantizer.bounds())
371    }
372
373    fn dist_calculator_from_id(&self, id: u32) -> Self::DistanceCalculator<'_> {
374        let (offset, chunk) = self.chunk(id);
375        let query_sq_code = chunk.sq_code_slice(id - offset);
376        let bounds = self.quantizer.bounds();
377        SQDistCalculator {
378            query_sq_code: SQQueryCode::Borrowed(query_sq_code),
379            scale: sq_distance_scale(&bounds),
380            storage: self,
381        }
382    }
383}
384
385#[inline]
386fn sq_distance_scale(bounds: &Range<f64>) -> f32 {
387    let range = (bounds.end - bounds.start) as f32;
388    (range * range) / (255.0_f32 * 255.0_f32)
389}
390
391pub struct SQDistCalculator<'a> {
392    query_sq_code: SQQueryCode<'a>,
393    scale: f32,
394    storage: &'a ScalarQuantizationStorage,
395}
396
397enum SQQueryCode<'a> {
398    Borrowed(&'a [u8]),
399    Owned(Vec<u8>),
400}
401
402impl SQQueryCode<'_> {
403    #[inline]
404    fn as_slice(&self) -> &[u8] {
405        match self {
406            Self::Borrowed(query) => query,
407            Self::Owned(query) => query,
408        }
409    }
410}
411
412impl<'a> SQDistCalculator<'a> {
413    fn new(query: ArrayRef, storage: &'a ScalarQuantizationStorage, bounds: Range<f64>) -> Self {
414        // This is okay-ish to use hand-rolled dynamic dispatch here
415        // since we search 10s-100s of partitions, we can afford the overhead
416        // this could be annoying at indexing time for HNSW, which requires constructing the
417        // dist calculator frequently. However, HNSW isn't first-class citizen in Lance yet. so be it.
418        let query_sq_code = match query.data_type() {
419            DataType::Float16 => {
420                scale_to_u8::<Float16Type>(query.as_primitive::<Float16Type>().values(), &bounds)
421            }
422            DataType::Float32 => {
423                scale_to_u8::<Float32Type>(query.as_primitive::<Float32Type>().values(), &bounds)
424            }
425            DataType::Float64 => {
426                scale_to_u8::<Float64Type>(query.as_primitive::<Float64Type>().values(), &bounds)
427            }
428            _ => {
429                panic!("Unsupported data type for ScalarQuantizationStorage");
430            }
431        };
432        Self {
433            query_sq_code: SQQueryCode::Owned(query_sq_code),
434            scale: sq_distance_scale(&bounds),
435            storage,
436        }
437    }
438}
439
440impl DistCalculator for SQDistCalculator<'_> {
441    fn distance(&self, id: u32) -> f32 {
442        let (offset, chunk) = self.storage.chunk(id);
443        let sq_code = chunk.sq_code_slice(id - offset);
444        let query_sq_code = self.query_sq_code.as_slice();
445        let dist = match self.storage.distance_type {
446            DistanceType::L2 | DistanceType::Cosine => l2_u8(sq_code, query_sq_code) as f32,
447            DistanceType::Dot => dot_distance(sq_code, query_sq_code),
448            _ => panic!("We should not reach here: sq distance can only be L2 or Dot"),
449        };
450        dist * self.scale
451    }
452
453    fn distance_all(&self, _k_hint: usize) -> Vec<f32> {
454        let query_sq_code = self.query_sq_code.as_slice();
455        match self.storage.distance_type {
456            DistanceType::L2 | DistanceType::Cosine => self
457                .storage
458                .chunks
459                .iter()
460                .flat_map(|c| {
461                    c.sq_codes
462                        .values()
463                        .chunks_exact(c.dim())
464                        .map(|sq_codes| l2_u8(sq_codes, query_sq_code) as f32)
465                })
466                .map(|dist| dist * self.scale)
467                .collect(),
468            DistanceType::Dot => self
469                .storage
470                .chunks
471                .iter()
472                .flat_map(|c| {
473                    c.sq_codes
474                        .values()
475                        .chunks_exact(c.dim())
476                        .map(|sq_codes| dot_distance(sq_codes, query_sq_code))
477                })
478                .map(|dist| dist * self.scale)
479                .collect(),
480            _ => panic!("We should not reach here: sq distance can only be L2 or Dot"),
481        }
482    }
483
484    #[allow(unused_variables)]
485    fn prefetch(&self, id: u32) {
486        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
487        {
488            const CACHE_LINE_SIZE: usize = 64;
489
490            let (offset, chunk) = self.storage.chunk(id);
491            let dim = chunk.dim();
492            let base_ptr = chunk.sq_code_slice(id - offset).as_ptr();
493
494            unsafe {
495                // Loop over the sq_code to prefetch each cache line
496                for offset in (0..dim).step_by(CACHE_LINE_SIZE) {
497                    {
498                        use core::arch::x86_64::{_MM_HINT_T0, _mm_prefetch};
499                        _mm_prefetch(base_ptr.add(offset) as *const i8, _MM_HINT_T0);
500                    }
501                }
502            }
503        }
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    use std::iter::repeat_with;
512    use std::sync::Arc;
513
514    use arrow_array::FixedSizeListArray;
515    use arrow_schema::{DataType, Field, Schema};
516    use lance_arrow::FixedSizeListArrayExt;
517    use lance_testing::datagen::generate_random_array;
518    use rand::prelude::*;
519
520    fn create_record_batch(row_ids: Range<u64>) -> RecordBatch {
521        const DIM: usize = 64;
522
523        let mut rng = rand::rng();
524        let row_ids = UInt64Array::from_iter_values(row_ids);
525        let sq_code = UInt8Array::from_iter_values(
526            repeat_with(|| rng.random::<u8>()).take(row_ids.len() * DIM),
527        );
528        let code_arr = FixedSizeListArray::try_new_from_values(sq_code, DIM as i32).unwrap();
529
530        let schema = Arc::new(Schema::new(vec![
531            Field::new(ROW_ID, DataType::UInt64, false),
532            Field::new(
533                SQ_CODE_COLUMN,
534                DataType::FixedSizeList(
535                    Arc::new(Field::new("item", DataType::UInt8, true)),
536                    DIM as i32,
537                ),
538                false,
539            ),
540        ]));
541        RecordBatch::try_new(schema, vec![Arc::new(row_ids), Arc::new(code_arr)]).unwrap()
542    }
543
544    #[test]
545    fn test_get_chunks() {
546        const DIM: usize = 64;
547
548        let storage = ScalarQuantizationStorage::try_new(
549            8,
550            DistanceType::L2,
551            -0.7..0.7,
552            (0..4).map(|start| create_record_batch(start * 100..(start + 1) * 100)),
553            None,
554        )
555        .unwrap();
556
557        assert_eq!(storage.len(), 400);
558
559        let (offset, chunk) = storage.chunk(0);
560        assert_eq!(offset, 0);
561        assert_eq!(chunk.row_id(20), 20);
562
563        let (offset, _) = storage.chunk(50);
564        assert_eq!(offset, 0);
565
566        let row_ids = UInt64Array::from_iter_values(100..250);
567        let vector_data = generate_random_array(row_ids.len() * DIM);
568        let fsl = FixedSizeListArray::try_new_from_values(vector_data, DIM as i32).unwrap();
569
570        let schema = Arc::new(Schema::new(vec![
571            Field::new(ROW_ID, DataType::UInt64, false),
572            Field::new(
573                "vector",
574                DataType::FixedSizeList(
575                    Arc::new(Field::new("item", DataType::Float32, true)),
576                    DIM as i32,
577                ),
578                false,
579            ),
580        ]));
581
582        let second_batch =
583            RecordBatch::try_new(schema, vec![Arc::new(row_ids), Arc::new(fsl)]).unwrap();
584        let storage = storage.append_batch(second_batch, "vector").unwrap();
585
586        assert_eq!(storage.len(), 550);
587        let (offset, chunk) = storage.chunk(112);
588        assert_eq!(offset, 100);
589        assert_eq!(chunk.row_id(10), 110);
590
591        let (offset, chunk) = storage.chunk(432);
592        assert_eq!(offset, 400);
593        assert_eq!(chunk.row_id(5), 105);
594    }
595}