Skip to main content

vecdb_core/index/
ivf.rs

1use std::collections::HashSet;
2use std::path::Path;
3
4use rayon::prelude::*;
5use serde::{Deserialize, Serialize};
6
7use crate::errors::{Result, VecDbError};
8use crate::index::backend::IndexBackend;
9use crate::index::distance::compute_distance;
10use crate::types::{CollectionConfig, DistanceMetric, IndexType, Vector, VectorId};
11
12const DEFAULT_N_LISTS: usize = 256;
13const DEFAULT_N_PROBE: usize = 16;
14const DEFAULT_MAX_ITER: usize = 25;
15
16#[derive(Serialize, Deserialize)]
17pub struct IvfIndex {
18    n_lists: usize,
19    n_probe: usize,
20    max_iter: usize,
21    dimension: usize,
22    metric: DistanceMetric,
23    centroids: Vec<Vec<f32>>,
24    lists: Vec<Vec<(VectorId, Vec<f32>)>>,
25    deleted: HashSet<VectorId>,
26    is_trained: bool,
27}
28
29impl IvfIndex {
30    pub fn new(config: &CollectionConfig) -> Self {
31        Self {
32            n_lists: DEFAULT_N_LISTS,
33            n_probe: DEFAULT_N_PROBE,
34            max_iter: DEFAULT_MAX_ITER,
35            dimension: config.dimension,
36            metric: config.metric.clone(),
37            centroids: Vec::new(),
38            lists: vec![Vec::new()],
39            deleted: HashSet::new(),
40            is_trained: false,
41        }
42    }
43
44    pub fn load_file(path: &Path) -> Result<Self> {
45        let json = std::fs::read_to_string(path)
46            .map_err(|e| VecDbError::StorageError(format!("ivf load failed: {e}")))?;
47        serde_json::from_str(&json).map_err(|e| VecDbError::SerializationError(e.to_string()))
48    }
49
50    fn train(&mut self, vectors: &[(VectorId, Vec<f32>)]) {
51        let n = vectors.len();
52        if n == 0 {
53            self.centroids.clear();
54            self.lists = vec![Vec::new()];
55            self.is_trained = false;
56            return;
57        }
58
59        let mut n_lists = self.n_lists;
60        if n < n_lists {
61            n_lists = n.max(1);
62        }
63
64        // Initialize centroids by sampling evenly (deterministic, no RNG)
65        let mut centroids: Vec<Vec<f32>> = (0..n_lists)
66            .map(|i| vectors[i * (n / n_lists)].1.clone())
67            .collect();
68
69        for _iter in 0..self.max_iter {
70            // Assign vectors to nearest centroid
71            let mut assignments: Vec<Vec<usize>> = vec![Vec::new(); n_lists];
72            for (vi, (_, v)) in vectors.iter().enumerate() {
73                let ci = Self::nearest_centroid_idx_of(&centroids, v, &self.metric);
74                assignments[ci].push(vi);
75            }
76
77            // Recompute centroids as mean of assigned vectors
78            let mut new_centroids = centroids.clone();
79            for (ci, assigned) in assignments.iter().enumerate() {
80                if assigned.is_empty() {
81                    // Reinitialize deterministically
82                    let fallback_idx = (ci * 7) % n;
83                    new_centroids[ci] = vectors[fallback_idx].1.clone();
84                } else {
85                    let dim = self.dimension;
86                    let mut mean = vec![0.0f32; dim];
87                    for &vi in assigned {
88                        for (j, x) in vectors[vi].1.iter().enumerate() {
89                            mean[j] += x;
90                        }
91                    }
92                    let count = assigned.len() as f32;
93                    for x in &mut mean {
94                        *x /= count;
95                    }
96                    new_centroids[ci] = mean;
97                }
98            }
99
100            // Break early if converged
101            let converged = centroids
102                .iter()
103                .zip(new_centroids.iter())
104                .all(|(old, new)| {
105                    old.iter()
106                        .zip(new.iter())
107                        .all(|(a, b)| (a - b).abs() < 1e-6)
108                });
109            centroids = new_centroids;
110            if converged {
111                break;
112            }
113        }
114
115        self.centroids = centroids;
116        self.n_lists = n_lists;
117
118        // Assign every vector to its nearest centroid
119        self.lists = vec![Vec::new(); n_lists];
120        for (id, v) in vectors {
121            let ci = Self::nearest_centroid_idx_of(&self.centroids, v, &self.metric);
122            self.lists[ci].push((id.clone(), v.clone()));
123        }
124
125        self.is_trained = true;
126    }
127
128    fn nearest_centroid_idx_of(
129        centroids: &[Vec<f32>],
130        query: &[f32],
131        metric: &DistanceMetric,
132    ) -> usize {
133        centroids
134            .iter()
135            .enumerate()
136            .map(|(i, c)| (i, compute_distance(query, c, metric)))
137            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
138            .map(|(i, _)| i)
139            .unwrap_or(0)
140    }
141
142    fn nearest_centroid_indices(&self, query: &[f32], n_probe: usize) -> Vec<usize> {
143        // Performance: centroid scoring parallelized via rayon.
144        // Before: sequential iterator over n_lists centroids
145        // After:  par_iter() splits work across CPU cores; ~n_threads× faster for large n_lists
146        let mut scored: Vec<(usize, f32)> = self
147            .centroids
148            .par_iter()
149            .enumerate()
150            .map(|(i, c)| (i, compute_distance(query, c, &self.metric)))
151            .collect();
152        scored.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
153        scored.truncate(n_probe);
154        scored.iter().map(|(i, _)| *i).collect()
155    }
156
157    fn total_count(&self) -> usize {
158        self.lists.iter().map(|l| l.len()).sum()
159    }
160
161    fn distance_to_score(&self, dist: f32) -> f32 {
162        match self.metric {
163            DistanceMetric::Cosine => 1.0 - dist,
164            DistanceMetric::Euclidean => 1.0 / (1.0 + dist),
165            DistanceMetric::DotProduct => 1.0 - dist,
166        }
167    }
168
169    fn rebuild_from_lists(&mut self) -> Result<()> {
170        let all_vectors: Vec<(VectorId, Vec<f32>)> = self
171            .lists
172            .iter()
173            .flat_map(|l| l.iter())
174            .filter(|(id, _)| !self.deleted.contains(id))
175            .map(|(id, v)| (id.clone(), v.clone()))
176            .collect();
177
178        self.deleted.clear();
179
180        if all_vectors.is_empty() {
181            self.centroids.clear();
182            self.lists = vec![Vec::new()];
183            self.is_trained = false;
184            return Ok(());
185        }
186
187        self.train(&all_vectors);
188        Ok(())
189    }
190}
191
192impl IndexBackend for IvfIndex {
193    fn build(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
194        self.deleted.clear();
195        self.train(&vectors);
196        Ok(())
197    }
198
199    fn insert(&mut self, id: VectorId, vector: Vector) -> Result<()> {
200        if vector.len() != self.dimension {
201            return Err(VecDbError::DimensionMismatch {
202                expected: self.dimension,
203                got: vector.len(),
204            });
205        }
206
207        // Remove from deleted set if re-inserting
208        self.deleted.remove(&id);
209
210        // Remove any existing entry for this id (update case)
211        for list in &mut self.lists {
212            list.retain(|(lid, _)| lid != &id);
213        }
214
215        if self.is_trained && !self.centroids.is_empty() {
216            let ci = Self::nearest_centroid_idx_of(&self.centroids, &vector, &self.metric);
217            self.lists[ci].push((id, vector));
218        } else {
219            // Not yet trained — buffer in lists[0]
220            if self.lists.is_empty() {
221                self.lists.push(Vec::new());
222            }
223            self.lists[0].push((id, vector));
224        }
225
226        let total = self.total_count();
227        if !self.is_trained || total.is_multiple_of(1000) {
228            self.rebuild_from_lists()?;
229        }
230
231        Ok(())
232    }
233
234    fn search(&self, query: &Vector, k: usize) -> Result<Vec<(VectorId, f32)>> {
235        if query.len() != self.dimension {
236            return Err(VecDbError::DimensionMismatch {
237                expected: self.dimension,
238                got: query.len(),
239            });
240        }
241
242        let mut candidates: Vec<(VectorId, f32)> = if !self.is_trained || self.centroids.is_empty()
243        {
244            // Brute-force across all lists.
245            // Performance: collect filtered pairs serially (cheap), then score in parallel.
246            // Before: sequential map over all vectors
247            // After:  par_iter() over pre-filtered pairs; ~n_threads× faster at large scale
248            let pairs: Vec<(&VectorId, &Vec<f32>)> = self
249                .lists
250                .iter()
251                .flat_map(|l| l.iter())
252                .filter(|(id, _)| !self.deleted.contains(id))
253                .map(|(id, v)| (id, v))
254                .collect();
255            let metric = &self.metric;
256            pairs
257                .par_iter()
258                .map(|(id, v)| {
259                    let dist = compute_distance(query, v, metric);
260                    let score = match metric {
261                        crate::types::DistanceMetric::Cosine => 1.0 - dist,
262                        crate::types::DistanceMetric::Euclidean => 1.0 / (1.0 + dist),
263                        crate::types::DistanceMetric::DotProduct => 1.0 - dist,
264                    };
265                    ((*id).clone(), score)
266                })
267                .collect()
268        } else {
269            let n_probe = self.n_probe.min(self.centroids.len());
270            let centroid_indices = self.nearest_centroid_indices(query, n_probe);
271            centroid_indices
272                .iter()
273                .flat_map(|&ci| self.lists[ci].iter())
274                .filter(|(id, _)| !self.deleted.contains(id))
275                .map(|(id, v)| {
276                    let dist = compute_distance(query, v, &self.metric);
277                    (id.clone(), self.distance_to_score(dist))
278                })
279                .collect()
280        };
281
282        candidates
283            .sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
284        candidates.truncate(k);
285        Ok(candidates)
286    }
287
288    fn delete(&mut self, id: &VectorId) -> Result<()> {
289        self.deleted.insert(id.clone());
290        Ok(())
291    }
292
293    fn save(&self, path: &Path) -> Result<()> {
294        let json = serde_json::to_string(self)
295            .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
296        std::fs::write(path, json)
297            .map_err(|e| VecDbError::StorageError(format!("ivf save failed: {e}")))?;
298        tracing::info!("IVF index saved to {:?}", path);
299        Ok(())
300    }
301
302    fn load_from(path: &Path, _config: &CollectionConfig) -> Result<Box<dyn IndexBackend>>
303    where
304        Self: Sized,
305    {
306        Ok(Box::new(Self::load_file(path)?))
307    }
308
309    fn len(&self) -> usize {
310        self.total_count().saturating_sub(self.deleted.len())
311    }
312
313    fn is_empty(&self) -> bool {
314        self.len() == 0
315    }
316
317    fn index_type(&self) -> IndexType {
318        IndexType::IVF
319    }
320
321    fn rebuild(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
322        self.build(vectors)
323    }
324}