1use ipfrs_core::{Error, Result};
4use nalgebra::{DMatrix, DVector};
5use serde::{Deserialize, Serialize};
6
7use super::scalar::{QuantizedVector, ScalarQuantizer};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ProductQuantizerConfig {
12 pub dimension: usize,
14 pub num_subquantizers: usize,
16 pub bits_per_subquantizer: u8,
18 pub codebooks: Vec<Vec<Vec<f32>>>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ProductQuantizer {
28 pub(crate) config: ProductQuantizerConfig,
30 subdimension: usize,
32 num_centroids: usize,
34 trained: bool,
36}
37
38impl ProductQuantizer {
39 pub fn new(dimension: usize, num_subquantizers: usize, bits: u8) -> Result<Self> {
46 if !dimension.is_multiple_of(num_subquantizers) {
47 return Err(Error::InvalidInput(format!(
48 "Dimension {} must be divisible by num_subquantizers {}",
49 dimension, num_subquantizers
50 )));
51 }
52
53 if bits > 16 {
54 return Err(Error::InvalidInput(
55 "Bits per subquantizer must be <= 16".to_string(),
56 ));
57 }
58
59 let subdimension = dimension / num_subquantizers;
60 let num_centroids = 1 << bits;
61
62 Ok(Self {
63 config: ProductQuantizerConfig {
64 dimension,
65 num_subquantizers,
66 bits_per_subquantizer: bits,
67 codebooks: Vec::new(),
68 },
69 subdimension,
70 num_centroids,
71 trained: false,
72 })
73 }
74
75 pub fn standard(dimension: usize) -> Result<Self> {
77 Self::new(dimension, 8, 8)
78 }
79
80 pub fn train(&mut self, vectors: &[Vec<f32>], max_iterations: usize) -> Result<()> {
86 if vectors.is_empty() {
87 return Err(Error::InvalidInput(
88 "Cannot train on empty vector set".to_string(),
89 ));
90 }
91
92 for (i, vec) in vectors.iter().enumerate() {
94 if vec.len() != self.config.dimension {
95 return Err(Error::InvalidInput(format!(
96 "Vector {} has dimension {}, expected {}",
97 i,
98 vec.len(),
99 self.config.dimension
100 )));
101 }
102 }
103
104 self.config.codebooks = Vec::with_capacity(self.config.num_subquantizers);
106
107 for sq in 0..self.config.num_subquantizers {
108 let start = sq * self.subdimension;
109 let end = start + self.subdimension;
110
111 let subvectors: Vec<Vec<f32>> =
113 vectors.iter().map(|v| v[start..end].to_vec()).collect();
114
115 let centroids = self.kmeans(&subvectors, self.num_centroids, max_iterations)?;
117 self.config.codebooks.push(centroids);
118 }
119
120 self.trained = true;
121 Ok(())
122 }
123
124 fn kmeans(&self, data: &[Vec<f32>], k: usize, max_iterations: usize) -> Result<Vec<Vec<f32>>> {
126 if data.is_empty() {
127 return Err(Error::InvalidInput("Empty data for k-means".to_string()));
128 }
129
130 let dim = data[0].len();
131 let n = data.len();
132 let actual_k = k.min(n); let mut centroids = Vec::with_capacity(actual_k);
136
137 centroids.push(data[0].clone());
139
140 for _ in 1..actual_k {
142 let mut best_idx = 0;
143 let mut best_dist = 0.0f32;
144
145 for (i, vec) in data.iter().enumerate() {
146 let min_dist = centroids
147 .iter()
148 .map(|c| self.l2_distance(vec, c))
149 .fold(f32::MAX, |a, b| a.min(b));
150
151 if min_dist > best_dist {
152 best_dist = min_dist;
153 best_idx = i;
154 }
155 }
156
157 centroids.push(data[best_idx].clone());
158 }
159
160 let mut assignments = vec![0usize; n];
162
163 for _iter in 0..max_iterations {
164 let mut changed = false;
166 for (i, vec) in data.iter().enumerate() {
167 let nearest = centroids
168 .iter()
169 .enumerate()
170 .map(|(j, c)| (j, self.l2_distance(vec, c)))
171 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
172 .map(|(j, _)| j)
173 .unwrap_or(0);
174
175 if assignments[i] != nearest {
176 assignments[i] = nearest;
177 changed = true;
178 }
179 }
180
181 if !changed {
182 break;
183 }
184
185 let mut new_centroids = vec![vec![0.0f32; dim]; actual_k];
187 let mut counts = vec![0usize; actual_k];
188
189 for (i, vec) in data.iter().enumerate() {
190 let cluster = assignments[i];
191 counts[cluster] += 1;
192 for (j, &val) in vec.iter().enumerate() {
193 new_centroids[cluster][j] += val;
194 }
195 }
196
197 for (i, centroid) in new_centroids.iter_mut().enumerate() {
198 if counts[i] > 0 {
199 for val in centroid.iter_mut() {
200 *val /= counts[i] as f32;
201 }
202 } else {
203 *centroid = centroids[i].clone();
205 }
206 }
207
208 centroids = new_centroids;
209 }
210
211 while centroids.len() < k {
213 centroids.push(centroids[centroids.len() - 1].clone());
214 }
215
216 Ok(centroids)
217 }
218
219 fn l2_distance(&self, a: &[f32], b: &[f32]) -> f32 {
220 a.iter()
221 .zip(b.iter())
222 .map(|(x, y)| (x - y) * (x - y))
223 .sum::<f32>()
224 .sqrt()
225 }
226
227 pub fn quantize(&self, vector: &[f32]) -> Result<PQCode> {
229 if !self.trained {
230 return Err(Error::InvalidInput(
231 "Product quantizer must be trained before use".to_string(),
232 ));
233 }
234
235 if vector.len() != self.config.dimension {
236 return Err(Error::InvalidInput(format!(
237 "Vector has dimension {}, expected {}",
238 vector.len(),
239 self.config.dimension
240 )));
241 }
242
243 let mut codes = Vec::with_capacity(self.config.num_subquantizers);
244
245 for sq in 0..self.config.num_subquantizers {
246 let start = sq * self.subdimension;
247 let end = start + self.subdimension;
248 let subvector = &vector[start..end];
249
250 let codebook = &self.config.codebooks[sq];
252 let (best_idx, _) = codebook
253 .iter()
254 .enumerate()
255 .map(|(i, c)| (i, self.l2_distance(subvector, c)))
256 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
257 .unwrap_or((0, 0.0));
258
259 codes.push(best_idx as u8);
260 }
261
262 Ok(PQCode { codes })
263 }
264
265 pub fn dequantize(&self, code: &PQCode) -> Result<Vec<f32>> {
267 if !self.trained {
268 return Err(Error::InvalidInput(
269 "Product quantizer must be trained before use".to_string(),
270 ));
271 }
272
273 if code.codes.len() != self.config.num_subquantizers {
274 return Err(Error::InvalidInput(format!(
275 "PQ code has {} elements, expected {}",
276 code.codes.len(),
277 self.config.num_subquantizers
278 )));
279 }
280
281 let mut result = Vec::with_capacity(self.config.dimension);
282
283 for (sq, &idx) in code.codes.iter().enumerate() {
284 let centroid = &self.config.codebooks[sq][idx as usize];
285 result.extend_from_slice(centroid);
286 }
287
288 Ok(result)
289 }
290
291 pub fn asymmetric_distance(&self, query: &[f32], code: &PQCode) -> Result<f32> {
295 if !self.trained {
296 return Err(Error::InvalidInput(
297 "Product quantizer must be trained".to_string(),
298 ));
299 }
300
301 let mut total_dist_sq = 0.0f32;
302
303 for sq in 0..self.config.num_subquantizers {
304 let start = sq * self.subdimension;
305 let end = start + self.subdimension;
306 let subquery = &query[start..end];
307 let centroid = &self.config.codebooks[sq][code.codes[sq] as usize];
308
309 for (q, c) in subquery.iter().zip(centroid.iter()) {
310 let diff = q - c;
311 total_dist_sq += diff * diff;
312 }
313 }
314
315 Ok(total_dist_sq.sqrt())
316 }
317
318 pub fn compute_distance_table(&self, query: &[f32]) -> Result<Vec<Vec<f32>>> {
322 if !self.trained {
323 return Err(Error::InvalidInput(
324 "Product quantizer must be trained".to_string(),
325 ));
326 }
327
328 let mut table = Vec::with_capacity(self.config.num_subquantizers);
329
330 for sq in 0..self.config.num_subquantizers {
331 let start = sq * self.subdimension;
332 let end = start + self.subdimension;
333 let subquery = &query[start..end];
334
335 let distances: Vec<f32> = self.config.codebooks[sq]
336 .iter()
337 .map(|c| {
338 subquery
339 .iter()
340 .zip(c.iter())
341 .map(|(q, c)| (q - c) * (q - c))
342 .sum::<f32>()
343 })
344 .collect();
345
346 table.push(distances);
347 }
348
349 Ok(table)
350 }
351
352 pub fn distance_from_table(&self, table: &[Vec<f32>], code: &PQCode) -> f32 {
354 let mut total = 0.0f32;
355 for (sq, &idx) in code.codes.iter().enumerate() {
356 total += table[sq][idx as usize];
357 }
358 total.sqrt()
359 }
360
361 pub fn compression_ratio(&self) -> f32 {
363 (self.config.dimension * 4) as f32 / self.config.num_subquantizers as f32
366 }
367
368 pub fn is_trained(&self) -> bool {
370 self.trained
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct PQCode {
377 pub codes: Vec<u8>,
379}
380
381impl PQCode {
382 pub fn size_bytes(&self) -> usize {
384 self.codes.len()
385 }
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct OptimizedProductQuantizer {
394 pq: ProductQuantizer,
396 #[serde(with = "rotation_matrix_serde")]
398 rotation: Option<DMatrix<f32>>,
399 rotation_trained: bool,
401}
402
403mod rotation_matrix_serde {
405 use super::DMatrix;
406 use serde::{Deserialize, Deserializer, Serialize, Serializer};
407
408 #[derive(Serialize, Deserialize)]
409 struct MatrixData {
410 nrows: usize,
411 ncols: usize,
412 data: Vec<f32>,
413 }
414
415 pub fn serialize<S>(
416 matrix: &Option<DMatrix<f32>>,
417 serializer: S,
418 ) -> std::result::Result<S::Ok, S::Error>
419 where
420 S: Serializer,
421 {
422 let opt_data = matrix.as_ref().map(|m| MatrixData {
423 nrows: m.nrows(),
424 ncols: m.ncols(),
425 data: m.as_slice().to_vec(),
426 });
427 opt_data.serialize(serializer)
428 }
429
430 pub fn deserialize<'de, D>(
431 deserializer: D,
432 ) -> std::result::Result<Option<DMatrix<f32>>, D::Error>
433 where
434 D: Deserializer<'de>,
435 {
436 let opt: Option<MatrixData> = Option::deserialize(deserializer)?;
437 Ok(opt.map(|data| DMatrix::from_vec(data.nrows, data.ncols, data.data)))
438 }
439}
440
441impl OptimizedProductQuantizer {
442 pub fn new(dimension: usize, num_subquantizers: usize, bits: u8) -> Result<Self> {
449 let pq = ProductQuantizer::new(dimension, num_subquantizers, bits)?;
450 Ok(Self {
451 pq,
452 rotation: None,
453 rotation_trained: false,
454 })
455 }
456
457 pub fn standard(dimension: usize) -> Result<Self> {
459 Self::new(dimension, 8, 8)
460 }
461
462 #[allow(clippy::too_many_arguments)]
471 pub fn train(
472 &mut self,
473 vectors: &[Vec<f32>],
474 max_iterations: usize,
475 rotation_iterations: usize,
476 ) -> Result<()> {
477 if vectors.is_empty() {
478 return Err(Error::InvalidInput(
479 "Cannot train on empty vector set".to_string(),
480 ));
481 }
482
483 let dim = self.pq.config.dimension;
484
485 for (i, vec) in vectors.iter().enumerate() {
487 if vec.len() != dim {
488 return Err(Error::InvalidInput(format!(
489 "Vector {} has dimension {}, expected {}",
490 i,
491 vec.len(),
492 dim
493 )));
494 }
495 }
496
497 let mut rotation = DMatrix::<f32>::identity(dim, dim);
499
500 for iteration in 0..rotation_iterations {
502 let rotated = self.apply_rotation_batch(vectors, &rotation);
504
505 self.pq.train(&rotated, max_iterations)?;
507
508 if iteration < rotation_iterations - 1 {
510 rotation = self.learn_rotation(vectors, &self.pq)?;
511 }
512 }
513
514 self.rotation = Some(rotation);
515 self.rotation_trained = true;
516
517 Ok(())
518 }
519
520 #[allow(dead_code)]
524 fn learn_rotation(&self, vectors: &[Vec<f32>], pq: &ProductQuantizer) -> Result<DMatrix<f32>> {
525 let dim = pq.config.dimension;
526 let n = vectors.len();
527
528 let mut cov = DMatrix::<f32>::zeros(dim, dim);
530
531 for vec in vectors {
532 let code = pq.quantize(vec)?;
534 let reconstructed = pq.dequantize(&code)?;
535
536 let v = DVector::from_vec(vec.clone());
538 let r = DVector::from_vec(reconstructed);
539 cov += v * r.transpose();
540 }
541
542 cov /= n as f32;
543
544 let svd = cov.svd(true, true);
546
547 match (svd.u, svd.v_t) {
549 (Some(u), Some(vt)) => Ok(u * vt),
550 _ => {
551 Ok(DMatrix::identity(dim, dim))
553 }
554 }
555 }
556
557 fn apply_rotation_batch(&self, vectors: &[Vec<f32>], rotation: &DMatrix<f32>) -> Vec<Vec<f32>> {
559 vectors
560 .iter()
561 .map(|v| self.apply_rotation(v, rotation))
562 .collect()
563 }
564
565 fn apply_rotation(&self, vector: &[f32], rotation: &DMatrix<f32>) -> Vec<f32> {
567 let v = DVector::from_vec(vector.to_vec());
568 let rotated = rotation * v;
569 rotated.as_slice().to_vec()
570 }
571
572 pub fn quantize(&self, vector: &[f32]) -> Result<PQCode> {
574 if !self.is_trained() {
575 return Err(Error::InvalidInput("OPQ must be trained".to_string()));
576 }
577
578 let rotated = match &self.rotation {
580 Some(r) => self.apply_rotation(vector, r),
581 None => vector.to_vec(),
582 };
583
584 self.pq.quantize(&rotated)
585 }
586
587 pub fn dequantize(&self, code: &PQCode) -> Result<Vec<f32>> {
589 if !self.is_trained() {
590 return Err(Error::InvalidInput("OPQ must be trained".to_string()));
591 }
592
593 let rotated = self.pq.dequantize(code)?;
595
596 match &self.rotation {
598 Some(r) => {
599 let r_inv = r.transpose();
601 Ok(self.apply_rotation(&rotated, &r_inv))
602 }
603 None => Ok(rotated),
604 }
605 }
606
607 pub fn asymmetric_distance(&self, query: &[f32], code: &PQCode) -> Result<f32> {
609 if !self.is_trained() {
610 return Err(Error::InvalidInput("OPQ must be trained".to_string()));
611 }
612
613 let rotated_query = match &self.rotation {
615 Some(r) => self.apply_rotation(query, r),
616 None => query.to_vec(),
617 };
618
619 self.pq.asymmetric_distance(&rotated_query, code)
620 }
621
622 pub fn compute_distance_table(&self, query: &[f32]) -> Result<Vec<Vec<f32>>> {
624 if !self.is_trained() {
625 return Err(Error::InvalidInput("OPQ must be trained".to_string()));
626 }
627
628 let rotated_query = match &self.rotation {
630 Some(r) => self.apply_rotation(query, r),
631 None => query.to_vec(),
632 };
633
634 self.pq.compute_distance_table(&rotated_query)
635 }
636
637 pub fn distance_from_table(&self, table: &[Vec<f32>], code: &PQCode) -> f32 {
639 self.pq.distance_from_table(table, code)
640 }
641
642 pub fn compression_ratio(&self) -> f32 {
644 self.pq.compression_ratio()
645 }
646
647 pub fn is_trained(&self) -> bool {
649 self.pq.is_trained() && self.rotation_trained
650 }
651
652 #[allow(dead_code)]
654 pub fn inner_pq(&self) -> &ProductQuantizer {
655 &self.pq
656 }
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
661pub struct QuantizationBenchmark {
662 pub recall_at_k: Vec<(usize, f32)>,
664 pub compression_ratio: f32,
666 pub avg_quantization_error: f32,
668 pub max_quantization_error: f32,
670 pub memory_savings: usize,
672 pub speed_factor: f32,
674}
675
676impl QuantizationBenchmark {
677 pub fn summary(&self) -> String {
679 let recall_str: Vec<String> = self
680 .recall_at_k
681 .iter()
682 .map(|(k, r)| format!("R@{}: {:.2}%", k, r * 100.0))
683 .collect();
684
685 format!(
686 "Compression: {:.1}x, Avg Error: {:.4}, {}, Memory Saved: {} bytes",
687 self.compression_ratio,
688 self.avg_quantization_error,
689 recall_str.join(", "),
690 self.memory_savings
691 )
692 }
693}
694
695pub struct QuantizationBenchmarker;
697
698impl QuantizationBenchmarker {
699 pub fn benchmark_scalar(
708 quantizer: &ScalarQuantizer,
709 vectors: &[Vec<f32>],
710 queries: &[Vec<f32>],
711 ground_truth: &[Vec<usize>],
712 k_values: &[usize],
713 ) -> Result<QuantizationBenchmark> {
714 if !quantizer.is_trained() {
715 return Err(Error::InvalidInput("Quantizer must be trained".to_string()));
716 }
717
718 let quantized: Vec<QuantizedVector> = vectors
720 .iter()
721 .map(|v| quantizer.quantize(v))
722 .collect::<Result<Vec<_>>>()?;
723
724 let mut total_error = 0.0f32;
726 let mut max_error = 0.0f32;
727
728 for (i, qv) in quantized.iter().enumerate() {
729 let restored = quantizer.dequantize(qv)?;
730 let error: f32 = vectors[i]
731 .iter()
732 .zip(restored.iter())
733 .map(|(a, b)| (a - b).powi(2))
734 .sum::<f32>()
735 .sqrt();
736 total_error += error;
737 max_error = max_error.max(error);
738 }
739
740 let avg_error = total_error / vectors.len() as f32;
741
742 let mut recall_at_k = Vec::new();
744
745 for &k in k_values {
746 let mut total_recall = 0.0f32;
747
748 for (qi, query) in queries.iter().enumerate() {
749 let query_quantized = quantizer.quantize(query)?;
750
751 let mut distances: Vec<(usize, f32)> = quantized
753 .iter()
754 .enumerate()
755 .map(|(i, qv)| {
756 let dist = quantizer
757 .distance_l2_quantized(&query_quantized, qv)
758 .unwrap_or(f32::MAX);
759 (i, dist)
760 })
761 .collect();
762
763 distances
764 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
765
766 let found: std::collections::HashSet<usize> =
767 distances.iter().take(k).map(|(i, _)| *i).collect();
768
769 let gt: std::collections::HashSet<usize> =
770 ground_truth[qi].iter().take(k).copied().collect();
771
772 let intersection = found.intersection(>).count();
773 total_recall += intersection as f32 / k.min(gt.len()) as f32;
774 }
775
776 let recall = total_recall / queries.len() as f32;
777 recall_at_k.push((k, recall));
778 }
779
780 let original_size = vectors.len() * vectors[0].len() * 4; let quantized_size = vectors.len() * vectors[0].len(); let memory_savings = original_size - quantized_size;
784
785 Ok(QuantizationBenchmark {
786 recall_at_k,
787 compression_ratio: quantizer.compression_ratio(),
788 avg_quantization_error: avg_error,
789 max_quantization_error: max_error,
790 memory_savings,
791 speed_factor: 2.0, })
793 }
794
795 pub fn benchmark_pq(
797 pq: &ProductQuantizer,
798 vectors: &[Vec<f32>],
799 queries: &[Vec<f32>],
800 ground_truth: &[Vec<usize>],
801 k_values: &[usize],
802 ) -> Result<QuantizationBenchmark> {
803 if !pq.is_trained() {
804 return Err(Error::InvalidInput("PQ must be trained".to_string()));
805 }
806
807 let codes: Vec<PQCode> = vectors
809 .iter()
810 .map(|v| pq.quantize(v))
811 .collect::<Result<Vec<_>>>()?;
812
813 let mut total_error = 0.0f32;
815 let mut max_error = 0.0f32;
816
817 for (i, code) in codes.iter().enumerate() {
818 let restored = pq.dequantize(code)?;
819 let error: f32 = vectors[i]
820 .iter()
821 .zip(restored.iter())
822 .map(|(a, b)| (a - b).powi(2))
823 .sum::<f32>()
824 .sqrt();
825 total_error += error;
826 max_error = max_error.max(error);
827 }
828
829 let avg_error = total_error / vectors.len() as f32;
830
831 let mut recall_at_k = Vec::new();
833
834 for &k in k_values {
835 let mut total_recall = 0.0f32;
836
837 for (qi, query) in queries.iter().enumerate() {
838 let table = pq.compute_distance_table(query)?;
840
841 let mut distances: Vec<(usize, f32)> = codes
842 .iter()
843 .enumerate()
844 .map(|(i, code)| (i, pq.distance_from_table(&table, code)))
845 .collect();
846
847 distances
848 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
849
850 let found: std::collections::HashSet<usize> =
851 distances.iter().take(k).map(|(i, _)| *i).collect();
852
853 let gt: std::collections::HashSet<usize> =
854 ground_truth[qi].iter().take(k).copied().collect();
855
856 let intersection = found.intersection(>).count();
857 total_recall += intersection as f32 / k.min(gt.len()) as f32;
858 }
859
860 let recall = total_recall / queries.len() as f32;
861 recall_at_k.push((k, recall));
862 }
863
864 let original_size = vectors.len() * vectors[0].len() * 4;
866 let quantized_size = vectors.len() * codes[0].size_bytes();
867 let memory_savings = original_size.saturating_sub(quantized_size);
868
869 Ok(QuantizationBenchmark {
870 recall_at_k,
871 compression_ratio: pq.compression_ratio(),
872 avg_quantization_error: avg_error,
873 max_quantization_error: max_error,
874 memory_savings,
875 speed_factor: 4.0, })
877 }
878
879 pub fn compute_ground_truth(
881 vectors: &[Vec<f32>],
882 queries: &[Vec<f32>],
883 k: usize,
884 ) -> Vec<Vec<usize>> {
885 queries
886 .iter()
887 .map(|query| {
888 let mut distances: Vec<(usize, f32)> = vectors
889 .iter()
890 .enumerate()
891 .map(|(i, v)| {
892 let dist: f32 = query
893 .iter()
894 .zip(v.iter())
895 .map(|(a, b)| (a - b).powi(2))
896 .sum();
897 (i, dist)
898 })
899 .collect();
900
901 distances
902 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
903
904 distances.iter().take(k).map(|(i, _)| *i).collect()
905 })
906 .collect()
907 }
908
909 pub fn compare_methods(
911 vectors: &[Vec<f32>],
912 queries: &[Vec<f32>],
913 k_values: &[usize],
914 ) -> Result<QuantizationComparison> {
915 let max_k = *k_values.iter().max().unwrap_or(&10);
916 let ground_truth = Self::compute_ground_truth(vectors, queries, max_k);
917
918 let mut sq = ScalarQuantizer::uint8(vectors[0].len());
920 sq.train(vectors)?;
921 let scalar_results =
922 Self::benchmark_scalar(&sq, vectors, queries, &ground_truth, k_values)?;
923
924 let dim = vectors[0].len();
926 let pq_results = if dim >= 8 && dim.is_multiple_of(8) {
927 let mut pq = ProductQuantizer::new(dim, 8, 8)?;
928 pq.train(vectors, 20)?;
929 Some(Self::benchmark_pq(
930 &pq,
931 vectors,
932 queries,
933 &ground_truth,
934 k_values,
935 )?)
936 } else {
937 None
938 };
939
940 Ok(QuantizationComparison {
941 scalar: scalar_results,
942 product: pq_results,
943 dataset_size: vectors.len(),
944 dimension: dim,
945 })
946 }
947}
948
949#[derive(Debug, Clone, Serialize, Deserialize)]
951pub struct QuantizationComparison {
952 pub scalar: QuantizationBenchmark,
954 pub product: Option<QuantizationBenchmark>,
956 pub dataset_size: usize,
958 pub dimension: usize,
960}
961
962impl QuantizationComparison {
963 pub fn summary(&self) -> String {
965 let mut result = format!(
966 "Dataset: {} vectors, {} dimensions\n\nScalar Quantization:\n {}\n",
967 self.dataset_size,
968 self.dimension,
969 self.scalar.summary()
970 );
971
972 if let Some(ref pq) = self.product {
973 result.push_str(&format!("\nProduct Quantization:\n {}\n", pq.summary()));
974 }
975
976 result
977 }
978
979 pub fn best_method_for_k(&self, k: usize) -> (&str, f32) {
981 let scalar_recall = self
982 .scalar
983 .recall_at_k
984 .iter()
985 .find(|(kv, _)| *kv == k)
986 .map(|(_, r)| *r)
987 .unwrap_or(0.0);
988
989 if let Some(ref pq) = self.product {
990 let pq_recall = pq
991 .recall_at_k
992 .iter()
993 .find(|(kv, _)| *kv == k)
994 .map(|(_, r)| *r)
995 .unwrap_or(0.0);
996
997 if pq_recall > scalar_recall {
998 return ("ProductQuantization", pq_recall);
999 }
1000 }
1001
1002 ("ScalarQuantization", scalar_recall)
1003 }
1004}