hermes_core/structures/vector/quantization/
rabitq.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct RaBitQConfig {
30 pub dim: usize,
32 pub query_bits: u8,
34 pub seed: u64,
36 #[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct QuantizedVector {
76 pub bits: Vec<u8>,
78 pub dist_to_centroid: f32,
80 pub self_dot: f32,
82 pub popcount: u32,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub ex_code: Vec<u8>,
89 #[serde(default)]
91 pub ex_scale: f32,
92 #[serde(default)]
95 pub ex_norm: f32,
96}
97
98impl QuantizedCode for QuantizedVector {
99 fn size_bytes(&self) -> usize {
100 self.bits.len() + 4 + 4 + 4 + self.ex_code.len() + 8
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct QuantizedQuery {
108 pub quantized: Vec<u8>,
110 pub dist_to_centroid: f32,
112 pub lower: f32,
114 pub width: f32,
116 pub sum: u32,
118 pub luts: Vec<[u16; 16]>,
120 pub transformed: Vec<f32>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct RaBitQCodebook {
130 pub config: RaBitQConfig,
132 pub random_signs: Vec<i8>,
134 pub random_perm: Vec<u32>,
136 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 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 let random_signs: Vec<i8> = (0..dim)
239 .map(|_| if rng.random::<bool>() { 1 } else { -1 })
240 .collect();
241
242 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 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 pub fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> QuantizedVector {
270 let dim = self.config.dim;
271
272 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 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 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 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 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 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 pub fn prepare_query(&self, query: &[f32], centroid: Option<&[f32]>) -> QuantizedQuery {
374 let dim = self.config.dim;
375
376 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 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 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 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 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 let sum: u32 = quantized_vals.iter().map(|&x| x as u32).sum();
436
437 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 pub fn estimate_distance(&self, query: &QuantizedQuery, code: &QuantizedVector) -> f32 {
468 if !code.ex_code.is_empty() {
470 return self.estimate_distance_extended(query, code);
471 }
472
473 let dim = self.config.dim;
474
475 let dot_sum = lut_dot_product_simd(&code.bits, &query.luts);
477
478 let scale = 1.0 / (dim as f32).sqrt();
479
480 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 let q_obar_dot = scale * (2.0 * sum_positive - sum_all);
486
487 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 let q_o_clamped = q_o_estimate.clamp(-1.0, 1.0);
496
497 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 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 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 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 pub fn size_bytes(&self) -> usize {
562 self.random_signs.len() + self.random_perm.len() * 4 + 64
563 }
564
565 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#[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#[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#[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#[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); 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 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 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 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 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 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}