Skip to main content

hermes_core/structures/vector/quantization/
rabitq.rs

1//! RaBitQ: Randomized Binary Quantization for Dense Vector Search
2//!
3//! Implementation of the RaBitQ algorithm from SIGMOD 2024:
4//! "RaBitQ: Quantizing High-Dimensional Vectors with a Theoretical Error Bound
5//! for Approximate Nearest Neighbor Search"
6//!
7//! Key features:
8//! - 32x compression (D-dimensional float32 → D-bit binary + 2 floats)
9//! - Theoretical error bound for distance estimation
10//! - SIMD-accelerated distance computation via LUT
11//! - Asymmetric quantization (binary data, 4-bit query)
12
13use rand::prelude::*;
14use serde::{Deserialize, Serialize};
15
16use super::super::ivf::cluster::QuantizedCode;
17use super::Quantizer;
18
19#[cfg(target_arch = "aarch64")]
20#[allow(unused_imports)]
21use std::arch::aarch64::*;
22
23#[cfg(target_arch = "x86_64")]
24#[allow(unused_imports)]
25use std::arch::x86_64::*;
26
27/// Configuration for RaBitQ quantization
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct RaBitQConfig {
30    /// Dimensionality of vectors
31    pub dim: usize,
32    /// Number of bits for query quantization (typically 4)
33    pub query_bits: u8,
34    /// Random seed for reproducible orthogonal matrix
35    pub seed: u64,
36    /// Extended RaBitQ: extra magnitude bits per dimension (0-7).
37    ///
38    /// 0 = classic 1-bit RaBitQ (sign only). N > 0 stores an additional
39    /// N-bit magnitude refinement per dimension (total N+1 bits/dim),
40    /// giving much tighter distance estimates — 3-5 extra bits typically
41    /// allow shrinking the exact-rerank pool (`rerank_factor`) by 2-3x,
42    /// cutting raw-vector I/O in disk-resident indexes.
43    ///
44    /// Follows the Extended RaBitQ line (Gao & Long, 2024; adopted by
45    /// NVIDIA cuVS IVF-RaBitQ): uniform magnitude quantization of the
46    /// rotated unit residual with a per-vector scale.
47    #[serde(default)]
48    pub ex_bits: u8,
49}
50
51impl RaBitQConfig {
52    pub fn new(dim: usize) -> Self {
53        Self {
54            dim,
55            query_bits: 4,
56            seed: 42,
57            ex_bits: 0,
58        }
59    }
60
61    pub fn with_seed(mut self, seed: u64) -> Self {
62        self.seed = seed;
63        self
64    }
65
66    /// Set total bits per dimension (1 = classic binary, 2-8 = extended).
67    pub fn with_bits(mut self, total_bits: u8) -> Self {
68        self.ex_bits = total_bits.clamp(1, 8) - 1;
69        self
70    }
71}
72
73/// Quantized representation of a single vector
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct QuantizedVector {
76    /// Binary quantization code (D bits packed into bytes)
77    pub bits: Vec<u8>,
78    /// Distance from original vector to centroid: ||o_raw - c||
79    pub dist_to_centroid: f32,
80    /// Dot product of normalized vector with its quantized form: <o, o_bar>
81    pub self_dot: f32,
82    /// Number of 1-bits in the binary code (for fast computation)
83    pub popcount: u32,
84    /// Extended RaBitQ magnitude codes: `ex_bits` per dimension, packed
85    /// LSB-first. Empty for classic 1-bit codes (serde default keeps old
86    /// segments readable).
87    #[serde(default, skip_serializing_if = "Vec::is_empty")]
88    pub ex_code: Vec<u8>,
89    /// Per-vector magnitude scale: max |transformed_i| (extended only)
90    #[serde(default)]
91    pub ex_scale: f32,
92    /// Norm of the reconstructed vector v̂ (extended only) — used to
93    /// normalize the refined inner-product estimate.
94    #[serde(default)]
95    pub ex_norm: f32,
96}
97
98impl QuantizedCode for QuantizedVector {
99    fn size_bytes(&self) -> usize {
100        // bits + dist_to_centroid + self_dot + popcount (+ extended payload)
101        self.bits.len() + 4 + 4 + 4 + self.ex_code.len() + 8
102    }
103}
104
105/// Pre-computed query representation for fast distance estimation
106#[derive(Debug, Clone)]
107pub struct QuantizedQuery {
108    /// 4-bit scalar quantized query (packed, 2 values per byte)
109    pub quantized: Vec<u8>,
110    /// Distance from query to centroid: ||q_raw - c||
111    pub dist_to_centroid: f32,
112    /// Lower bound of quantization range
113    pub lower: f32,
114    /// Width of quantization range (upper - lower)
115    pub width: f32,
116    /// Sum of all quantized values
117    pub sum: u32,
118    /// Look-up tables for fast dot product (16 entries per 4-bit sub-segment)
119    pub luts: Vec<[u16; 16]>,
120    /// Rotated, normalized query (full precision) — used by the extended
121    /// multi-bit estimator for exact dot products against reconstructed codes.
122    pub transformed: Vec<f32>,
123}
124
125/// RaBitQ codebook (random transform parameters)
126///
127/// Trained once, shared across all segments for merge compatibility.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct RaBitQCodebook {
130    /// Configuration
131    pub config: RaBitQConfig,
132    /// Random signs for transform (+1 or -1)
133    pub random_signs: Vec<i8>,
134    /// Random permutation for transform
135    pub random_perm: Vec<u32>,
136    /// Version for merge compatibility checking
137    pub version: u64,
138}
139
140impl RaBitQCodebook {
141    pub(crate) fn validate(&self) -> Result<(), String> {
142        let dim = self.config.dim;
143        if dim == 0 {
144            return Err("RaBitQ dimension must be greater than zero".to_string());
145        }
146        if !(1..=8).contains(&self.config.query_bits) {
147            return Err(format!(
148                "RaBitQ query_bits must be in 1..=8, got {}",
149                self.config.query_bits
150            ));
151        }
152        if self.config.ex_bits > 7 {
153            return Err(format!("RaBitQ ex_bits {} exceeds 7", self.config.ex_bits));
154        }
155        if self.random_signs.len() != dim || self.random_perm.len() != dim {
156            return Err(format!(
157                "RaBitQ transform lengths ({}, {}) do not match dimension {dim}",
158                self.random_signs.len(),
159                self.random_perm.len()
160            ));
161        }
162        if self
163            .random_signs
164            .iter()
165            .any(|&sign| sign != -1 && sign != 1)
166        {
167            return Err("RaBitQ random signs contain a value other than -1 or 1".to_string());
168        }
169        let mut seen = vec![false; dim];
170        for &source in &self.random_perm {
171            let source = source as usize;
172            if source >= dim || seen[source] {
173                return Err("RaBitQ random permutation is invalid".to_string());
174            }
175            seen[source] = true;
176        }
177        Ok(())
178    }
179
180    pub(crate) fn validate_vector(&self, vector: &QuantizedVector) -> Result<(), String> {
181        let expected_bits = self.config.dim.div_ceil(8);
182        if vector.bits.len() != expected_bits {
183            return Err(format!(
184                "RaBitQ code has {} sign bytes, expected {expected_bits}",
185                vector.bits.len()
186            ));
187        }
188        let expected_extended = self
189            .config
190            .dim
191            .checked_mul(self.config.ex_bits as usize)
192            .ok_or_else(|| "RaBitQ extended code size overflow".to_string())?
193            .div_ceil(8);
194        if vector.ex_code.len() != expected_extended {
195            return Err(format!(
196                "RaBitQ code has {} extended bytes, expected {expected_extended}",
197                vector.ex_code.len()
198            ));
199        }
200        if !vector.dist_to_centroid.is_finite()
201            || !vector.self_dot.is_finite()
202            || !vector.ex_scale.is_finite()
203            || !vector.ex_norm.is_finite()
204        {
205            return Err("RaBitQ code contains non-finite metadata".to_string());
206        }
207        if vector.dist_to_centroid < 0.0 || vector.ex_scale < 0.0 || vector.ex_norm < 0.0 {
208            return Err("RaBitQ code contains negative norm/scale metadata".to_string());
209        }
210        let actual_popcount: u32 = vector.bits.iter().map(|byte| byte.count_ones()).sum();
211        if vector.popcount != actual_popcount || vector.popcount as usize > self.config.dim {
212            return Err(format!(
213                "RaBitQ code popcount {} does not match packed bits {actual_popcount}",
214                vector.popcount
215            ));
216        }
217        let padding_bits = expected_bits
218            .checked_mul(8)
219            .and_then(|bits| bits.checked_sub(self.config.dim))
220            .ok_or_else(|| "RaBitQ padding size overflow".to_string())?;
221        if padding_bits > 0
222            && vector
223                .bits
224                .last()
225                .is_some_and(|last| last >> (8 - padding_bits) != 0)
226        {
227            return Err("RaBitQ code has non-zero padding bits".to_string());
228        }
229        Ok(())
230    }
231
232    /// Create a new RaBitQ codebook with random transform
233    pub fn new(config: RaBitQConfig) -> Self {
234        let dim = config.dim;
235        let mut rng = rand::rngs::StdRng::seed_from_u64(config.seed);
236
237        // Generate random signs (+1 or -1) for each dimension
238        let random_signs: Vec<i8> = (0..dim)
239            .map(|_| if rng.random::<bool>() { 1 } else { -1 })
240            .collect();
241
242        // Generate random permutation
243        let mut random_perm: Vec<u32> = (0..dim as u32).collect();
244        for i in (1..dim).rev() {
245            let j = rng.random_range(0..=i);
246            random_perm.swap(i, j);
247        }
248
249        // Version derived from config — codebook is deterministic (same seed+dim
250        // always produces identical random_signs and random_perm), so segments
251        // built with the same config are merge-compatible. ex_bits participates
252        // so 1-bit and multi-bit segments never merge (the term is 0 for
253        // ex_bits = 0, preserving historical versions).
254        let version = config.seed
255            ^ (config.dim as u64).wrapping_mul(0x9e3779b97f4a7c15)
256            ^ (config.ex_bits as u64).wrapping_mul(0xd6e8_feb8_6659_fd93);
257
258        Self {
259            config,
260            random_signs,
261            random_perm,
262            version,
263        }
264    }
265
266    /// Encode a vector to binary quantized form
267    ///
268    /// If centroid is provided, encodes the residual (vector - centroid).
269    pub fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> QuantizedVector {
270        let dim = self.config.dim;
271
272        // Step 1: Center + normalize in-place (single allocation instead of two)
273        let mut normalized: Vec<f32> = if let Some(c) = centroid {
274            vector.iter().zip(c).map(|(&v, &c)| v - c).collect()
275        } else {
276            vector.to_vec()
277        };
278
279        let norm: f32 = normalized.iter().map(|x| x * x).sum::<f32>().sqrt();
280        let dist_to_centroid = norm;
281
282        if norm > 1e-10 {
283            let inv_norm = 1.0 / norm;
284            for x in normalized.iter_mut() {
285                *x *= inv_norm;
286            }
287        }
288
289        // Step 2: Apply random transform (sign flip + permutation)
290        let transformed: Vec<f32> = (0..dim)
291            .map(|i| {
292                let src_idx = self.random_perm[i] as usize;
293                normalized[src_idx] * self.random_signs[src_idx] as f32
294            })
295            .collect();
296
297        // Step 3: Binary quantize
298        let num_bytes = dim.div_ceil(8);
299        let mut bits = vec![0u8; num_bytes];
300        let mut popcount = 0u32;
301
302        for i in 0..dim {
303            if transformed[i] >= 0.0 {
304                bits[i / 8] |= 1 << (i % 8);
305                popcount += 1;
306            }
307        }
308
309        // Step 4: Compute self dot product <o, o_bar>
310        let scale = 1.0 / (dim as f32).sqrt();
311        let mut self_dot = 0.0f32;
312        for i in 0..dim {
313            let o_bar_i = if (bits[i / 8] >> (i % 8)) & 1 == 1 {
314                scale
315            } else {
316                -scale
317            };
318            self_dot += transformed[i] * o_bar_i;
319        }
320
321        // Step 5 (extended): quantize per-dim magnitudes with a per-vector
322        // uniform scale. Reconstruction v̂_i = sign_i * (code_i + 0.5) * step.
323        let (ex_code, ex_scale, ex_norm) = if self.config.ex_bits > 0 {
324            let ex_bits = self.config.ex_bits as u32;
325            let levels = 1u32 << ex_bits;
326            let max_abs = transformed.iter().fold(0.0f32, |m, &x| m.max(x.abs()));
327            let ex_scale = if max_abs > 1e-10 { max_abs } else { 1.0 };
328            let step = ex_scale / levels as f32;
329
330            let total_bits = dim * ex_bits as usize;
331            let mut ex_code = vec![0u8; total_bits.div_ceil(8)];
332            let mut norm_sq = 0.0f64;
333            let mut bit_pos = 0usize;
334            for &t in &transformed {
335                let mag = (t.abs() / step) as u32;
336                let code = mag.min(levels - 1);
337                // Pack `ex_bits` LSB-first at bit_pos
338                let mut v = code;
339                let mut remaining = ex_bits as usize;
340                let mut pos = bit_pos;
341                while remaining > 0 {
342                    let byte = pos / 8;
343                    let offset = pos % 8;
344                    let take = remaining.min(8 - offset);
345                    ex_code[byte] |= ((v & ((1 << take) - 1)) as u8) << offset;
346                    v >>= take;
347                    pos += take;
348                    remaining -= take;
349                }
350                bit_pos += ex_bits as usize;
351
352                let recon = (code as f32 + 0.5) * step;
353                norm_sq += (recon as f64) * (recon as f64);
354            }
355
356            (ex_code, ex_scale, (norm_sq.sqrt()) as f32)
357        } else {
358            (Vec::new(), 0.0, 0.0)
359        };
360
361        QuantizedVector {
362            bits,
363            dist_to_centroid,
364            self_dot,
365            popcount,
366            ex_code,
367            ex_scale,
368            ex_norm,
369        }
370    }
371
372    /// Prepare a query for fast distance estimation
373    pub fn prepare_query(&self, query: &[f32], centroid: Option<&[f32]>) -> QuantizedQuery {
374        let dim = self.config.dim;
375
376        // Step 1: Center + normalize in-place (single allocation instead of two)
377        let mut normalized: Vec<f32> = if let Some(c) = centroid {
378            query.iter().zip(c).map(|(&v, &c)| v - c).collect()
379        } else {
380            query.to_vec()
381        };
382
383        let norm: f32 = normalized.iter().map(|x| x * x).sum::<f32>().sqrt();
384        let dist_to_centroid = norm;
385
386        if norm > 1e-10 {
387            let inv_norm = 1.0 / norm;
388            for x in normalized.iter_mut() {
389                *x *= inv_norm;
390            }
391        }
392
393        // Step 2: Apply random transform
394        let transformed: Vec<f32> = (0..dim)
395            .map(|i| {
396                let src_idx = self.random_perm[i] as usize;
397                normalized[src_idx] * self.random_signs[src_idx] as f32
398            })
399            .collect();
400
401        // Step 3: Scalar quantize to 4-bit
402        let min_val = transformed.iter().cloned().fold(f32::INFINITY, f32::min);
403        let max_val = transformed
404            .iter()
405            .cloned()
406            .fold(f32::NEG_INFINITY, f32::max);
407        let lower = min_val;
408        let width = if max_val > min_val {
409            max_val - min_val
410        } else {
411            1.0
412        };
413
414        // Quantize to 0-15 range
415        let quantized_vals: Vec<u8> = transformed
416            .iter()
417            .map(|&x| {
418                let normalized = (x - lower) / width;
419                (normalized * 15.0).round().clamp(0.0, 15.0) as u8
420            })
421            .collect();
422
423        // Pack into bytes (2 values per byte)
424        let num_bytes = dim.div_ceil(2);
425        let mut quantized = vec![0u8; num_bytes];
426        for i in 0..dim {
427            if i % 2 == 0 {
428                quantized[i / 2] |= quantized_vals[i];
429            } else {
430                quantized[i / 2] |= quantized_vals[i] << 4;
431            }
432        }
433
434        // Compute sum of quantized values
435        let sum: u32 = quantized_vals.iter().map(|&x| x as u32).sum();
436
437        // Step 4: Build LUTs for fast dot product
438        let num_luts = dim.div_ceil(4);
439        let mut luts = vec![[0u16; 16]; num_luts];
440
441        for (lut_idx, lut) in luts.iter_mut().enumerate() {
442            let base_dim = lut_idx * 4;
443            for pattern in 0u8..16 {
444                let mut dot = 0u16;
445                for bit in 0..4 {
446                    let dim_idx = base_dim + bit;
447                    if dim_idx < dim && (pattern >> bit) & 1 == 1 {
448                        dot += quantized_vals[dim_idx] as u16;
449                    }
450                }
451                lut[pattern as usize] = dot;
452            }
453        }
454
455        QuantizedQuery {
456            quantized,
457            dist_to_centroid,
458            lower,
459            width,
460            sum,
461            luts,
462            transformed,
463        }
464    }
465
466    /// Estimate squared distance between query and a quantized vector
467    pub fn estimate_distance(&self, query: &QuantizedQuery, code: &QuantizedVector) -> f32 {
468        // Extended multi-bit codes get the refined estimator
469        if !code.ex_code.is_empty() {
470            return self.estimate_distance_extended(query, code);
471        }
472
473        let dim = self.config.dim;
474
475        // Compute dot product using SIMD-accelerated LUT lookup
476        let dot_sum = lut_dot_product_simd(&code.bits, &query.luts);
477
478        let scale = 1.0 / (dim as f32).sqrt();
479
480        // Dequantize the dot product
481        let sum_positive = code.popcount as f32 * query.lower + dot_sum as f32 * query.width / 15.0;
482        let sum_all = dim as f32 * query.lower + query.sum as f32 * query.width / 15.0;
483
484        // <q, o_bar> = scale * (2 * sum_positive - sum_all)
485        let q_obar_dot = scale * (2.0 * sum_positive - sum_all);
486
487        // Estimate <q, o> using the corrective factor <o, o_bar>
488        let q_o_estimate = if code.self_dot.abs() > 1e-6 {
489            q_obar_dot / code.self_dot
490        } else {
491            q_obar_dot
492        };
493
494        // Clamp the inner product to valid range [-1, 1]
495        let q_o_clamped = q_o_estimate.clamp(-1.0, 1.0);
496
497        // Compute squared distance
498        let dist_sq = code.dist_to_centroid * code.dist_to_centroid
499            + query.dist_to_centroid * query.dist_to_centroid
500            - 2.0 * code.dist_to_centroid * query.dist_to_centroid * q_o_clamped;
501
502        dist_sq.max(0.0)
503    }
504
505    /// Refined distance estimate from extended multi-bit magnitude codes.
506    ///
507    /// Reconstructs v̂_i = sign_i * (code_i + 0.5) * step in the rotated
508    /// space and computes an exact dot product against the full-precision
509    /// rotated query, normalized by ||v̂||. Estimation error shrinks
510    /// roughly 2x per extra bit, allowing much smaller exact-rerank pools.
511    fn estimate_distance_extended(&self, query: &QuantizedQuery, code: &QuantizedVector) -> f32 {
512        let dim = self.config.dim;
513        let ex_bits = self.config.ex_bits as usize;
514        debug_assert!(ex_bits > 0 && ex_bits <= 8);
515        let levels = 1u32 << ex_bits;
516        let step = code.ex_scale / levels as f32;
517        let mask = levels - 1;
518
519        // Rolling bit-buffer reader over the packed magnitude codes
520        let mut acc: u64 = 0;
521        let mut acc_bits: usize = 0;
522        let mut byte_idx: usize = 0;
523        let ex_code = &code.ex_code;
524
525        let mut dot = 0.0f32;
526        for (i, &q) in query.transformed.iter().enumerate().take(dim) {
527            while acc_bits < ex_bits {
528                acc |= (ex_code.get(byte_idx).copied().unwrap_or(0) as u64) << acc_bits;
529                byte_idx += 1;
530                acc_bits += 8;
531            }
532            let mag_code = (acc as u32) & mask;
533            acc >>= ex_bits;
534            acc_bits -= ex_bits;
535
536            let magnitude = (mag_code as f32 + 0.5) * step;
537            let signed = if (code.bits[i / 8] >> (i % 8)) & 1 == 1 {
538                magnitude
539            } else {
540                -magnitude
541            };
542            dot += q * signed;
543        }
544
545        // <q, o> estimate: v̂ approximates the unit residual direction
546        let q_o_estimate = if code.ex_norm > 1e-10 {
547            dot / code.ex_norm
548        } else {
549            dot
550        };
551        let q_o_clamped = q_o_estimate.clamp(-1.0, 1.0);
552
553        let dist_sq = code.dist_to_centroid * code.dist_to_centroid
554            + query.dist_to_centroid * query.dist_to_centroid
555            - 2.0 * code.dist_to_centroid * query.dist_to_centroid * q_o_clamped;
556
557        dist_sq.max(0.0)
558    }
559
560    /// Memory usage in bytes
561    pub fn size_bytes(&self) -> usize {
562        self.random_signs.len() + self.random_perm.len() * 4 + 64
563    }
564
565    /// Estimated memory usage in bytes (alias for size_bytes)
566    pub fn estimated_memory_bytes(&self) -> usize {
567        self.size_bytes()
568    }
569}
570
571impl Quantizer for RaBitQCodebook {
572    type Code = QuantizedVector;
573    type Config = RaBitQConfig;
574    type QueryData = QuantizedQuery;
575
576    fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> Self::Code {
577        self.encode(vector, centroid)
578    }
579
580    fn prepare_query(&self, query: &[f32], centroid: Option<&[f32]>) -> Self::QueryData {
581        self.prepare_query(query, centroid)
582    }
583
584    fn compute_distance(&self, query_data: &Self::QueryData, code: &Self::Code) -> f32 {
585        self.estimate_distance(query_data, code)
586    }
587
588    fn size_bytes(&self) -> usize {
589        self.size_bytes()
590    }
591}
592
593// ============================================================================
594// SIMD-accelerated LUT dot product
595// ============================================================================
596
597/// SIMD-accelerated LUT dot product for RaBitQ
598#[inline]
599fn lut_dot_product_simd(bits: &[u8], luts: &[[u16; 16]]) -> u32 {
600    #[cfg(target_arch = "aarch64")]
601    {
602        if let Some(result) = lut_dot_product_neon(bits, luts) {
603            return result;
604        }
605    }
606
607    #[cfg(target_arch = "x86_64")]
608    {
609        if is_x86_feature_detected!("ssse3") {
610            unsafe {
611                if let Some(result) = lut_dot_product_ssse3(bits, luts) {
612                    return result;
613                }
614            }
615        }
616    }
617
618    lut_dot_product_scalar(bits, luts)
619}
620
621/// Scalar implementation of LUT dot product
622#[inline]
623fn lut_dot_product_scalar(bits: &[u8], luts: &[[u16; 16]]) -> u32 {
624    let mut dot_sum = 0u32;
625
626    for (lut_idx, lut) in luts.iter().enumerate() {
627        let base_bit = lut_idx * 4;
628        let byte_idx = base_bit / 8;
629        let bit_offset = base_bit % 8;
630
631        let byte = bits.get(byte_idx).copied().unwrap_or(0);
632        let next_byte = bits.get(byte_idx + 1).copied().unwrap_or(0);
633
634        let pattern = if bit_offset <= 4 {
635            (byte >> bit_offset) & 0x0F
636        } else {
637            ((byte >> bit_offset) | (next_byte << (8 - bit_offset))) & 0x0F
638        };
639
640        dot_sum += lut[pattern as usize] as u32;
641    }
642
643    dot_sum
644}
645
646/// NEON-accelerated LUT dot product (ARM64)
647#[cfg(target_arch = "aarch64")]
648#[inline]
649fn lut_dot_product_neon(bits: &[u8], luts: &[[u16; 16]]) -> Option<u32> {
650    if luts.len() < 8 {
651        return None;
652    }
653
654    let mut total = 0u32;
655    let num_luts = luts.len();
656    let mut lut_idx = 0;
657
658    while lut_idx + 2 <= num_luts {
659        let base_bit0 = lut_idx * 4;
660        let base_bit1 = (lut_idx + 1) * 4;
661
662        let byte_idx0 = base_bit0 / 8;
663        let bit_offset0 = base_bit0 % 8;
664        let byte_idx1 = base_bit1 / 8;
665        let bit_offset1 = base_bit1 % 8;
666
667        let byte0 = bits.get(byte_idx0).copied().unwrap_or(0);
668        let next0 = bits.get(byte_idx0 + 1).copied().unwrap_or(0);
669        let byte1 = bits.get(byte_idx1).copied().unwrap_or(0);
670        let next1 = bits.get(byte_idx1 + 1).copied().unwrap_or(0);
671
672        let pattern0 = if bit_offset0 <= 4 {
673            (byte0 >> bit_offset0) & 0x0F
674        } else {
675            ((byte0 >> bit_offset0) | (next0 << (8 - bit_offset0))) & 0x0F
676        };
677
678        let pattern1 = if bit_offset1 <= 4 {
679            (byte1 >> bit_offset1) & 0x0F
680        } else {
681            ((byte1 >> bit_offset1) | (next1 << (8 - bit_offset1))) & 0x0F
682        };
683
684        total += luts[lut_idx][pattern0 as usize] as u32;
685        total += luts[lut_idx + 1][pattern1 as usize] as u32;
686
687        lut_idx += 2;
688    }
689
690    while lut_idx < num_luts {
691        let base_bit = lut_idx * 4;
692        let byte_idx = base_bit / 8;
693        let bit_offset = base_bit % 8;
694
695        let byte = bits.get(byte_idx).copied().unwrap_or(0);
696        let next_byte = bits.get(byte_idx + 1).copied().unwrap_or(0);
697
698        let pattern = if bit_offset <= 4 {
699            (byte >> bit_offset) & 0x0F
700        } else {
701            ((byte >> bit_offset) | (next_byte << (8 - bit_offset))) & 0x0F
702        };
703
704        total += luts[lut_idx][pattern as usize] as u32;
705        lut_idx += 1;
706    }
707
708    Some(total)
709}
710
711/// SSSE3-accelerated LUT dot product (x86_64)
712#[cfg(target_arch = "x86_64")]
713#[target_feature(enable = "ssse3")]
714#[inline]
715unsafe fn lut_dot_product_ssse3(bits: &[u8], luts: &[[u16; 16]]) -> Option<u32> {
716    if luts.len() < 8 {
717        return None;
718    }
719    Some(lut_dot_product_scalar(bits, luts))
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725
726    #[test]
727    fn test_rabitq_codebook_basic() {
728        let config = RaBitQConfig::new(128);
729        let codebook = RaBitQCodebook::new(config);
730
731        assert_eq!(codebook.random_signs.len(), 128);
732        assert_eq!(codebook.random_perm.len(), 128);
733    }
734
735    #[test]
736    fn test_encode_decode() {
737        let config = RaBitQConfig::new(64);
738        let codebook = RaBitQCodebook::new(config);
739
740        let vector: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) / 32.0).collect();
741        let code = codebook.encode(&vector, None);
742
743        assert_eq!(code.bits.len(), 8); // 64 bits = 8 bytes
744        assert!(code.dist_to_centroid > 0.0);
745    }
746
747    #[test]
748    fn test_distance_estimation() {
749        let config = RaBitQConfig::new(64);
750        let codebook = RaBitQCodebook::new(config);
751
752        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
753        let v1: Vec<f32> = (0..64).map(|_| rng.random::<f32>() - 0.5).collect();
754        let v2: Vec<f32> = (0..64).map(|_| rng.random::<f32>() - 0.5).collect();
755
756        let code = codebook.encode(&v1, None);
757        let query = codebook.prepare_query(&v2, None);
758
759        let estimated = codebook.estimate_distance(&query, &code);
760        assert!(estimated >= 0.0);
761    }
762
763    #[test]
764    fn test_extended_bits_reduce_estimation_error() {
765        let dim = 128;
766        let n = 200;
767        let mut rng = rand::rngs::StdRng::seed_from_u64(7);
768        let vectors: Vec<Vec<f32>> = (0..n)
769            .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
770            .collect();
771        let query: Vec<f32> = (0..dim).map(|_| rng.random::<f32>() - 0.5).collect();
772
773        let exact =
774            |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum() };
775
776        let mut errors = Vec::new();
777        for bits in [1u8, 5u8] {
778            let config = RaBitQConfig::new(dim).with_bits(bits);
779            let codebook = RaBitQCodebook::new(config);
780            let q = codebook.prepare_query(&query, None);
781
782            let mut total_err = 0.0f64;
783            for v in &vectors {
784                let code = codebook.encode(v, None);
785                let est = codebook.estimate_distance(&q, &code);
786                let truth = exact(&query, v);
787                total_err += ((est - truth).abs() / truth.max(1e-6)) as f64;
788            }
789            errors.push(total_err / n as f64);
790        }
791
792        // 5-bit codes must estimate distances much more accurately than 1-bit
793        assert!(
794            errors[1] < errors[0] * 0.5,
795            "extended codes should at least halve mean relative error: 1-bit={:.4}, 5-bit={:.4}",
796            errors[0],
797            errors[1]
798        );
799        // And be tight in absolute terms
800        assert!(
801            errors[1] < 0.05,
802            "5-bit mean relative error should be <5%, got {:.4}",
803            errors[1]
804        );
805    }
806
807    #[test]
808    fn test_extended_code_serde_roundtrip_and_legacy() {
809        let dim = 64;
810        let config = RaBitQConfig::new(dim).with_bits(4);
811        let codebook = RaBitQCodebook::new(config);
812        let v: Vec<f32> = (0..dim).map(|i| (i as f32 - 32.0) / 32.0).collect();
813        let code = codebook.encode(&v, None);
814        assert!(!code.ex_code.is_empty());
815        assert!(code.ex_norm > 0.0);
816
817        // Round-trip
818        let json = serde_json::to_vec(&code).unwrap();
819        let back: QuantizedVector = serde_json::from_slice(&json).unwrap();
820        assert_eq!(back.ex_code, code.ex_code);
821        assert_eq!(back.ex_scale, code.ex_scale);
822
823        // Legacy payload without extended fields still deserializes (old segments)
824        let legacy = serde_json::json!({
825            "bits": code.bits,
826            "dist_to_centroid": code.dist_to_centroid,
827            "self_dot": code.self_dot,
828            "popcount": code.popcount,
829        });
830        let old: QuantizedVector = serde_json::from_value(legacy).unwrap();
831        assert!(old.ex_code.is_empty());
832    }
833
834    #[test]
835    fn test_extended_version_differs_from_classic() {
836        let dim = 64;
837        let classic = RaBitQCodebook::new(RaBitQConfig::new(dim));
838        let extended = RaBitQCodebook::new(RaBitQConfig::new(dim).with_bits(4));
839        assert_ne!(
840            classic.version, extended.version,
841            "1-bit and multi-bit segments must not be merge-compatible"
842        );
843    }
844
845    #[test]
846    fn test_quantizer_trait() {
847        let config = RaBitQConfig::new(32);
848        let codebook = RaBitQCodebook::new(config);
849
850        let vector: Vec<f32> = (0..32).map(|i| i as f32 / 32.0).collect();
851        let query: Vec<f32> = (0..32).map(|i| (31 - i) as f32 / 32.0).collect();
852
853        // Use trait methods
854        let code = Quantizer::encode(&codebook, &vector, None);
855        let query_data = Quantizer::prepare_query(&codebook, &query, None);
856        let dist = Quantizer::compute_distance(&codebook, &query_data, &code);
857
858        assert!(dist >= 0.0);
859    }
860}