1use crate::{
9 pq::{PQConfig, PQIndex},
10 Vector, VectorIndex,
11};
12use anyhow::{anyhow, Result};
13use std::sync::{Arc, RwLock};
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum QuantizationStrategy {
18 None,
20 ProductQuantization(PQConfig),
22 ResidualQuantization {
24 levels: usize,
25 pq_configs: Vec<PQConfig>,
26 },
27 MultiCodebook {
29 num_codebooks: usize,
30 pq_configs: Vec<PQConfig>,
31 },
32}
33
34#[derive(Debug, Clone)]
36pub struct IvfConfig {
37 pub n_clusters: usize,
39 pub n_probes: usize,
41 pub max_iterations: usize,
43 pub convergence_threshold: f32,
45 pub seed: Option<u64>,
47 pub quantization: QuantizationStrategy,
49 pub enable_residual_quantization: bool,
51 pub pq_config: Option<PQConfig>,
53}
54
55impl Default for IvfConfig {
56 fn default() -> Self {
57 Self {
58 n_clusters: 256,
59 n_probes: 8,
60 max_iterations: 100,
61 convergence_threshold: 1e-4,
62 seed: None,
63 quantization: QuantizationStrategy::None,
64 enable_residual_quantization: false,
65 pq_config: None,
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72enum VectorStorage {
73 Full(Vector),
75 Quantized(Vec<u8>),
77 MultiLevelQuantized {
79 levels: Vec<Vec<u8>>, final_residual: Option<Vector>, },
82 MultiCodebook {
84 codebooks: Vec<Vec<u8>>, weights: Vec<f32>, },
87}
88
89#[derive(Debug, Clone)]
91struct InvertedList {
92 vectors: Vec<(String, VectorStorage)>,
94 quantization: QuantizationStrategy,
96 pq_index: Option<PQIndex>,
98 multi_level_pq: Vec<PQIndex>,
100 multi_codebook_pq: Vec<PQIndex>,
102 codebook_weights: Vec<f32>,
104}
105
106impl InvertedList {
107 fn new() -> Self {
108 Self {
109 vectors: Vec::new(),
110 quantization: QuantizationStrategy::None,
111 pq_index: None,
112 multi_level_pq: Vec::new(),
113 multi_codebook_pq: Vec::new(),
114 codebook_weights: Vec::new(),
115 }
116 }
117
118 fn new_with_quantization(quantization: QuantizationStrategy) -> Result<Self> {
119 let mut list = Self {
120 vectors: Vec::new(),
121 quantization: quantization.clone(),
122 pq_index: None,
123 multi_level_pq: Vec::new(),
124 multi_codebook_pq: Vec::new(),
125 codebook_weights: Vec::new(),
126 };
127
128 match quantization {
129 QuantizationStrategy::None => {}
130 QuantizationStrategy::ProductQuantization(pq_config) => {
131 list.pq_index = Some(PQIndex::new(pq_config));
132 }
133 QuantizationStrategy::ResidualQuantization {
134 levels: _,
135 ref pq_configs,
136 } => {
137 for pq_config in pq_configs {
138 list.multi_level_pq.push(PQIndex::new(pq_config.clone()));
139 }
140 }
141 QuantizationStrategy::MultiCodebook {
142 num_codebooks,
143 ref pq_configs,
144 } => {
145 for pq_config in pq_configs {
146 list.multi_codebook_pq.push(PQIndex::new(pq_config.clone()));
147 }
148 list.codebook_weights = vec![1.0 / num_codebooks as f32; num_codebooks];
150 }
151 }
152
153 Ok(list)
154 }
155
156 fn new_with_pq(pq_config: PQConfig) -> Result<Self> {
158 Self::new_with_quantization(QuantizationStrategy::ProductQuantization(pq_config))
159 }
160
161 fn add_full(&mut self, uri: String, vector: Vector) {
162 self.vectors.push((uri, VectorStorage::Full(vector)));
163 }
164
165 fn add_residual(&mut self, uri: String, residual: Vector, _centroid: &Vector) -> Result<()> {
166 match &self.quantization {
167 QuantizationStrategy::ProductQuantization(_) => {
168 if let Some(ref mut pq_index) = self.pq_index {
169 if !pq_index.is_trained() {
171 let training_residuals = vec![residual.clone()];
172 pq_index.train(&training_residuals)?;
173 }
174
175 let codes = pq_index.encode(&residual)?;
176 self.vectors.push((uri, VectorStorage::Quantized(codes)));
177 } else {
178 return Err(anyhow!(
179 "PQ index not initialized for residual quantization"
180 ));
181 }
182 }
183 QuantizationStrategy::ResidualQuantization { levels, .. } => {
184 self.add_multi_level_residual(uri, residual, *levels)?;
185 }
186 QuantizationStrategy::MultiCodebook { .. } => {
187 self.add_multi_codebook(uri, residual)?;
188 }
189 QuantizationStrategy::None => {
190 self.add_full(uri, residual);
191 }
192 }
193 Ok(())
194 }
195
196 fn add_multi_level_residual(
198 &mut self,
199 uri: String,
200 mut residual: Vector,
201 levels: usize,
202 ) -> Result<()> {
203 let mut level_codes = Vec::new();
204
205 for level in 0..levels.min(self.multi_level_pq.len()) {
206 if !self.multi_level_pq[level].is_trained() {
208 let training_residuals = vec![residual.clone()];
209 self.multi_level_pq[level].train(&training_residuals)?;
210 }
211
212 let codes = self.multi_level_pq[level].encode(&residual)?;
214 level_codes.push(codes);
215
216 let approximation = self.multi_level_pq[level].decode_vector(&level_codes[level])?;
218 residual = residual.subtract(&approximation)?;
219 }
220
221 let final_residual = if level_codes.len() < levels {
223 Some(residual)
224 } else {
225 None
226 };
227
228 self.vectors.push((
229 uri,
230 VectorStorage::MultiLevelQuantized {
231 levels: level_codes,
232 final_residual,
233 },
234 ));
235
236 Ok(())
237 }
238
239 fn add_multi_codebook(&mut self, uri: String, residual: Vector) -> Result<()> {
241 let mut codebook_codes = Vec::new();
242
243 for pq_index in self.multi_codebook_pq.iter_mut() {
244 if !pq_index.is_trained() {
246 let training_residuals = vec![residual.clone()];
247 pq_index.train(&training_residuals)?;
248 }
249
250 let codes = pq_index.encode(&residual)?;
252 codebook_codes.push(codes);
253 }
254
255 self.vectors.push((
256 uri,
257 VectorStorage::MultiCodebook {
258 codebooks: codebook_codes,
259 weights: self.codebook_weights.clone(),
260 },
261 ));
262
263 Ok(())
264 }
265
266 fn search(&self, query: &Vector, centroid: &Vector, k: usize) -> Result<Vec<(String, f32)>> {
267 let mut distances: Vec<(String, f32)> = Vec::new();
268 let query_residual = query.subtract(centroid)?;
269
270 for (uri, storage) in &self.vectors {
271 let distance = match storage {
272 VectorStorage::Full(vec) => query.euclidean_distance(vec).unwrap_or(f32::INFINITY),
273 VectorStorage::Quantized(codes) => {
274 if let Some(ref pq_index) = self.pq_index {
275 pq_index.compute_distance(&query_residual, codes)?
276 } else {
277 f32::INFINITY
278 }
279 }
280 VectorStorage::MultiLevelQuantized {
281 levels,
282 final_residual,
283 } => self.compute_multi_level_distance(&query_residual, levels, final_residual)?,
284 VectorStorage::MultiCodebook { codebooks, weights } => {
285 self.compute_multi_codebook_distance(&query_residual, codebooks, weights)?
286 }
287 };
288 distances.push((uri.clone(), distance));
289 }
290
291 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
292 distances.truncate(k);
293
294 Ok(distances
296 .into_iter()
297 .map(|(uri, dist)| (uri, 1.0 / (1.0 + dist)))
298 .collect())
299 }
300
301 fn compute_multi_level_distance(
303 &self,
304 query_residual: &Vector,
305 level_codes: &[Vec<u8>],
306 final_residual: &Option<Vector>,
307 ) -> Result<f32> {
308 let mut reconstructed_residual = Vector::new(vec![0.0; query_residual.dimensions]);
309
310 for (level, codes) in level_codes.iter().enumerate() {
312 if level < self.multi_level_pq.len() {
313 let level_reconstruction = self.multi_level_pq[level].decode_vector(codes)?;
314 reconstructed_residual = reconstructed_residual.add(&level_reconstruction)?;
315 }
316 }
317
318 if let Some(final_res) = final_residual {
320 reconstructed_residual = reconstructed_residual.add(final_res)?;
321 }
322
323 query_residual.euclidean_distance(&reconstructed_residual)
325 }
326
327 fn compute_multi_codebook_distance(
329 &self,
330 query_residual: &Vector,
331 codebook_codes: &[Vec<u8>],
332 weights: &[f32],
333 ) -> Result<f32> {
334 let mut weighted_distance = 0.0;
335 let mut total_weight = 0.0;
336
337 for (i, codes) in codebook_codes.iter().enumerate() {
339 if i < self.multi_codebook_pq.len() && i < weights.len() {
340 let codebook_distance =
341 self.multi_codebook_pq[i].compute_distance(query_residual, codes)?;
342 weighted_distance += weights[i] * codebook_distance;
343 total_weight += weights[i];
344 }
345 }
346
347 if total_weight > 0.0 {
349 Ok(weighted_distance / total_weight)
350 } else {
351 Ok(f32::INFINITY)
352 }
353 }
354
355 fn reconstruct_multi_level_residual(
357 &self,
358 level_codes: &[Vec<u8>],
359 final_residual: &Option<Vector>,
360 dims: usize,
361 ) -> Result<Vector> {
362 let mut reconstructed_residual = Vector::new(vec![0.0; dims]);
363
364 for (level, codes) in level_codes.iter().enumerate() {
365 if level < self.multi_level_pq.len() {
366 let level_reconstruction = self.multi_level_pq[level].decode_vector(codes)?;
367 reconstructed_residual = reconstructed_residual.add(&level_reconstruction)?;
368 }
369 }
370
371 if let Some(final_res) = final_residual {
372 reconstructed_residual = reconstructed_residual.add(final_res)?;
373 }
374
375 Ok(reconstructed_residual)
376 }
377
378 fn reconstruct_multi_codebook_residual(
380 &self,
381 codebook_codes: &[Vec<u8>],
382 weights: &[f32],
383 dims: usize,
384 ) -> Result<Vector> {
385 let mut acc = Vector::new(vec![0.0; dims]);
386 let mut total_weight = 0.0;
387
388 for (i, codes) in codebook_codes.iter().enumerate() {
389 if i < self.multi_codebook_pq.len() && i < weights.len() {
390 let recon = self.multi_codebook_pq[i].decode_vector(codes)?;
391 acc = acc.add(&recon.scale(weights[i]))?;
392 total_weight += weights[i];
393 }
394 }
395
396 if total_weight > 0.0 {
397 acc = acc.scale(1.0 / total_weight);
398 }
399
400 Ok(acc)
401 }
402
403 fn reconstruct_all(&self, centroid: &Vector, residual_mode: bool) -> Vec<(String, Vector)> {
410 let dims = centroid.dimensions;
411 self.vectors
412 .iter()
413 .filter_map(|(uri, storage)| {
414 let reconstructed = match storage {
415 VectorStorage::Full(v) => {
416 if residual_mode {
417 centroid.add(v).ok()
418 } else {
419 Some(v.clone())
420 }
421 }
422 VectorStorage::Quantized(codes) => self
423 .pq_index
424 .as_ref()
425 .and_then(|pq| pq.decode_vector(codes).ok())
426 .and_then(|residual| centroid.add(&residual).ok()),
427 VectorStorage::MultiLevelQuantized {
428 levels,
429 final_residual,
430 } => self
431 .reconstruct_multi_level_residual(levels, final_residual, dims)
432 .ok()
433 .and_then(|residual| centroid.add(&residual).ok()),
434 VectorStorage::MultiCodebook { codebooks, weights } => self
435 .reconstruct_multi_codebook_residual(codebooks, weights, dims)
436 .ok()
437 .and_then(|residual| centroid.add(&residual).ok()),
438 };
439 reconstructed.map(|v| (uri.clone(), v))
440 })
441 .collect()
442 }
443
444 fn train_pq(&mut self, residuals: &[Vector]) -> Result<()> {
446 match &self.quantization {
447 QuantizationStrategy::ProductQuantization(_) => {
448 if let Some(ref mut pq_index) = self.pq_index {
449 pq_index.train(residuals)?;
450 }
451 }
452 QuantizationStrategy::ResidualQuantization { levels, .. } => {
453 self.train_multi_level_pq(residuals, *levels)?;
454 }
455 QuantizationStrategy::MultiCodebook { .. } => {
456 self.train_multi_codebook_pq(residuals)?;
457 }
458 QuantizationStrategy::None => {}
459 }
460 Ok(())
461 }
462
463 fn train_multi_level_pq(&mut self, residuals: &[Vector], levels: usize) -> Result<()> {
465 let mut current_residuals = residuals.to_vec();
466
467 for level in 0..levels.min(self.multi_level_pq.len()) {
468 self.multi_level_pq[level].train(¤t_residuals)?;
470
471 let mut next_residuals = Vec::new();
473 for residual in ¤t_residuals {
474 let codes = self.multi_level_pq[level].encode(residual)?;
475 let approximation = self.multi_level_pq[level].decode_vector(&codes)?;
476 let next_residual = residual.subtract(&approximation)?;
477 next_residuals.push(next_residual);
478 }
479 current_residuals = next_residuals;
480 }
481
482 Ok(())
483 }
484
485 fn train_multi_codebook_pq(&mut self, residuals: &[Vector]) -> Result<()> {
487 for pq_index in &mut self.multi_codebook_pq {
489 pq_index.train(residuals)?;
490 }
491
492 self.optimize_codebook_weights(residuals)?;
494
495 Ok(())
496 }
497
498 fn optimize_codebook_weights(&mut self, residuals: &[Vector]) -> Result<()> {
500 if self.multi_codebook_pq.is_empty() || residuals.is_empty() {
501 return Ok(());
502 }
503
504 let num_codebooks = self.multi_codebook_pq.len();
505 let mut reconstruction_errors = vec![0.0; num_codebooks];
506
507 for (i, pq_index) in self.multi_codebook_pq.iter().enumerate() {
509 let mut total_error = 0.0;
510 for residual in residuals {
511 let codes = pq_index.encode(residual)?;
512 let reconstruction = pq_index.decode_vector(&codes)?;
513 let error = residual
514 .euclidean_distance(&reconstruction)
515 .unwrap_or(f32::INFINITY);
516 total_error += error;
517 }
518 reconstruction_errors[i] = total_error / residuals.len() as f32;
519 }
520
521 let max_error = reconstruction_errors.iter().fold(0.0f32, |a, &b| a.max(b));
523 if max_error > 0.0 {
524 let mut total_weight = 0.0;
525 for (i, &error) in reconstruction_errors.iter().enumerate().take(num_codebooks) {
526 self.codebook_weights[i] = (max_error - error + 1e-6) / max_error;
528 total_weight += self.codebook_weights[i];
529 }
530
531 if total_weight > 0.0 {
533 for weight in &mut self.codebook_weights {
534 *weight /= total_weight;
535 }
536 }
537 }
538
539 Ok(())
540 }
541
542 fn stats(&self) -> InvertedListStats {
544 let mut full_vectors = 0;
545 let mut quantized_vectors = 0;
546 let mut multi_level_vectors = 0;
547 let mut multi_codebook_vectors = 0;
548
549 for (_, storage) in &self.vectors {
550 match storage {
551 VectorStorage::Full(_) => full_vectors += 1,
552 VectorStorage::Quantized(_) => quantized_vectors += 1,
553 VectorStorage::MultiLevelQuantized { .. } => {
554 quantized_vectors += 1;
555 multi_level_vectors += 1;
556 }
557 VectorStorage::MultiCodebook { .. } => {
558 quantized_vectors += 1;
559 multi_codebook_vectors += 1;
560 }
561 }
562 }
563
564 let total_vectors = self.vectors.len();
565 let compression_ratio = if total_vectors > 0 {
566 quantized_vectors as f32 / total_vectors as f32
567 } else {
568 0.0
569 };
570
571 InvertedListStats {
572 total_vectors,
573 full_vectors,
574 quantized_vectors,
575 compression_ratio,
576 multi_level_vectors,
577 multi_codebook_vectors,
578 quantization_strategy: self.quantization.clone(),
579 }
580 }
581}
582
583#[derive(Debug, Clone)]
585pub struct InvertedListStats {
586 pub total_vectors: usize,
587 pub full_vectors: usize,
588 pub quantized_vectors: usize,
589 pub compression_ratio: f32,
590 pub multi_level_vectors: usize,
591 pub multi_codebook_vectors: usize,
592 pub quantization_strategy: QuantizationStrategy,
593}
594
595pub struct IvfIndex {
597 config: IvfConfig,
598 centroids: Vec<Vector>,
600 inverted_lists: Vec<Arc<RwLock<InvertedList>>>,
602 dimensions: Option<usize>,
604 n_vectors: usize,
606 is_trained: bool,
608}
609
610impl IvfIndex {
611 pub fn new(config: IvfConfig) -> Result<Self> {
613 let mut inverted_lists = Vec::with_capacity(config.n_clusters);
614
615 let quantization = if config.enable_residual_quantization {
617 if let Some(ref pq_config) = config.pq_config {
618 QuantizationStrategy::ProductQuantization(pq_config.clone())
619 } else {
620 return Err(anyhow!(
621 "PQ config required when residual quantization is enabled"
622 ));
623 }
624 } else {
625 config.quantization.clone()
626 };
627
628 for _ in 0..config.n_clusters {
629 let inverted_list = Arc::new(RwLock::new(InvertedList::new_with_quantization(
630 quantization.clone(),
631 )?));
632 inverted_lists.push(inverted_list);
633 }
634
635 Ok(Self {
636 config,
637 centroids: Vec::new(),
638 inverted_lists,
639 dimensions: None,
640 n_vectors: 0,
641 is_trained: false,
642 })
643 }
644
645 pub fn new_with_product_quantization(
647 n_clusters: usize,
648 n_probes: usize,
649 pq_config: PQConfig,
650 ) -> Result<Self> {
651 let config = IvfConfig {
652 n_clusters,
653 n_probes,
654 quantization: QuantizationStrategy::ProductQuantization(pq_config),
655 ..Default::default()
656 };
657 Self::new(config)
658 }
659
660 pub fn new_with_multi_level_quantization(
662 n_clusters: usize,
663 n_probes: usize,
664 levels: usize,
665 pq_configs: Vec<PQConfig>,
666 ) -> Result<Self> {
667 if pq_configs.len() < levels {
668 return Err(anyhow!(
669 "Number of PQ configs must be at least equal to levels"
670 ));
671 }
672
673 let config = IvfConfig {
674 n_clusters,
675 n_probes,
676 quantization: QuantizationStrategy::ResidualQuantization { levels, pq_configs },
677 ..Default::default()
678 };
679 Self::new(config)
680 }
681
682 pub fn new_with_multi_codebook_quantization(
684 n_clusters: usize,
685 n_probes: usize,
686 num_codebooks: usize,
687 pq_configs: Vec<PQConfig>,
688 ) -> Result<Self> {
689 if pq_configs.len() != num_codebooks {
690 return Err(anyhow!(
691 "Number of PQ configs must equal number of codebooks"
692 ));
693 }
694
695 let config = IvfConfig {
696 n_clusters,
697 n_probes,
698 quantization: QuantizationStrategy::MultiCodebook {
699 num_codebooks,
700 pq_configs,
701 },
702 ..Default::default()
703 };
704 Self::new(config)
705 }
706
707 pub fn new_with_residual_quantization(
709 n_clusters: usize,
710 n_probes: usize,
711 pq_config: PQConfig,
712 ) -> Result<Self> {
713 Self::new_with_product_quantization(n_clusters, n_probes, pq_config)
714 }
715
716 pub fn config(&self) -> &IvfConfig {
718 &self.config
719 }
720
721 pub fn train(&mut self, training_vectors: &[Vector]) -> Result<()> {
723 if training_vectors.is_empty() {
724 return Err(anyhow!("Cannot train IVF index with empty training set"));
725 }
726
727 let dims = training_vectors[0].dimensions;
729 if !training_vectors.iter().all(|v| v.dimensions == dims) {
730 return Err(anyhow!(
731 "All training vectors must have the same dimensions"
732 ));
733 }
734
735 self.dimensions = Some(dims);
736
737 self.centroids = self.initialize_centroids_kmeans_plus_plus(training_vectors)?;
739
740 let mut iteration = 0;
742 let mut prev_error = f32::INFINITY;
743
744 while iteration < self.config.max_iterations {
745 let mut clusters: Vec<Vec<&Vector>> = vec![Vec::new(); self.config.n_clusters];
747
748 for vector in training_vectors {
749 let nearest_idx = self.find_nearest_centroid(vector)?;
750 clusters[nearest_idx].push(vector);
751 }
752
753 let mut total_error = 0.0;
755 for (i, cluster) in clusters.iter().enumerate() {
756 if !cluster.is_empty() {
757 let new_centroid = self.compute_centroid(cluster);
758 total_error += self.centroids[i]
759 .euclidean_distance(&new_centroid)
760 .unwrap_or(0.0);
761 self.centroids[i] = new_centroid;
762 }
763 }
764
765 if (prev_error - total_error).abs() < self.config.convergence_threshold {
767 break;
768 }
769
770 prev_error = total_error;
771 iteration += 1;
772 }
773
774 self.is_trained = true;
775
776 if !matches!(self.config.quantization, QuantizationStrategy::None)
778 || self.config.enable_residual_quantization
779 {
780 self.train_residual_quantization(training_vectors)?;
781 }
782
783 Ok(())
784 }
785
786 fn train_residual_quantization(&mut self, training_vectors: &[Vector]) -> Result<()> {
788 let mut cluster_residuals: Vec<Vec<Vector>> = vec![Vec::new(); self.config.n_clusters];
790
791 for vector in training_vectors {
792 let cluster_idx = self.find_nearest_centroid(vector)?;
793 let centroid = &self.centroids[cluster_idx];
794 let residual = vector.subtract(centroid)?;
795 cluster_residuals[cluster_idx].push(residual);
796 }
797
798 for (cluster_idx, residuals) in cluster_residuals.iter().enumerate() {
800 if residuals.len() > 10 {
801 let mut list = self.inverted_lists[cluster_idx]
803 .write()
804 .expect("inverted_lists lock should not be poisoned");
805 list.train_pq(residuals)?;
806 }
807 }
808
809 Ok(())
810 }
811
812 fn initialize_centroids_kmeans_plus_plus(&self, vectors: &[Vector]) -> Result<Vec<Vector>> {
814 use std::collections::hash_map::DefaultHasher;
815 use std::hash::{Hash, Hasher};
816
817 let mut hasher = DefaultHasher::new();
818 self.config.seed.unwrap_or(42).hash(&mut hasher);
819 let mut rng_state = hasher.finish();
820
821 let mut centroids = Vec::with_capacity(self.config.n_clusters);
822
823 let first_idx = (rng_state as usize) % vectors.len();
825 centroids.push(vectors[first_idx].clone());
826
827 while centroids.len() < self.config.n_clusters {
829 let mut distances = Vec::with_capacity(vectors.len());
830 let mut sum_distances = 0.0;
831
832 for vector in vectors {
834 let min_dist = centroids
835 .iter()
836 .map(|c| vector.euclidean_distance(c).unwrap_or(f32::INFINITY))
837 .fold(f32::INFINITY, |a, b| a.min(b));
838
839 distances.push(min_dist * min_dist); sum_distances += min_dist * min_dist;
841 }
842
843 rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
845 let threshold = (rng_state as f32 / u64::MAX as f32) * sum_distances;
846
847 let mut cumulative = 0.0;
848 for (i, &dist) in distances.iter().enumerate() {
849 cumulative += dist;
850 if cumulative >= threshold {
851 centroids.push(vectors[i].clone());
852 break;
853 }
854 }
855 }
856
857 Ok(centroids)
858 }
859
860 fn compute_centroid(&self, cluster: &[&Vector]) -> Vector {
862 if cluster.is_empty() {
863 return Vector::new(vec![0.0; self.dimensions.unwrap_or(0)]);
864 }
865
866 let dims = cluster[0].dimensions;
867 let mut sum = vec![0.0; dims];
868
869 for vector in cluster {
870 let values = vector.as_f32();
871 for (i, &val) in values.iter().enumerate() {
872 sum[i] += val;
873 }
874 }
875
876 let count = cluster.len() as f32;
877 for val in &mut sum {
878 *val /= count;
879 }
880
881 Vector::new(sum)
882 }
883
884 fn find_nearest_centroid(&self, vector: &Vector) -> Result<usize> {
886 if self.centroids.is_empty() {
887 return Err(anyhow!("No centroids available"));
888 }
889
890 let mut min_distance = f32::INFINITY;
891 let mut nearest_idx = 0;
892
893 for (i, centroid) in self.centroids.iter().enumerate() {
894 let distance = vector.euclidean_distance(centroid)?;
895 if distance < min_distance {
896 min_distance = distance;
897 nearest_idx = i;
898 }
899 }
900
901 Ok(nearest_idx)
902 }
903
904 fn find_nearest_centroids(&self, query: &Vector, n_probes: usize) -> Result<Vec<usize>> {
906 let mut distances: Vec<(usize, f32)> = self
907 .centroids
908 .iter()
909 .enumerate()
910 .map(|(i, centroid)| {
911 let dist = query.euclidean_distance(centroid).unwrap_or(f32::INFINITY);
912 (i, dist)
913 })
914 .collect();
915
916 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
917
918 Ok(distances
919 .into_iter()
920 .take(n_probes.min(self.centroids.len()))
921 .map(|(i, _)| i)
922 .collect())
923 }
924
925 pub fn stats(&self) -> IvfStats {
927 let mut total_list_stats = InvertedListStats {
928 total_vectors: 0,
929 full_vectors: 0,
930 quantized_vectors: 0,
931 compression_ratio: 0.0,
932 multi_level_vectors: 0,
933 multi_codebook_vectors: 0,
934 quantization_strategy: QuantizationStrategy::None,
935 };
936
937 let mut cluster_stats = Vec::new();
938 let mut vectors_per_cluster = Vec::new();
939 let mut non_empty_clusters = 0;
940
941 for list in &self.inverted_lists {
942 let list_guard = list
943 .read()
944 .expect("inverted list lock should not be poisoned");
945 let stats = list_guard.stats();
946
947 total_list_stats.total_vectors += stats.total_vectors;
948 total_list_stats.full_vectors += stats.full_vectors;
949 total_list_stats.quantized_vectors += stats.quantized_vectors;
950 total_list_stats.multi_level_vectors += stats.multi_level_vectors;
951 total_list_stats.multi_codebook_vectors += stats.multi_codebook_vectors;
952
953 vectors_per_cluster.push(stats.total_vectors);
954 if stats.total_vectors > 0 {
955 non_empty_clusters += 1;
956 }
957
958 cluster_stats.push(stats);
959 }
960
961 if total_list_stats.total_vectors > 0 {
963 total_list_stats.compression_ratio =
964 total_list_stats.quantized_vectors as f32 / total_list_stats.total_vectors as f32;
965 }
966
967 let avg_vectors_per_cluster = if self.config.n_clusters > 0 {
968 self.n_vectors as f32 / self.config.n_clusters as f32
969 } else {
970 0.0
971 };
972
973 IvfStats {
974 n_clusters: self.config.n_clusters,
975 n_probes: self.config.n_probes,
976 n_vectors: self.n_vectors,
977 is_trained: self.is_trained,
978 dimensions: self.dimensions,
979 vectors_per_cluster,
980 avg_vectors_per_cluster,
981 non_empty_clusters,
982 enable_residual_quantization: self.config.enable_residual_quantization,
983 quantization_strategy: self.config.quantization.clone(),
984 compression_stats: Some(total_list_stats),
985 cluster_stats,
986 }
987 }
988}
989
990impl VectorIndex for IvfIndex {
991 fn insert(&mut self, uri: String, vector: Vector) -> Result<()> {
992 if !self.is_trained {
993 return Err(anyhow!(
994 "IVF index must be trained before inserting vectors"
995 ));
996 }
997
998 if let Some(dims) = self.dimensions {
1000 if vector.dimensions != dims {
1001 return Err(anyhow!(
1002 "Vector dimensions {} don't match index dimensions {}",
1003 vector.dimensions,
1004 dims
1005 ));
1006 }
1007 }
1008
1009 let cluster_idx = self.find_nearest_centroid(&vector)?;
1011 let centroid = &self.centroids[cluster_idx];
1012
1013 let mut list = self.inverted_lists[cluster_idx]
1014 .write()
1015 .expect("inverted_lists lock should not be poisoned");
1016
1017 match &self.config.quantization {
1019 QuantizationStrategy::None => {
1020 if self.config.enable_residual_quantization {
1021 let residual = vector.subtract(centroid)?;
1023 list.add_residual(uri, residual, centroid)?;
1024 } else {
1025 list.add_full(uri, vector);
1026 }
1027 }
1028 _ => {
1029 let residual = vector.subtract(centroid)?;
1031 list.add_residual(uri, residual, centroid)?;
1032 }
1033 }
1034
1035 self.n_vectors += 1;
1036 Ok(())
1037 }
1038
1039 fn search_knn(&self, query: &Vector, k: usize) -> Result<Vec<(String, f32)>> {
1040 if !self.is_trained {
1041 return Err(anyhow!("IVF index must be trained before searching"));
1042 }
1043
1044 let probe_indices = self.find_nearest_centroids(query, self.config.n_probes)?;
1046
1047 let mut all_results = Vec::new();
1049 for idx in probe_indices {
1050 let list = self.inverted_lists[idx]
1051 .read()
1052 .expect("inverted_lists lock should not be poisoned");
1053 let centroid = &self.centroids[idx];
1054 let mut results = list.search(query, centroid, k)?;
1055 all_results.append(&mut results);
1056 }
1057
1058 all_results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1060 all_results.truncate(k);
1061
1062 Ok(all_results)
1063 }
1064
1065 fn search_threshold(&self, query: &Vector, threshold: f32) -> Result<Vec<(String, f32)>> {
1066 if !self.is_trained {
1067 return Err(anyhow!("IVF index must be trained before searching"));
1068 }
1069
1070 let probe_indices = self.find_nearest_centroids(query, self.config.n_probes)?;
1072
1073 let mut all_results = Vec::new();
1075 for idx in probe_indices {
1076 let list = self.inverted_lists[idx]
1077 .read()
1078 .expect("inverted_lists lock should not be poisoned");
1079 let centroid = &self.centroids[idx];
1080 let results = list.search(query, centroid, self.n_vectors)?;
1081
1082 for (uri, similarity) in results {
1084 if similarity >= threshold {
1085 all_results.push((uri, similarity));
1086 }
1087 }
1088 }
1089
1090 all_results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1092
1093 Ok(all_results)
1094 }
1095
1096 fn get_vector(&self, _uri: &str) -> Option<&Vector> {
1097 None
1101 }
1102
1103 fn iter_vectors(&self) -> Vec<(String, Vector)> {
1104 let residual_mode = self.config.enable_residual_quantization
1105 && matches!(self.config.quantization, QuantizationStrategy::None);
1106 let mut all = Vec::with_capacity(self.n_vectors);
1107 for (idx, list_lock) in self.inverted_lists.iter().enumerate() {
1108 let Some(centroid) = self.centroids.get(idx) else {
1109 continue;
1110 };
1111 let Ok(list) = list_lock.read() else {
1112 continue;
1113 };
1114 all.extend(list.reconstruct_all(centroid, residual_mode));
1115 }
1116 all
1117 }
1118
1119 fn supports_enumeration(&self) -> bool {
1120 true
1121 }
1122}
1123
1124#[derive(Debug, Clone)]
1126pub struct IvfStats {
1127 pub n_vectors: usize,
1128 pub n_clusters: usize,
1129 pub n_probes: usize,
1130 pub is_trained: bool,
1131 pub dimensions: Option<usize>,
1132 pub vectors_per_cluster: Vec<usize>,
1133 pub avg_vectors_per_cluster: f32,
1134 pub non_empty_clusters: usize,
1135 pub enable_residual_quantization: bool,
1136 pub quantization_strategy: QuantizationStrategy,
1137 pub compression_stats: Option<InvertedListStats>,
1138 pub cluster_stats: Vec<InvertedListStats>,
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::*;
1144
1145 #[test]
1146 fn test_ivf_basic() -> Result<()> {
1147 let config = IvfConfig {
1148 n_clusters: 4,
1149 n_probes: 2,
1150 ..Default::default()
1151 };
1152
1153 let mut index = IvfIndex::new(config)?;
1154
1155 let training_vectors = vec![
1157 Vector::new(vec![1.0, 0.0]),
1158 Vector::new(vec![0.0, 1.0]),
1159 Vector::new(vec![-1.0, 0.0]),
1160 Vector::new(vec![0.0, -1.0]),
1161 Vector::new(vec![0.5, 0.5]),
1162 Vector::new(vec![-0.5, 0.5]),
1163 Vector::new(vec![-0.5, -0.5]),
1164 Vector::new(vec![0.5, -0.5]),
1165 ];
1166
1167 index.train(&training_vectors)?;
1169 assert!(index.is_trained);
1170
1171 for (i, vec) in training_vectors.iter().enumerate() {
1173 index.insert(format!("vec{i}"), vec.clone())?;
1174 }
1175
1176 let query = Vector::new(vec![0.9, 0.1]);
1178 let results = index.search_knn(&query, 3)?;
1179
1180 assert!(!results.is_empty());
1181 assert!(results.len() <= 3);
1182
1183 assert_eq!(results[0].0, "vec0");
1185 Ok(())
1186 }
1187
1188 #[test]
1189 fn test_ivf_threshold_search() -> Result<()> {
1190 let config = IvfConfig {
1191 n_clusters: 2,
1192 n_probes: 2,
1193 ..Default::default()
1194 };
1195
1196 let mut index = IvfIndex::new(config)?;
1197
1198 let training_vectors = vec![
1200 Vector::new(vec![1.0, 0.0, 0.0]),
1201 Vector::new(vec![0.0, 1.0, 0.0]),
1202 Vector::new(vec![0.0, 0.0, 1.0]),
1203 Vector::new(vec![0.5, 0.5, 0.0]),
1204 ];
1205
1206 index.train(&training_vectors)?;
1207
1208 index.insert("v1".to_string(), training_vectors[0].clone())?;
1210 index.insert("v2".to_string(), training_vectors[1].clone())?;
1211 index.insert("v3".to_string(), training_vectors[2].clone())?;
1212 index.insert("v4".to_string(), training_vectors[3].clone())?;
1213
1214 let query = Vector::new(vec![0.9, 0.1, 0.0]);
1216 let results = index.search_threshold(&query, 0.5)?;
1217
1218 assert!(!results.is_empty());
1219 for (_, similarity) in &results {
1221 assert!(*similarity >= 0.5);
1222 }
1223 Ok(())
1224 }
1225
1226 #[test]
1227 fn test_ivf_stats() -> Result<()> {
1228 let config = IvfConfig {
1229 n_clusters: 3,
1230 n_probes: 1,
1231 ..Default::default()
1232 };
1233
1234 let mut index = IvfIndex::new(config)?;
1235
1236 let training_vectors = vec![
1238 Vector::new(vec![1.0, 0.0]),
1239 Vector::new(vec![0.0, 1.0]),
1240 Vector::new(vec![-1.0, -1.0]),
1241 ];
1242
1243 index.train(&training_vectors)?;
1244
1245 index.insert("a".to_string(), Vector::new(vec![1.1, 0.1]))?;
1247 index.insert("b".to_string(), Vector::new(vec![0.1, 1.1]))?;
1248
1249 let stats = index.stats();
1250 assert_eq!(stats.n_vectors, 2);
1251 assert_eq!(stats.n_clusters, 3);
1252 assert!(stats.is_trained);
1253 assert_eq!(stats.dimensions, Some(2));
1254 Ok(())
1255 }
1256
1257 #[test]
1258 fn test_ivf_multi_level_quantization() -> Result<()> {
1259 use crate::pq::PQConfig;
1260
1261 let pq_config_1 = PQConfig {
1263 n_subquantizers: 2,
1264 n_bits: 8,
1265 ..Default::default()
1266 };
1267 let pq_config_2 = PQConfig {
1268 n_subquantizers: 2,
1269 n_bits: 4,
1270 ..Default::default()
1271 };
1272
1273 let mut index =
1274 IvfIndex::new_with_multi_level_quantization(4, 2, 2, vec![pq_config_1, pq_config_2])?;
1275
1276 let training_vectors = vec![
1278 Vector::new(vec![1.0, 0.0, 0.0, 0.0]),
1279 Vector::new(vec![0.0, 1.0, 0.0, 0.0]),
1280 Vector::new(vec![0.0, 0.0, 1.0, 0.0]),
1281 Vector::new(vec![0.0, 0.0, 0.0, 1.0]),
1282 Vector::new(vec![0.5, 0.5, 0.0, 0.0]),
1283 Vector::new(vec![0.0, 0.0, 0.5, 0.5]),
1284 ];
1285
1286 index.train(&training_vectors)?;
1288 assert!(index.is_trained);
1289
1290 for (i, vec) in training_vectors.iter().enumerate() {
1292 index.insert(format!("vec{i}"), vec.clone())?;
1293 }
1294
1295 let query = Vector::new(vec![0.9, 0.1, 0.0, 0.0]);
1297 let results = index.search_knn(&query, 3)?;
1298
1299 assert!(!results.is_empty());
1300 assert!(results.len() <= 3);
1301
1302 let stats = index.stats();
1304 assert!(matches!(
1305 stats.quantization_strategy,
1306 QuantizationStrategy::ResidualQuantization { .. }
1307 ));
1308 if let Some(compression_stats) = &stats.compression_stats {
1309 assert!(compression_stats.multi_level_vectors > 0);
1310 }
1311 Ok(())
1312 }
1313
1314 #[test]
1315 fn test_ivf_multi_codebook_quantization() -> Result<()> {
1316 use crate::pq::PQConfig;
1317
1318 let pq_config_1 = PQConfig {
1320 n_subquantizers: 2,
1321 n_bits: 8,
1322 ..Default::default()
1323 };
1324 let pq_config_2 = PQConfig {
1325 n_subquantizers: 2,
1326 n_bits: 8,
1327 ..Default::default()
1328 };
1329
1330 let mut index = IvfIndex::new_with_multi_codebook_quantization(
1331 4,
1332 2,
1333 2,
1334 vec![pq_config_1, pq_config_2],
1335 )?;
1336
1337 let training_vectors = vec![
1339 Vector::new(vec![1.0, 0.0, 0.0, 0.0]),
1340 Vector::new(vec![0.0, 1.0, 0.0, 0.0]),
1341 Vector::new(vec![0.0, 0.0, 1.0, 0.0]),
1342 Vector::new(vec![0.0, 0.0, 0.0, 1.0]),
1343 Vector::new(vec![0.5, 0.5, 0.5, 0.5]),
1344 ];
1345
1346 index.train(&training_vectors)?;
1348 assert!(index.is_trained);
1349
1350 for (i, vec) in training_vectors.iter().enumerate() {
1352 index.insert(format!("vec{i}"), vec.clone())?;
1353 }
1354
1355 let query = Vector::new(vec![0.9, 0.1, 0.0, 0.0]);
1357 let results = index.search_knn(&query, 2)?;
1358
1359 assert!(!results.is_empty());
1360 assert!(results.len() <= 2);
1361
1362 let stats = index.stats();
1364 assert!(matches!(
1365 stats.quantization_strategy,
1366 QuantizationStrategy::MultiCodebook { .. }
1367 ));
1368 if let Some(compression_stats) = &stats.compression_stats {
1369 assert!(compression_stats.multi_codebook_vectors > 0);
1370 }
1371 Ok(())
1372 }
1373
1374 #[test]
1375 fn test_quantization_strategies() {
1376 use crate::pq::PQConfig;
1377
1378 let pq_config = PQConfig::default();
1379
1380 let strategies = vec![
1382 QuantizationStrategy::None,
1383 QuantizationStrategy::ProductQuantization(pq_config.clone()),
1384 QuantizationStrategy::ResidualQuantization {
1385 levels: 2,
1386 pq_configs: vec![pq_config.clone(), pq_config.clone()],
1387 },
1388 QuantizationStrategy::MultiCodebook {
1389 num_codebooks: 2,
1390 pq_configs: vec![pq_config.clone(), pq_config.clone()],
1391 },
1392 ];
1393
1394 for strategy in strategies {
1395 let config = IvfConfig {
1396 n_clusters: 2,
1397 n_probes: 1,
1398 quantization: strategy.clone(),
1399 ..Default::default()
1400 };
1401
1402 let index = IvfIndex::new(config);
1403 assert!(
1404 index.is_ok(),
1405 "Failed to create index with strategy: {strategy:?}"
1406 );
1407 }
1408 }
1409}