nodedb_vector/hnsw/graph/index/
compact.rs1use super::HnswIndex;
6use crate::hnsw::graph::types::Node;
7
8impl HnswIndex {
9 pub fn compact(&mut self) -> usize {
14 self.compact_with_map().0
15 }
16
17 pub fn compact_with_map(&mut self) -> (usize, Vec<u32>) {
23 let tombstone_count = self.tombstone_count();
24 if tombstone_count == 0 {
25 let identity: Vec<u32> = (0..self.nodes.len() as u32).collect();
26 return (0, identity);
27 }
28 self.ensure_mutable_neighbors();
29 self.bridge_deleted_neighbors();
30
31 let mut id_map: Vec<u32> = Vec::with_capacity(self.nodes.len());
32 let mut new_id = 0u32;
33 for node in &self.nodes {
34 if node.deleted {
35 id_map.push(u32::MAX);
36 } else {
37 id_map.push(new_id);
38 new_id += 1;
39 }
40 }
41
42 let mut new_nodes: Vec<Node> = Vec::with_capacity(new_id as usize);
43 for node in self.nodes.drain(..) {
44 if node.deleted {
45 continue;
46 }
47 let remapped_neighbors: Vec<Vec<u32>> = node
48 .neighbors
49 .into_iter()
50 .map(|layer_neighbors| {
51 layer_neighbors
52 .into_iter()
53 .filter_map(|old_nid| {
54 let new_nid = id_map[old_nid as usize];
55 if new_nid == u32::MAX {
56 None
57 } else {
58 Some(new_nid)
59 }
60 })
61 .collect()
62 })
63 .collect();
64 new_nodes.push(Node {
65 storage: node.storage,
66 neighbors: remapped_neighbors,
67 deleted: false,
68 });
69 }
70
71 self.entry_point = if let Some(old_ep) = self.entry_point {
72 let new_ep = id_map[old_ep as usize];
73 if new_ep == u32::MAX {
74 new_nodes
75 .iter()
76 .enumerate()
77 .max_by_key(|(_, n)| n.neighbors.len())
78 .map(|(i, _)| i as u32)
79 } else {
80 Some(new_ep)
81 }
82 } else {
83 None
84 };
85
86 self.max_layer = new_nodes
87 .iter()
88 .map(|n| n.neighbors.len().saturating_sub(1))
89 .max()
90 .unwrap_or(0);
91
92 self.nodes = new_nodes;
93 self.connect_unreachable();
94 (tombstone_count, id_map)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use std::collections::VecDeque;
101
102 use nodedb_types::vector_dtype::VectorStorageDtype;
103
104 use crate::distance::DistanceMetric;
105 use crate::hnsw::{HnswIndex, HnswParams};
106
107 fn params(m: usize, m0: usize) -> HnswParams {
108 HnswParams {
109 m,
110 m0,
111 ef_construction: 32,
112 metric: DistanceMetric::L2,
113 dtype: VectorStorageDtype::F32,
114 }
115 }
116
117 fn reached_at_layer0(idx: &HnswIndex) -> Vec<bool> {
119 let mut reached = vec![false; idx.nodes.len()];
120 let Some(ep) = idx.entry_point() else {
121 return reached;
122 };
123 reached[ep as usize] = true;
124 let mut queue = VecDeque::from([ep]);
125 while let Some(cur) = queue.pop_front() {
126 for &next in &idx.nodes[cur as usize].neighbors[0] {
127 if !reached[next as usize] {
128 reached[next as usize] = true;
129 queue.push_back(next);
130 }
131 }
132 }
133 reached
134 }
135
136 #[test]
141 fn compaction_keeps_node_whose_only_in_edge_came_from_a_deleted_node() {
142 let mut idx = HnswIndex::with_seed(3, params(1 << 40, 2), 1);
144 for x in 0..4u8 {
145 idx.insert(vec![f32::from(x), 0.0, 0.0]).unwrap();
146 }
147 assert_eq!(idx.max_layer(), 0);
148 let into_3: Vec<usize> = idx
149 .nodes
150 .iter()
151 .enumerate()
152 .filter(|(_, n)| n.neighbors[0].contains(&3))
153 .map(|(id, _)| id)
154 .collect();
155 assert_eq!(
156 into_3,
157 vec![2],
158 "fixture: node 2 holds the only edge into node 3"
159 );
160
161 assert!(idx.delete(2));
162 let query = [3.0, 0.0, 0.0];
163 assert_eq!(idx.search(&query, 1, 32).unwrap()[0].id, 3);
164
165 let (removed, id_map) = idx.compact_with_map();
166 assert_eq!(removed, 1);
167 let hits = idx.search(&query, 1, 32).unwrap();
168 assert_eq!(
169 hits.first().map(|h| h.id),
170 Some(id_map[3]),
171 "node 3 is unreachable after compaction, got {hits:?}"
172 );
173 assert!(reached_at_layer0(&idx).iter().all(|&r| r));
174 }
175
176 #[test]
179 fn compaction_after_heavy_deletes_keeps_every_node_reachable() {
180 use crate::test_support::seeded_vectors;
181
182 let vectors = seeded_vectors(400, 8, 0x9E37_79B9_7F4A_7C15);
183 let mut idx = HnswIndex::with_seed(
184 8,
185 HnswParams {
186 ef_construction: 64,
187 ..params(4, 8)
188 },
189 7,
190 );
191 for v in &vectors {
192 idx.insert(v.clone()).unwrap();
193 }
194 for id in 0..vectors.len() as u32 {
195 if id % 5 != 0 {
196 assert!(idx.delete(id));
197 }
198 }
199 let (_, id_map) = idx.compact_with_map();
200
201 let reached = reached_at_layer0(&idx);
202 let lost = reached.iter().filter(|&&r| !r).count();
203 assert_eq!(lost, 0, "{lost} of {} nodes unreachable", reached.len());
204 for old in (0..vectors.len()).step_by(5) {
205 let hits = idx.search(&vectors[old], 1, idx.len()).unwrap();
206 assert_eq!(
207 hits.first().map(|h| h.id),
208 Some(id_map[old]),
209 "old node {old} not found"
210 );
211 }
212 let (m, m0) = (idx.params().m, idx.params().m0);
213 for node in &idx.nodes {
214 for (layer, list) in node.neighbors.iter().enumerate() {
215 assert!(list.len() <= if layer == 0 { m0 } else { m });
216 }
217 }
218 }
219}