Skip to main content

oxirs_vec/
ivf.rs

1//! Inverted File (IVF) index implementation for approximate nearest neighbor search
2//!
3//! IVF is a clustering-based indexing method that partitions the vector space into
4//! Voronoi cells. Each cell has a centroid, and vectors are assigned to their nearest
5//! centroid. During search, only a subset of cells are examined, greatly reducing
6//! search time at the cost of some accuracy.
7
8use crate::{
9    pq::{PQConfig, PQIndex},
10    Vector, VectorIndex,
11};
12use anyhow::{anyhow, Result};
13use std::sync::{Arc, RwLock};
14
15/// Quantization strategy for residuals
16#[derive(Debug, Clone, PartialEq)]
17pub enum QuantizationStrategy {
18    /// No quantization - store full residuals
19    None,
20    /// Single-level product quantization
21    ProductQuantization(PQConfig),
22    /// Residual quantization with multiple levels
23    ResidualQuantization {
24        levels: usize,
25        pq_configs: Vec<PQConfig>,
26    },
27    /// Multi-codebook quantization for improved accuracy
28    MultiCodebook {
29        num_codebooks: usize,
30        pq_configs: Vec<PQConfig>,
31    },
32}
33
34/// Configuration for IVF index
35#[derive(Debug, Clone)]
36pub struct IvfConfig {
37    /// Number of clusters (Voronoi cells)
38    pub n_clusters: usize,
39    /// Number of probes during search (cells to examine)
40    pub n_probes: usize,
41    /// Maximum iterations for k-means clustering
42    pub max_iterations: usize,
43    /// Convergence threshold for k-means
44    pub convergence_threshold: f32,
45    /// Random seed for reproducibility
46    pub seed: Option<u64>,
47    /// Quantization strategy for residuals
48    pub quantization: QuantizationStrategy,
49    /// Enable residual quantization for compression (deprecated - use quantization field)
50    pub enable_residual_quantization: bool,
51    /// Product quantization configuration for residuals (deprecated - use quantization field)
52    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/// Storage format for vectors in inverted lists
71#[derive(Debug, Clone)]
72enum VectorStorage {
73    /// Store full vectors
74    Full(Vector),
75    /// Store quantized residuals with PQ codes
76    Quantized(Vec<u8>),
77    /// Store multi-level residual quantization codes
78    MultiLevelQuantized {
79        levels: Vec<Vec<u8>>,           // PQ codes for each quantization level
80        final_residual: Option<Vector>, // Optional final unquantized residual
81    },
82    /// Store multi-codebook quantization codes
83    MultiCodebook {
84        codebooks: Vec<Vec<u8>>, // PQ codes from different codebooks
85        weights: Vec<f32>,       // Weights for combining codebook predictions
86    },
87}
88
89/// Inverted list storing vectors for a single cluster
90#[derive(Debug, Clone)]
91struct InvertedList {
92    /// Vectors in this cluster with their storage format
93    vectors: Vec<(String, VectorStorage)>,
94    /// Quantization strategy used for this list
95    quantization: QuantizationStrategy,
96    /// Product quantizer for single-level quantization
97    pq_index: Option<PQIndex>,
98    /// Multiple PQ indexes for multi-level residual quantization
99    multi_level_pq: Vec<PQIndex>,
100    /// Multiple PQ indexes for multi-codebook quantization
101    multi_codebook_pq: Vec<PQIndex>,
102    /// Codebook weights for multi-codebook quantization
103    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                // Initialize equal weights for all codebooks
149                list.codebook_weights = vec![1.0 / num_codebooks as f32; num_codebooks];
150            }
151        }
152
153        Ok(list)
154    }
155
156    // Deprecated - use new_with_quantization instead
157    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                    // Train PQ on residuals if not already trained
170                    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    /// Add vector using multi-level residual quantization
197    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            // Train this level's PQ if not already trained
207            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            // Encode residual at this level
213            let codes = self.multi_level_pq[level].encode(&residual)?;
214            level_codes.push(codes);
215
216            // Compute and subtract the quantized approximation to get next level residual
217            let approximation = self.multi_level_pq[level].decode_vector(&level_codes[level])?;
218            residual = residual.subtract(&approximation)?;
219        }
220
221        // Store the final residual if we haven't exhausted all levels
222        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    /// Add vector using multi-codebook quantization
240    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            // Train this codebook's PQ if not already trained
245            if !pq_index.is_trained() {
246                let training_residuals = vec![residual.clone()];
247                pq_index.train(&training_residuals)?;
248            }
249
250            // Encode residual with this codebook
251            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        // Convert distances to similarities (1 / (1 + distance))
295        Ok(distances
296            .into_iter()
297            .map(|(uri, dist)| (uri, 1.0 / (1.0 + dist)))
298            .collect())
299    }
300
301    /// Compute distance for multi-level residual quantization
302    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        // Reconstruct vector from quantized levels
311        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        // Add final unquantized residual if present
319        if let Some(final_res) = final_residual {
320            reconstructed_residual = reconstructed_residual.add(final_res)?;
321        }
322
323        // Compute distance between query residual and reconstructed residual
324        query_residual.euclidean_distance(&reconstructed_residual)
325    }
326
327    /// Compute distance for multi-codebook quantization
328    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        // Compute weighted combination of distances from all codebooks
338        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        // Normalize by total weight
348        if total_weight > 0.0 {
349            Ok(weighted_distance / total_weight)
350        } else {
351            Ok(f32::INFINITY)
352        }
353    }
354
355    /// Reconstruct the residual encoded by multi-level residual quantization.
356    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    /// Reconstruct the residual encoded by multi-codebook quantization.
379    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    /// Reconstruct all (uri, full vector) pairs held by this inverted list.
404    ///
405    /// `residual_mode` indicates whether [`VectorStorage::Full`] entries hold
406    /// an absolute vector (`false`) or an unquantized residual relative to
407    /// `centroid` (`true`) — see `IvfIndex::insert`'s `QuantizationStrategy::None`
408    /// branch, which stores either depending on `enable_residual_quantization`.
409    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    /// Train product quantizer on collected residuals
445    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    /// Train multi-level residual quantization
464    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            // Train PQ at this level
469            self.multi_level_pq[level].train(&current_residuals)?;
470
471            // Compute residuals for next level by subtracting quantized approximation
472            let mut next_residuals = Vec::new();
473            for residual in &current_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    /// Train multi-codebook quantization
486    fn train_multi_codebook_pq(&mut self, residuals: &[Vector]) -> Result<()> {
487        // Train each codebook independently on the same residuals
488        for pq_index in &mut self.multi_codebook_pq {
489            pq_index.train(residuals)?;
490        }
491
492        // Optionally, optimize codebook weights based on reconstruction quality
493        self.optimize_codebook_weights(residuals)?;
494
495        Ok(())
496    }
497
498    /// Optimize weights for multi-codebook quantization
499    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        // Compute reconstruction error for each codebook
508        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        // Compute weights inversely proportional to reconstruction error
522        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                // Higher weight for lower error
527                self.codebook_weights[i] = (max_error - error + 1e-6) / max_error;
528                total_weight += self.codebook_weights[i];
529            }
530
531            // Normalize weights
532            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    /// Get statistics about this inverted list
543    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/// Statistics for an inverted list
584#[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
595/// IVF index for approximate nearest neighbor search
596pub struct IvfIndex {
597    config: IvfConfig,
598    /// Cluster centroids
599    centroids: Vec<Vector>,
600    /// Inverted lists (one per cluster)
601    inverted_lists: Vec<Arc<RwLock<InvertedList>>>,
602    /// Dimensions of vectors
603    dimensions: Option<usize>,
604    /// Total number of vectors
605    n_vectors: usize,
606    /// Whether the index has been trained
607    is_trained: bool,
608}
609
610impl IvfIndex {
611    /// Create a new IVF index
612    pub fn new(config: IvfConfig) -> Result<Self> {
613        let mut inverted_lists = Vec::with_capacity(config.n_clusters);
614
615        // Determine quantization strategy (backward compatibility support)
616        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    /// Create a new IVF index with product quantization
646    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    /// Create a new IVF index with multi-level residual quantization
661    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    /// Create a new IVF index with multi-codebook quantization
683    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    /// Create a new IVF index with residual quantization enabled (deprecated)
708    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    /// Get the configuration of this index
717    pub fn config(&self) -> &IvfConfig {
718        &self.config
719    }
720
721    /// Train the index with a sample of vectors
722    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        // Validate dimensions
728        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        // Initialize centroids using k-means++
738        self.centroids = self.initialize_centroids_kmeans_plus_plus(training_vectors)?;
739
740        // Run k-means clustering
741        let mut iteration = 0;
742        let mut prev_error = f32::INFINITY;
743
744        while iteration < self.config.max_iterations {
745            // Assign vectors to nearest centroids
746            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            // Update centroids
754            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            // Check convergence
766            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        // Train quantization if enabled
777        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    /// Train residual quantization on all clusters
787    fn train_residual_quantization(&mut self, training_vectors: &[Vector]) -> Result<()> {
788        // Collect residuals for each cluster
789        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        // Train PQ for each cluster that has enough residuals
799        for (cluster_idx, residuals) in cluster_residuals.iter().enumerate() {
800            if residuals.len() > 10 {
801                // Minimum threshold for training
802                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    /// Initialize centroids using k-means++ algorithm
813    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        // Choose first centroid randomly
824        let first_idx = (rng_state as usize) % vectors.len();
825        centroids.push(vectors[first_idx].clone());
826
827        // Choose remaining centroids
828        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            // Calculate distance to nearest centroid for each vector
833            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); // Square for k-means++
840                sum_distances += min_dist * min_dist;
841            }
842
843            // Choose next centroid with probability proportional to squared distance
844            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    /// Compute centroid of a cluster
861    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    /// Find the nearest centroid for a vector
885    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    /// Find the n_probes nearest centroids for a query
905    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    /// Get comprehensive statistics about the IVF index including compression info
926    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        // Calculate overall compression ratio
962        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        // Validate dimensions
999        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        // Find nearest centroid
1010        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        // Handle quantization based on strategy
1018        match &self.config.quantization {
1019            QuantizationStrategy::None => {
1020                if self.config.enable_residual_quantization {
1021                    // Backward compatibility: use residual quantization
1022                    let residual = vector.subtract(centroid)?;
1023                    list.add_residual(uri, residual, centroid)?;
1024                } else {
1025                    list.add_full(uri, vector);
1026                }
1027            }
1028            _ => {
1029                // Use new quantization strategies
1030                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        // Find nearest centroids to probe
1045        let probe_indices = self.find_nearest_centroids(query, self.config.n_probes)?;
1046
1047        // Search in selected inverted lists
1048        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        // Sort and truncate to k results
1059        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        // Find nearest centroids to probe
1071        let probe_indices = self.find_nearest_centroids(query, self.config.n_probes)?;
1072
1073        // Search in selected inverted lists
1074        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            // Filter by threshold
1083            for (uri, similarity) in results {
1084                if similarity >= threshold {
1085                    all_results.push((uri, similarity));
1086                }
1087            }
1088        }
1089
1090        // Sort by similarity
1091        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        // IVF doesn't maintain a direct URI to vector mapping
1098        // Would need to search through all inverted lists
1099        // For efficiency, we return None
1100        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/// Statistics for IVF index
1125#[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        // Create training vectors
1156        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        // Train the index
1168        index.train(&training_vectors)?;
1169        assert!(index.is_trained);
1170
1171        // Insert vectors
1172        for (i, vec) in training_vectors.iter().enumerate() {
1173            index.insert(format!("vec{i}"), vec.clone())?;
1174        }
1175
1176        // Search for nearest neighbors
1177        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        // The first result should be vec0 (closest to [1.0, 0.0])
1184        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        // Create and train with vectors
1199        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        // Insert vectors
1209        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        // Search with threshold
1215        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        // Should find vectors with similarity >= 0.5
1220        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        // Train with simple vectors
1237        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        // Insert some vectors
1246        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        // Create PQ configs for different levels
1262        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        // Create training vectors
1277        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        // Train the index
1287        index.train(&training_vectors)?;
1288        assert!(index.is_trained);
1289
1290        // Insert vectors
1291        for (i, vec) in training_vectors.iter().enumerate() {
1292            index.insert(format!("vec{i}"), vec.clone())?;
1293        }
1294
1295        // Search for nearest neighbors
1296        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        // Check stats
1303        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        // Create PQ configs for different codebooks
1319        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        // Create training vectors
1338        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        // Train the index
1347        index.train(&training_vectors)?;
1348        assert!(index.is_trained);
1349
1350        // Insert vectors
1351        for (i, vec) in training_vectors.iter().enumerate() {
1352            index.insert(format!("vec{i}"), vec.clone())?;
1353        }
1354
1355        // Search for nearest neighbors
1356        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        // Check stats
1363        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        // Test different quantization strategies
1381        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}