nodedb_vector/delta/
index.rs1use std::collections::HashSet;
10
11use crate::distance::distance;
12use crate::error::{VectorError, check_dim};
13use nodedb_types::vector_distance::DistanceMetric;
14
15pub struct DeltaIndex {
18 pub dim: usize,
19 fresh: Vec<(u32, Vec<f32>)>,
21 tombstones: HashSet<u32>,
23 max_fresh: usize,
25}
26
27impl DeltaIndex {
28 pub fn new(dim: usize, max_fresh: usize) -> Self {
33 Self {
34 dim,
35 fresh: Vec::new(),
36 tombstones: HashSet::new(),
37 max_fresh,
38 }
39 }
40
41 pub fn insert(&mut self, id: u32, vector: Vec<f32>) -> Result<(), VectorError> {
45 check_dim(self.dim, vector.len())?;
46 self.fresh.push((id, vector));
47 Ok(())
48 }
49
50 pub fn tombstone(&mut self, id: u32) {
53 self.tombstones.insert(id);
54 }
55
56 pub fn is_full(&self) -> bool {
58 self.fresh.len() >= self.max_fresh
59 }
60
61 pub fn fresh_len(&self) -> usize {
63 self.fresh.len()
64 }
65
66 pub fn search(
70 &self,
71 query: &[f32],
72 k: usize,
73 metric: DistanceMetric,
74 ) -> Result<Vec<(u32, f32)>, VectorError> {
75 check_dim(self.dim, query.len())?;
76 if k == 0 {
77 return Ok(Vec::new());
78 }
79
80 let mut scored: Vec<(u32, f32)> = self
81 .fresh
82 .iter()
83 .filter(|(id, _)| !self.tombstones.contains(id))
84 .map(|(id, vec)| (*id, distance(query, vec, metric)))
85 .collect();
86
87 if k < scored.len() {
89 scored.select_nth_unstable_by(k, |a, b| {
90 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
91 });
92 scored.truncate(k);
93 }
94
95 scored.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
96
97 Ok(scored)
98 }
99
100 pub fn drain_fresh(&mut self) -> Vec<(u32, Vec<f32>)> {
103 std::mem::take(&mut self.fresh)
104 }
105
106 pub fn drain_tombstones(&mut self) -> Vec<u32> {
109 std::mem::take(&mut self.tombstones).into_iter().collect()
110 }
111
112 pub fn is_tombstoned(&self, id: u32) -> bool {
114 self.tombstones.contains(&id)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use nodedb_types::vector_distance::DistanceMetric;
122
123 fn make_delta() -> DeltaIndex {
124 let mut d = DeltaIndex::new(3, 16);
125 for i in 0u32..10 {
126 let v = vec![i as f32, 0.0, 0.0];
127 d.insert(i, v).unwrap();
128 }
129 d
130 }
131
132 #[test]
133 fn top_k_returns_nearest() {
134 let d = make_delta();
135 let query = [0.0f32, 0.0, 0.0];
136 let results = d.search(&query, 3, DistanceMetric::L2).unwrap();
137 assert_eq!(results.len(), 3);
138 assert_eq!(results[0].0, 0);
140 assert_eq!(results[1].0, 1);
141 assert_eq!(results[2].0, 2);
142 }
143
144 #[test]
145 fn tombstone_excluded_from_search() {
146 let mut d = make_delta();
147 d.tombstone(0);
148 let query = [0.0f32, 0.0, 0.0];
149 let results = d.search(&query, 3, DistanceMetric::L2).unwrap();
150 assert!(results.iter().all(|(id, _)| *id != 0));
151 }
152
153 #[test]
154 fn is_full_triggers_at_threshold() {
155 let mut d = DeltaIndex::new(3, 3);
156 assert!(!d.is_full());
157 d.insert(0, vec![0.0, 0.0, 0.0]).unwrap();
158 d.insert(1, vec![1.0, 0.0, 0.0]).unwrap();
159 assert!(!d.is_full());
160 d.insert(2, vec![2.0, 0.0, 0.0]).unwrap();
161 assert!(d.is_full());
162 }
163
164 #[test]
165 fn drain_fresh_empties_buffer() {
166 let mut d = make_delta();
167 let drained = d.drain_fresh();
168 assert_eq!(drained.len(), 10);
169 assert_eq!(d.fresh_len(), 0);
170 }
171
172 #[test]
173 fn drain_tombstones_empties_set() {
174 let mut d = make_delta();
175 d.tombstone(3);
176 d.tombstone(7);
177 let ts = d.drain_tombstones();
178 assert_eq!(ts.len(), 2);
179 assert!(!d.is_tombstoned(3));
180 assert!(!d.is_tombstoned(7));
181 }
182
183 #[test]
184 fn wrong_dimension_is_a_typed_error() {
185 let mut d = DeltaIndex::new(3, 8);
186 assert!(matches!(
187 d.insert(0, vec![0.0; 2]),
188 Err(VectorError::DimensionMismatch {
189 expected: 3,
190 got: 2
191 })
192 ));
193 assert!(matches!(
194 d.search(&[0.0; 4], 1, DistanceMetric::L2),
195 Err(VectorError::DimensionMismatch {
196 expected: 3,
197 got: 4
198 })
199 ));
200 }
201}