1use std::collections::HashMap;
14
15use nodedb_mem::ScopedMemory;
16use roaring::RoaringBitmap;
17
18use crate::distance::distance;
19use crate::error::{VectorError, check_dim};
20use crate::quantize::pq::PqCodec;
21
22use super::kmeans::kmeans_centroids;
23use super::params::IvfPqParams;
24
25pub(super) const TRAIN_ITERATIONS: usize = 20;
27
28#[derive(Debug, Clone, Default, zerompk::ToMessagePack, zerompk::FromMessagePack)]
32pub struct IvfCell {
33 pub(super) ids: Vec<u32>,
34 pub(super) codes: Vec<u8>,
35 pub(super) vectors: Vec<f32>,
36}
37
38pub struct IvfPqIndex {
40 pub(super) dim: usize,
41 pub(super) params: IvfPqParams,
42 pub(super) centroids: Vec<Vec<f32>>,
44 pub(super) pq: Option<PqCodec>,
46 pub(super) cells: Vec<IvfCell>,
47 pub(super) slots: HashMap<u32, (u32, u32)>,
49 pub(super) deleted: RoaringBitmap,
51 pub(super) next_id: u32,
53 pub(super) trained_on: usize,
55 pub(super) trained_at_ms: u64,
58}
59
60impl IvfPqIndex {
61 pub fn new(dim: usize, params: IvfPqParams) -> Self {
63 Self {
64 dim,
65 params,
66 centroids: Vec::new(),
67 pq: None,
68 cells: Vec::new(),
69 slots: HashMap::new(),
70 deleted: RoaringBitmap::new(),
71 next_id: 0,
72 trained_on: 0,
73 trained_at_ms: 0,
74 }
75 }
76
77 pub fn train(&mut self, vectors: &[&[f32]], memory: ScopedMemory) -> Result<(), VectorError> {
86 if vectors.is_empty() {
87 return Err(VectorError::InvalidInput {
88 detail: "IVF-PQ training needs at least one vector".into(),
89 });
90 }
91 if !self.slots.is_empty() {
92 return Err(VectorError::InvalidInput {
93 detail: format!(
94 "IVF-PQ index already holds {} vectors; train an empty index",
95 self.slots.len()
96 ),
97 });
98 }
99 if self.dim == 0 || self.params.pq_m == 0 || !self.dim.is_multiple_of(self.params.pq_m) {
100 return Err(VectorError::InvalidInput {
101 detail: format!(
102 "IVF-PQ dimension {} must be non-zero and divisible by pq_m {}",
103 self.dim, self.params.pq_m
104 ),
105 });
106 }
107 for v in vectors {
108 check_dim(self.dim, v.len())?;
109 }
110
111 let n_cells = self.params.n_cells.min(vectors.len());
112 let centroids = kmeans_centroids(vectors, self.dim, n_cells, TRAIN_ITERATIONS);
113 let residuals: Vec<Vec<f32>> = vectors
114 .iter()
115 .map(|v| residual(v, ¢roids[nearest(¢roids, v, &self.params)]))
116 .collect();
117 let res_refs: Vec<&[f32]> = residuals.iter().map(|r| r.as_slice()).collect();
118 let pq = PqCodec::train(
119 &res_refs,
120 self.dim,
121 self.params.pq_m,
122 self.params.pq_k,
123 TRAIN_ITERATIONS,
124 memory,
125 )?;
126
127 self.cells = vec![IvfCell::default(); centroids.len()];
128 self.centroids = centroids;
129 self.pq = Some(pq);
130 self.trained_on = vectors.len();
131 Ok(())
132 }
133
134 pub fn insert_with_id(&mut self, id: u32, vector: Vec<f32>) -> Result<(), VectorError> {
140 check_dim(self.dim, vector.len())?;
141 let Some(pq) = self.pq.as_ref() else {
142 return Err(VectorError::InvalidInput {
143 detail: "IVF-PQ index must be trained before add".into(),
144 });
145 };
146 if self.slots.contains_key(&id) {
147 return Err(VectorError::InvalidInput {
148 detail: format!("IVF-PQ index already holds vector id {id}"),
149 });
150 }
151 let cell_idx = nearest(&self.centroids, &vector, &self.params);
152 let code = pq.encode(&residual(&vector, &self.centroids[cell_idx]));
153 let cell = &mut self.cells[cell_idx];
154 let pos = cell.ids.len() as u32;
155 cell.ids.push(id);
156 cell.codes.extend_from_slice(&code);
157 cell.vectors.extend_from_slice(&vector);
158 self.slots.insert(id, (cell_idx as u32, pos));
159 self.next_id = self.next_id.max(id.saturating_add(1));
160 Ok(())
161 }
162
163 pub fn add(&mut self, vector: &[f32]) -> Result<u32, VectorError> {
167 let id = self.next_id;
168 self.insert_with_id(id, vector.to_vec())?;
169 Ok(id)
170 }
171
172 pub fn add_batch(&mut self, vectors: &[&[f32]]) -> Result<(), VectorError> {
175 for v in vectors {
176 self.add(v)?;
177 }
178 Ok(())
179 }
180
181 pub fn is_trained(&self) -> bool {
183 self.pq.is_some()
184 }
185
186 pub fn contains(&self, id: u32) -> bool {
188 self.slots.contains_key(&id)
189 }
190
191 pub fn is_deleted(&self, id: u32) -> bool {
193 self.deleted.contains(id)
194 }
195
196 pub fn delete(&mut self, id: u32) -> bool {
198 self.contains(id) && self.deleted.insert(id)
199 }
200
201 pub fn undelete(&mut self, id: u32) -> bool {
203 self.deleted.remove(id)
204 }
205
206 pub fn get_vector(&self, id: u32) -> Option<&[f32]> {
208 if self.deleted.contains(id) {
209 return None;
210 }
211 let &(cell, pos) = self.slots.get(&id)?;
212 let start = pos as usize * self.dim;
213 self.cells
214 .get(cell as usize)?
215 .vectors
216 .get(start..start + self.dim)
217 }
218
219 pub fn live_vectors(&self) -> Vec<(u32, Vec<f32>)> {
221 let mut out: Vec<(u32, Vec<f32>)> = Vec::with_capacity(self.live_count());
222 for cell in &self.cells {
223 for (pos, &id) in cell.ids.iter().enumerate() {
224 if !self.deleted.contains(id) {
225 let start = pos * self.dim;
226 out.push((id, cell.vectors[start..start + self.dim].to_vec()));
227 }
228 }
229 }
230 out.sort_unstable_by_key(|(id, _)| *id);
231 out
232 }
233
234 pub fn roll_back_to(&mut self, next_id: u32) {
238 self.retain_entries(|id| id < next_id);
239 self.deleted.remove_range(next_id..);
240 self.next_id = self.next_id.min(next_id);
241 }
242
243 pub fn compact(&mut self) -> usize {
246 let deleted = std::mem::take(&mut self.deleted);
247 self.retain_entries(|id| !deleted.contains(id))
248 }
249
250 fn retain_entries(&mut self, keep: impl Fn(u32) -> bool) -> usize {
253 let m = self.pq.as_ref().map_or(0, |pq| pq.m);
254 let dim = self.dim;
255 let mut removed = 0;
256 self.slots.clear();
257 for (cell_idx, cell) in self.cells.iter_mut().enumerate() {
258 let mut kept = IvfCell::default();
259 for (pos, &id) in cell.ids.iter().enumerate() {
260 if !keep(id) {
261 removed += 1;
262 continue;
263 }
264 self.slots
265 .insert(id, (cell_idx as u32, kept.ids.len() as u32));
266 kept.ids.push(id);
267 kept.codes
268 .extend_from_slice(&cell.codes[pos * m..(pos + 1) * m]);
269 kept.vectors
270 .extend_from_slice(&cell.vectors[pos * dim..(pos + 1) * dim]);
271 }
272 *cell = kept;
273 }
274 removed
275 }
276
277 pub fn len(&self) -> usize {
279 self.slots.len()
280 }
281
282 pub fn live_count(&self) -> usize {
284 self.slots.len() - self.deleted.len() as usize
285 }
286
287 pub fn tombstone_count(&self) -> usize {
289 self.deleted.len() as usize
290 }
291
292 pub fn is_empty(&self) -> bool {
293 self.slots.is_empty()
294 }
295
296 pub fn dim(&self) -> usize {
297 self.dim
298 }
299
300 pub fn n_cells(&self) -> usize {
301 self.centroids.len()
302 }
303
304 pub fn params(&self) -> &IvfPqParams {
305 &self.params
306 }
307
308 pub fn trained_on(&self) -> usize {
310 self.trained_on
311 }
312
313 pub fn trained_at_ms(&self) -> u64 {
315 self.trained_at_ms
316 }
317
318 pub fn set_trained_at_ms(&mut self, ms: u64) {
320 self.trained_at_ms = ms;
321 }
322
323 pub fn memory_bytes(&self) -> usize {
325 let f32_size = std::mem::size_of::<f32>();
326 let centroids = self.centroids.len() * self.dim * f32_size;
327 let entries: usize = self
328 .cells
329 .iter()
330 .map(|c| {
331 c.ids.len() * std::mem::size_of::<u32>()
332 + c.codes.len()
333 + c.vectors.len() * f32_size
334 })
335 .sum();
336 centroids + entries
337 }
338}
339
340fn nearest(centroids: &[Vec<f32>], vector: &[f32], params: &IvfPqParams) -> usize {
343 let mut best = 0;
344 let mut best_dist = f32::MAX;
345 for (i, c) in centroids.iter().enumerate() {
346 let d = distance(vector, c, params.metric);
347 if d < best_dist {
348 best_dist = d;
349 best = i;
350 }
351 }
352 best
353}
354
355pub(super) fn residual(vector: &[f32], centroid: &[f32]) -> Vec<f32> {
357 vector.iter().zip(centroid).map(|(a, b)| a - b).collect()
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::distance::DistanceMetric;
364 use crate::test_support::test_memory;
365
366 fn make_vectors(n: usize, dim: usize) -> Vec<Vec<f32>> {
367 (0..n)
368 .map(|i| (0..dim).map(|d| ((i * dim + d) as f32) * 0.01).collect())
369 .collect()
370 }
371
372 fn small_params() -> IvfPqParams {
373 IvfPqParams {
374 n_cells: 4,
375 pq_m: 4,
376 pq_k: 8,
377 nprobe: 4,
378 metric: DistanceMetric::L2,
379 }
380 }
381
382 fn trained(vecs: &[Vec<f32>]) -> IvfPqIndex {
383 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
384 let mut idx = IvfPqIndex::new(8, small_params());
385 idx.train(&refs, test_memory()).unwrap();
386 idx
387 }
388
389 #[test]
390 fn rolling_back_withdraws_every_vector_added_after_the_mark() {
391 let vecs = make_vectors(64, 8);
392 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
393 let mut idx = trained(&vecs);
394 idx.add_batch(&refs[..40]).unwrap();
395 let mark = idx.len() as u32;
396 let before: Vec<u32> = idx
397 .search(&vecs[5], 40)
398 .unwrap()
399 .iter()
400 .map(|r| r.id)
401 .collect();
402
403 idx.add_batch(&refs[40..]).unwrap();
404 idx.roll_back_to(mark);
405
406 assert_eq!(idx.len(), 40);
407 assert!(idx.is_trained(), "the training the index held stays");
408 let after_ids: Vec<u32> = idx
409 .search(&vecs[5], 64)
410 .unwrap()
411 .iter()
412 .map(|r| r.id)
413 .collect();
414 assert!(after_ids.iter().all(|id| *id < mark));
415 assert_eq!(after_ids, before, "the search reads as before the adds");
416
417 assert_eq!(idx.add(&vecs[63]).unwrap(), mark);
419 }
420
421 #[test]
422 fn caller_ids_survive_delete_compact_and_reads() {
423 let vecs = make_vectors(16, 8);
424 let mut idx = trained(&vecs);
425 for (i, v) in vecs.iter().enumerate() {
426 idx.insert_with_id(100 + i as u32, v.clone()).unwrap();
427 }
428 assert!(matches!(
429 idx.insert_with_id(100, vecs[0].clone()),
430 Err(VectorError::InvalidInput { .. })
431 ));
432 assert!(idx.delete(103));
433 assert!(!idx.delete(103), "a second delete finds nothing live");
434 assert!(idx.get_vector(103).is_none());
435 assert_eq!(idx.live_count(), 15);
436
437 assert_eq!(idx.compact(), 1);
438 assert_eq!(idx.len(), 15);
439 assert!(!idx.contains(103));
440 assert_eq!(idx.get_vector(104), Some(vecs[4].as_slice()));
441 assert_eq!(idx.add(&vecs[0]).unwrap(), 116);
442 }
443
444 #[test]
445 fn a_trained_index_holding_vectors_refuses_to_retrain() {
446 let vecs = make_vectors(16, 8);
447 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
448 let mut idx = trained(&vecs);
449 idx.add(&vecs[0]).unwrap();
450 assert!(matches!(
451 idx.train(&refs, test_memory()),
452 Err(VectorError::InvalidInput { .. })
453 ));
454 assert_eq!(idx.len(), 1);
455 }
456
457 #[test]
458 fn wrong_dimension_is_a_typed_error() {
459 let vecs: Vec<Vec<f32>> = (0..32)
460 .map(|i| (0..8).map(|d| ((i * 8 + d) % 17) as f32).collect())
461 .collect();
462 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
463 let mut idx = IvfPqIndex::new(8, small_params());
464 assert!(matches!(
465 idx.add(&[0.0; 8]),
466 Err(VectorError::InvalidInput { .. })
467 ));
468 idx.train(&refs, test_memory()).unwrap();
469 idx.add_batch(&refs).unwrap();
470 assert!(matches!(
471 idx.search(&[0.0; 3], 5),
472 Err(VectorError::DimensionMismatch {
473 expected: 8,
474 got: 3
475 })
476 ));
477 assert!(matches!(
478 idx.add(&[0.0; 3]),
479 Err(VectorError::DimensionMismatch {
480 expected: 8,
481 got: 3
482 })
483 ));
484 let short = [0.0_f32; 3];
485 let mut untrained = IvfPqIndex::new(8, IvfPqParams::default());
486 assert!(matches!(
487 untrained.train(&[&short], test_memory()),
488 Err(VectorError::DimensionMismatch {
489 expected: 8,
490 got: 3
491 })
492 ));
493 }
494}