1use std::collections::HashMap;
2use std::io::{Read, Write};
3
4use engram_embeddings::ThreeFieldEmbedding;
5use engram_hnsw::{HnswError, HnswGraph, HnswParams};
6
7use crate::error::CoreError;
8
9const DEDUP_SEARCH_K: usize = 16;
10
11pub struct IndexSet {
12 context_index: HnswGraph,
13 action_index: HnswGraph,
14 result_index: HnswGraph,
15 id_map: HashMap<u64, String>,
16}
17
18impl IndexSet {
19 pub fn new(
20 build_params: impl Fn() -> Result<HnswParams, CoreError>,
21 ) -> Result<Self, CoreError> {
22 let context_index = HnswGraph::new(build_params()?);
23 let action_index = HnswGraph::new(build_params()?);
24 let result_index = HnswGraph::new(build_params()?);
25 Ok(Self {
26 context_index,
27 action_index,
28 result_index,
29 id_map: HashMap::new(),
30 })
31 }
32
33 pub fn insert_atomic(
34 &mut self,
35 id: u64,
36 memory_id: &str,
37 embedding: &ThreeFieldEmbedding,
38 rng_value: f64,
39 ) -> Result<(), CoreError> {
40 if let Some(existing_id) = self.id_map.get(&id) {
41 if existing_id == memory_id {
42 return Ok(());
43 }
44 return Err(CoreError::IndexHashCollision {
45 hash: id,
46 existing_id: existing_id.clone(),
47 conflicting_id: memory_id.to_string(),
48 });
49 }
50
51 self.context_index
52 .insert(id, embedding.context.clone(), rng_value)?;
53 if let Err(error) = self
54 .action_index
55 .insert(id, embedding.action.clone(), rng_value)
56 {
57 let _ = self.context_index.delete(id);
58 return Err(CoreError::from(error));
59 }
60 if let Err(error) = self
61 .result_index
62 .insert(id, embedding.result.clone(), rng_value)
63 {
64 let _ = self.context_index.delete(id);
65 let _ = self.action_index.delete(id);
66 return Err(CoreError::from(error));
67 }
68 self.id_map.insert(id, memory_id.to_string());
69 Ok(())
70 }
71
72 pub fn resolve_node_id(&self, node_id: u64) -> Option<&str> {
73 self.id_map.get(&node_id).map(String::as_str)
74 }
75
76 pub fn rebuild_id_map(&mut self, entries: impl Iterator<Item = (u64, String)>) {
77 self.id_map.clear();
78 for (hash, memory_id) in entries {
79 self.id_map.insert(hash, memory_id);
80 }
81 }
82
83 pub fn search(&self, query: &[f32], top_k: usize) -> Result<Vec<(u64, f32)>, HnswError> {
84 let mut scores: HashMap<u64, f32> = HashMap::new();
85 merge_scores(&mut scores, &self.context_index.search(query, top_k)?);
86 merge_scores(&mut scores, &self.action_index.search(query, top_k)?);
87 merge_scores(&mut scores, &self.result_index.search(query, top_k)?);
88 let mut ranked: Vec<(u64, f32)> = scores.into_iter().collect();
89 ranked.sort_by(|a, b| b.1.total_cmp(&a.1));
90 ranked.truncate(top_k);
91 Ok(ranked)
92 }
93
94 pub fn find_duplicate(
101 &self,
102 embedding: &ThreeFieldEmbedding,
103 threshold: f32,
104 ) -> Result<Option<(u64, f32)>, HnswError> {
105 let context_hits = self
106 .context_index
107 .search(&embedding.context, DEDUP_SEARCH_K)?;
108 let action_scores: HashMap<u64, f32> = self
109 .action_index
110 .search(&embedding.action, DEDUP_SEARCH_K)?
111 .into_iter()
112 .collect();
113 let result_scores: HashMap<u64, f32> = self
114 .result_index
115 .search(&embedding.result, DEDUP_SEARCH_K)?
116 .into_iter()
117 .collect();
118 let mut best: Option<(u64, f32)> = None;
119 for (node, context_score) in context_hits {
120 let (Some(&action_score), Some(&result_score)) =
121 (action_scores.get(&node), result_scores.get(&node))
122 else {
123 continue;
124 };
125 if context_score > threshold && action_score > threshold && result_score > threshold {
126 let min_similarity = context_score.min(action_score).min(result_score);
127 if best.is_none_or(|(_, best_min)| min_similarity > best_min) {
128 best = Some((node, min_similarity));
129 }
130 }
131 }
132 Ok(best)
133 }
134
135 pub fn delete(&mut self, id: u64) -> Result<(), HnswError> {
136 self.context_index.delete(id)?;
137 self.action_index.delete(id)?;
138 self.result_index.delete(id)?;
139 self.id_map.remove(&id);
140 Ok(())
141 }
142
143 pub fn contains(&self, id: u64) -> bool {
144 self.context_index.contains(id)
145 }
146
147 pub fn len(&self) -> usize {
148 self.context_index.len()
149 }
150
151 pub fn is_empty(&self) -> bool {
152 self.context_index.is_empty()
153 }
154
155 pub fn serialize<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
156 self.context_index.serialize(writer)?;
157 self.action_index.serialize(writer)?;
158 self.result_index.serialize(writer)?;
159 Ok(())
160 }
161
162 pub fn deserialize<R: Read>(reader: &mut R) -> Result<Self, HnswError> {
163 let context_index = HnswGraph::deserialize(reader)?;
164 let action_index = HnswGraph::deserialize(reader)?;
165 let result_index = HnswGraph::deserialize(reader)?;
166 Ok(Self {
167 context_index,
168 action_index,
169 result_index,
170 id_map: HashMap::new(),
171 })
172 }
173}
174
175fn merge_scores(scores: &mut HashMap<u64, f32>, results: &[(u64, f32)]) {
176 for &(id, similarity) in results {
177 scores
178 .entry(id)
179 .and_modify(|existing| *existing = existing.max(similarity))
180 .or_insert(similarity);
181 }
182}
183
184pub mod instrumentation {
185 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
186
187 static READER_TRACKING_ENABLED: AtomicBool = AtomicBool::new(false);
188 static CURRENT_READERS: AtomicUsize = AtomicUsize::new(0);
189 static MAX_READERS: AtomicUsize = AtomicUsize::new(0);
190
191 pub fn enable_reader_tracking() {
192 READER_TRACKING_ENABLED.store(true, Ordering::Relaxed);
193 }
194
195 pub fn disable_reader_tracking() {
196 READER_TRACKING_ENABLED.store(false, Ordering::Relaxed);
197 }
198
199 pub fn reset_reader_counters() {
200 CURRENT_READERS.store(0, Ordering::Relaxed);
201 MAX_READERS.store(0, Ordering::Relaxed);
202 }
203
204 pub fn concurrent_readers_max() -> usize {
205 MAX_READERS.load(Ordering::Relaxed)
206 }
207
208 pub struct ReaderTracker;
209
210 impl ReaderTracker {
211 pub fn new() -> Self {
212 if READER_TRACKING_ENABLED.load(Ordering::Relaxed) {
213 let current = CURRENT_READERS.fetch_add(1, Ordering::AcqRel) + 1;
214 MAX_READERS.fetch_max(current, Ordering::AcqRel);
215 }
216 Self
217 }
218 }
219
220 impl Drop for ReaderTracker {
221 fn drop(&mut self) {
222 if READER_TRACKING_ENABLED.load(Ordering::Relaxed) {
223 CURRENT_READERS.fetch_sub(1, Ordering::AcqRel);
224 }
225 }
226 }
227
228 impl Default for ReaderTracker {
229 fn default() -> Self {
230 Self::new()
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 const DIMENSION: usize = 8;
240 const THRESHOLD: f32 = 0.95;
241
242 fn new_index_set() -> IndexSet {
243 IndexSet::new(|| HnswParams::new(DIMENSION).map_err(CoreError::from))
244 .expect("index set construction succeeds")
245 }
246
247 fn axis_vector(axis: usize) -> Vec<f32> {
251 let mut vector = vec![0.0_f32; DIMENSION];
252 vector[axis % DIMENSION] = 1.0;
253 vector
254 }
255
256 fn embedding(
257 context_axis: usize,
258 action_axis: usize,
259 result_axis: usize,
260 ) -> ThreeFieldEmbedding {
261 ThreeFieldEmbedding {
262 context: axis_vector(context_axis),
263 action: axis_vector(action_axis),
264 result: axis_vector(result_axis),
265 }
266 }
267
268 #[test]
269 fn identical_embedding_is_a_duplicate() {
270 let mut index_set = new_index_set();
271 let stored = embedding(0, 1, 2);
272 index_set
273 .insert_atomic(7, "memory-7", &stored, 0.5)
274 .expect("insert succeeds");
275
276 let found = index_set
277 .find_duplicate(&stored, THRESHOLD)
278 .expect("search succeeds");
279
280 let (node, min_similarity) = found.expect("identical embedding qualifies as duplicate");
281 assert_eq!(node, 7);
282 assert_eq!(index_set.resolve_node_id(node), Some("memory-7"));
283 assert!((min_similarity - 1.0).abs() < 1e-4);
286 }
287
288 #[test]
289 fn only_context_matching_is_not_a_duplicate() {
290 let mut index_set = new_index_set();
291 let stored = embedding(0, 1, 2);
292 index_set
293 .insert_atomic(7, "memory-7", &stored, 0.5)
294 .expect("insert succeeds");
295
296 let query = embedding(0, 4, 5);
300 let found = index_set
301 .find_duplicate(&query, THRESHOLD)
302 .expect("search succeeds");
303
304 assert_eq!(found, None);
305 }
306
307 #[test]
308 fn empty_index_has_no_duplicate() {
309 let index_set = new_index_set();
310 let query = embedding(0, 1, 2);
311
312 let found = index_set
313 .find_duplicate(&query, THRESHOLD)
314 .expect("search succeeds");
315
316 assert_eq!(found, None);
317 }
318
319 #[test]
320 fn embedding_below_threshold_is_not_a_duplicate() {
321 let mut index_set = new_index_set();
322 let stored = embedding(0, 1, 2);
323 index_set
324 .insert_atomic(7, "memory-7", &stored, 0.5)
325 .expect("insert succeeds");
326
327 let query = embedding(3, 4, 5);
330 let found = index_set
331 .find_duplicate(&query, THRESHOLD)
332 .expect("search succeeds");
333
334 assert_eq!(found, None);
335 }
336}