nodedb_vector/codec_index/
build.rs1use std::cmp::Reverse;
9use std::collections::{BinaryHeap, HashSet};
10
11use nodedb_codec::vector_quant::codec::VectorCodec;
12
13use super::graph::{HnswCodecIndex, NodeC};
14use crate::error::{VectorError, check_dim};
15
16#[derive(Clone, Copy, PartialEq)]
18struct Cand {
19 dist: f32,
20 idx: u32,
22}
23
24impl Eq for Cand {}
25
26impl PartialOrd for Cand {
27 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
28 Some(self.cmp(other))
29 }
30}
31
32impl Ord for Cand {
33 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
34 self.dist
35 .partial_cmp(&other.dist)
36 .unwrap_or(std::cmp::Ordering::Equal)
37 .then(self.idx.cmp(&other.idx))
38 }
39}
40
41impl<C: VectorCodec> HnswCodecIndex<C> {
42 pub fn insert(&mut self, id: u32, v: &[f32]) -> Result<(), VectorError> {
48 check_dim(self.dim, v.len())?;
49 let quantized = self.codec.encode(v);
50 let node_layer = self.random_layer();
51
52 let new_idx = self.nodes.len() as u32;
54
55 let neighbors = vec![Vec::new(); node_layer + 1];
57
58 self.nodes.push(NodeC {
59 id,
60 deleted: false,
61 layer: node_layer,
62 quantized,
63 neighbors,
64 });
65
66 let Some(ep) = self.entry_point else {
67 self.entry_point = Some(new_idx);
69 self.max_layer = node_layer;
70 return Ok(());
71 };
72
73 let mut cur_ep = ep;
76 for layer in (node_layer + 1..=self.max_layer).rev() {
77 cur_ep = self.greedy_nearest(new_idx, cur_ep, layer);
78 }
79
80 let ef = self.ef_construction;
82 for layer in (0..=node_layer.min(self.max_layer)).rev() {
83 let candidates = self.search_layer_build(new_idx, cur_ep, ef, layer);
84
85 let max_nb = self.max_neighbors(layer);
87 let chosen: Vec<u32> = candidates
88 .iter()
89 .filter(|c| c.idx != new_idx)
90 .take(max_nb)
91 .map(|c| c.idx)
92 .collect();
93
94 self.nodes[new_idx as usize].neighbors[layer] = chosen.clone();
96
97 for &nb_idx in &chosen {
99 let new_dist = {
100 let nb_q = &self.nodes[nb_idx as usize].quantized as *const C::Quantized;
101 let new_q = &self.nodes[new_idx as usize].quantized as *const C::Quantized;
102 unsafe { self.codec.fast_symmetric_distance(&*nb_q, &*new_q) }
105 };
106
107 if layer < self.nodes[nb_idx as usize].neighbors.len() {
108 let nb_layer = &mut self.nodes[nb_idx as usize].neighbors[layer];
109 if !nb_layer.contains(&new_idx) {
110 nb_layer.push(new_idx);
111 }
112 if nb_layer.len() > max_nb {
114 self.prune_neighbors(nb_idx, layer, max_nb, new_dist, new_idx);
115 }
116 }
117 }
118
119 if let Some(best) = candidates.first() {
121 cur_ep = best.idx;
122 }
123 }
124
125 if node_layer > self.max_layer {
127 self.entry_point = Some(new_idx);
128 self.max_layer = node_layer;
129 }
130 Ok(())
131 }
132
133 fn greedy_nearest(&self, query_idx: u32, ep_idx: u32, layer: usize) -> u32 {
136 let mut best_idx = ep_idx;
137 let mut best_dist = self.sym_dist(query_idx, ep_idx);
138
139 loop {
140 let mut improved = false;
141 for &nb in self.neighbors_at(best_idx, layer) {
142 if self.nodes[nb as usize].deleted {
143 continue;
144 }
145 let d = self.sym_dist(query_idx, nb);
146 if d < best_dist {
147 best_dist = d;
148 best_idx = nb;
149 improved = true;
150 }
151 }
152 if !improved {
153 break;
154 }
155 }
156
157 best_idx
158 }
159
160 fn search_layer_build(
163 &self,
164 query_idx: u32,
165 ep_idx: u32,
166 ef: usize,
167 layer: usize,
168 ) -> Vec<Cand> {
169 let mut visited: HashSet<u32> = HashSet::new();
170 visited.insert(ep_idx);
171
172 let ep_dist = self.sym_dist(query_idx, ep_idx);
173 let ep_cand = Cand {
174 dist: ep_dist,
175 idx: ep_idx,
176 };
177
178 let mut candidates: BinaryHeap<Reverse<Cand>> = BinaryHeap::new();
179 candidates.push(Reverse(ep_cand));
180
181 let mut results: BinaryHeap<Cand> = BinaryHeap::new();
182 if !self.nodes[ep_idx as usize].deleted {
183 results.push(ep_cand);
184 }
185
186 while let Some(Reverse(cur)) = candidates.pop() {
187 let worst = results.peek().map_or(f32::INFINITY, |w| w.dist);
188 if cur.dist > worst && results.len() >= ef {
189 break;
190 }
191
192 for &nb in self.neighbors_at(cur.idx, layer) {
193 if !visited.insert(nb) {
194 continue;
195 }
196 let d = self.sym_dist(query_idx, nb);
197 let worst_now = results.peek().map_or(f32::INFINITY, |w| w.dist);
198 if d < worst_now || results.len() < ef {
199 candidates.push(Reverse(Cand { dist: d, idx: nb }));
200 }
201 if !self.nodes[nb as usize].deleted {
202 results.push(Cand { dist: d, idx: nb });
203 if results.len() > ef {
204 results.pop();
205 }
206 }
207 }
208 }
209
210 let mut out: Vec<Cand> = results.into_vec();
211 out.sort_unstable_by(|a, b| a.dist.total_cmp(&b.dist));
212 out
213 }
214
215 fn prune_neighbors(
218 &mut self,
219 nb_idx: u32,
220 layer: usize,
221 max_nb: usize,
222 _hint_dist: f32,
223 _hint_id: u32,
224 ) {
225 let nb_list = self.nodes[nb_idx as usize].neighbors[layer].clone();
227 let mut scored: Vec<(f32, u32)> = nb_list
228 .iter()
229 .map(|&cand_idx| {
230 let d = {
231 let a = &self.nodes[nb_idx as usize].quantized as *const C::Quantized;
232 let b = &self.nodes[cand_idx as usize].quantized as *const C::Quantized;
233 unsafe { self.codec.fast_symmetric_distance(&*a, &*b) }
235 };
236 (d, cand_idx)
237 })
238 .collect();
239
240 scored.sort_unstable_by(|a, b| a.0.total_cmp(&b.0));
242 scored.truncate(max_nb);
243
244 self.nodes[nb_idx as usize].neighbors[layer] =
245 scored.into_iter().map(|(_, idx)| idx).collect();
246 }
247
248 #[inline]
250 pub(crate) fn sym_dist(&self, a_idx: u32, b_idx: u32) -> f32 {
251 let a = &self.nodes[a_idx as usize].quantized;
252 let b = &self.nodes[b_idx as usize].quantized;
253 self.codec.fast_symmetric_distance(a, b)
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::quantize::Sq8Codec;
263
264 fn make_sq8(dim: usize, n: usize) -> Sq8Codec {
265 let vecs: Vec<Vec<f32>> = (0..n)
266 .map(|i| (0..dim).map(|d| (i * dim + d) as f32 * 0.1).collect())
267 .collect();
268 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
269 Sq8Codec::calibrate(&refs, dim).unwrap()
270 }
271
272 #[test]
273 fn insert_sets_entry_point() {
274 let codec = make_sq8(4, 10);
275 let mut idx: HnswCodecIndex<Sq8Codec> = HnswCodecIndex::new(4, 8, 50, codec, 1);
276 idx.insert(0, &[0.1, 0.2, 0.3, 0.4]).unwrap();
277 assert!(idx.entry_point.is_some());
278 assert_eq!(idx.len(), 1);
279 }
280
281 #[test]
282 fn insert_multiple_grows_nodes() {
283 let codec = make_sq8(4, 30);
284 let mut idx: HnswCodecIndex<Sq8Codec> = HnswCodecIndex::new(4, 8, 50, codec, 42);
285 for i in 0..20u32 {
286 let v: Vec<f32> = (0..4).map(|d| (i as usize * 4 + d) as f32).collect();
287 idx.insert(i, &v).unwrap();
288 }
289 assert_eq!(idx.len(), 20);
290 assert!(idx.entry_point.is_some());
291 }
292}