Flush the delta buffer into the main HNSW. ## Steps 1. Drain tombstones → call `HnswIndex::delete` on each. 2. Drain fresh vectors → call `HnswIndex::insert` on each. 3. After each insert, estimate local topology drift for the newly assigned node and accumulate the overlap fraction. 4. If average overlap fraction < `drift_threshold`, increment `drift_subgraphs`. `k_neighbors` and `ef_constructi
(
&mut self,
_k_neighbors: usize,
_ef_construction: usize,
)
| 81 | /// current HNSW implementation derives its own neighbor count from |
| 82 | /// `HnswParams` stored on the index, so these values are informational. |
| 83 | pub fn patch( |
| 84 | &mut self, |
| 85 | _k_neighbors: usize, |
| 86 | _ef_construction: usize, |
| 87 | ) -> Result<PatchStats, VectorError> { |
| 88 | let mut stats = PatchStats::default(); |
| 89 | |
| 90 | // --- Step 1: Forward tombstones to the main HNSW --- |
| 91 | let tombstone_ids = self.delta.drain_tombstones(); |
| 92 | for id in tombstone_ids { |
| 93 | if self.main.delete(id) { |
| 94 | stats.tombstoned_marked += 1; |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | // --- Step 2 + 3: Insert fresh vectors and estimate drift --- |
| 99 | let fresh = self.delta.drain_fresh(); |
| 100 | |
| 101 | // Collect the node IDs that will be assigned to freshly inserted nodes |
| 102 | // so we can measure neighborhood overlap after each insert. |
| 103 | // The HNSW appends nodes sequentially, so the new id = len() before insert. |
| 104 | let mut overlap_fractions: Vec<f32> = Vec::with_capacity(fresh.len()); |
| 105 | // Track the set of recently-patched node ids for overlap estimation. |
| 106 | let mut patched_ids: std::collections::HashSet<u32> = |
| 107 | std::collections::HashSet::with_capacity(fresh.len()); |
| 108 | |
| 109 | for (user_id, vector) in fresh { |
| 110 | // Skip tombstoned fresh inserts — they were deleted before we |
| 111 | // could patch them. |
| 112 | if self.delta.is_tombstoned(user_id) { |
| 113 | continue; |
| 114 | } |
| 115 | |
| 116 | // The HNSW uses its own internal monotonic IDs (insertion order). |
| 117 | // We record what the next id will be before the insert. |
| 118 | let new_internal_id = self.main.len() as u32; |
| 119 | |
| 120 | self.main.insert(vector)?; |
| 121 | stats.patched += 1; |
| 122 | |
| 123 | // --- Drift estimation (LIRE approximation) --- |
| 124 | // Inspect neighbors assigned to the new node at layer 0. |
| 125 | let neighbors_l0 = self.main.hnsw_neighbors_layer0(new_internal_id); |
| 126 | |
| 127 | let overlap_fraction = if neighbors_l0.is_empty() { |
| 128 | // First node or isolated — perfect connectivity by definition. |
| 129 | 1.0f32 |
| 130 | } else { |
| 131 | // Count how many neighbors are themselves in the current |
| 132 | // patched-ids set (i.e., recently inserted into this batch). |
| 133 | let overlap = neighbors_l0 |
| 134 | .iter() |
| 135 | .filter(|&&nid| patched_ids.contains(&nid)) |
| 136 | .count(); |
| 137 | overlap as f32 / neighbors_l0.len() as f32 |
| 138 | }; |
| 139 | |
| 140 | overlap_fractions.push(overlap_fraction); |