nodedb_vector/collection/
ivf_mode.rs1use nodedb_mem::ScopedMemory;
17
18use crate::error::VectorError;
19use crate::index_config::{IndexConfig, IndexType};
20use crate::ivf::IvfPqIndex;
21
22use super::lifecycle::VectorCollection;
23use super::lifecycle_insert_ops::sealed_vector;
24
25impl VectorCollection {
26 pub fn is_ivf(&self) -> bool {
28 self.index_config.index_type == IndexType::IvfPq
29 }
30
31 pub fn index_config(&self) -> &IndexConfig {
33 &self.index_config
34 }
35
36 pub fn set_index_config(&mut self, config: IndexConfig) {
39 self.index_config = IndexConfig {
40 hnsw: self.params.clone(),
41 ..config
42 };
43 }
44
45 pub fn ivf_training_threshold(&self) -> usize {
47 self.index_config.to_ivf_params().training_threshold()
48 }
49
50 pub fn needs_ivf_training(&self) -> bool {
53 self.is_ivf() && self.ivf.is_none() && self.live_count() >= self.ivf_training_threshold()
54 }
55
56 pub fn ivf_index(&self) -> Option<&IvfPqIndex> {
58 self.ivf.as_ref()
59 }
60
61 pub fn train_ivf(
69 &mut self,
70 memory: ScopedMemory,
71 trained_at_ms: u64,
72 ) -> Result<(), VectorError> {
73 if !self.is_ivf() || self.ivf.is_some() {
74 return Err(VectorError::InvalidInput {
75 detail: "IVF-PQ training needs an untrained ivf_pq collection".into(),
76 });
77 }
78 let live = self.gather_live_vectors();
79 let threshold = self.ivf_training_threshold();
80 if live.len() < threshold {
81 return Err(VectorError::InvalidInput {
82 detail: format!(
83 "IVF-PQ training needs {threshold} live vectors; the collection holds {}",
84 live.len()
85 ),
86 });
87 }
88 let mut ivf = IvfPqIndex::new(self.dim, self.index_config.to_ivf_params());
89 {
90 let refs: Vec<&[f32]> = live.iter().map(|(_, v)| v.as_slice()).collect();
91 ivf.train(&refs, memory)?;
92 }
93 for (id, vector) in live {
94 ivf.insert_with_id(id, vector)?;
95 }
96 ivf.set_trained_at_ms(trained_at_ms);
97 self.clear_segments();
98 self.ivf = Some(ivf);
99 Ok(())
100 }
101
102 fn gather_live_vectors(&self) -> Vec<(u32, Vec<f32>)> {
104 let mut out = Vec::with_capacity(self.live_count());
105 for seg in &self.sealed {
106 for local in 0..seg.index.len() as u32 {
107 if !seg.index.is_deleted(local)
108 && let Some(v) = sealed_vector(seg, local)
109 {
110 out.push((seg.base_id + local, v));
111 }
112 }
113 }
114 for seg in &self.building {
115 for local in 0..seg.flat.len() as u32 {
116 if let Some(v) = seg.flat.get_vector(local) {
117 out.push((seg.base_id + local, v.to_vec()));
118 }
119 }
120 }
121 for local in 0..self.growing.len() as u32 {
122 if let Some(v) = self.growing.get_vector(local) {
123 out.push((self.growing_base_id + local, v.to_vec()));
124 }
125 }
126 out
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use nodedb_types::Surrogate;
133
134 use super::*;
135 use crate::test_support::test_memory;
136
137 const DIM: usize = 8;
138
139 fn ivf_collection() -> VectorCollection {
142 let config = IndexConfig {
143 hnsw: crate::hnsw::HnswParams {
144 metric: crate::distance::DistanceMetric::L2,
145 ..crate::hnsw::HnswParams::default()
146 },
147 index_type: IndexType::IvfPq,
148 pq_m: 4,
149 ivf_cells: 4,
150 ivf_nprobe: 4,
151 ..IndexConfig::default()
152 };
153 VectorCollection::with_index_config(DIM, config)
154 }
155
156 fn vector(i: usize) -> Vec<f32> {
158 [1, 7, 11, 13, 17, 19, 23, 29]
159 .iter()
160 .map(|&m| (if m == 1 { i + 1 } else { i % m + 1 }) as f32)
161 .collect()
162 }
163
164 fn all_ids(coll: &VectorCollection, query: &[f32]) -> Vec<u32> {
165 let mut ids: Vec<u32> = coll
166 .search(query, 10_000, 64)
167 .unwrap()
168 .iter()
169 .map(|r| r.id)
170 .collect();
171 ids.sort_unstable();
172 ids
173 }
174
175 #[test]
176 fn below_the_threshold_vectors_wait_in_the_exact_buffer() {
177 let mut coll = ivf_collection();
178 assert_eq!(coll.ivf_training_threshold(), 256);
179 for i in 0..10 {
180 coll.insert_with_surrogate(vector(i), Surrogate::new(i as u32 + 1))
181 .unwrap();
182 }
183 assert!(!coll.needs_ivf_training());
184 assert!(!coll.needs_seal());
185 let hit = &coll.search(&vector(3), 1, 64).unwrap()[0];
186 assert_eq!(hit.id, 3);
187 assert_eq!(hit.distance, 0.0, "the buffer is searched exactly");
188 }
189
190 #[test]
191 fn training_moves_every_vector_once_and_later_inserts_follow() {
192 let mut coll = ivf_collection();
193 for i in 0..256 {
194 coll.insert_with_surrogate(vector(i), Surrogate::new(i as u32 + 1))
195 .unwrap();
196 }
197 coll.delete(5);
198 coll.insert(vector(256)).unwrap();
199 assert!(coll.needs_ivf_training());
200 let before = all_ids(&coll, &vector(0));
201
202 coll.train_ivf(test_memory(), 42).unwrap();
203
204 assert!(!coll.needs_ivf_training());
205 assert!(coll.growing_is_empty());
206 let ivf = coll.ivf_index().unwrap();
207 assert_eq!(ivf.trained_on(), 256);
208 assert_eq!(ivf.trained_at_ms(), 42);
209 assert_eq!(
210 all_ids(&coll, &vector(0)),
211 before,
212 "no vector lost or doubled"
213 );
214
215 let id = coll
216 .insert_with_surrogate(vector(300), Surrogate::new(9_999))
217 .unwrap();
218 assert_eq!(id, 257);
219 assert!(
220 coll.growing_is_empty(),
221 "a trained collection inserts into IVF"
222 );
223 assert_eq!(coll.search(&vector(300), 1, 64).unwrap()[0].id, 257);
224 assert_eq!(
225 coll.vector_for_surrogate(Surrogate::new(9_999)),
226 Some(vector(300))
227 );
228 assert!(coll.delete_by_surrogate(Surrogate::new(9_999)));
229 assert!(coll.search(&vector(300), 1, 64).unwrap()[0].id != 257);
230 }
231
232 #[test]
233 fn training_below_the_threshold_is_refused() {
234 let mut coll = ivf_collection();
235 coll.insert(vector(0)).unwrap();
236 assert!(matches!(
237 coll.train_ivf(test_memory(), 0),
238 Err(VectorError::InvalidInput { .. })
239 ));
240 assert!(coll.ivf_index().is_none());
241 assert_eq!(coll.live_count(), 1);
242 }
243
244 #[test]
245 fn a_trained_collection_survives_a_checkpoint() {
246 let mut coll = ivf_collection();
247 for i in 0..260 {
248 coll.insert(vector(i)).unwrap();
249 }
250 coll.train_ivf(test_memory(), 7).unwrap();
251 coll.insert(vector(400)).unwrap();
252 coll.delete(9);
253
254 let bytes = coll.checkpoint_to_bytes(None).unwrap();
255 let restored = VectorCollection::from_checkpoint(&bytes, None, test_memory()).unwrap();
256
257 assert!(restored.is_ivf());
258 assert_eq!(restored.ivf_index().unwrap().trained_at_ms(), 7);
259 assert_eq!(all_ids(&restored, &vector(0)), all_ids(&coll, &vector(0)));
260 assert_eq!(restored.search(&vector(400), 1, 64).unwrap()[0].id, 260);
261 }
262
263 #[test]
264 fn an_untrained_buffer_survives_a_checkpoint_and_trains_after() {
265 let mut coll = ivf_collection();
266 for i in 0..100 {
267 coll.insert(vector(i)).unwrap();
268 }
269 let bytes = coll.checkpoint_to_bytes(None).unwrap();
270 let mut restored = VectorCollection::from_checkpoint(&bytes, None, test_memory()).unwrap();
271 assert!(restored.is_ivf());
272 assert!(restored.ivf_index().is_none());
273 for i in 100..256 {
274 restored.insert(vector(i)).unwrap();
275 }
276 assert!(restored.needs_ivf_training());
277 restored.train_ivf(test_memory(), 1).unwrap();
278 assert_eq!(
279 all_ids(&restored, &vector(0)),
280 (0..256).collect::<Vec<u32>>()
281 );
282 }
283}