1use 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 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 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 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#[derive(Clone, Debug)]
157pub struct ProductQuantizationStorage {
158 metadata: ProductQuantizationMetadata,
159 distance_type: DistanceType,
160 batch: RecordBatch,
161
162 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 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(), 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 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 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 pub async fn write_partition(
471 &self,
472 writer: &mut PreviousFileWriter<ManifestDescribing>,
473 ) -> Result<usize> {
474 let batch_size: usize = 10240; 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 let codebook = match &metadata.codebook {
526 Some(codebook) => codebook.clone(),
527 None => {
528 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 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 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 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 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
779pub 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 debug_assert!(
892 false,
893 "cosine distance should be converted to normalized L2 distance"
894 );
895 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 = ¢roids[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 = ¢roids[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 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}