nodedb_vector/collection/
build.rs1use nodedb_mem::ScopedMemory;
16use nodedb_types::VectorQuantization;
17
18use crate::error::VectorError;
19use crate::hnsw::HnswIndex;
20use crate::index_config::IndexType;
21
22use super::lifecycle::VectorCollection;
23use super::lifecycle_insert_ops::sealed_vector;
24use super::segment::{BuildKind, BuildRequest, SealedSegment};
25
26impl VectorCollection {
27 pub fn building_segment_ids(&self) -> Vec<u32> {
29 self.building.iter().map(|b| b.segment_id).collect()
30 }
31
32 pub fn build_request_for(&self, key: &str, segment_id: u32) -> Option<BuildRequest> {
35 let seg = self.building.iter().find(|b| b.segment_id == segment_id)?;
36 let vectors = (0..seg.flat.len() as u32)
37 .filter_map(|i| seg.flat.get_vector_raw(i).map(<[f32]>::to_vec))
38 .collect();
39 Some(BuildRequest {
40 key: key.to_string(),
41 segment_id,
42 kind: BuildKind::Seal,
43 vectors,
44 dim: self.dim,
45 params: self.params.clone(),
46 })
47 }
48
49 pub fn sealed_base_ids(&self) -> Vec<u32> {
51 self.sealed
52 .iter()
53 .filter(|s| !s.index.is_empty())
54 .map(|s| s.base_id)
55 .collect()
56 }
57
58 pub fn rebuild_request_for(
66 &mut self,
67 key: &str,
68 base_id: u32,
69 ) -> Result<Option<BuildRequest>, VectorError> {
70 let Some(seg) = self.sealed.iter().find(|s| s.base_id == base_id) else {
71 return Ok(None);
72 };
73 let len = seg.index.len();
74 let mut vectors = Vec::with_capacity(len);
75 for local in 0..len as u32 {
76 let v = sealed_vector(seg, local).ok_or(VectorError::VectorUnavailable {
77 id: base_id + local,
78 })?;
79 vectors.push(v);
80 }
81 let segment_id = self.next_segment_id;
82 self.next_segment_id += 1;
83 Ok(Some(BuildRequest {
84 key: key.to_string(),
85 segment_id,
86 kind: BuildKind::Rebuild { base_id, len },
87 vectors,
88 dim: self.dim,
89 params: self.params.clone(),
90 }))
91 }
92
93 pub fn complete_build(
98 &mut self,
99 segment_id: u32,
100 index: HnswIndex,
101 memory: ScopedMemory,
102 ) -> bool {
103 let Some(pos) = self
104 .building
105 .iter()
106 .position(|b| b.segment_id == segment_id)
107 else {
108 return false;
109 };
110 let mut index = index;
111 if index.len() != self.building[pos].flat.len() {
112 tracing::error!(
113 segment_id,
114 built = index.len(),
115 expected = self.building[pos].flat.len(),
116 "HNSW build does not match its segment; segment stays on brute force"
117 );
118 return false;
119 }
120 let building = self.building.remove(pos);
121 for local in 0..building.flat.len() as u32 {
122 if building.flat.is_deleted(local) {
123 index.delete(local);
124 }
125 }
126 let seg = self.sealed_segment_from(segment_id, building.base_id, index, &memory);
127 self.sealed.push(seg);
128 self.builds_completed += 1;
129 self.refresh_codec_dispatch();
130 true
131 }
132
133 pub fn complete_rebuild(
139 &mut self,
140 segment_id: u32,
141 base_id: u32,
142 len: usize,
143 index: HnswIndex,
144 memory: ScopedMemory,
145 ) -> bool {
146 let Some(pos) = self
147 .sealed
148 .iter()
149 .position(|s| s.base_id == base_id && s.index.len() == len)
150 else {
151 return false;
152 };
153 if index.len() != len {
154 tracing::error!(
155 base_id,
156 built = index.len(),
157 expected = len,
158 "HNSW rebuild does not match its segment; the old segment stays"
159 );
160 return false;
161 }
162 let mut index = index;
163 for local in 0..len as u32 {
164 if self.sealed[pos].index.is_deleted(local) {
165 index.delete(local);
166 }
167 }
168 let seg = self.sealed_segment_from(segment_id, base_id, index, &memory);
169 let old = std::mem::replace(&mut self.sealed[pos], seg);
170 self.drop_sealed(old);
171 self.builds_completed += 1;
172 self.refresh_codec_dispatch();
173 true
174 }
175
176 pub fn note_build_failed(&mut self) {
178 self.builds_failed += 1;
179 }
180
181 fn sealed_segment_from(
184 &mut self,
185 segment_id: u32,
186 base_id: u32,
187 index: HnswIndex,
188 memory: &ScopedMemory,
189 ) -> SealedSegment {
190 let use_codec_dispatch = self.codec_dispatch_tag().is_some();
191 let use_pq = !use_codec_dispatch && self.index_config.index_type == IndexType::HnswPq;
192 let (sq8, pq) = if use_codec_dispatch {
193 (None, None)
194 } else if use_pq {
195 (
196 None,
197 Self::build_pq_for_index(&index, self.index_config.pq_m, memory.clone()),
198 )
199 } else {
200 (Self::build_sq8_for_index(&index), None)
201 };
202 let (tier, mmap_vectors) = self.resolve_tier_for_build(segment_id, base_id, &index, memory);
203 SealedSegment {
204 index,
205 base_id,
206 sq8,
207 pq,
208 tier,
209 mmap_vectors,
210 }
211 }
212
213 fn drop_sealed(&mut self, seg: SealedSegment) {
215 let mmap_path = seg.mmap_vectors.as_ref().map(|m| m.path().to_path_buf());
216 drop(seg);
218 if let Some(path) = mmap_path {
219 self.mmap_segment_count = self.mmap_segment_count.saturating_sub(1);
220 if let Err(e) = std::fs::remove_file(&path) {
221 tracing::warn!(
222 path = %path.display(),
223 error = %e,
224 "vector rebuild: replaced mmap segment file not removed"
225 );
226 }
227 }
228 }
229
230 fn codec_dispatch_tag(&self) -> Option<&'static str> {
232 match self.quantization {
233 VectorQuantization::RaBitQ => Some("rabitq"),
234 VectorQuantization::Bbq => Some("bbq"),
235 _ => None,
236 }
237 }
238
239 fn refresh_codec_dispatch(&mut self) {
242 let Some(tag) = self.codec_dispatch_tag() else {
243 return;
244 };
245 if let Err(e) = self.build_codec_dispatch(tag).map(|_| ()) {
246 tracing::error!(error = %e, tag, "codec-dispatch build failed; searching sealed segments directly");
249 self.codec_dispatch = None;
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use nodedb_types::Surrogate;
257
258 use super::*;
259 use crate::hnsw::HnswParams;
260 use crate::test_support::test_memory;
261
262 fn vector(i: usize) -> Vec<f32> {
263 vec![(i + 1) as f32, (i % 7 + 1) as f32, (i % 11 + 1) as f32]
264 }
265
266 fn l2() -> HnswParams {
267 HnswParams {
268 metric: crate::distance::DistanceMetric::L2,
269 ..HnswParams::default()
270 }
271 }
272
273 fn build(req: &BuildRequest) -> HnswIndex {
274 let mut index = HnswIndex::with_seed(req.dim, req.params.clone(), 7);
275 for v in &req.vectors {
276 index.insert(v.clone()).unwrap();
277 }
278 index
279 }
280
281 fn collection() -> VectorCollection {
283 let mut coll = VectorCollection::with_seal_threshold(3, l2(), 10);
284 for i in 0..10 {
285 coll.insert_with_surrogate(vector(i), Surrogate::new(i as u32 + 1))
286 .unwrap();
287 }
288 coll
289 }
290
291 #[test]
292 fn a_seal_build_keeps_ids_and_the_deletes_made_meanwhile() {
293 let mut coll = collection();
294 coll.delete(2);
295 let req = coll.seal("k").unwrap();
296 assert_eq!(req.vectors.len(), 10, "deleted vectors keep their slot");
297 coll.delete(4);
298
299 assert!(coll.complete_build(req.segment_id, build(&req), test_memory()));
300
301 assert!(coll.building.is_empty());
302 assert!(!coll.is_live(2) && !coll.is_live(4) && coll.is_live(9));
303 assert_eq!(coll.vector_for_id(7), Some(vector(7)));
304 assert_eq!(coll.search(&vector(7), 1, 64).unwrap()[0].id, 7);
305 assert_eq!(coll.get_surrogate(7), Some(Surrogate::new(8)));
306 }
307
308 #[test]
309 fn a_build_of_the_wrong_size_is_refused() {
310 let mut coll = collection();
311 let req = coll.seal("k").unwrap();
312 let mut short = HnswIndex::new(3, l2());
313 short.insert(vector(0)).unwrap();
314 assert!(!coll.complete_build(req.segment_id, short, test_memory()));
315 assert_eq!(coll.building.len(), 1, "the segment stays on brute force");
316 }
317
318 #[test]
319 fn a_rebuild_keeps_ids_codes_and_tombstones() {
320 let mut coll = VectorCollection::with_pq_config(3, l2(), 3);
321 coll.set_seal_threshold(10);
322 for i in 0..10 {
323 coll.insert_with_surrogate(vector(i), Surrogate::new(i as u32 + 1))
324 .unwrap();
325 }
326 let req = coll.seal("k").unwrap();
327 coll.complete_build(req.segment_id, build(&req), test_memory());
328 coll.delete(1);
329
330 let req = coll.rebuild_request_for("k", 0).unwrap().unwrap();
331 assert_eq!(
332 req.kind,
333 BuildKind::Rebuild {
334 base_id: 0,
335 len: 10
336 }
337 );
338 coll.delete(6);
339 let BuildKind::Rebuild { base_id, len } = req.kind else {
340 panic!("a rebuild request");
341 };
342 assert!(coll.complete_rebuild(req.segment_id, base_id, len, build(&req), test_memory()));
343
344 assert_eq!(coll.sealed.len(), 1);
345 assert!(
346 coll.sealed[0].pq.is_some(),
347 "the segment is quantized again"
348 );
349 assert!(!coll.is_live(1) && !coll.is_live(6));
350 for i in [0usize, 3, 9] {
351 assert_eq!(coll.search(&vector(i), 1, 64).unwrap()[0].id, i as u32);
352 assert_eq!(
353 coll.get_surrogate(i as u32),
354 Some(Surrogate::new(i as u32 + 1))
355 );
356 }
357 }
358
359 #[test]
360 fn a_rebuild_of_a_compacted_segment_is_refused() {
361 let mut coll = collection();
362 let req = coll.seal("k").unwrap();
363 coll.complete_build(req.segment_id, build(&req), test_memory());
364 coll.delete(3);
365 let req = coll.rebuild_request_for("k", 0).unwrap().unwrap();
366 assert_eq!(coll.compact(), 1);
367 let BuildKind::Rebuild { base_id, len } = req.kind else {
368 panic!("a rebuild request");
369 };
370 assert!(!coll.complete_rebuild(req.segment_id, base_id, len, build(&req), test_memory()));
371 assert_eq!(coll.sealed[0].index.len(), 9, "the compacted segment stays");
372 }
373
374 #[test]
375 fn an_unbuilt_segment_survives_a_checkpoint_as_building() {
376 let mut coll = collection();
377 coll.delete(5);
378 coll.seal("k").unwrap();
379 let bytes = coll.checkpoint_to_bytes(None).unwrap();
380 let restored = VectorCollection::from_checkpoint(&bytes, None, test_memory()).unwrap();
381
382 let ids = restored.building_segment_ids();
383 assert_eq!(ids.len(), 1);
384 let req = restored.build_request_for("k", ids[0]).unwrap();
385 assert_eq!(req.vectors.len(), 10);
386 assert!(!restored.is_live(5));
387 assert_eq!(restored.search(&vector(8), 1, 64).unwrap()[0].id, 8);
388 }
389}