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    /// Create a new RaBitQ codebook with random transform
142    pub fn new(config: RaBitQConfig) -> Self {
143        let dim = config.dim;
144        let mut rng = rand::rngs::StdRng::seed_from_u64(config.seed);
145
146        // Generate random signs (+1 or -1) for each dimension
147        let random_signs: Vec<i8> = (0..dim)
148            .map(|_| if rng.random::<bool>() { 1 } else { -1 })
149            .collect();
150
151        // Generate random permutation
152        let mut random_perm: Vec<u32> = (0..dim as u32).collect();
153        for i in (1..dim).rev() {
154            let j = rng.random_range(0..=i);
155            random_perm.swap(i, j);
156        }
157
158        // Version derived from config — codebook is deterministic (same seed+dim
159        // always produces identical random_signs and random_perm), so segments
160        // built with the same config are merge-compatible. ex_bits participates
161        // so 1-bit and multi-bit segments never merge (the term is 0 for
162        // ex_bits = 0, preserving historical versions).
163        let version = config.seed
164            ^ (config.dim as u64).wrapping_mul(0x9e3779b97f4a7c15)
165            ^ (config.ex_bits as u64).wrapping_mul(0xd6e8_feb8_6659_fd93);
166
167        Self {
168            config,
169            random_signs,
170            random_perm,
171            version,
172        }
173    }
174
175    /// Encode a vector to binary quantized form
176    ///
177    /// If centroid is provided, encodes the residual (vector - centroid).
178    pub fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> QuantizedVector {
179        let dim = self.config.dim;
180
181        // Step 1: Center + normalize in-place (single allocation instead of two)
182        let mut normalized: Vec<f32> = if let Some(c) = centroid {
183            vector.iter().zip(c).map(|(&v, &c)| v - c).collect()
184        } else {
185            vector.to_vec()
186        };
187
188        let norm: f32 = normalized.iter().map(|x| x * x).sum::<f32>().sqrt();
189        let dist_to_centroid = norm;
190
191        if norm > 1e-10 {
192            let inv_norm = 1.0 / norm;
193            for x in normalized.iter_mut() {
194                *x *= inv_norm;
195            }
196        }
197
198        // Step 2: Apply random transform (sign flip + permutation)
199        let transformed: Vec<f32> = (0..dim)
200            .map(|i| {
201                let src_idx = self.random_perm[i] as usize;
202                normalized[src_idx] * self.random_signs[src_idx] as f32
203            })
204            .collect();
205
206        // Step 3: Binary quantize
207        let num_bytes = dim.div_ceil(8);
208        let mut bits = vec![0u8; num_bytes];
209        let mut popcount = 0u32;
210
211        for i in 0..dim {
212            if transformed[i] >= 0.0 {
213                bits[i / 8] |= 1 << (i % 8);
214                popcount += 1;
215            }
216        }
217
218        // Step 4: Compute self dot product <o, o_bar>
219        let scale = 1.0 / (dim as f32).sqrt();
220        let mut self_dot = 0.0f32;
221        for i in 0..dim {
222            let o_bar_i = if (bits[i / 8] >> (i % 8)) & 1 == 1 {
223                scale
224            } else {
225                -scale
226            };
227            self_dot += transformed[i] * o_bar_i;
228        }
229
230        // Step 5 (extended): quantize per-dim magnitudes with a per-vector
231        // uniform scale. Reconstruction v̂_i = sign_i * (code_i + 0.5) * step.
232        let (ex_code, ex_scale, ex_norm) = if self.config.ex_bits > 0 {
233            let ex_bits = self.config.ex_bits as u32;
234            let levels = 1u32 << ex_bits;
235            let max_abs = transformed.iter().fold(0.0f32, |m, &x| m.max(x.abs()));
236            let ex_scale = if max_abs > 1e-10 { max_abs } else { 1.0 };
237            let step = ex_scale / levels as f32;
238
239            let total_bits = dim * ex_bits as usize;
240            let mut ex_code = vec![0u8; total_bits.div_ceil(8)];
241            let mut norm_sq = 0.0f64;
242            let mut bit_pos = 0usize;
243            for &t in &transformed {
244                let mag = (t.abs() / step) as u32;
245                let code = mag.min(levels - 1);
246                // Pack `ex_bits` LSB-first at bit_pos
247                let mut v = code;
248                let mut remaining = ex_bits as usize;
249                let mut pos = bit_pos;
250                while remaining > 0 {
251                    let byte = pos / 8;
252                    let offset = pos % 8;
253                    let take = remaining.min(8 - offset);
254                    ex_code[byte] |= ((v & ((1 << take) - 1)) as u8) << offset;
255                    v >>= take;
256                    pos += take;
257                    remaining -= take;
258                }
259                bit_pos += ex_bits as usize;
260
261                let recon = (code as f32 + 0.5) * step;
262                norm_sq += (recon as f64) * (recon as f64);
263            }
264
265            (ex_code, ex_scale, (norm_sq.sqrt()) as f32)
266        } else {
267            (Vec::new(), 0.0, 0.0)
268        };
269
270        QuantizedVector {
271            bits,
272            dist_to_centroid,
273            self_dot,
274            popcount,
275            ex_code,
276            ex_scale,
277            ex_norm,
278        }
279    }
280
281    /// Prepare a query for fast distance estimation
282    pub fn prepare_query(&self, query: &[f32], centroid: Option<&[f32]>) -> QuantizedQuery {
283        let dim = self.config.dim;
284
285        // Step 1: Center + normalize in-place (single allocation instead of two)
286        let mut normalized: Vec<f32> = if let Some(c) = centroid {
287            query.iter().zip(c).map(|(&v, &c)| v - c).collect()
288        } else {
289            query.to_vec()
290        };
291
292        let norm: f32 = normalized.iter().map(|x| x * x).sum::<f32>().sqrt();
293        let dist_to_centroid = norm;
294
295        if norm > 1e-10 {
296            let inv_norm = 1.0 / norm;
297            for x in normalized.iter_mut() {
298                *x *= inv_norm;
299            }
300        }
301
302        // Step 2: Apply random transform
303        let transformed: Vec<f32> = (0..dim)
304            .map(|i| {
305                let src_idx = self.random_perm[i] as usize;
306                normalized[src_idx] * self.random_signs[src_idx] as f32
307            })
308            .collect();
309
310        // Step 3: Scalar quantize to 4-bit
311        let min_val = transformed.iter().cloned().fold(f32::INFINITY, f32::min);
312        let max_val = transformed
313            .iter()
314            .cloned()
315            .fold(f32::NEG_INFINITY, f32::max);
316        let lower = min_val;
317        let width = if max_val > min_val {
318            max_val - min_val
319        } else {
320            1.0
321        };
322
323        // Quantize to 0-15 range
324        let quantized_vals: Vec<u8> = transformed
325            .iter()
326            .map(|&x| {
327                let normalized = (x - lower) / width;
328                (normalized * 15.0).round().clamp(0.0, 15.0) as u8
329            })
330            .collect();
331
332        // Pack into bytes (2 values per byte)
333        let num_bytes = dim.div_ceil(2);
334        let mut quantized = vec![0u8; num_bytes];
335        for i in 0..dim {
336            if i % 2 == 0 {
337                quantized[i / 2] |= quantized_vals[i];
338            } else {
339                quantized[i / 2] |= quantized_vals[i] << 4;
340            }
341        }
342
343        // Compute sum of quantized values
344        let sum: u32 = quantized_vals.iter().map(|&x| x as u32).sum();
345
346        // Step 4: Build LUTs for fast dot product
347        let num_luts = dim.div_ceil(4);
348        let mut luts = vec![[0u16; 16]; num_luts];
349
350        for (lut_idx, lut) in luts.iter_mut().enumerate() {
351            let base_dim = lut_idx * 4;
352            for pattern in 0u8..16 {
353                let mut dot = 0u16;
354                for bit in 0..4 {
355                    let dim_idx = base_dim + bit;
356                    if dim_idx < dim && (pattern >> bit) & 1 == 1 {
357                        dot += quantized_vals[dim_idx] as u16;
358                    }
359                }
360                lut[pattern as usize] = dot;
361            }
362        }
363
364        QuantizedQuery {
365            quantized,
366            dist_to_centroid,
367            lower,
368            width,
369            sum,
370            luts,
371            transformed,
372        }
373    }
374
375    /// Estimate squared distance between query and a quantized vector
376    pub fn estimate_distance(&self, query: &QuantizedQuery, code: &QuantizedVector) -> f32 {
377        // Extended multi-bit codes get the refined estimator
378        if !code.ex_code.is_empty() {
379            return self.estimate_distance_extended(query, code);
380        }
381
382        let dim = self.config.dim;
383
384        // Compute dot product using SIMD-accelerated LUT lookup
385        let dot_sum = lut_dot_product_simd(&code.bits, &query.luts);
386
387        let scale = 1.0 / (dim as f32).sqrt();
388
389        // Dequantize the dot product
390        let sum_positive = code.popcount as f32 * query.lower + dot_sum as f32 * query.width / 15.0;
391        let sum_all = dim as f32 * query.lower + query.sum as f32 * query.width / 15.0;
392
393        // <q, o_bar> = scale * (2 * sum_positive - sum_all)
394        let q_obar_dot = scale * (2.0 * sum_positive - sum_all);
395
396        // Estimate <q, o> using the corrective factor <o, o_bar>
397        let q_o_estimate = if code.self_dot.abs() > 1e-6 {
398            q_obar_dot / code.self_dot
399        } else {
400            q_obar_dot
401        };
402
403        // Clamp the inner product to valid range [-1, 1]
404        let q_o_clamped = q_o_estimate.clamp(-1.0, 1.0);
405
406        // Compute squared distance
407        let dist_sq = code.dist_to_centroid * code.dist_to_centroid
408            + query.dist_to_centroid * query.dist_to_centroid
409            - 2.0 * code.dist_to_centroid * query.dist_to_centroid * q_o_clamped;
410
411        dist_sq.max(0.0)
412    }
413
414    /// Refined distance estimate from extended multi-bit magnitude codes.
415    ///
416    /// Reconstructs v̂_i = sign_i * (code_i + 0.5) * step in the rotated
417    /// space and computes an exact dot product against the full-precision
418    /// rotated query, normalized by ||v̂||. Estimation error shrinks
419    /// roughly 2x per extra bit, allowing much smaller exact-rerank pools.
420    fn estimate_distance_extended(&self, query: &QuantizedQuery, code: &QuantizedVector) -> f32 {
421        let dim = self.config.dim;
422        let ex_bits = self.config.ex_bits as usize;
423        debug_assert!(ex_bits > 0 && ex_bits <= 8);
424        let levels = 1u32 << ex_bits;
425        let step = code.ex_scale / levels as f32;
426        let mask = levels - 1;
427
428        // Rolling bit-buffer reader over the packed magnitude codes
429        let mut acc: u64 = 0;
430        let mut acc_bits: usize = 0;
431        let mut byte_idx: usize = 0;
432        let ex_code = &code.ex_code;
433
434        let mut dot = 0.0f32;
435        for (i, &q) in query.transformed.iter().enumerate().take(dim) {
436            while acc_bits < ex_bits {
437                acc |= (ex_code.get(byte_idx).copied().unwrap_or(0) as u64) << acc_bits;
438                byte_idx += 1;
439                acc_bits += 8;
440            }
441            let mag_code = (acc as u32) & mask;
442            acc >>= ex_bits;
443            acc_bits -= ex_bits;
444
445            let magnitude = (mag_code as f32 + 0.5) * step;
446            let signed = if (code.bits[i / 8] >> (i % 8)) & 1 == 1 {
447                magnitude
448            } else {
449                -magnitude
450            };
451            dot += q * signed;
452        }
453
454        // <q, o> estimate: v̂ approximates the unit residual direction
455        let q_o_estimate = if code.ex_norm > 1e-10 {
456            dot / code.ex_norm
457        } else {
458            dot
459        };
460        let q_o_clamped = q_o_estimate.clamp(-1.0, 1.0);
461
462        let dist_sq = code.dist_to_centroid * code.dist_to_centroid
463            + query.dist_to_centroid * query.dist_to_centroid
464            - 2.0 * code.dist_to_centroid * query.dist_to_centroid * q_o_clamped;
465
466        dist_sq.max(0.0)
467    }
468
469    /// Memory usage in bytes
470    pub fn size_bytes(&self) -> usize {
471        self.random_signs.len() + self.random_perm.len() * 4 + 64
472    }
473
474    /// Estimated memory usage in bytes (alias for size_bytes)
475    pub fn estimated_memory_bytes(&self) -> usize {
476        self.size_bytes()
477    }
478}
479
480impl Quantizer for RaBitQCodebook {
481    type Code = QuantizedVector;
482    type Config = RaBitQConfig;
483    type QueryData = QuantizedQuery;
484
485    fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> Self::Code {
486        self.encode(vector, centroid)
487    }
488
489    fn prepare_query(&self, query: &[f32], centroid: Option<&[f32]>) -> Self::QueryData {
490        self.prepare_query(query, centroid)
491    }
492
493    fn compute_distance(&self, query_data: &Self::QueryData, code: &Self::Code) -> f32 {
494        self.estimate_distance(query_data, code)
495    }
496
497    fn size_bytes(&self) -> usize {
498        self.size_bytes()
499    }
500}
501
502// ============================================================================
503// SIMD-accelerated LUT dot product
504// ============================================================================
505
506/// SIMD-accelerated LUT dot product for RaBitQ
507#[inline]
508fn lut_dot_product_simd(bits: &[u8], luts: &[[u16; 16]]) -> u32 {
509    #[cfg(target_arch = "aarch64")]
510    {
511        if let Some(result) = lut_dot_product_neon(bits, luts) {
512            return result;
513        }
514    }
515
516    #[cfg(target_arch = "x86_64")]
517    {
518        if is_x86_feature_detected!("ssse3") {
519            unsafe {
520                if let Some(result) = lut_dot_product_ssse3(bits, luts) {
521                    return result;
522                }
523            }
524        }
525    }
526
527    lut_dot_product_scalar(bits, luts)
528}
529
530/// Scalar implementation of LUT dot product
531#[inline]
532fn lut_dot_product_scalar(bits: &[u8], luts: &[[u16; 16]]) -> u32 {
533    let mut dot_sum = 0u32;
534
535    for (lut_idx, lut) in luts.iter().enumerate() {
536        let base_bit = lut_idx * 4;
537        let byte_idx = base_bit / 8;
538        let bit_offset = base_bit % 8;
539
540        let byte = bits.get(byte_idx).copied().unwrap_or(0);
541        let next_byte = bits.get(byte_idx + 1).copied().unwrap_or(0);
542
543        let pattern = if bit_offset <= 4 {
544            (byte >> bit_offset) & 0x0F
545        } else {
546            ((byte >> bit_offset) | (next_byte << (8 - bit_offset))) & 0x0F
547        };
548
549        dot_sum += lut[pattern as usize] as u32;
550    }
551
552    dot_sum
553}
554
555/// NEON-accelerated LUT dot product (ARM64)
556#[cfg(target_arch = "aarch64")]
557#[inline]
558fn lut_dot_product_neon(bits: &[u8], luts: &[[u16; 16]]) -> Option<u32> {
559    if luts.len() < 8 {
560        return None;
561    }
562
563    let mut total = 0u32;
564    let num_luts = luts.len();
565    let mut lut_idx = 0;
566
567    while lut_idx + 2 <= num_luts {
568        let base_bit0 = lut_idx * 4;
569        let base_bit1 = (lut_idx + 1) * 4;
570
571        let byte_idx0 = base_bit0 / 8;
572        let bit_offset0 = base_bit0 % 8;
573        let byte_idx1 = base_bit1 / 8;
574        let bit_offset1 = base_bit1 % 8;
575
576        let byte0 = bits.get(byte_idx0).copied().unwrap_or(0);
577        let next0 = bits.get(byte_idx0 + 1).copied().unwrap_or(0);
578        let byte1 = bits.get(byte_idx1).copied().unwrap_or(0);
579        let next1 = bits.get(byte_idx1 + 1).copied().unwrap_or(0);
580
581        let pattern0 = if bit_offset0 <= 4 {
582            (byte0 >> bit_offset0) & 0x0F
583        } else {
584            ((byte0 >> bit_offset0) | (next0 << (8 - bit_offset0))) & 0x0F
585        };
586
587        let pattern1 = if bit_offset1 <= 4 {
588            (byte1 >> bit_offset1) & 0x0F
589        } else {
590            ((byte1 >> bit_offset1) | (next1 << (8 - bit_offset1))) & 0x0F
591        };
592
593        total += luts[lut_idx][pattern0 as usize] as u32;
594        total += luts[lut_idx + 1][pattern1 as usize] as u32;
595
596        lut_idx += 2;
597    }
598
599    while lut_idx < num_luts {
600        let base_bit = lut_idx * 4;
601        let byte_idx = base_bit / 8;
602        let bit_offset = base_bit % 8;
603
604        let byte = bits.get(byte_idx).copied().unwrap_or(0);
605        let next_byte = bits.get(byte_idx + 1).copied().unwrap_or(0);
606
607        let pattern = if bit_offset <= 4 {
608            (byte >> bit_offset) & 0x0F
609        } else {
610            ((byte >> bit_offset) | (next_byte << (8 - bit_offset))) & 0x0F
611        };
612
613        total += luts[lut_idx][pattern as usize] as u32;
614        lut_idx += 1;
615    }
616
617    Some(total)
618}
619
620/// SSSE3-accelerated LUT dot product (x86_64)
621#[cfg(target_arch = "x86_64")]
622#[target_feature(enable = "ssse3")]
623#[inline]
624unsafe fn lut_dot_product_ssse3(bits: &[u8], luts: &[[u16; 16]]) -> Option<u32> {
625    if luts.len() < 8 {
626        return None;
627    }
628    Some(lut_dot_product_scalar(bits, luts))
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn test_rabitq_codebook_basic() {
637        let config = RaBitQConfig::new(128);
638        let codebook = RaBitQCodebook::new(config);
639
640        assert_eq!(codebook.random_signs.len(), 128);
641        assert_eq!(codebook.random_perm.len(), 128);
642    }
643
644    #[test]
645    fn test_encode_decode() {
646        let config = RaBitQConfig::new(64);
647        let codebook = RaBitQCodebook::new(config);
648
649        let vector: Vec<f32> = (0..64).map(|i| (i as f32 - 32.0) / 32.0).collect();
650        let code = codebook.encode(&vector, None);
651
652        assert_eq!(code.bits.len(), 8); // 64 bits = 8 bytes
653        assert!(code.dist_to_centroid > 0.0);
654    }
655
656    #[test]
657    fn test_distance_estimation() {
658        let config = RaBitQConfig::new(64);
659        let codebook = RaBitQCodebook::new(config);
660
661        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
662        let v1: Vec<f32> = (0..64).map(|_| rng.random::<f32>() - 0.5).collect();
663        let v2: Vec<f32> = (0..64).map(|_| rng.random::<f32>() - 0.5).collect();
664
665        let code = codebook.encode(&v1, None);
666        let query = codebook.prepare_query(&v2, None);
667
668        let estimated = codebook.estimate_distance(&query, &code);
669        assert!(estimated >= 0.0);
670    }
671
672    #[test]
673    fn test_extended_bits_reduce_estimation_error() {
674        let dim = 128;
675        let n = 200;
676        let mut rng = rand::rngs::StdRng::seed_from_u64(7);
677        let vectors: Vec<Vec<f32>> = (0..n)
678            .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
679            .collect();
680        let query: Vec<f32> = (0..dim).map(|_| rng.random::<f32>() - 0.5).collect();
681
682        let exact =
683            |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum() };
684
685        let mut errors = Vec::new();
686        for bits in [1u8, 5u8] {
687            let config = RaBitQConfig::new(dim).with_bits(bits);
688            let codebook = RaBitQCodebook::new(config);
689            let q = codebook.prepare_query(&query, None);
690
691            let mut total_err = 0.0f64;
692            for v in &vectors {
693                let code = codebook.encode(v, None);
694                let est = codebook.estimate_distance(&q, &code);
695                let truth = exact(&query, v);
696                total_err += ((est - truth).abs() / truth.max(1e-6)) as f64;
697            }
698            errors.push(total_err / n as f64);
699        }
700
701        // 5-bit codes must estimate distances much more accurately than 1-bit
702        assert!(
703            errors[1] < errors[0] * 0.5,
704            "extended codes should at least halve mean relative error: 1-bit={:.4}, 5-bit={:.4}",
705            errors[0],
706            errors[1]
707        );
708        // And be tight in absolute terms
709        assert!(
710            errors[1] < 0.05,
711            "5-bit mean relative error should be <5%, got {:.4}",
712            errors[1]
713        );
714    }
715
716    #[test]
717    fn test_extended_code_serde_roundtrip_and_legacy() {
718        let dim = 64;
719        let config = RaBitQConfig::new(dim).with_bits(4);
720        let codebook = RaBitQCodebook::new(config);
721        let v: Vec<f32> = (0..dim).map(|i| (i as f32 - 32.0) / 32.0).collect();
722        let code = codebook.encode(&v, None);
723        assert!(!code.ex_code.is_empty());
724        assert!(code.ex_norm > 0.0);
725
726        // Round-trip
727        let json = serde_json::to_vec(&code).unwrap();
728        let back: QuantizedVector = serde_json::from_slice(&json).unwrap();
729        assert_eq!(back.ex_code, code.ex_code);
730        assert_eq!(back.ex_scale, code.ex_scale);
731
732        // Legacy payload without extended fields still deserializes (old segments)
733        let legacy = serde_json::json!({
734            "bits": code.bits,
735            "dist_to_centroid": code.dist_to_centroid,
736            "self_dot": code.self_dot,
737            "popcount": code.popcount,
738        });
739        let old: QuantizedVector = serde_json::from_value(legacy).unwrap();
740        assert!(old.ex_code.is_empty());
741    }
742
743    #[test]
744    fn test_extended_version_differs_from_classic() {
745        let dim = 64;
746        let classic = RaBitQCodebook::new(RaBitQConfig::new(dim));
747        let extended = RaBitQCodebook::new(RaBitQConfig::new(dim).with_bits(4));
748        assert_ne!(
749            classic.version, extended.version,
750            "1-bit and multi-bit segments must not be merge-compatible"
751        );
752    }
753
754    #[test]
755    fn test_quantizer_trait() {
756        let config = RaBitQConfig::new(32);
757        let codebook = RaBitQCodebook::new(config);
758
759        let vector: Vec<f32> = (0..32).map(|i| i as f32 / 32.0).collect();
760        let query: Vec<f32> = (0..32).map(|i| (31 - i) as f32 / 32.0).collect();
761
762        // Use trait methods
763        let code = Quantizer::encode(&codebook, &vector, None);
764        let query_data = Quantizer::prepare_query(&codebook, &query, None);
765        let dist = Quantizer::compute_distance(&codebook, &query_data, &code);
766
767        assert!(dist >= 0.0);
768    }
769}