1use std::io::{self, Read, Write};
10
11use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
12#[cfg(not(feature = "native"))]
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(all(target_arch = "x86_64", feature = "native"))]
24#[allow(unused_imports)]
25use std::arch::x86_64::*;
26
27const CODEBOOK_MAGIC: u32 = 0x5343424B; pub const DEFAULT_NUM_CENTROIDS: usize = 256;
32
33pub const DEFAULT_DIMS_PER_BLOCK: usize = 2;
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct PQConfig {
39 pub dim: usize,
41 pub num_subspaces: usize,
43 pub dims_per_block: usize,
45 pub num_centroids: usize,
47 pub seed: u64,
49 pub anisotropic: bool,
51 pub aniso_eta: f32,
53 pub aniso_threshold: f32,
55 pub use_opq: bool,
57 pub opq_iters: usize,
59}
60
61impl PQConfig {
62 pub fn new(dim: usize) -> Self {
64 let dims_per_block = DEFAULT_DIMS_PER_BLOCK;
65 let num_subspaces = dim / dims_per_block;
66
67 Self {
68 dim,
69 num_subspaces,
70 dims_per_block,
71 num_centroids: DEFAULT_NUM_CENTROIDS,
72 seed: 42,
73 anisotropic: true,
74 aniso_eta: 10.0,
75 aniso_threshold: 0.2,
76 use_opq: true,
77 opq_iters: 10,
78 }
79 }
80
81 pub fn new_fast(dim: usize) -> Self {
83 let num_subspaces = if dim >= 256 {
84 8
85 } else if dim >= 64 {
86 4
87 } else {
88 2
89 };
90 let dims_per_block = dim / num_subspaces;
91
92 Self {
93 dim,
94 num_subspaces,
95 dims_per_block,
96 num_centroids: DEFAULT_NUM_CENTROIDS,
97 seed: 42,
98 anisotropic: true,
99 aniso_eta: 10.0,
100 aniso_threshold: 0.2,
101 use_opq: false,
102 opq_iters: 0,
103 }
104 }
105
106 pub fn new_balanced(dim: usize) -> Self {
109 let num_subspaces = if dim >= 128 {
110 16
111 } else if dim >= 64 {
112 8
113 } else {
114 4
115 };
116 let dims_per_block = dim / num_subspaces;
117
118 Self {
119 dim,
120 num_subspaces,
121 dims_per_block,
122 num_centroids: DEFAULT_NUM_CENTROIDS,
123 seed: 42,
124 anisotropic: true,
125 aniso_eta: 10.0,
126 aniso_threshold: 0.2,
127 use_opq: false,
128 opq_iters: 0,
129 }
130 }
131
132 pub fn with_dims_per_block(mut self, d: usize) -> Self {
133 assert!(
134 self.dim.is_multiple_of(d),
135 "Dimension must be divisible by dims_per_block"
136 );
137 self.dims_per_block = d;
138 self.num_subspaces = self.dim / d;
139 self
140 }
141
142 pub fn with_subspaces(mut self, m: usize) -> Self {
143 assert!(
144 self.dim.is_multiple_of(m),
145 "Dimension must be divisible by num_subspaces"
146 );
147 self.num_subspaces = m;
148 self.dims_per_block = self.dim / m;
149 self
150 }
151
152 pub fn with_centroids(mut self, k: usize) -> Self {
153 assert!(k <= 256, "Max 256 centroids for u8 codes");
154 self.num_centroids = k;
155 self
156 }
157
158 pub fn with_anisotropic(mut self, enabled: bool, eta: f32) -> Self {
159 self.anisotropic = enabled;
160 self.aniso_eta = eta;
161 self
162 }
163
164 pub fn with_opq(mut self, enabled: bool, iters: usize) -> Self {
165 self.use_opq = enabled;
166 self.opq_iters = iters;
167 self
168 }
169
170 pub fn subspace_dim(&self) -> usize {
172 self.dims_per_block
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct PQVector {
179 pub codes: Vec<u8>,
181 pub norm: f32,
183}
184
185impl PQVector {
186 pub fn new(codes: Vec<u8>, norm: f32) -> Self {
187 Self { codes, norm }
188 }
189}
190
191impl QuantizedCode for PQVector {
192 fn size_bytes(&self) -> usize {
193 self.codes.len() + 4 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct PQCodebook {
202 pub config: PQConfig,
204 pub rotation_matrix: Option<Vec<f32>>,
206 pub centroids: Vec<f32>,
208 pub version: u64,
210 pub centroid_norms: Option<Vec<f32>>,
212}
213
214impl PQCodebook {
215 pub(crate) fn validate(&self) -> Result<(), String> {
216 let config = &self.config;
217 if config.dim == 0
218 || config.num_subspaces == 0
219 || config.dims_per_block == 0
220 || config.num_centroids == 0
221 || config.num_centroids > 256
222 {
223 return Err("PQ codebook has invalid zero/unbounded dimensions".to_string());
224 }
225 let covered_dim = config
226 .num_subspaces
227 .checked_mul(config.dims_per_block)
228 .ok_or_else(|| "PQ subspace dimension overflow".to_string())?;
229 if covered_dim != config.dim {
230 return Err(format!(
231 "PQ subspaces cover {covered_dim} dimensions, expected {}",
232 config.dim
233 ));
234 }
235 if !config.aniso_eta.is_finite()
236 || config.aniso_eta < 0.0
237 || !config.aniso_threshold.is_finite()
238 {
239 return Err(
240 "PQ anisotropic eta must be non-negative and all parameters must be finite"
241 .to_string(),
242 );
243 }
244 let expected_centroids = config
245 .num_subspaces
246 .checked_mul(config.num_centroids)
247 .and_then(|count| count.checked_mul(config.dims_per_block))
248 .ok_or_else(|| "PQ centroid size overflow".to_string())?;
249 if self.centroids.len() != expected_centroids
250 || self.centroids.iter().any(|value| !value.is_finite())
251 {
252 return Err(format!(
253 "PQ centroid table is invalid: got {}, expected {expected_centroids}",
254 self.centroids.len()
255 ));
256 }
257 if let Some(rotation) = &self.rotation_matrix {
258 let expected = config
259 .dim
260 .checked_mul(config.dim)
261 .ok_or_else(|| "PQ rotation size overflow".to_string())?;
262 if rotation.len() != expected || rotation.iter().any(|value| !value.is_finite()) {
263 return Err(format!(
264 "PQ rotation matrix is invalid: got {}, expected {expected}",
265 rotation.len()
266 ));
267 }
268 }
269 if let Some(norms) = &self.centroid_norms {
270 let expected = config
271 .num_subspaces
272 .checked_mul(config.num_centroids)
273 .ok_or_else(|| "PQ norm table size overflow".to_string())?;
274 if norms.len() != expected
275 || norms.iter().any(|value| !value.is_finite() || *value < 0.0)
276 {
277 return Err(format!(
278 "PQ centroid norm table is invalid: got {}, expected {expected}",
279 norms.len()
280 ));
281 }
282 }
283 Ok(())
284 }
285
286 #[cfg(feature = "native")]
288 pub fn train(config: PQConfig, vectors: &[Vec<f32>], max_iters: usize) -> Self {
289 use kentro::KMeans;
290 use ndarray::Array2;
291
292 assert!(!vectors.is_empty(), "Cannot train on empty vector set");
293 assert_eq!(vectors[0].len(), config.dim, "Vector dimension mismatch");
294
295 let m = config.num_subspaces;
296 let k = config.num_centroids;
297 let sub_dim = config.subspace_dim();
298 let n = vectors.len();
299
300 let rotation_matrix = if config.use_opq && config.opq_iters > 0 {
302 Some(Self::learn_opq_rotation(&config, vectors, max_iters))
303 } else {
304 None
305 };
306
307 let rotated_vectors: Vec<Vec<f32>> = if let Some(ref r) = rotation_matrix {
309 vectors
310 .iter()
311 .map(|v| Self::apply_rotation(r, v, config.dim))
312 .collect()
313 } else {
314 vectors.to_vec()
315 };
316
317 let mut centroids = Vec::with_capacity(m * k * sub_dim);
319
320 for subspace_idx in 0..m {
321 let offset = subspace_idx * sub_dim;
322
323 let subdata: Vec<f32> = rotated_vectors
324 .iter()
325 .flat_map(|v| v[offset..offset + sub_dim].iter().copied())
326 .collect();
327
328 let actual_k = k.min(n);
329
330 let data = Array2::from_shape_vec((n, sub_dim), subdata)
331 .expect("Failed to create subspace array");
332 let mut kmeans = KMeans::new(actual_k)
333 .with_euclidean(true)
334 .with_iterations(max_iters);
335 let _ = kmeans
336 .train(data.view(), None)
337 .expect("K-means training failed");
338
339 let subspace_centroids: Vec<f32> = kmeans
340 .centroids()
341 .expect("No centroids")
342 .iter()
343 .copied()
344 .collect();
345
346 centroids.extend(subspace_centroids);
347
348 while centroids.len() < (subspace_idx + 1) * k * sub_dim {
350 let last_start = centroids.len() - sub_dim;
351 let last: Vec<f32> = centroids[last_start..].to_vec();
352 centroids.extend(last);
353 }
354 }
355
356 let centroid_norms: Vec<f32> = (0..m * k)
358 .map(|i| {
359 let start = i * sub_dim;
360 if start + sub_dim <= centroids.len() {
361 centroids[start..start + sub_dim]
362 .iter()
363 .map(|x| x * x)
364 .sum::<f32>()
365 .sqrt()
366 } else {
367 0.0
368 }
369 })
370 .collect();
371
372 let version = std::time::SystemTime::now()
373 .duration_since(std::time::UNIX_EPOCH)
374 .unwrap_or_default()
375 .as_millis() as u64;
376
377 Self {
378 config,
379 rotation_matrix,
380 centroids,
381 version,
382 centroid_norms: Some(centroid_norms),
383 }
384 }
385
386 #[cfg(not(feature = "native"))]
388 pub fn train(config: PQConfig, vectors: &[Vec<f32>], max_iters: usize) -> Self {
389 assert!(!vectors.is_empty(), "Cannot train on empty vector set");
390 assert_eq!(vectors[0].len(), config.dim, "Vector dimension mismatch");
391
392 let m = config.num_subspaces;
393 let k = config.num_centroids;
394 let sub_dim = config.subspace_dim();
395 let mut rng = rand::rngs::StdRng::seed_from_u64(config.seed);
396
397 let rotation_matrix = None;
398 let mut centroids = Vec::with_capacity(m * k * sub_dim);
399
400 for subspace_idx in 0..m {
401 let offset = subspace_idx * sub_dim;
402 let subvectors: Vec<Vec<f32>> = vectors
403 .iter()
404 .map(|v| v[offset..offset + sub_dim].to_vec())
405 .collect();
406
407 let subspace_centroids =
408 Self::train_subspace_scalar(&subvectors, k, sub_dim, max_iters, &mut rng);
409 centroids.extend(subspace_centroids);
410 }
411
412 let centroid_norms: Vec<f32> = (0..m * k)
413 .map(|i| {
414 let start = i * sub_dim;
415 centroids[start..start + sub_dim]
416 .iter()
417 .map(|x| x * x)
418 .sum::<f32>()
419 .sqrt()
420 })
421 .collect();
422
423 let version = std::time::SystemTime::now()
424 .duration_since(std::time::UNIX_EPOCH)
425 .unwrap_or_default()
426 .as_millis() as u64;
427
428 Self {
429 config,
430 rotation_matrix,
431 centroids,
432 version,
433 centroid_norms: Some(centroid_norms),
434 }
435 }
436
437 #[cfg(feature = "native")]
439 fn learn_opq_rotation(config: &PQConfig, vectors: &[Vec<f32>], max_iters: usize) -> Vec<f32> {
440 use nalgebra::DMatrix;
441
442 let dim = config.dim;
443 let n = vectors.len();
444
445 let mut rotation = DMatrix::<f32>::identity(dim, dim);
446 let data: Vec<f32> = vectors.iter().flat_map(|v| v.iter().copied()).collect();
447 let x = DMatrix::from_row_slice(n, dim, &data);
448
449 for _iter in 0..config.opq_iters.min(max_iters) {
450 let rotated = &x * &rotation;
451 let assignments = Self::compute_pq_assignments(config, &rotated);
452 let reconstructed = Self::reconstruct_from_assignments(config, &rotated, &assignments);
453
454 let xtx_hat = x.transpose() * &reconstructed;
455 let svd = xtx_hat.svd(true, true);
456 if let (Some(u), Some(vt)) = (svd.u, svd.v_t) {
457 let new_rotation: DMatrix<f32> = vt.transpose() * u.transpose();
458 rotation = new_rotation;
459 }
460 }
461
462 rotation.iter().copied().collect()
463 }
464
465 #[cfg(feature = "native")]
466 fn compute_pq_assignments(
467 config: &PQConfig,
468 rotated: &nalgebra::DMatrix<f32>,
469 ) -> Vec<Vec<usize>> {
470 use kentro::KMeans;
471 use ndarray::Array2;
472
473 let m = config.num_subspaces;
474 let k = config.num_centroids.min(rotated.nrows());
475 let sub_dim = config.subspace_dim();
476 let n = rotated.nrows();
477
478 let mut all_assignments = vec![vec![0usize; m]; n];
479
480 for subspace_idx in 0..m {
481 let mut subdata: Vec<f32> = Vec::with_capacity(n * sub_dim);
482 for row in 0..n {
483 for col in 0..sub_dim {
484 subdata.push(rotated[(row, subspace_idx * sub_dim + col)]);
485 }
486 }
487
488 let data = Array2::from_shape_vec((n, sub_dim), subdata)
489 .expect("Failed to create subspace array");
490 let mut kmeans = KMeans::new(k).with_euclidean(true).with_iterations(5);
491 let clusters = kmeans
492 .train(data.view(), None)
493 .expect("K-means training failed");
494
495 for (cluster_id, point_indices) in clusters.iter().enumerate() {
497 for &point_idx in point_indices {
498 all_assignments[point_idx][subspace_idx] = cluster_id;
499 }
500 }
501 }
502
503 all_assignments
504 }
505
506 #[cfg(feature = "native")]
507 fn reconstruct_from_assignments(
508 config: &PQConfig,
509 rotated: &nalgebra::DMatrix<f32>,
510 assignments: &[Vec<usize>],
511 ) -> nalgebra::DMatrix<f32> {
512 use kentro::KMeans;
513 use ndarray::Array2;
514
515 let m = config.num_subspaces;
516 let sub_dim = config.subspace_dim();
517 let n = rotated.nrows();
518 let dim = config.dim;
519
520 let mut reconstructed = nalgebra::DMatrix::<f32>::zeros(n, dim);
521
522 for subspace_idx in 0..m {
523 let mut subdata: Vec<f32> = Vec::with_capacity(n * sub_dim);
524 for row in 0..n {
525 for col in 0..sub_dim {
526 subdata.push(rotated[(row, subspace_idx * sub_dim + col)]);
527 }
528 }
529
530 let k = config.num_centroids.min(n);
531 let data = Array2::from_shape_vec((n, sub_dim), subdata)
532 .expect("Failed to create subspace array");
533 let mut kmeans = KMeans::new(k).with_euclidean(true).with_iterations(5);
534 let _ = kmeans
535 .train(data.view(), None)
536 .expect("K-means training failed");
537
538 let centroids = kmeans.centroids().expect("No centroids");
539
540 for (row, assignment) in assignments.iter().enumerate() {
541 let centroid_idx = assignment[subspace_idx];
542 if centroid_idx < k {
543 for col in 0..sub_dim {
544 reconstructed[(row, subspace_idx * sub_dim + col)] =
545 centroids[[centroid_idx, col]];
546 }
547 }
548 }
549 }
550
551 reconstructed
552 }
553
554 fn apply_rotation(rotation: &[f32], vector: &[f32], dim: usize) -> Vec<f32> {
556 let mut result = vec![0.0f32; dim];
557 for i in 0..dim {
558 result[i] = crate::structures::simd::dot_product_f32(
559 &rotation[i * dim..(i + 1) * dim],
560 vector,
561 dim,
562 );
563 }
564 result
565 }
566
567 #[cfg(not(feature = "native"))]
569 fn train_subspace_scalar(
570 subvectors: &[Vec<f32>],
571 k: usize,
572 sub_dim: usize,
573 max_iters: usize,
574 rng: &mut impl Rng,
575 ) -> Vec<f32> {
576 let actual_k = k.min(subvectors.len());
577 let mut centroids = Self::kmeans_plusplus_init_scalar(subvectors, actual_k, sub_dim, rng);
578
579 for _ in 0..max_iters {
580 let assignments: Vec<usize> = subvectors
581 .iter()
582 .map(|v| Self::find_nearest_scalar(¢roids, v, sub_dim))
583 .collect();
584
585 let mut new_centroids = vec![0.0f32; actual_k * sub_dim];
586 let mut counts = vec![0usize; actual_k];
587
588 for (subvec, &assignment) in subvectors.iter().zip(assignments.iter()) {
589 counts[assignment] += 1;
590 let offset = assignment * sub_dim;
591 for (j, &val) in subvec.iter().enumerate() {
592 new_centroids[offset + j] += val;
593 }
594 }
595
596 for (c, &count) in counts.iter().enumerate().take(actual_k) {
597 if count > 0 {
598 let offset = c * sub_dim;
599 for j in 0..sub_dim {
600 new_centroids[offset + j] /= count as f32;
601 }
602 }
603 }
604
605 centroids = new_centroids;
606 }
607
608 while centroids.len() < k * sub_dim {
609 let last_start = centroids.len() - sub_dim;
610 let last: Vec<f32> = centroids[last_start..].to_vec();
611 centroids.extend(last);
612 }
613
614 centroids
615 }
616
617 #[cfg(not(feature = "native"))]
618 fn kmeans_plusplus_init_scalar(
619 subvectors: &[Vec<f32>],
620 k: usize,
621 sub_dim: usize,
622 rng: &mut impl Rng,
623 ) -> Vec<f32> {
624 let mut centroids = Vec::with_capacity(k * sub_dim);
625 let first_idx = rng.random_range(0..subvectors.len());
626 centroids.extend_from_slice(&subvectors[first_idx]);
627
628 for _ in 1..k {
629 let distances: Vec<f32> = subvectors
630 .iter()
631 .map(|v| Self::min_dist_to_centroids_scalar(¢roids, v, sub_dim))
632 .collect();
633
634 let total: f32 = distances.iter().sum();
635 let mut r = rng.random::<f32>() * total;
636 let mut chosen_idx = 0;
637 for (i, &d) in distances.iter().enumerate() {
638 r -= d;
639 if r <= 0.0 {
640 chosen_idx = i;
641 break;
642 }
643 }
644 centroids.extend_from_slice(&subvectors[chosen_idx]);
645 }
646
647 centroids
648 }
649
650 #[cfg(not(feature = "native"))]
651 fn min_dist_to_centroids_scalar(centroids: &[f32], vector: &[f32], sub_dim: usize) -> f32 {
652 let num_centroids = centroids.len() / sub_dim;
653 (0..num_centroids)
654 .map(|c| {
655 let offset = c * sub_dim;
656 vector
657 .iter()
658 .zip(¢roids[offset..offset + sub_dim])
659 .map(|(&a, &b)| (a - b) * (a - b))
660 .sum()
661 })
662 .fold(f32::MAX, f32::min)
663 }
664
665 #[cfg(not(feature = "native"))]
666 fn find_nearest_scalar(centroids: &[f32], vector: &[f32], sub_dim: usize) -> usize {
667 let num_centroids = centroids.len() / sub_dim;
668 (0..num_centroids)
669 .map(|c| {
670 let offset = c * sub_dim;
671 let dist: f32 = vector
672 .iter()
673 .zip(¢roids[offset..offset + sub_dim])
674 .map(|(&a, &b)| (a - b) * (a - b))
675 .sum();
676 (c, dist)
677 })
678 .min_by(|a, b| a.1.total_cmp(&b.1))
679 .map(|(c, _)| c)
680 .unwrap_or(0)
681 }
682
683 fn find_nearest(centroids: &[f32], vector: &[f32], sub_dim: usize) -> usize {
685 let num_centroids = centroids.len() / sub_dim;
686 let mut best_idx = 0;
687 let mut best_dist = f32::MAX;
688
689 for c in 0..num_centroids {
690 let offset = c * sub_dim;
691 let dist: f32 = vector
692 .iter()
693 .zip(¢roids[offset..offset + sub_dim])
694 .map(|(&a, &b)| (a - b) * (a - b))
695 .sum();
696
697 if dist < best_dist {
698 best_dist = dist;
699 best_idx = c;
700 }
701 }
702
703 best_idx
704 }
705
706 pub fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> PQVector {
708 let m = self.config.num_subspaces;
709 let k = self.config.num_centroids;
710 let sub_dim = self.config.subspace_dim();
711
712 let residual: Vec<f32> = if let Some(c) = centroid {
714 vector.iter().zip(c).map(|(&v, &c)| v - c).collect()
715 } else {
716 vector.to_vec()
717 };
718
719 let rotated: Vec<f32>;
721 let vec_to_encode = if let Some(ref r) = self.rotation_matrix {
722 rotated = Self::apply_rotation(r, &residual, self.config.dim);
723 &rotated
724 } else {
725 &residual
726 };
727
728 let mut codes = Vec::with_capacity(m);
729
730 for subspace_idx in 0..m {
731 let vec_offset = subspace_idx * sub_dim;
732 let subvec = &vec_to_encode[vec_offset..vec_offset + sub_dim];
733
734 let centroid_base = subspace_idx * k * sub_dim;
735 let centroids_slice = &self.centroids[centroid_base..centroid_base + k * sub_dim];
736
737 let nearest = Self::find_nearest(centroids_slice, subvec, sub_dim);
738 codes.push(nearest as u8);
739 }
740
741 let norm = vector.iter().map(|x| x * x).sum::<f32>().sqrt();
742 PQVector::new(codes, norm)
743 }
744
745 pub fn decode(&self, codes: &[u8]) -> Vec<f32> {
747 let m = self.config.num_subspaces;
748 let k = self.config.num_centroids;
749 let sub_dim = self.config.subspace_dim();
750
751 let mut rotated_vector = Vec::with_capacity(self.config.dim);
752
753 for (subspace_idx, &code) in codes.iter().enumerate().take(m) {
754 let centroid_base = subspace_idx * k * sub_dim;
755 let centroid_offset = centroid_base + (code as usize) * sub_dim;
756 rotated_vector
757 .extend_from_slice(&self.centroids[centroid_offset..centroid_offset + sub_dim]);
758 }
759
760 if let Some(ref r) = self.rotation_matrix {
762 Self::apply_rotation_transpose(r, &rotated_vector, self.config.dim)
763 } else {
764 rotated_vector
765 }
766 }
767
768 fn apply_rotation_transpose(rotation: &[f32], vector: &[f32], dim: usize) -> Vec<f32> {
770 let mut result = vec![0.0f32; dim];
771 for i in 0..dim {
772 for j in 0..dim {
773 result[i] += rotation[j * dim + i] * vector[j];
774 }
775 }
776 result
777 }
778
779 #[inline]
781 pub fn get_centroid(&self, subspace_idx: usize, code: u8) -> &[f32] {
782 let k = self.config.num_centroids;
783 let sub_dim = self.config.subspace_dim();
784 let offset = subspace_idx * k * sub_dim + (code as usize) * sub_dim;
785 &self.centroids[offset..offset + sub_dim]
786 }
787
788 pub fn rotate_query(&self, query: &[f32]) -> Vec<f32> {
790 if let Some(ref r) = self.rotation_matrix {
791 Self::apply_rotation(r, query, self.config.dim)
792 } else {
793 query.to_vec()
794 }
795 }
796
797 pub fn save(&self, path: &std::path::Path) -> io::Result<()> {
799 let mut file = std::fs::File::create(path)?;
800 self.write_to(&mut file)
801 }
802
803 pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
805 writer.write_u32::<LittleEndian>(CODEBOOK_MAGIC)?;
806 writer.write_u32::<LittleEndian>(2)?;
807 writer.write_u64::<LittleEndian>(self.version)?;
808 writer.write_u32::<LittleEndian>(self.config.dim as u32)?;
809 writer.write_u32::<LittleEndian>(self.config.num_subspaces as u32)?;
810 writer.write_u32::<LittleEndian>(self.config.dims_per_block as u32)?;
811 writer.write_u32::<LittleEndian>(self.config.num_centroids as u32)?;
812 writer.write_u8(if self.config.anisotropic { 1 } else { 0 })?;
813 writer.write_f32::<LittleEndian>(self.config.aniso_eta)?;
814 writer.write_f32::<LittleEndian>(self.config.aniso_threshold)?;
815 writer.write_u8(if self.config.use_opq { 1 } else { 0 })?;
816 writer.write_u32::<LittleEndian>(self.config.opq_iters as u32)?;
817
818 if let Some(ref rotation) = self.rotation_matrix {
819 writer.write_u8(1)?;
820 for &val in rotation {
821 writer.write_f32::<LittleEndian>(val)?;
822 }
823 } else {
824 writer.write_u8(0)?;
825 }
826
827 for &val in &self.centroids {
828 writer.write_f32::<LittleEndian>(val)?;
829 }
830
831 if let Some(ref norms) = self.centroid_norms {
832 writer.write_u8(1)?;
833 for &val in norms {
834 writer.write_f32::<LittleEndian>(val)?;
835 }
836 } else {
837 writer.write_u8(0)?;
838 }
839
840 Ok(())
841 }
842
843 pub fn load(path: &std::path::Path) -> io::Result<Self> {
845 let data = std::fs::read(path)?;
846 Self::read_from(&mut std::io::Cursor::new(data))
847 }
848
849 pub fn read_from<R: Read>(reader: &mut R) -> io::Result<Self> {
851 let magic = reader.read_u32::<LittleEndian>()?;
852 if magic != CODEBOOK_MAGIC {
853 return Err(io::Error::new(
854 io::ErrorKind::InvalidData,
855 "Invalid codebook file magic",
856 ));
857 }
858
859 let file_version = reader.read_u32::<LittleEndian>()?;
860 let version = reader.read_u64::<LittleEndian>()?;
861 let dim = reader.read_u32::<LittleEndian>()? as usize;
862 let num_subspaces = reader.read_u32::<LittleEndian>()? as usize;
863
864 let (
865 dims_per_block,
866 num_centroids,
867 anisotropic,
868 aniso_eta,
869 aniso_threshold,
870 use_opq,
871 opq_iters,
872 ) = if file_version >= 2 {
873 let dpb = reader.read_u32::<LittleEndian>()? as usize;
874 let nc = reader.read_u32::<LittleEndian>()? as usize;
875 let aniso = reader.read_u8()? != 0;
876 let eta = reader.read_f32::<LittleEndian>()?;
877 let thresh = reader.read_f32::<LittleEndian>()?;
878 let opq = reader.read_u8()? != 0;
879 let iters = reader.read_u32::<LittleEndian>()? as usize;
880 (dpb, nc, aniso, eta, thresh, opq, iters)
881 } else {
882 let nc = reader.read_u32::<LittleEndian>()? as usize;
883 let aniso = reader.read_u8()? != 0;
884 let thresh = reader.read_f32::<LittleEndian>()?;
885 let dpb = dim / num_subspaces;
886 (dpb, nc, aniso, 10.0, thresh, false, 0)
887 };
888
889 let config = PQConfig {
890 dim,
891 num_subspaces,
892 dims_per_block,
893 num_centroids,
894 seed: 42,
895 anisotropic,
896 aniso_eta,
897 aniso_threshold,
898 use_opq,
899 opq_iters,
900 };
901
902 let rotation_matrix = if file_version >= 2 {
903 let has_rotation = reader.read_u8()? != 0;
904 if has_rotation {
905 let mut rotation = vec![0.0f32; dim * dim];
906 for val in &mut rotation {
907 *val = reader.read_f32::<LittleEndian>()?;
908 }
909 Some(rotation)
910 } else {
911 None
912 }
913 } else {
914 None
915 };
916
917 let centroid_count = num_subspaces * num_centroids * config.subspace_dim();
918 let mut centroids = vec![0.0f32; centroid_count];
919 for val in &mut centroids {
920 *val = reader.read_f32::<LittleEndian>()?;
921 }
922
923 let has_norms = reader.read_u8()? != 0;
924 let centroid_norms = if has_norms {
925 let mut norms = vec![0.0f32; num_subspaces * num_centroids];
926 for val in &mut norms {
927 *val = reader.read_f32::<LittleEndian>()?;
928 }
929 Some(norms)
930 } else {
931 None
932 };
933
934 Ok(Self {
935 config,
936 rotation_matrix,
937 centroids,
938 version,
939 centroid_norms,
940 })
941 }
942
943 pub fn size_bytes(&self) -> usize {
945 let centroids_size = self.centroids.len() * 4;
946 let norms_size = self
947 .centroid_norms
948 .as_ref()
949 .map(|n| n.len() * 4)
950 .unwrap_or(0);
951 let rotation_size = self
952 .rotation_matrix
953 .as_ref()
954 .map(|r| r.len() * 4)
955 .unwrap_or(0);
956 centroids_size + norms_size + rotation_size + 64
957 }
958
959 pub fn estimated_memory_bytes(&self) -> usize {
961 self.size_bytes()
962 }
963}
964
965#[derive(Debug, Clone)]
967pub struct DistanceTable {
968 pub distances: Vec<f32>,
970 pub num_subspaces: usize,
972 pub num_centroids: usize,
974}
975
976impl DistanceTable {
977 pub fn build(codebook: &PQCodebook, query: &[f32], centroid: Option<&[f32]>) -> Self {
979 let m = codebook.config.num_subspaces;
980 let k = codebook.config.num_centroids;
981 let sub_dim = codebook.config.subspace_dim();
982
983 let residual: Vec<f32> = if let Some(c) = centroid {
985 query.iter().zip(c).map(|(&v, &c)| v - c).collect()
986 } else {
987 query.to_vec()
988 };
989
990 let rotated_query = codebook.rotate_query(&residual);
992
993 let mut distances = Vec::with_capacity(m * k);
994
995 for subspace_idx in 0..m {
996 let query_offset = subspace_idx * sub_dim;
997 let query_sub = &rotated_query[query_offset..query_offset + sub_dim];
998
999 let centroid_base = subspace_idx * k * sub_dim;
1000
1001 for centroid_idx in 0..k {
1002 let centroid_offset = centroid_base + centroid_idx * sub_dim;
1003 let centroid = &codebook.centroids[centroid_offset..centroid_offset + sub_dim];
1004
1005 let dist: f32 = query_sub
1006 .iter()
1007 .zip(centroid.iter())
1008 .map(|(&a, &b)| (a - b) * (a - b))
1009 .sum();
1010
1011 distances.push(dist);
1012 }
1013 }
1014
1015 Self {
1016 distances,
1017 num_subspaces: m,
1018 num_centroids: k,
1019 }
1020 }
1021
1022 #[inline]
1024 pub fn compute_distance(&self, codes: &[u8]) -> f32 {
1025 let k = self.num_centroids;
1026 let mut total = 0.0f32;
1027
1028 for (subspace_idx, &code) in codes.iter().enumerate() {
1029 let table_offset = subspace_idx * k + code as usize;
1030 total += self.distances[table_offset];
1031 }
1032
1033 total
1034 }
1035}
1036
1037impl Quantizer for PQCodebook {
1038 type Code = PQVector;
1039 type Config = PQConfig;
1040 type QueryData = DistanceTable;
1041
1042 fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> Self::Code {
1043 self.encode(vector, centroid)
1044 }
1045
1046 fn prepare_query(&self, query: &[f32], centroid: Option<&[f32]>) -> Self::QueryData {
1047 DistanceTable::build(self, query, centroid)
1048 }
1049
1050 fn compute_distance(&self, query_data: &Self::QueryData, code: &Self::Code) -> f32 {
1051 query_data.compute_distance(&code.codes)
1052 }
1053
1054 fn decode(&self, code: &Self::Code) -> Option<Vec<f32>> {
1055 Some(self.decode(&code.codes))
1056 }
1057
1058 fn size_bytes(&self) -> usize {
1059 self.size_bytes()
1060 }
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065 use super::*;
1066 use rand::prelude::*;
1067
1068 #[test]
1069 fn test_pq_config() {
1070 let config = PQConfig::new(128);
1071 assert_eq!(config.dim, 128);
1072 assert_eq!(config.dims_per_block, 2);
1073 assert_eq!(config.num_subspaces, 64);
1074 }
1075
1076 #[test]
1077 fn test_pq_encode_decode() {
1078 let dim = 32;
1079 let config = PQConfig::new(dim).with_opq(false, 0);
1080
1081 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1082 let vectors: Vec<Vec<f32>> = (0..100)
1083 .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
1084 .collect();
1085
1086 let codebook = PQCodebook::train(config, &vectors, 10);
1087
1088 let test_vec: Vec<f32> = (0..dim).map(|i| i as f32 / dim as f32).collect();
1089 let code = codebook.encode(&test_vec, None);
1090
1091 assert_eq!(code.codes.len(), 16); }
1093
1094 #[test]
1095 fn test_distance_table() {
1096 let dim = 16;
1097 let config = PQConfig::new(dim).with_opq(false, 0);
1098
1099 let mut rng = rand::rngs::StdRng::seed_from_u64(123);
1100 let vectors: Vec<Vec<f32>> = (0..50)
1101 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
1102 .collect();
1103
1104 let codebook = PQCodebook::train(config, &vectors, 5);
1105
1106 let query: Vec<f32> = (0..dim).map(|_| rng.random::<f32>()).collect();
1107 let table = DistanceTable::build(&codebook, &query, None);
1108
1109 let code = codebook.encode(&vectors[0], None);
1110 let dist = table.compute_distance(&code.codes);
1111
1112 assert!(dist >= 0.0);
1113 }
1114}