Skip to main content

lance_index/vector/pq/
storage.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Product Quantization storage
5//!
6//! Used as storage backend for Graph based algorithms.
7
8use std::{
9    cmp::min,
10    collections::HashMap,
11    sync::{Arc, OnceLock},
12};
13
14use arrow::datatypes::{self, UInt8Type};
15use arrow_array::{ArrayRef, ArrowPrimitiveType, PrimitiveArray};
16use arrow_array::{
17    FixedSizeListArray, RecordBatch, UInt8Array, UInt64Array,
18    cast::AsArray,
19    types::{Float32Type, UInt64Type},
20};
21use arrow_schema::{DataType, SchemaRef};
22use async_trait::async_trait;
23use bytes::{Bytes, BytesMut};
24use lance_arrow::{FixedSizeListArrayExt, RecordBatchExt};
25use lance_core::deepsize::DeepSizeOf;
26use lance_core::{Error, ROW_ID, Result};
27use lance_file::previous::{
28    reader::FileReader as PreviousFileReader, writer::FileWriter as PreviousFileWriter,
29};
30use lance_io::{object_store::ObjectStore, utils::read_message};
31use lance_linalg::distance::{Cosine, DistanceType, Dot, L2};
32use lance_table::utils::LanceIteratorExtension;
33use lance_table::{format::SelfDescribingFileReader, io::manifest::ManifestDescribing};
34use object_store::path::Path;
35use prost::Message;
36use serde::{Deserialize, Serialize};
37
38use super::ProductQuantizer;
39use super::distance::{build_distance_table_dot, build_distance_table_l2, compute_pq_distance};
40use crate::frag_reuse::FragReuseIndex;
41use crate::vector::graph::{OrderedFloat, OrderedNode};
42use crate::{
43    INDEX_METADATA_SCHEMA_KEY, IndexMetadata, pb,
44    vector::{
45        PQ_CODE_COLUMN,
46        pq::transform::PQTransformer,
47        quantizer::{QuantizerMetadata, QuantizerStorage},
48        storage::{DistCalculator, VectorStore},
49        transform::Transformer,
50    },
51};
52
53pub const PQ_METADATA_KEY: &str = "lance:pq";
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ProductQuantizationMetadata {
57    pub codebook_position: usize,
58    pub nbits: u32,
59    pub num_sub_vectors: usize,
60    pub dimension: usize,
61
62    #[serde(skip)]
63    pub codebook: Option<FixedSizeListArray>,
64
65    // empty for v1 format
66    // used for v3 format
67    // deprecated in later version
68    pub codebook_tensor: Vec<u8>,
69    pub transposed: bool,
70}
71
72impl DeepSizeOf for ProductQuantizationMetadata {
73    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
74        self.codebook
75            .as_ref()
76            .map(|codebook| (codebook as &dyn arrow_array::Array).deep_size_of_children(context))
77            .unwrap_or(0)
78    }
79}
80
81impl PartialEq for ProductQuantizationMetadata {
82    fn eq(&self, other: &Self) -> bool {
83        self.num_sub_vectors == other.num_sub_vectors
84            && self.nbits == other.nbits
85            && self.dimension == other.dimension
86            && self.codebook == other.codebook
87    }
88}
89
90#[async_trait]
91impl QuantizerMetadata for ProductQuantizationMetadata {
92    fn buffer_index(&self) -> Option<u32> {
93        if self.codebook_position > 0 {
94            // the global buffer index starts from 1
95            Some(self.codebook_position as u32)
96        } else {
97            None
98        }
99    }
100
101    fn set_buffer_index(&mut self, index: u32) {
102        self.codebook_position = index as usize;
103    }
104
105    fn parse_buffer(&mut self, bytes: Bytes) -> Result<()> {
106        debug_assert!(!bytes.is_empty());
107        debug_assert!(self.codebook.is_none());
108        let codebook_tensor: pb::Tensor = pb::Tensor::decode(bytes)?;
109        self.codebook = Some(FixedSizeListArray::try_from(&codebook_tensor)?);
110        Ok(())
111    }
112
113    fn extra_metadata(&self) -> Result<Option<Bytes>> {
114        if let Some(codebook) = &self.codebook {
115            let codebook_tensor: pb::Tensor = pb::Tensor::try_from(codebook)?;
116            let mut bytes = BytesMut::new();
117            codebook_tensor.encode(&mut bytes)?;
118            Ok(Some(bytes.freeze()))
119        } else if !self.codebook_tensor.is_empty() {
120            // Legacy format: codebook is stored inline in the metadata JSON.
121            // Return it as-is; it's already a protobuf-encoded Tensor that
122            // parse_buffer() can handle.
123            Ok(Some(Bytes::from(self.codebook_tensor.clone())))
124        } else {
125            Ok(None)
126        }
127    }
128
129    async fn load(reader: &PreviousFileReader) -> Result<Self> {
130        let metadata = reader
131            .schema()
132            .metadata
133            .get(PQ_METADATA_KEY)
134            .ok_or(Error::index(format!(
135                "Reading PQ storage: metadata key {} not found",
136                PQ_METADATA_KEY
137            )))?;
138        let mut metadata: Self = serde_json::from_str(metadata)
139            .map_err(|_| Error::index(format!("Failed to parse PQ metadata: {}", metadata)))?;
140
141        debug_assert!(metadata.codebook.is_none());
142        debug_assert!(metadata.codebook_tensor.is_empty());
143
144        let codebook_tensor: pb::Tensor =
145            read_message(reader.object_reader.as_ref(), metadata.codebook_position).await?;
146        metadata.codebook = Some(FixedSizeListArray::try_from(&codebook_tensor)?);
147        Ok(metadata)
148    }
149}
150
151/// Product Quantization Storage
152///
153/// It stores PQ code, as well as the row ID to the original vectors.
154///
155/// It is possible to store additional metadata to accelerate filtering later.
156#[derive(Clone, Debug)]
157pub struct ProductQuantizationStorage {
158    metadata: ProductQuantizationMetadata,
159    distance_type: DistanceType,
160    batch: RecordBatch,
161
162    // For easy access
163    pq_code: Arc<UInt8Array>,
164    row_ids: Arc<UInt64Array>,
165    pairwise_distance_table: Arc<OnceLock<Vec<f32>>>,
166}
167
168impl DeepSizeOf for ProductQuantizationStorage {
169    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
170        self.batch.deep_size_of_children(context)
171            + self
172                .metadata
173                .codebook
174                .as_ref()
175                .map(|codebook| {
176                    (codebook as &dyn arrow_array::Array).deep_size_of_children(context)
177                })
178                .unwrap_or(0)
179            + self
180                .pairwise_distance_table
181                .get()
182                .map(|table| table.deep_size_of_children(context))
183                .unwrap_or(0)
184    }
185}
186
187impl PartialEq for ProductQuantizationStorage {
188    fn eq(&self, other: &Self) -> bool {
189        self.distance_type == other.distance_type
190            && self.metadata.eq(&other.metadata)
191            && self.batch.columns().eq(other.batch.columns())
192    }
193}
194
195impl ProductQuantizationStorage {
196    #[allow(clippy::too_many_arguments)]
197    pub fn new(
198        codebook: FixedSizeListArray,
199        mut batch: RecordBatch,
200        num_bits: u32,
201        num_sub_vectors: usize,
202        dimension: usize,
203        distance_type: DistanceType,
204        transposed: bool,
205        frag_reuse_index: Option<Arc<FragReuseIndex>>,
206    ) -> Result<Self> {
207        if batch.num_columns() != 2 {
208            log::warn!(
209                "PQ storage should have 2 columns, but got {} columns: {}",
210                batch.num_columns(),
211                batch.schema(),
212            );
213            batch = batch.project(&[
214                batch.schema().index_of(ROW_ID)?,
215                batch.schema().index_of(PQ_CODE_COLUMN)?,
216            ])?;
217        }
218
219        let Some(row_ids) = batch.column_by_name(ROW_ID) else {
220            return Err(Error::index(
221                "Row ID column not found from PQ storage".to_string(),
222            ));
223        };
224        let mut row_ids: Arc<UInt64Array> = row_ids
225            .as_primitive_opt::<UInt64Type>()
226            .ok_or(Error::index(
227                "Row ID column is not of type UInt64".to_string(),
228            ))?
229            .clone()
230            .into();
231
232        if !transposed {
233            let num_sub_vectors_in_byte = if num_bits == 4 {
234                num_sub_vectors / 2
235            } else {
236                num_sub_vectors
237            };
238            let pq_col = batch[PQ_CODE_COLUMN].as_fixed_size_list();
239            let transposed_code = transpose(
240                pq_col.values().as_primitive::<UInt8Type>(),
241                row_ids.len(),
242                num_sub_vectors_in_byte,
243            );
244            let pq_code_fsl = Arc::new(FixedSizeListArray::try_new_from_values(
245                transposed_code,
246                num_sub_vectors_in_byte as i32,
247            )?);
248            batch = batch.replace_column_by_name(PQ_CODE_COLUMN, pq_code_fsl)?;
249        }
250
251        let mut pq_code: Arc<UInt8Array> = batch[PQ_CODE_COLUMN]
252            .as_fixed_size_list()
253            .values()
254            .as_primitive()
255            .clone()
256            .into();
257
258        if let Some(frag_reuse_index_ref) = frag_reuse_index.as_ref() {
259            let transposed_codes = pq_code.values();
260            let mut new_row_ids = Vec::with_capacity(row_ids.len());
261            let mut new_codes = Vec::with_capacity(row_ids.len() * num_sub_vectors);
262
263            let row_ids_values = row_ids.values();
264            for (i, row_id) in row_ids_values.iter().enumerate() {
265                if let Some(mapped_value) = frag_reuse_index_ref.remap_row_id(*row_id) {
266                    new_row_ids.push(mapped_value);
267                    new_codes.extend(get_pq_code(
268                        transposed_codes,
269                        num_bits,
270                        num_sub_vectors,
271                        i as u32,
272                    ));
273                }
274            }
275
276            let new_row_ids = Arc::new(UInt64Array::from(new_row_ids));
277            let new_codes = UInt8Array::from(new_codes);
278            batch = if new_row_ids.is_empty() {
279                RecordBatch::new_empty(batch.schema())
280            } else {
281                let num_bytes_in_code = new_codes.len() / new_row_ids.len();
282                let new_transposed_codes =
283                    transpose(&new_codes, new_row_ids.len(), num_bytes_in_code);
284                let codes_fsl = Arc::new(FixedSizeListArray::try_new_from_values(
285                    new_transposed_codes,
286                    num_bytes_in_code as i32,
287                )?);
288                RecordBatch::try_new(batch.schema(), vec![new_row_ids, codes_fsl])?
289            };
290            pq_code = batch[PQ_CODE_COLUMN]
291                .as_fixed_size_list()
292                .values()
293                .as_primitive::<UInt8Type>()
294                .clone()
295                .into();
296            // Refresh the stored row ids from the remapped batch. Without this
297            // the storage keeps the pre-remap (compacted-away) addresses while
298            // its codes are remapped, so search returns stale row ids and the
299            // take fails with "fragment ... does not exist".
300            row_ids = batch[ROW_ID].as_primitive::<UInt64Type>().clone().into();
301        }
302
303        let distance_type = match distance_type {
304            DistanceType::Cosine => DistanceType::L2,
305            _ => distance_type,
306        };
307        let metadata = ProductQuantizationMetadata {
308            codebook_position: 0,
309            nbits: num_bits,
310            num_sub_vectors,
311            dimension,
312            codebook: Some(codebook),
313            codebook_tensor: Vec::new(), // empty for v1 format
314            transposed: true,
315        };
316        Ok(Self {
317            metadata,
318            distance_type,
319            batch,
320            pq_code,
321            row_ids,
322            pairwise_distance_table: Arc::new(OnceLock::new()),
323        })
324    }
325
326    pub fn batch(&self) -> &RecordBatch {
327        &self.batch
328    }
329
330    /// Build a PQ storage from ProductQuantizer and a RecordBatch.
331    ///
332    /// Parameters
333    /// ----------
334    /// quantizer: ProductQuantizer
335    ///    The quantizer used to transform the vectors.
336    /// batch: RecordBatch
337    ///   The batch of vectors to be transformed.
338    /// vector_col: &str
339    ///   The name of the column containing the vectors.
340    pub async fn build(
341        quantizer: ProductQuantizer,
342        batch: &RecordBatch,
343        vector_col: &str,
344        frag_reuse_index: Option<Arc<FragReuseIndex>>,
345    ) -> Result<Self> {
346        let codebook = quantizer.codebook.clone();
347        let num_bits = quantizer.num_bits;
348        let dimension = quantizer.dimension;
349        let num_sub_vectors = quantizer.num_sub_vectors;
350        let metric_type = quantizer.distance_type;
351        let transform = PQTransformer::new(quantizer, vector_col, PQ_CODE_COLUMN);
352        let batch = transform.transform(batch)?;
353        Self::new(
354            codebook,
355            batch,
356            num_bits,
357            num_sub_vectors,
358            dimension,
359            metric_type,
360            false,
361            frag_reuse_index,
362        )
363    }
364
365    pub fn codebook(&self) -> &FixedSizeListArray {
366        self.metadata.codebook.as_ref().unwrap()
367    }
368
369    /// Load full PQ storage from disk.
370    ///
371    /// Parameters
372    /// ----------
373    /// object_store: &ObjectStore
374    ///   The object store to load the storage from.
375    /// path: &Path
376    ///  The path to the storage.
377    ///
378    /// Returns
379    /// --------
380    /// Self
381    ///
382    /// Currently it loads everything in memory.
383    /// TODO: support lazy loading later.
384    pub async fn load(
385        object_store: &ObjectStore,
386        path: &Path,
387        frag_reuse_index: Option<Arc<FragReuseIndex>>,
388    ) -> Result<Self> {
389        let reader = PreviousFileReader::try_new_self_described(object_store, path, None).await?;
390        let schema = reader.schema();
391
392        let metadata_str = schema
393            .metadata
394            .get(INDEX_METADATA_SCHEMA_KEY)
395            .ok_or(Error::index(format!(
396                "Reading PQ storage: index key {} not found",
397                INDEX_METADATA_SCHEMA_KEY
398            )))?;
399        let index_metadata: IndexMetadata = serde_json::from_str(metadata_str).map_err(|_| {
400            Error::index(format!("Failed to parse index metadata: {}", metadata_str))
401        })?;
402        let distance_type: DistanceType =
403            DistanceType::try_from(index_metadata.distance_type.as_str())?;
404
405        let metadata = ProductQuantizationMetadata::load(&reader).await?;
406        Self::load_partition(
407            &reader,
408            0..reader.len(),
409            distance_type,
410            &metadata,
411            frag_reuse_index,
412        )
413        .await
414    }
415
416    pub fn schema(&self) -> SchemaRef {
417        self.batch.schema()
418    }
419
420    pub fn get_row_ids(&self, ids: &[u32]) -> Vec<u64> {
421        ids.iter()
422            .map(|&id| self.row_ids.value(id as usize))
423            .collect()
424    }
425
426    fn pairwise_distance_table(&self) -> &[f32] {
427        self.pairwise_distance_table
428            .get_or_init(|| {
429                let codebook = self.metadata.codebook.as_ref().unwrap();
430                match codebook.value_type() {
431                    DataType::Float16 => build_pairwise_distance_table(
432                        codebook
433                            .values()
434                            .as_primitive::<datatypes::Float16Type>()
435                            .values(),
436                        self.metadata.nbits,
437                        self.metadata.num_sub_vectors,
438                        self.metadata.dimension,
439                        self.distance_type,
440                    ),
441                    DataType::Float32 => build_pairwise_distance_table(
442                        codebook
443                            .values()
444                            .as_primitive::<datatypes::Float32Type>()
445                            .values(),
446                        self.metadata.nbits,
447                        self.metadata.num_sub_vectors,
448                        self.metadata.dimension,
449                        self.distance_type,
450                    ),
451                    DataType::Float64 => build_pairwise_distance_table(
452                        codebook
453                            .values()
454                            .as_primitive::<datatypes::Float64Type>()
455                            .values(),
456                        self.metadata.nbits,
457                        self.metadata.num_sub_vectors,
458                        self.metadata.dimension,
459                        self.distance_type,
460                    ),
461                    _ => unimplemented!("Unsupported data type: {:?}", codebook.value_type()),
462                }
463            })
464            .as_slice()
465    }
466
467    /// Write the PQ storage as a Lance partition to disk,
468    /// and returns the number of rows written.
469    ///
470    pub async fn write_partition(
471        &self,
472        writer: &mut PreviousFileWriter<ManifestDescribing>,
473    ) -> Result<usize> {
474        let batch_size: usize = 10240; // TODO: make it configurable
475        for offset in (0..self.batch.num_rows()).step_by(batch_size) {
476            let length = min(batch_size, self.batch.num_rows() - offset);
477            let slice = self.batch.slice(offset, length);
478            writer.write(&[slice]).await?;
479        }
480        Ok(self.batch.num_rows())
481    }
482}
483
484pub fn transpose<T: ArrowPrimitiveType>(
485    original: &PrimitiveArray<T>,
486    num_rows: usize,
487    num_columns: usize,
488) -> PrimitiveArray<T>
489where
490    PrimitiveArray<T>: From<Vec<T::Native>>,
491{
492    if original.is_empty() {
493        return original.clone();
494    }
495
496    let mut transposed_codes = vec![T::default_value(); original.len()];
497    for (vec_idx, codes) in original.values().chunks_exact(num_columns).enumerate() {
498        for (sub_vec_idx, code) in codes.iter().enumerate() {
499            transposed_codes[sub_vec_idx * num_rows + vec_idx] = *code;
500        }
501    }
502
503    transposed_codes.into()
504}
505
506#[async_trait]
507impl QuantizerStorage for ProductQuantizationStorage {
508    type Metadata = ProductQuantizationMetadata;
509
510    fn try_from_batch(
511        batch: RecordBatch,
512        metadata: &Self::Metadata,
513        distance_type: DistanceType,
514        frag_reuse_index: Option<Arc<FragReuseIndex>>,
515    ) -> Result<Self>
516    where
517        Self: Sized,
518    {
519        let distance_type = match distance_type {
520            DistanceType::Cosine => DistanceType::L2,
521            _ => distance_type,
522        };
523
524        // now it supports only Float32Type
525        let codebook = match &metadata.codebook {
526            Some(codebook) => codebook.clone(),
527            None => {
528                // legacy format would contains codebook tensor but not codebook
529                debug_assert!(!metadata.codebook_tensor.is_empty());
530                let codebook_tensor = pb::Tensor::decode(metadata.codebook_tensor.as_slice())?;
531                FixedSizeListArray::try_from(&codebook_tensor)?
532            }
533        };
534
535        Self::new(
536            codebook,
537            batch,
538            metadata.nbits,
539            metadata.num_sub_vectors,
540            metadata.dimension,
541            distance_type,
542            metadata.transposed,
543            frag_reuse_index,
544        )
545    }
546
547    fn metadata(&self) -> &Self::Metadata {
548        &self.metadata
549    }
550
551    // we can't use the default implementation of remap,
552    // because PQ Storage transposed the PQ codes
553    fn remap(&self, mapping: &HashMap<u64, Option<u64>>) -> Result<Self> {
554        let transposed_codes = self.pq_code.values();
555        let mut new_row_ids = Vec::with_capacity(self.len());
556        let mut new_codes = Vec::with_capacity(self.len() * self.metadata.num_sub_vectors);
557
558        let row_ids = self.row_ids.values();
559        for (i, row_id) in row_ids.iter().enumerate() {
560            match mapping.get(row_id) {
561                Some(Some(new_id)) => {
562                    new_row_ids.push(*new_id);
563                    new_codes.extend(get_pq_code(
564                        transposed_codes,
565                        self.metadata.nbits,
566                        self.metadata.num_sub_vectors,
567                        i as u32,
568                    ));
569                }
570                Some(None) => {}
571                None => {
572                    new_row_ids.push(*row_id);
573                    new_codes.extend(get_pq_code(
574                        transposed_codes,
575                        self.metadata.nbits,
576                        self.metadata.num_sub_vectors,
577                        i as u32,
578                    ));
579                }
580            }
581        }
582
583        let new_row_ids = Arc::new(UInt64Array::from(new_row_ids));
584        let new_codes = UInt8Array::from(new_codes);
585        let batch = if new_row_ids.is_empty() {
586            RecordBatch::new_empty(self.schema())
587        } else {
588            let num_bytes_in_code = new_codes.len() / new_row_ids.len();
589            let new_transposed_codes = transpose(&new_codes, new_row_ids.len(), num_bytes_in_code);
590            let codes_fsl = Arc::new(FixedSizeListArray::try_new_from_values(
591                new_transposed_codes,
592                num_bytes_in_code as i32,
593            )?);
594            RecordBatch::try_new(self.schema(), vec![new_row_ids.clone(), codes_fsl])?
595        };
596        let transposed_codes = batch[PQ_CODE_COLUMN]
597            .as_fixed_size_list()
598            .values()
599            .as_primitive::<UInt8Type>()
600            .clone();
601
602        Ok(Self {
603            metadata: self.metadata.clone(),
604            distance_type: self.distance_type,
605            batch,
606            pq_code: Arc::new(transposed_codes),
607            row_ids: new_row_ids,
608            pairwise_distance_table: self.pairwise_distance_table.clone(),
609        })
610    }
611
612    /// Load a partition of PQ storage from disk.
613    ///
614    /// Parameters
615    /// ----------
616    /// - *reader: &PreviousFileReader
617    async fn load_partition(
618        reader: &PreviousFileReader,
619        range: std::ops::Range<usize>,
620        distance_type: DistanceType,
621        metadata: &Self::Metadata,
622        frag_reuse_index: Option<Arc<FragReuseIndex>>,
623    ) -> Result<Self> {
624        // Hard coded to float32 for now
625        let codebook = metadata
626            .codebook
627            .as_ref()
628            .ok_or(Error::index(
629                "Codebook not found in PQ metadata".to_string(),
630            ))?
631            .values()
632            .as_primitive::<Float32Type>()
633            .clone();
634
635        let codebook =
636            FixedSizeListArray::try_new_from_values(codebook, metadata.dimension as i32)?;
637
638        let schema = reader.schema();
639        let batch = reader.read_range(range, schema).await?;
640
641        Self::new(
642            codebook,
643            batch,
644            metadata.nbits,
645            metadata.num_sub_vectors,
646            metadata.dimension,
647            distance_type,
648            metadata.transposed,
649            frag_reuse_index,
650        )
651    }
652}
653
654impl VectorStore for ProductQuantizationStorage {
655    type DistanceCalculator<'a> = PQDistCalculator;
656
657    fn to_batches(&self) -> Result<impl Iterator<Item = RecordBatch>> {
658        Ok(std::iter::once(self.batch.clone()))
659    }
660
661    fn append_batch(&self, _batch: RecordBatch, _vector_column: &str) -> Result<Self> {
662        unimplemented!()
663    }
664
665    fn schema(&self) -> &SchemaRef {
666        self.batch.schema_ref()
667    }
668
669    fn as_any(&self) -> &dyn std::any::Any {
670        self
671    }
672
673    fn len(&self) -> usize {
674        self.batch.num_rows()
675    }
676
677    fn distance_type(&self) -> DistanceType {
678        self.distance_type
679    }
680
681    fn row_id(&self, id: u32) -> u64 {
682        self.row_ids.values()[id as usize]
683    }
684
685    fn row_ids(&self) -> impl Iterator<Item = &u64> {
686        self.row_ids.values().iter()
687    }
688
689    fn dist_calculator(&self, query: ArrayRef, _dist_q_c: f32) -> Self::DistanceCalculator<'_> {
690        let codebook = self.metadata.codebook.as_ref().unwrap();
691        match codebook.value_type() {
692            DataType::Float16 => PQDistCalculator::new(
693                codebook
694                    .values()
695                    .as_primitive::<datatypes::Float16Type>()
696                    .values(),
697                self.metadata.nbits,
698                self.metadata.num_sub_vectors,
699                self.pq_code.clone(),
700                query.as_primitive::<datatypes::Float16Type>().values(),
701                self.distance_type,
702            ),
703            DataType::Float32 => PQDistCalculator::new(
704                codebook
705                    .values()
706                    .as_primitive::<datatypes::Float32Type>()
707                    .values(),
708                self.metadata.nbits,
709                self.metadata.num_sub_vectors,
710                self.pq_code.clone(),
711                query.as_primitive::<datatypes::Float32Type>().values(),
712                self.distance_type,
713            ),
714            DataType::Float64 => PQDistCalculator::new(
715                codebook
716                    .values()
717                    .as_primitive::<datatypes::Float64Type>()
718                    .values(),
719                self.metadata.nbits,
720                self.metadata.num_sub_vectors,
721                self.pq_code.clone(),
722                query.as_primitive::<datatypes::Float64Type>().values(),
723                self.distance_type,
724            ),
725            _ => unimplemented!("Unsupported data type: {:?}", codebook.value_type()),
726        }
727    }
728
729    fn dist_calculator_from_id(&self, id: u32) -> Self::DistanceCalculator<'_> {
730        let codes = get_pq_code(
731            self.pq_code.values(),
732            self.metadata.nbits,
733            self.metadata.num_sub_vectors,
734            id,
735        );
736        PQDistCalculator::new_from_codes(
737            self.pairwise_distance_table(),
738            self.metadata.nbits,
739            self.metadata.num_sub_vectors,
740            self.pq_code.clone(),
741            codes,
742            self.distance_type,
743        )
744    }
745
746    fn dist_between(&self, u: u32, v: u32) -> f32 {
747        // this is a fast way to compute distance between two vectors in the same storage.
748        // it doesn't construct the distance table.
749        let pq_codes = self.pq_code.values();
750        let u_codes = get_pq_code(
751            pq_codes,
752            self.metadata.nbits,
753            self.metadata.num_sub_vectors,
754            u,
755        );
756        let v_codes = get_pq_code(
757            pq_codes,
758            self.metadata.nbits,
759            self.metadata.num_sub_vectors,
760            v,
761        );
762        pq_code_distance(
763            self.pairwise_distance_table(),
764            self.metadata.nbits,
765            self.metadata.num_sub_vectors,
766            u_codes,
767            v_codes,
768            self.distance_type,
769        )
770    }
771
772    fn prefers_candidate(&self, candidate: &OrderedNode, selected: &[OrderedNode]) -> bool {
773        selected
774            .iter()
775            .all(|other| candidate.dist < OrderedFloat(self.dist_between(candidate.id, other.id)))
776    }
777}
778
779/// Distance calculator backed by PQ code.
780pub struct PQDistCalculator {
781    distance_table: Vec<f32>,
782    pq_code: Arc<UInt8Array>,
783    num_sub_vectors: usize,
784    num_bits: u32,
785    distance_type: DistanceType,
786}
787
788impl PQDistCalculator {
789    fn new<T: L2 + Dot>(
790        codebook: &[T],
791        num_bits: u32,
792        num_sub_vectors: usize,
793        pq_code: Arc<UInt8Array>,
794        query: &[T],
795        distance_type: DistanceType,
796    ) -> Self {
797        let distance_table = match distance_type {
798            DistanceType::L2 | DistanceType::Cosine => {
799                build_distance_table_l2(codebook, num_bits, num_sub_vectors, query)
800            }
801            DistanceType::Dot => {
802                build_distance_table_dot(codebook, num_bits, num_sub_vectors, query)
803            }
804            _ => unimplemented!("DistanceType is not supported: {:?}", distance_type),
805        };
806        Self {
807            distance_table,
808            num_sub_vectors,
809            pq_code,
810            num_bits,
811            distance_type,
812        }
813    }
814
815    fn new_from_codes(
816        pairwise_distance_table: &[f32],
817        num_bits: u32,
818        num_sub_vectors: usize,
819        pq_code: Arc<UInt8Array>,
820        query_codes: impl Iterator<Item = u8>,
821        distance_type: DistanceType,
822    ) -> Self {
823        let distance_table = distance_table_from_codes(
824            pairwise_distance_table,
825            num_bits,
826            num_sub_vectors,
827            query_codes,
828        );
829        Self {
830            distance_table,
831            num_sub_vectors,
832            pq_code,
833            num_bits,
834            distance_type,
835        }
836    }
837
838    fn get_pq_code(&self, id: u32) -> impl Iterator<Item = usize> + '_ {
839        get_pq_code(
840            self.pq_code.values(),
841            self.num_bits,
842            self.num_sub_vectors,
843            id,
844        )
845        .map(|v| v as usize)
846    }
847}
848
849impl DistCalculator for PQDistCalculator {
850    fn distance(&self, id: u32) -> f32 {
851        let num_centroids = 2_usize.pow(self.num_bits);
852        let pq_code = self.get_pq_code(id);
853        let diff = self.num_sub_vectors as f32 - 1.0;
854        let dist = if self.num_bits == 4 {
855            pq_code
856                .enumerate()
857                .map(|(i, c)| {
858                    let current_idx = c & 0x0F;
859                    let next_idx = c >> 4;
860
861                    self.distance_table[2 * i * num_centroids + current_idx]
862                        + self.distance_table[(2 * i + 1) * num_centroids + next_idx]
863                })
864                .sum()
865        } else {
866            pq_code
867                .enumerate()
868                .map(|(i, c)| self.distance_table[i * num_centroids + c])
869                .sum()
870        };
871
872        if self.distance_type == DistanceType::Dot {
873            dist - diff
874        } else {
875            dist
876        }
877    }
878
879    fn distance_all(&self, k_hint: usize) -> Vec<f32> {
880        match self.distance_type {
881            DistanceType::L2 => compute_pq_distance(
882                &self.distance_table,
883                self.num_bits,
884                self.num_sub_vectors,
885                self.pq_code.values(),
886                k_hint,
887            ),
888            DistanceType::Cosine => {
889                // it seems we implemented cosine distance at some version,
890                // but from now on, we should use normalized L2 distance.
891                debug_assert!(
892                    false,
893                    "cosine distance should be converted to normalized L2 distance"
894                );
895                // L2 over normalized vectors:  ||x - y|| = x^2 + y^2 - 2 * xy = 1 + 1 - 2 * xy = 2 * (1 - xy)
896                // Cosine distance: 1 - |xy| / (||x|| * ||y||) = 1 - xy / (x^2 * y^2) = 1 - xy / (1 * 1) = 1 - xy
897                // Therefore, Cosine = L2 / 2
898                let l2_dists = compute_pq_distance(
899                    &self.distance_table,
900                    self.num_bits,
901                    self.num_sub_vectors,
902                    self.pq_code.values(),
903                    k_hint,
904                );
905                l2_dists.into_iter().map(|v| v / 2.0).collect()
906            }
907            DistanceType::Dot => {
908                let dot_dists = compute_pq_distance(
909                    &self.distance_table,
910                    self.num_bits,
911                    self.num_sub_vectors,
912                    self.pq_code.values(),
913                    k_hint,
914                );
915                let diff = self.num_sub_vectors as f32 - 1.0;
916                dot_dists.into_iter().map(|v| v - diff).collect()
917            }
918            _ => unimplemented!("distance type is not supported: {:?}", self.distance_type),
919        }
920    }
921}
922
923fn build_pairwise_distance_table<T: L2 + Cosine + Dot>(
924    codebook: &[T],
925    num_bits: u32,
926    num_sub_vectors: usize,
927    dimension: usize,
928    distance_type: DistanceType,
929) -> Vec<f32> {
930    let num_centroids = 2_usize.pow(num_bits);
931    let sub_vector_width = dimension / num_sub_vectors;
932    let mut result = Vec::with_capacity(num_sub_vectors * num_centroids * num_centroids);
933    let distance_fn = distance_type.func();
934    for sub_vector_idx in 0..num_sub_vectors {
935        let sub_vector_offset = sub_vector_idx * num_centroids * sub_vector_width;
936        let centroids =
937            &codebook[sub_vector_offset..sub_vector_offset + num_centroids * sub_vector_width];
938        for query_centroid_idx in 0..num_centroids {
939            let query_offset = query_centroid_idx * sub_vector_width;
940            let query = &centroids[query_offset..query_offset + sub_vector_width];
941            for centroid_idx in 0..num_centroids {
942                let centroid_offset = centroid_idx * sub_vector_width;
943                let centroid = &centroids[centroid_offset..centroid_offset + sub_vector_width];
944                result.push(distance_fn(query, centroid));
945            }
946        }
947    }
948    result
949}
950
951fn distance_table_from_codes(
952    pairwise_distance_table: &[f32],
953    num_bits: u32,
954    num_sub_vectors: usize,
955    query_codes: impl Iterator<Item = u8>,
956) -> Vec<f32> {
957    let num_centroids = 2_usize.pow(num_bits);
958    let mut distance_table = Vec::with_capacity(num_sub_vectors * num_centroids);
959    if num_bits == 4 {
960        for (byte_idx, query_code) in query_codes.enumerate() {
961            let current_idx = (query_code & 0x0F) as usize;
962            let current_sub_vector_idx = 2 * byte_idx;
963            extend_pairwise_distance_row(
964                &mut distance_table,
965                pairwise_distance_table,
966                num_centroids,
967                current_sub_vector_idx,
968                current_idx,
969            );
970
971            let next_idx = (query_code >> 4) as usize;
972            let next_sub_vector_idx = current_sub_vector_idx + 1;
973            extend_pairwise_distance_row(
974                &mut distance_table,
975                pairwise_distance_table,
976                num_centroids,
977                next_sub_vector_idx,
978                next_idx,
979            );
980        }
981    } else {
982        for (sub_vector_idx, query_code) in query_codes.enumerate() {
983            extend_pairwise_distance_row(
984                &mut distance_table,
985                pairwise_distance_table,
986                num_centroids,
987                sub_vector_idx,
988                query_code as usize,
989            );
990        }
991    }
992    distance_table
993}
994
995fn extend_pairwise_distance_row(
996    distance_table: &mut Vec<f32>,
997    pairwise_distance_table: &[f32],
998    num_centroids: usize,
999    sub_vector_idx: usize,
1000    query_centroid_idx: usize,
1001) {
1002    let start = (sub_vector_idx * num_centroids + query_centroid_idx) * num_centroids;
1003    distance_table.extend_from_slice(&pairwise_distance_table[start..start + num_centroids]);
1004}
1005
1006fn pq_code_distance(
1007    pairwise_distance_table: &[f32],
1008    num_bits: u32,
1009    num_sub_vectors: usize,
1010    lhs_codes: impl Iterator<Item = u8>,
1011    rhs_codes: impl Iterator<Item = u8>,
1012    distance_type: DistanceType,
1013) -> f32 {
1014    let num_centroids = 2_usize.pow(num_bits);
1015    let dist = if num_bits == 4 {
1016        lhs_codes
1017            .zip(rhs_codes)
1018            .enumerate()
1019            .map(|(byte_idx, (lhs, rhs))| {
1020                let current_sub_vector_idx = 2 * byte_idx;
1021                pairwise_distance(
1022                    pairwise_distance_table,
1023                    num_centroids,
1024                    current_sub_vector_idx,
1025                    (lhs & 0x0F) as usize,
1026                    (rhs & 0x0F) as usize,
1027                ) + pairwise_distance(
1028                    pairwise_distance_table,
1029                    num_centroids,
1030                    current_sub_vector_idx + 1,
1031                    (lhs >> 4) as usize,
1032                    (rhs >> 4) as usize,
1033                )
1034            })
1035            .sum()
1036    } else {
1037        lhs_codes
1038            .zip(rhs_codes)
1039            .enumerate()
1040            .map(|(sub_vector_idx, (lhs, rhs))| {
1041                pairwise_distance(
1042                    pairwise_distance_table,
1043                    num_centroids,
1044                    sub_vector_idx,
1045                    lhs as usize,
1046                    rhs as usize,
1047                )
1048            })
1049            .sum()
1050    };
1051
1052    if distance_type == DistanceType::Dot {
1053        dist - (num_sub_vectors as f32 - 1.0)
1054    } else {
1055        dist
1056    }
1057}
1058
1059fn pairwise_distance(
1060    pairwise_distance_table: &[f32],
1061    num_centroids: usize,
1062    sub_vector_idx: usize,
1063    query_centroid_idx: usize,
1064    centroid_idx: usize,
1065) -> f32 {
1066    pairwise_distance_table
1067        [(sub_vector_idx * num_centroids + query_centroid_idx) * num_centroids + centroid_idx]
1068}
1069
1070fn get_pq_code(
1071    pq_code: &[u8],
1072    num_bits: u32,
1073    num_sub_vectors: usize,
1074    id: u32,
1075) -> impl Iterator<Item = u8> + '_ {
1076    let num_bytes = if num_bits == 4 {
1077        num_sub_vectors / 2
1078    } else {
1079        num_sub_vectors
1080    };
1081
1082    let num_vectors = pq_code.len() / num_bytes;
1083    pq_code
1084        .iter()
1085        .skip(id as usize)
1086        .step_by(num_vectors)
1087        .copied()
1088        .exact_size(num_bytes)
1089}
1090
1091#[cfg(test)]
1092fn get_centroids<T: Clone>(
1093    codebook: &[T],
1094    num_bits: u32,
1095    num_sub_vectors: usize,
1096    dimension: usize,
1097    codes: impl Iterator<Item = u8>,
1098) -> Vec<T> {
1099    // codebook[i][j] is the j-th centroid of the i-th sub-vector.
1100    // the codebook is stored as a flat array, codebook[i * num_centroids + j] = codebook[i][j]
1101
1102    if num_bits == 4 {
1103        return get_centroids_4bit(codebook, num_sub_vectors, dimension, codes);
1104    }
1105
1106    let num_centroids: usize = 2_usize.pow(8);
1107    let sub_vector_width = dimension / num_sub_vectors;
1108    let mut centroids = Vec::with_capacity(dimension);
1109    for (sub_vec_idx, centroid_idx) in codes.enumerate() {
1110        let centroid_idx = centroid_idx as usize;
1111        let centroid = &codebook[sub_vec_idx * num_centroids * sub_vector_width
1112            + centroid_idx * sub_vector_width
1113            ..sub_vec_idx * num_centroids * sub_vector_width
1114                + (centroid_idx + 1) * sub_vector_width];
1115        centroids.extend_from_slice(centroid);
1116    }
1117    centroids
1118}
1119
1120#[cfg(test)]
1121fn get_centroids_4bit<T: Clone>(
1122    codebook: &[T],
1123    num_sub_vectors: usize,
1124    dimension: usize,
1125    codes: impl Iterator<Item = u8>,
1126) -> Vec<T> {
1127    let num_centroids: usize = 16;
1128    let sub_vector_width = dimension / num_sub_vectors;
1129    let mut centroids = Vec::with_capacity(dimension);
1130    for (sub_vec_idx, centroid_idx) in codes.into_iter().enumerate() {
1131        let current_idx = (centroid_idx & 0x0F) as usize;
1132        let offset = 2 * sub_vec_idx * num_centroids * sub_vector_width;
1133        let current_centroid = &codebook[offset + current_idx * sub_vector_width
1134            ..offset + (current_idx + 1) * sub_vector_width];
1135        centroids.extend_from_slice(current_centroid);
1136
1137        let next_idx = (centroid_idx >> 4) as usize;
1138        let offset = (2 * sub_vec_idx + 1) * num_centroids * sub_vector_width;
1139        let next_centroid = &codebook
1140            [offset + next_idx * sub_vector_width..offset + (next_idx + 1) * sub_vector_width];
1141        centroids.extend_from_slice(next_centroid);
1142    }
1143    centroids
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148    use crate::vector::storage::StorageBuilder;
1149
1150    use super::*;
1151
1152    use arrow_array::{Float32Array, UInt32Array};
1153    use arrow_schema::{DataType, Field, Schema as ArrowSchema};
1154    use lance_arrow::FixedSizeListArrayExt;
1155    use lance_core::ROW_ID_FIELD;
1156    use rand::Rng;
1157
1158    const DIM: usize = 32;
1159    const TOTAL: usize = 512;
1160    const NUM_SUB_VECTORS: usize = 16;
1161
1162    async fn create_pq_storage() -> ProductQuantizationStorage {
1163        let codebook = Float32Array::from_iter_values((0..256 * DIM).map(|_| rand::random()));
1164        let codebook = FixedSizeListArray::try_new_from_values(codebook, DIM as i32).unwrap();
1165        let pq = ProductQuantizer::new(NUM_SUB_VECTORS, 8, DIM, codebook, DistanceType::Dot);
1166
1167        let schema = ArrowSchema::new(vec![
1168            Field::new(
1169                "vec",
1170                DataType::FixedSizeList(
1171                    Field::new_list_field(DataType::Float32, true).into(),
1172                    DIM as i32,
1173                ),
1174                true,
1175            ),
1176            ROW_ID_FIELD.clone(),
1177        ]);
1178        let vectors = Float32Array::from_iter_values((0..TOTAL * DIM).map(|_| rand::random()));
1179        let row_ids = UInt64Array::from_iter_values((0..TOTAL).map(|v| v as u64));
1180        let fsl = FixedSizeListArray::try_new_from_values(vectors, DIM as i32).unwrap();
1181        let batch =
1182            RecordBatch::try_new(schema.into(), vec![Arc::new(fsl), Arc::new(row_ids)]).unwrap();
1183
1184        StorageBuilder::new("vec".to_owned(), pq.distance_type, pq, None)
1185            .unwrap()
1186            .build(vec![batch])
1187            .unwrap()
1188    }
1189
1190    async fn create_pq_storage_with_extra_column() -> ProductQuantizationStorage {
1191        let codebook = Float32Array::from_iter_values((0..256 * DIM).map(|_| rand::random()));
1192        let codebook = FixedSizeListArray::try_new_from_values(codebook, DIM as i32).unwrap();
1193        let pq = ProductQuantizer::new(NUM_SUB_VECTORS, 8, DIM, codebook, DistanceType::Dot);
1194
1195        let schema = ArrowSchema::new(vec![
1196            Field::new(
1197                "vec",
1198                DataType::FixedSizeList(
1199                    Field::new_list_field(DataType::Float32, true).into(),
1200                    DIM as i32,
1201                ),
1202                true,
1203            ),
1204            ROW_ID_FIELD.clone(),
1205            Field::new("extra", DataType::UInt32, true),
1206        ]);
1207        let vectors = Float32Array::from_iter_values((0..TOTAL * DIM).map(|_| rand::random()));
1208        let row_ids = UInt64Array::from_iter_values((0..TOTAL).map(|v| v as u64));
1209        let extra_column = UInt32Array::from_iter_values((0..TOTAL).map(|v| v as u32));
1210        let fsl = FixedSizeListArray::try_new_from_values(vectors, DIM as i32).unwrap();
1211        let batch = RecordBatch::try_new(
1212            schema.into(),
1213            vec![Arc::new(fsl), Arc::new(row_ids), Arc::new(extra_column)],
1214        )
1215        .unwrap();
1216
1217        StorageBuilder::new("vec".to_owned(), pq.distance_type, pq, None)
1218            .unwrap()
1219            .build(vec![batch])
1220            .unwrap()
1221    }
1222
1223    #[tokio::test]
1224    async fn test_build_pq_storage() {
1225        let storage = create_pq_storage().await;
1226        assert_eq!(storage.len(), TOTAL);
1227        assert_eq!(storage.metadata.num_sub_vectors, NUM_SUB_VECTORS);
1228        assert_eq!(
1229            storage.metadata.codebook.as_ref().unwrap().values().len(),
1230            256 * DIM
1231        );
1232        assert_eq!(storage.pq_code.len(), TOTAL * NUM_SUB_VECTORS);
1233        assert_eq!(storage.row_ids.len(), TOTAL);
1234    }
1235
1236    #[tokio::test]
1237    async fn test_distance_all() {
1238        let storage = create_pq_storage().await;
1239        let query = Arc::new(Float32Array::from_iter_values((0..DIM).map(|v| v as f32)));
1240        let dist_calc = storage.dist_calculator(query, 0.0);
1241        let expected = (0..storage.len())
1242            .map(|id| dist_calc.distance(id as u32))
1243            .collect::<Vec<_>>();
1244        let distances = dist_calc.distance_all(100);
1245        assert_eq!(distances, expected);
1246    }
1247
1248    #[tokio::test]
1249    async fn test_dist_between() {
1250        let mut rng = rand::rng();
1251        let storage = create_pq_storage().await;
1252        let u = rng.random_range(0..storage.len() as u32);
1253        let v = rng.random_range(0..storage.len() as u32);
1254        let dist1 = storage.dist_between(u, v);
1255        let dist2 = storage.dist_between(v, u);
1256        assert_eq!(dist1, dist2);
1257    }
1258
1259    #[tokio::test]
1260    async fn test_dist_calculator_from_id_matches_reconstructed_distance() {
1261        let mut rng = rand::rng();
1262        let storage = create_pq_storage().await;
1263        let u = rng.random_range(0..storage.len() as u32);
1264        let v = rng.random_range(0..storage.len() as u32);
1265        let codebook = storage
1266            .metadata
1267            .codebook
1268            .as_ref()
1269            .unwrap()
1270            .values()
1271            .as_primitive::<datatypes::Float32Type>();
1272        let pq_codes = storage.pq_code.values();
1273        let qu = get_centroids(
1274            codebook.values(),
1275            storage.metadata.nbits,
1276            storage.metadata.num_sub_vectors,
1277            storage.metadata.dimension,
1278            get_pq_code(
1279                pq_codes,
1280                storage.metadata.nbits,
1281                storage.metadata.num_sub_vectors,
1282                u,
1283            ),
1284        );
1285        let qv = get_centroids(
1286            codebook.values(),
1287            storage.metadata.nbits,
1288            storage.metadata.num_sub_vectors,
1289            storage.metadata.dimension,
1290            get_pq_code(
1291                pq_codes,
1292                storage.metadata.nbits,
1293                storage.metadata.num_sub_vectors,
1294                v,
1295            ),
1296        );
1297        let expected = storage.distance_type.func()(&qu, &qv);
1298        let dist_calc = storage.dist_calculator_from_id(u);
1299        assert!((dist_calc.distance(v) - expected).abs() < 1e-4);
1300        assert!((storage.dist_between(u, v) - expected).abs() < 1e-4);
1301    }
1302
1303    #[tokio::test]
1304    async fn test_remap_with_extra_column() {
1305        let storage = create_pq_storage_with_extra_column().await;
1306        let mut mapping = HashMap::new();
1307        for i in 0..TOTAL / 2 {
1308            mapping.insert(i as u64, Some((TOTAL + i) as u64));
1309        }
1310        for i in TOTAL / 2..TOTAL {
1311            mapping.insert(i as u64, None);
1312        }
1313        let new_storage = storage.remap(&mapping).unwrap();
1314        assert_eq!(new_storage.len(), TOTAL / 2);
1315        assert_eq!(new_storage.row_ids.len(), TOTAL / 2);
1316        for (i, row_id) in new_storage.row_ids().enumerate() {
1317            assert_eq!(*row_id, (TOTAL + i) as u64);
1318        }
1319        assert_eq!(new_storage.batch.num_columns(), 2);
1320        assert!(new_storage.batch.column_by_name(ROW_ID).is_some());
1321        assert!(new_storage.batch.column_by_name(PQ_CODE_COLUMN).is_some());
1322    }
1323}