Skip to main content

lance_index/vector/bq/
storage.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use arrow::array::AsArray;
8use arrow::datatypes::{Float16Type, Float32Type, Float64Type, UInt8Type, UInt64Type};
9use arrow_array::{
10    Array, FixedSizeListArray, Float32Array, RecordBatch, UInt8Array, UInt32Array, UInt64Array,
11};
12use arrow_schema::{DataType, SchemaRef};
13use async_trait::async_trait;
14use bytes::{Bytes, BytesMut};
15use deepsize::DeepSizeOf;
16use itertools::Itertools;
17use lance_arrow::{ArrowFloatType, FixedSizeListArrayExt, FloatArray, RecordBatchExt};
18use lance_core::{Error, ROW_ID, Result};
19use lance_file::previous::reader::FileReader as PreviousFileReader;
20use lance_linalg::distance::{DistanceType, Dot};
21use lance_linalg::simd::dist_table::{BATCH_SIZE, PERM0, PERM0_INVERSE};
22use lance_linalg::simd::{self};
23use lance_table::utils::LanceIteratorExtension;
24use num_traits::AsPrimitive;
25use prost::Message;
26use serde::{Deserialize, Serialize};
27
28use crate::frag_reuse::FragReuseIndex;
29use crate::pb;
30use crate::vector::bq::RQRotationType;
31use crate::vector::bq::rotation::apply_fast_rotation;
32use crate::vector::bq::transform::{ADD_FACTORS_COLUMN, SCALE_FACTORS_COLUMN};
33use crate::vector::pq::storage::transpose;
34use crate::vector::quantizer::{QuantizerMetadata, QuantizerStorage};
35use crate::vector::storage::{DistCalculator, VectorStore};
36
37pub const RABIT_METADATA_KEY: &str = "lance:rabit";
38pub const RABIT_CODE_COLUMN: &str = "_rabit_codes";
39pub const SEGMENT_LENGTH: usize = 4;
40pub const SEGMENT_NUM_CODES: usize = 1 << SEGMENT_LENGTH;
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct RabitQuantizationMetadata {
44    // this rotate matrix is large, and lance index would store all metadata in schema metadata,
45    // which is in JSON format, so we skip it in serialization and deserialization, and store it
46    // in the global buffer, which is a binary format (protobuf for now) for efficiency.
47    #[serde(skip)]
48    pub rotate_mat: Option<FixedSizeListArray>,
49    #[serde(default)]
50    pub rotate_mat_position: Option<u32>,
51    #[serde(default)]
52    pub fast_rotation_signs: Option<Vec<u8>>,
53    #[serde(default = "default_rotation_type_compat")]
54    pub rotation_type: RQRotationType,
55    #[serde(default)]
56    pub code_dim: u32,
57    pub num_bits: u8,
58    pub packed: bool,
59}
60
61fn default_rotation_type_compat() -> RQRotationType {
62    // Older metadata does not have this field and always used dense matrices.
63    RQRotationType::Matrix
64}
65
66impl DeepSizeOf for RabitQuantizationMetadata {
67    fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
68        self.rotate_mat
69            .as_ref()
70            .map(|inv_p| inv_p.get_array_memory_size())
71            .unwrap_or(0)
72            + self
73                .fast_rotation_signs
74                .as_ref()
75                .map(|signs| signs.len())
76                .unwrap_or(0)
77    }
78}
79
80#[async_trait]
81impl QuantizerMetadata for RabitQuantizationMetadata {
82    fn buffer_index(&self) -> Option<u32> {
83        match self.rotation_type {
84            RQRotationType::Matrix => self.rotate_mat_position,
85            RQRotationType::Fast => None,
86        }
87    }
88
89    fn set_buffer_index(&mut self, index: u32) {
90        self.rotate_mat_position = Some(index);
91    }
92
93    fn parse_buffer(&mut self, bytes: Bytes) -> Result<()> {
94        if self.rotation_type != RQRotationType::Matrix {
95            return Ok(());
96        }
97        debug_assert!(!bytes.is_empty());
98        let codebook_tensor: pb::Tensor = pb::Tensor::decode(bytes)?;
99        self.rotate_mat = Some(FixedSizeListArray::try_from(&codebook_tensor)?);
100        if self.code_dim == 0 {
101            self.code_dim = self
102                .rotate_mat
103                .as_ref()
104                .map(|rotate_mat| rotate_mat.len() as u32)
105                .unwrap_or(0);
106        }
107        Ok(())
108    }
109
110    fn extra_metadata(&self) -> Result<Option<Bytes>> {
111        match self.rotation_type {
112            RQRotationType::Matrix => {
113                if let Some(inv_p) = &self.rotate_mat {
114                    let inv_p_tensor = pb::Tensor::try_from(inv_p)?;
115                    let mut bytes = BytesMut::new();
116                    inv_p_tensor.encode(&mut bytes)?;
117                    Ok(Some(bytes.freeze()))
118                } else {
119                    Ok(None)
120                }
121            }
122            RQRotationType::Fast => Ok(None),
123        }
124    }
125
126    async fn load(reader: &PreviousFileReader) -> Result<Self> {
127        let metadata_str = reader
128            .schema()
129            .metadata
130            .get(RABIT_METADATA_KEY)
131            .ok_or(Error::index(format!(
132                "Reading Rabit metadata: metadata key {} not found",
133                RABIT_METADATA_KEY
134            )))?;
135        serde_json::from_str(metadata_str)
136            .map_err(|_| Error::index(format!("Failed to parse index metadata: {}", metadata_str)))
137    }
138}
139
140#[derive(Debug, Clone)]
141pub struct RabitQuantizationStorage {
142    metadata: RabitQuantizationMetadata,
143    batch: RecordBatch,
144    distance_type: DistanceType,
145
146    // helper fields
147    row_ids: UInt64Array,
148    codes: FixedSizeListArray,
149    add_factors: Float32Array,
150    scale_factors: Float32Array,
151}
152
153impl DeepSizeOf for RabitQuantizationStorage {
154    fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
155        self.metadata.deep_size_of_children(context) + self.batch.get_array_memory_size()
156    }
157}
158
159impl RabitQuantizationStorage {
160    fn rotate_query_vector_dense<T: ArrowFloatType>(
161        rotate_mat: &FixedSizeListArray,
162        qr: &dyn Array,
163    ) -> Vec<f32>
164    where
165        T::Native: Dot,
166    {
167        let d = qr.len();
168        let code_dim = rotate_mat.len();
169        let rotate_mat = rotate_mat
170            .values()
171            .as_any()
172            .downcast_ref::<T::ArrayType>()
173            .unwrap()
174            .as_slice();
175
176        let qr = qr
177            .as_any()
178            .downcast_ref::<T::ArrayType>()
179            .unwrap()
180            .as_slice();
181
182        rotate_mat
183            .chunks_exact(code_dim)
184            .map(|chunk| lance_linalg::distance::dot(&chunk[..d], qr))
185            .collect()
186    }
187
188    fn rotate_query_vector_fast<T: ArrowFloatType>(
189        code_dim: usize,
190        signs: &[u8],
191        qr: &dyn Array,
192    ) -> Vec<f32>
193    where
194        T::Native: AsPrimitive<f32>,
195    {
196        let qr = qr
197            .as_any()
198            .downcast_ref::<T::ArrayType>()
199            .unwrap()
200            .as_slice();
201
202        let mut output = vec![0.0f32; code_dim];
203        apply_fast_rotation(qr, &mut output, signs);
204        output
205    }
206}
207
208pub struct RabitDistCalculator<'a> {
209    dim: usize,
210    // num_bits is the number of bits per dimension,
211    // it's always 1 for now
212    num_bits: u8,
213    // n * d * num_bits / 8 bytes
214    codes: &'a [u8],
215    // this is a flattened 2D array of size d/4 * 16,
216    // we split the query codes into d/4 chunks, each chunk is with 4 elements,
217    // then dist_table[i][j] is the distance between the i-th query code and the code j
218    dist_table: Vec<f32>,
219    add_factors: &'a [f32],
220    scale_factors: &'a [f32],
221    query_factor: f32,
222
223    sum_q: f32,
224    sqrt_d: f32,
225}
226
227impl<'a> RabitDistCalculator<'a> {
228    #[allow(clippy::too_many_arguments)]
229    pub fn new(
230        dim: usize,
231        num_bits: u8,
232        dist_table: Vec<f32>,
233        sum_q: f32,
234        codes: &'a [u8],
235        add_factors: &'a [f32],
236        scale_factors: &'a [f32],
237        query_factor: f32,
238    ) -> Self {
239        Self {
240            dim,
241            num_bits,
242            codes,
243            dist_table,
244            add_factors,
245            scale_factors,
246            query_factor,
247            sqrt_d: (dim as f32 * num_bits as f32).sqrt(),
248            sum_q,
249        }
250    }
251}
252
253#[inline]
254fn lowbit(x: usize) -> usize {
255    1 << x.trailing_zeros()
256}
257
258#[inline]
259pub fn build_dist_table_direct<T: ArrowFloatType>(qc: &[T::Native]) -> Vec<f32>
260where
261    T::Native: AsPrimitive<f32>,
262{
263    // every 4 bits (SEGMENT_LENGTH) is a segment, and we need to compute the distance between the segment and all the codes
264    // so there are dim/4 segments, and the number of codes is 16 (2^{SEGMENT_LENGTH}),
265    // so we have dim/4 * 16 = dim * 4 elements in the dist_table
266    let mut dist_table = vec![0.0; qc.len() * 4];
267    qc.chunks_exact(SEGMENT_LENGTH)
268        .zip(dist_table.chunks_exact_mut(SEGMENT_NUM_CODES))
269        .for_each(|(sub_vec, dist_table)| build_dist_table_for_subvec::<T>(sub_vec, dist_table));
270    dist_table
271}
272
273#[inline(always)]
274fn build_dist_table_for_subvec<T: ArrowFloatType>(sub_vec: &[T::Native], dist_table: &mut [f32])
275where
276    T::Native: AsPrimitive<f32>,
277{
278    // skip 0 because it's always 0
279    (1..SEGMENT_NUM_CODES).for_each(|j| {
280        // this is a little bit tricky,
281        // j represents a subset of 4 bits, that if the i-th bit of `j` is 1,
282        // then we need to add the distance of the i-th dim of the segment.
283        // but we don't need to check all bits of `j`,
284        // because `j` = `j - lowbit(j)` + `lowbit(j)`,
285        // where `j-lowbit(j)` is less than `j`,
286        // which means dist_table[j-lowbit(j)] is already computed,
287        // and we can use it to compute dist_table[j]
288        // for example, if j = 0b1010, then j - lowbit(j) = 0b1000,
289        // and dist_table[0b1000] is already computed,
290        // so dist_table[0b1010] = dist_table[0b1000] + sub_vec[LOWBIT_IDX[0b1010]];
291        // where lowbit(0b1010) = 0b10, LOWBIT_IDX[0b1010] = LOWBIT_IDX[0b10] = 1.
292        dist_table[j] = dist_table[j - lowbit(j)] + sub_vec[LOWBIT_IDX[j]].as_();
293    })
294}
295
296// Quantize the distance table to u8, map distance `d` to `(d-qmin) * 255 / (qmax-qmin)`
297#[inline]
298fn quantize_dist_table(dist_table: &[f32]) -> (f32, f32, Vec<u8>) {
299    let (qmin, qmax) = dist_table
300        .iter()
301        .cloned()
302        .minmax_by(|a, b| a.total_cmp(b))
303        .into_option()
304        .unwrap();
305    // this happens if the query is all zeros
306    if qmin == qmax {
307        return (qmin, qmax, vec![0; dist_table.len()]);
308    }
309    let factor = 255.0 / (qmax - qmin);
310    let quantized_dist_table = dist_table
311        .iter()
312        .map(|&d| ((d - qmin) * factor).round() as u8)
313        .collect();
314
315    (qmin, qmax, quantized_dist_table)
316}
317
318#[inline]
319fn compute_rq_distance_flat(
320    dist_table: &[f32],
321    codes: &[u8],
322    offset: usize,
323    length: usize,
324    dists: &mut [f32],
325) {
326    let d = dist_table.len() / 4;
327    let code_len = d / u8::BITS as usize;
328    let codes = &codes[offset * code_len..(offset + length) * code_len];
329    let dists = &mut dists[offset..offset + length];
330
331    for (sub_vec_idx, codes) in codes.chunks_exact(length).enumerate() {
332        let current_dist_table = &dist_table
333            [sub_vec_idx * 2 * SEGMENT_NUM_CODES..(sub_vec_idx * 2 + 1) * SEGMENT_NUM_CODES];
334        let next_dist_table = &dist_table
335            [(sub_vec_idx * 2 + 1) * SEGMENT_NUM_CODES..(sub_vec_idx * 2 + 2) * SEGMENT_NUM_CODES];
336
337        codes.iter().zip(dists.iter_mut()).for_each(|(code, dist)| {
338            let current_code = (code & 0x0F) as usize;
339            let next_code = (code >> 4) as usize;
340            *dist += current_dist_table[current_code] + next_dist_table[next_code];
341        });
342    }
343}
344
345impl DistCalculator for RabitDistCalculator<'_> {
346    #[inline(always)]
347    fn distance(&self, id: u32) -> f32 {
348        let id = id as usize;
349        let code_len = self.dim * (self.num_bits as usize) / u8::BITS as usize;
350        let num_vectors = self.codes.len() / code_len;
351        let code = get_rq_code(self.codes, id, num_vectors, code_len);
352        let dist = code
353            .zip(self.dist_table.chunks_exact(SEGMENT_NUM_CODES).tuples())
354            .map(|(code_byte, (dist_table, next_dist_table))| {
355                // code is a bit vector, we iterate over 8 bits at a time,
356                // every 4 bits is a sub-vector, we need to extract the bits
357                let current_code = (code_byte & 0x0F) as usize;
358                let next_code = (code_byte >> 4) as usize;
359                dist_table[current_code] + next_dist_table[next_code]
360            })
361            .sum::<f32>();
362
363        // distance between quantized vector and query vector
364        let dist_vq_qr = (2.0 * dist - self.sum_q) / self.sqrt_d;
365        dist_vq_qr * self.scale_factors[id] + self.add_factors[id] + self.query_factor
366    }
367
368    #[inline(always)]
369    fn distance_all(&self, _: usize) -> Vec<f32> {
370        let code_len = self.dim * (self.num_bits as usize) / u8::BITS as usize;
371        let n = self.codes.len() / code_len;
372        if n == 0 {
373            return Vec::new();
374        }
375
376        let mut dists = vec![0.0; n];
377
378        let (qmin, qmax, quantized_dists_table) = quantize_dist_table(&self.dist_table);
379        let mut quantized_dists = vec![0; n];
380
381        let remainder = n % BATCH_SIZE;
382        simd::dist_table::sum_4bit_dist_table(
383            n - remainder,
384            code_len,
385            self.codes,
386            &quantized_dists_table,
387            &mut quantized_dists,
388        );
389        if remainder > 0 {
390            compute_rq_distance_flat(
391                &self.dist_table,
392                self.codes,
393                n - remainder,
394                remainder,
395                &mut dists,
396            );
397        }
398
399        let range = (qmax - qmin) / 255.0;
400        let num_tables = quantized_dists_table.len() / 16;
401        let sum_min = num_tables as f32 * qmin;
402        dists
403            .iter_mut()
404            .take(n - remainder)
405            .zip(quantized_dists.into_iter().take(n - remainder))
406            .for_each(|(dist, q_dist)| {
407                *dist = (q_dist as f32) * range + sum_min;
408            });
409
410        dists
411            .into_iter()
412            .enumerate()
413            .map(|(id, dist)| {
414                let dist_vq_qr = (2.0 * dist - self.sum_q) / self.sqrt_d;
415                dist_vq_qr * self.scale_factors[id] + self.add_factors[id] + self.query_factor
416            })
417            .collect()
418    }
419}
420
421impl VectorStore for RabitQuantizationStorage {
422    type DistanceCalculator<'a> = RabitDistCalculator<'a>;
423
424    fn as_any(&self) -> &dyn std::any::Any {
425        self
426    }
427
428    fn schema(&self) -> &SchemaRef {
429        self.batch.schema_ref()
430    }
431
432    fn to_batches(&self) -> Result<impl Iterator<Item = RecordBatch> + Send> {
433        Ok(std::iter::once(self.batch.clone()))
434    }
435
436    fn append_batch(&self, _batch: RecordBatch, _vector_column: &str) -> Result<Self> {
437        unimplemented!("RabitQ does not support append_batch")
438    }
439
440    fn len(&self) -> usize {
441        self.batch.num_rows()
442    }
443
444    fn row_id(&self, id: u32) -> u64 {
445        self.row_ids.value(id as usize)
446    }
447
448    fn row_ids(&self) -> impl Iterator<Item = &u64> {
449        self.row_ids.values().iter()
450    }
451
452    fn distance_type(&self) -> DistanceType {
453        self.distance_type
454    }
455
456    // qr = (q-c)
457    #[inline(never)]
458    fn dist_calculator(&self, qr: Arc<dyn Array>, dist_q_c: f32) -> Self::DistanceCalculator<'_> {
459        let codes = self.codes.values().as_primitive::<UInt8Type>().values();
460        let code_dim = if self.metadata.code_dim > 0 {
461            self.metadata.code_dim as usize
462        } else {
463            self.metadata
464                .rotate_mat
465                .as_ref()
466                .map(|rotate_mat| rotate_mat.len())
467                .unwrap_or_default()
468        };
469
470        let rotated_qr = match self.metadata.rotation_type {
471            RQRotationType::Matrix => {
472                let rotate_mat = self
473                    .metadata
474                    .rotate_mat
475                    .as_ref()
476                    .expect("RabitQ dense rotation metadata not loaded");
477
478                match rotate_mat.value_type() {
479                    DataType::Float16 => {
480                        Self::rotate_query_vector_dense::<Float16Type>(rotate_mat, &qr)
481                    }
482                    DataType::Float32 => {
483                        Self::rotate_query_vector_dense::<Float32Type>(rotate_mat, &qr)
484                    }
485                    DataType::Float64 => {
486                        Self::rotate_query_vector_dense::<Float64Type>(rotate_mat, &qr)
487                    }
488                    dt => unimplemented!("RabitQ does not support data type: {}", dt),
489                }
490            }
491            RQRotationType::Fast => {
492                let signs = self
493                    .metadata
494                    .fast_rotation_signs
495                    .as_ref()
496                    .expect("RabitQ fast rotation metadata not loaded");
497                match qr.data_type() {
498                    DataType::Float16 => {
499                        Self::rotate_query_vector_fast::<Float16Type>(code_dim, signs, &qr)
500                    }
501                    DataType::Float32 => {
502                        Self::rotate_query_vector_fast::<Float32Type>(code_dim, signs, &qr)
503                    }
504                    DataType::Float64 => {
505                        Self::rotate_query_vector_fast::<Float64Type>(code_dim, signs, &qr)
506                    }
507                    dt => unimplemented!("RabitQ does not support data type: {}", dt),
508                }
509            }
510        };
511
512        let dist_table = build_dist_table_direct::<Float32Type>(&rotated_qr);
513        let sum_q = rotated_qr.into_iter().sum();
514
515        let q_factor = match self.distance_type {
516            DistanceType::L2 => dist_q_c,
517            DistanceType::Cosine | DistanceType::Dot => dist_q_c - 1.0,
518            _ => unimplemented!(
519                "RabitQ does not support distance type: {}",
520                self.distance_type
521            ),
522        };
523        RabitDistCalculator::new(
524            qr.len(),
525            self.metadata.num_bits,
526            dist_table,
527            sum_q,
528            codes,
529            self.add_factors.values(),
530            self.scale_factors.values(),
531            q_factor,
532        )
533    }
534
535    // TODO: implement this
536    // This method is required for HNSW, we can't support HNSW_RABIT before this is implemented
537    fn dist_calculator_from_id(&self, _: u32) -> Self::DistanceCalculator<'_> {
538        unimplemented!("RabitQ does not support dist_calculator_from_id")
539    }
540}
541
542const LOWBIT_IDX: [usize; 16] = {
543    let mut array = [0; 16];
544    let mut i = 1;
545    while i < 16 {
546        array[i] = i.trailing_zeros() as usize;
547        i += 1;
548    }
549    array
550};
551
552fn get_column(
553    quantization_code: &[u8],
554    code_len: usize,
555    row: usize,
556    col_idx: usize,
557    codes: &mut [u8; 32],
558) {
559    for (i, code) in codes.iter_mut().enumerate() {
560        let vec_idx = row + i;
561        *code = quantization_code[vec_idx * code_len + col_idx];
562    }
563}
564
565pub fn pack_codes(codes: &FixedSizeListArray) -> FixedSizeListArray {
566    let code_len = codes.value_length() as usize;
567
568    // round up num of vectors to multiple of batch size (32)
569    let num_blocks = codes.len() / BATCH_SIZE;
570    let num_packed_vectors = num_blocks * BATCH_SIZE;
571
572    // calculate total size for packed blocks
573    // we pack each 32 vectors into a block, each block contains 2 codes (1byte) of each vector
574    // so every 32 vectors would produce code_len blocks
575    // the low 16 bytes of each block is the codes for the low 4 bits of each vector
576    // the high 16 bytes of each block is the codes for the high 4 bits of each vector
577    let mut blocks = vec![0u8; codes.values().len()];
578
579    let codes_values = codes
580        .slice(0, num_packed_vectors)
581        .values()
582        .as_primitive::<UInt8Type>()
583        .clone();
584    let codes_values = codes_values.values();
585
586    // Pack codes batch by batch
587    // Each batch contains codes for 32 vectors
588    let mut col = [0u8; 32];
589    let mut col_0 = [0u8; 32]; // lower 4 bits
590    let mut col_1 = [0u8; 32]; // higher 4 bits
591    for row in (0..num_packed_vectors).step_by(BATCH_SIZE) {
592        // Get quantization codes for each column for each batch
593        // i.e., we get the codes for 8 dims of 32 vectors and reorganize the data layout
594        // based on the shuffle SIMD instruction used during querying
595        for i in 0..code_len {
596            get_column(codes_values, code_len, row, i, &mut col);
597
598            for j in 0..32 {
599                col_0[j] = col[j] & 0xF;
600                col_1[j] = col[j] >> 4;
601            }
602
603            let block_offset = (row / BATCH_SIZE) * code_len * BATCH_SIZE + i * BATCH_SIZE;
604            for j in 0..16 {
605                // The lower 4 bits represent vector 0 to 15
606                // The upper 4 bits represent vector 16 to 31
607                let val0 = col_0[PERM0[j]] | (col_0[PERM0[j] + 16] << 4);
608                let val1 = col_1[PERM0[j]] | (col_1[PERM0[j] + 16] << 4);
609                blocks[block_offset + j] = val0;
610                blocks[block_offset + j + 16] = val1;
611            }
612        }
613    }
614
615    // for the left codes, transpose them for better cache locality
616    let transposed_codes = transpose(
617        &codes.values().as_primitive::<UInt8Type>().slice(
618            num_packed_vectors * code_len,
619            (codes.len() - num_packed_vectors) * code_len,
620        ),
621        codes.len() - num_packed_vectors,
622        code_len,
623    );
624
625    let offset = codes.values().len() - transposed_codes.len();
626    for (i, v) in transposed_codes.values().iter().enumerate() {
627        blocks[offset + i] = *v;
628    }
629
630    assert_eq!(blocks.len(), codes.values().len());
631    FixedSizeListArray::try_new_from_values(UInt8Array::from(blocks), code_len as i32).unwrap()
632}
633
634// Inverse of pack_codes
635pub fn unpack_codes(codes: &FixedSizeListArray) -> FixedSizeListArray {
636    let code_len = codes.value_length() as usize;
637    let num_vectors = codes.len();
638
639    // Calculate number of complete batches
640    let num_blocks = num_vectors / BATCH_SIZE;
641    let num_packed_vectors = num_blocks * BATCH_SIZE;
642
643    let mut unpacked = vec![0u8; codes.values().len()];
644
645    let codes_values = codes.values().as_primitive::<UInt8Type>().values();
646
647    // Unpack complete batches
648    for batch_idx in 0..num_blocks {
649        let block_start = batch_idx * code_len * BATCH_SIZE;
650
651        for i in 0..code_len {
652            let block_offset = block_start + i * BATCH_SIZE;
653            let block = &codes_values[block_offset..block_offset + BATCH_SIZE];
654
655            // Reverse the permutation
656            for j in 0..16 {
657                let val0 = block[j];
658                let val1 = block[j + 16];
659
660                let low_0 = val0 & 0xF;
661                let high_0 = val0 >> 4;
662                let low_1 = val1 & 0xF;
663                let high_1 = val1 >> 4;
664
665                let vec_idx_0 = batch_idx * BATCH_SIZE + PERM0[j];
666                let vec_idx_1 = batch_idx * BATCH_SIZE + PERM0[j] + 16;
667
668                unpacked[vec_idx_0 * code_len + i] = low_0 | (low_1 << 4);
669                unpacked[vec_idx_1 * code_len + i] = high_0 | (high_1 << 4);
670            }
671        }
672    }
673
674    // Transpose back the remainder
675    if num_packed_vectors < num_vectors {
676        let remainder = num_vectors - num_packed_vectors;
677        let offset = num_packed_vectors * code_len;
678        let transposed_data = &codes_values[offset..];
679
680        // Transpose from column-major back to row-major
681        for row in 0..remainder {
682            for col in 0..code_len {
683                unpacked[offset + row * code_len + col] = transposed_data[col * remainder + row];
684            }
685        }
686    }
687
688    FixedSizeListArray::try_new_from_values(UInt8Array::from(unpacked), code_len as i32).unwrap()
689}
690
691#[async_trait]
692impl QuantizerStorage for RabitQuantizationStorage {
693    type Metadata = RabitQuantizationMetadata;
694
695    fn try_from_batch(
696        batch: RecordBatch,
697        metadata: &Self::Metadata,
698        distance_type: DistanceType,
699        _fri: Option<Arc<FragReuseIndex>>,
700    ) -> Result<Self> {
701        let row_ids = batch[ROW_ID].as_primitive::<UInt64Type>().clone();
702        let codes = batch[RABIT_CODE_COLUMN].as_fixed_size_list().clone();
703        let add_factors = batch[ADD_FACTORS_COLUMN]
704            .as_primitive::<Float32Type>()
705            .clone();
706        let scale_factors = batch[SCALE_FACTORS_COLUMN]
707            .as_primitive::<Float32Type>()
708            .clone();
709
710        let (batch, codes) = if !metadata.packed {
711            let codes = pack_codes(&codes);
712            let batch = batch.replace_column_by_name(RABIT_CODE_COLUMN, Arc::new(codes))?;
713            let codes = batch[RABIT_CODE_COLUMN].as_fixed_size_list().clone();
714            (batch, codes)
715        } else {
716            (batch, codes)
717        };
718
719        let mut metadata = metadata.clone();
720        metadata.packed = true;
721
722        Ok(Self {
723            metadata,
724            batch,
725            distance_type,
726            row_ids,
727            codes,
728            add_factors,
729            scale_factors,
730        })
731    }
732
733    fn metadata(&self) -> &Self::Metadata {
734        &self.metadata
735    }
736
737    async fn load_partition(
738        reader: &PreviousFileReader,
739        range: std::ops::Range<usize>,
740        distance_type: DistanceType,
741        metadata: &Self::Metadata,
742        frag_reuse_index: Option<Arc<FragReuseIndex>>,
743    ) -> Result<Self> {
744        let schema = reader.schema();
745        let batch = reader.read_range(range, schema).await?;
746        Self::try_from_batch(batch, metadata, distance_type, frag_reuse_index)
747    }
748
749    fn remap(&self, mapping: &HashMap<u64, Option<u64>>) -> Result<Self> {
750        let num_vectors = self.codes.len();
751        let num_code_bytes = self.codes.value_length() as usize;
752        let codes = self.codes.values().as_primitive::<UInt8Type>().values();
753        let mut indices = Vec::with_capacity(num_vectors);
754        let mut new_row_ids = Vec::with_capacity(num_vectors);
755        let mut new_codes = Vec::with_capacity(codes.len());
756
757        let row_ids = self.row_ids.values();
758        for (i, row_id) in row_ids.iter().enumerate() {
759            match mapping.get(row_id) {
760                Some(Some(new_id)) => {
761                    indices.push(i as u32);
762                    new_row_ids.push(*new_id);
763                    new_codes.extend(get_rq_code(codes, i, num_vectors, num_code_bytes));
764                }
765                Some(None) => {}
766                None => {
767                    indices.push(i as u32);
768                    new_row_ids.push(*row_id);
769                    new_codes.extend(get_rq_code(codes, i, num_vectors, num_code_bytes));
770                }
771            }
772        }
773
774        let new_row_ids = UInt64Array::from(new_row_ids);
775        let new_codes = FixedSizeListArray::try_new_from_values(
776            UInt8Array::from(new_codes),
777            num_code_bytes as i32,
778        )?;
779        let batch = if new_row_ids.is_empty() {
780            RecordBatch::new_empty(self.schema().clone())
781        } else {
782            let codes = Arc::new(pack_codes(&new_codes));
783            self.batch
784                .take(&UInt32Array::from(indices))?
785                .replace_column_by_name(ROW_ID, Arc::new(new_row_ids.clone()))?
786                .replace_column_by_name(RABIT_CODE_COLUMN, codes)?
787        };
788        let codes = batch[RABIT_CODE_COLUMN].as_fixed_size_list().clone();
789
790        Ok(Self {
791            metadata: self.metadata.clone(),
792            distance_type: self.distance_type,
793            batch,
794            codes,
795            add_factors: self.add_factors.clone(),
796            scale_factors: self.scale_factors.clone(),
797            row_ids: new_row_ids,
798        })
799    }
800}
801
802#[inline]
803fn get_rq_code(
804    codes: &[u8],
805    id: usize,
806    num_vectors: usize,
807    num_code_bytes: usize,
808) -> impl Iterator<Item = u8> + '_ {
809    let remainder = num_vectors % BATCH_SIZE;
810
811    if id < num_vectors - remainder {
812        // the codes are packed
813        let codes = &codes[id / BATCH_SIZE * BATCH_SIZE * num_code_bytes
814            ..(id / BATCH_SIZE + 1) * BATCH_SIZE * num_code_bytes];
815
816        let id_in_batch = id % BATCH_SIZE;
817        if id_in_batch < 16 {
818            let idx = PERM0_INVERSE[id_in_batch];
819            codes
820                .chunks_exact(BATCH_SIZE)
821                .map(|block| (block[idx] & 0xF) | (block[idx + 16] << 4))
822                .exact_size(num_code_bytes)
823                .collect_vec()
824                .into_iter()
825        } else {
826            let idx = PERM0_INVERSE[id_in_batch - 16];
827            codes
828                .chunks_exact(BATCH_SIZE)
829                .map(|block| (block[idx] >> 4) | (block[idx + 16] & 0xF0))
830                .exact_size(num_code_bytes)
831                .collect_vec()
832                .into_iter()
833        }
834    } else {
835        let id = id - (num_vectors - remainder);
836        let codes = &codes[(num_vectors - remainder) * num_code_bytes..];
837        codes
838            .iter()
839            .skip(id)
840            .step_by(remainder)
841            .copied()
842            .exact_size(num_code_bytes)
843            .collect_vec()
844            .into_iter()
845    }
846}
847
848#[cfg(test)]
849mod tests {
850    use super::*;
851
852    fn build_dist_table_not_optimized<T: ArrowFloatType>(
853        sub_vec: &[T::Native],
854        dist_table: &mut [f32],
855    ) where
856        T::Native: AsPrimitive<f32>,
857    {
858        for (j, dist) in dist_table.iter_mut().enumerate().take(SEGMENT_NUM_CODES) {
859            for (k, v) in sub_vec.iter().enumerate().take(SEGMENT_LENGTH) {
860                if j & (1 << k) != 0 {
861                    *dist += v.as_();
862                }
863            }
864        }
865    }
866
867    #[test]
868    fn test_build_dist_table_not_optimized() {
869        let sub_vec = vec![1.0, 2.0, 3.0, 4.0];
870        let mut expected = vec![0.0; SEGMENT_NUM_CODES];
871        build_dist_table_not_optimized::<Float32Type>(&sub_vec, &mut expected);
872        let mut dist_table = vec![0.0; SEGMENT_NUM_CODES];
873        build_dist_table_for_subvec::<Float32Type>(&sub_vec, &mut dist_table);
874        assert_eq!(dist_table, expected);
875    }
876
877    #[test]
878    fn test_pack_unpack_codes() {
879        // Test with multiple batch sizes to cover both packed and transposed sections
880        for num_vectors in [10, 32, 50, 64, 100] {
881            let code_len = 8;
882
883            // Create test data with known pattern
884            let mut codes_data = Vec::new();
885            for i in 0..num_vectors {
886                for j in 0..code_len {
887                    codes_data.push((i * code_len + j) as u8);
888                }
889            }
890
891            let original_codes = FixedSizeListArray::try_new_from_values(
892                UInt8Array::from(codes_data.clone()),
893                code_len,
894            )
895            .unwrap();
896
897            // Pack and then unpack
898            let packed = pack_codes(&original_codes);
899            let unpacked = unpack_codes(&packed);
900
901            // Verify they match
902            assert_eq!(original_codes.len(), unpacked.len());
903            assert_eq!(original_codes.value_length(), unpacked.value_length());
904
905            let original_values = original_codes.values().as_primitive::<UInt8Type>().values();
906            let unpacked_values = unpacked.values().as_primitive::<UInt8Type>().values();
907
908            assert_eq!(
909                original_values, unpacked_values,
910                "Mismatch for num_vectors={}",
911                num_vectors
912            );
913        }
914    }
915}