1use std::collections::HashSet;
4use std::path::Path;
5
6use mentedb_core::MemoryNode;
7use mentedb_core::error::MenteResult;
8use mentedb_core::types::{MemoryId, Timestamp};
9
10use crate::bitmap::BitmapIndex;
11use crate::hnsw::{HnswConfig, HnswIndex};
12use crate::salience::SalienceIndex;
13use crate::temporal::TemporalIndex;
14
15#[derive(Default)]
17pub struct IndexManagerConfig {
18 pub hnsw: HnswConfig,
20}
21
22pub struct IndexManager {
24 pub hnsw: HnswIndex,
26 pub bitmap: BitmapIndex,
28 pub temporal: TemporalIndex,
30 pub salience: SalienceIndex,
32}
33
34impl IndexManager {
35 pub fn new(config: IndexManagerConfig) -> Self {
37 Self {
38 hnsw: HnswIndex::new(config.hnsw),
39 bitmap: BitmapIndex::new(),
40 temporal: TemporalIndex::new(),
41 salience: SalienceIndex::new(),
42 }
43 }
44
45 pub fn save(&self, dir: &Path) -> MenteResult<()> {
47 std::fs::create_dir_all(dir)?;
48 self.hnsw.save(&dir.join("hnsw.json"))?;
49 self.bitmap.save(&dir.join("bitmap.json"))?;
50 self.temporal.save(&dir.join("temporal.json"))?;
51 self.salience.save(&dir.join("salience.json"))?;
52 Ok(())
53 }
54
55 pub fn load(dir: &Path) -> MenteResult<Self> {
57 let hnsw = HnswIndex::load(&dir.join("hnsw.json"), HnswConfig::default().ef_search)?;
58 let bitmap = BitmapIndex::load(&dir.join("bitmap.json"))?;
59 let temporal = TemporalIndex::load(&dir.join("temporal.json"))?;
60 let salience = SalienceIndex::load(&dir.join("salience.json"))?;
61 Ok(Self {
62 hnsw,
63 bitmap,
64 temporal,
65 salience,
66 })
67 }
68
69 pub fn index_memory(&self, node: &MemoryNode) {
71 if !node.embedding.is_empty() {
73 let _ = self.hnsw.insert(node.id, &node.embedding);
74 }
75
76 for tag in &node.tags {
78 self.bitmap.add_tag(node.id, tag);
79 }
80
81 self.temporal.insert(node.id, node.created_at);
83
84 self.salience.insert(node.id, node.salience);
86 }
87
88 pub fn remove_memory(&self, id: MemoryId, node: &MemoryNode) {
90 let _ = self.hnsw.remove(id);
91 self.bitmap.remove_all(id);
92 self.temporal.remove(id, node.created_at);
93 self.salience.remove(id, node.salience);
94 }
95
96 pub fn hybrid_search(
104 &self,
105 query_embedding: &[f32],
106 tags: Option<&[&str]>,
107 time_range: Option<(Timestamp, Timestamp)>,
108 k: usize,
109 ) -> Vec<(MemoryId, f32)> {
110 if k == 0 {
111 return Vec::new();
112 }
113
114 let vector_candidates = self.hnsw.search(query_embedding, k * 4);
116
117 if vector_candidates.is_empty() {
118 return Vec::new();
119 }
120
121 let tag_filter: Option<HashSet<MemoryId>> = tags.map(|t| {
123 if t.is_empty() {
124 HashSet::new()
125 } else {
126 self.bitmap.query_tags_and(t).into_iter().collect()
127 }
128 });
129
130 let time_filter: Option<HashSet<MemoryId>> =
132 time_range.map(|(start, end)| self.temporal.range(start, end).into_iter().collect());
133
134 let max_dist = vector_candidates
136 .iter()
137 .map(|(_, d)| *d)
138 .fold(f32::NEG_INFINITY, f32::max)
139 .max(f32::EPSILON);
140
141 let max_ts = vector_candidates
143 .iter()
144 .filter_map(|(id, _)| self.temporal.get_timestamp(*id))
145 .max()
146 .unwrap_or(1) as f64;
147
148 let mut scored: Vec<(MemoryId, f32)> = vector_candidates
150 .into_iter()
151 .filter(|(id, _)| {
152 if let Some(ref tf) = tag_filter
153 && !tf.contains(id)
154 {
155 return false;
156 }
157 if let Some(ref trf) = time_filter
158 && !trf.contains(id)
159 {
160 return false;
161 }
162 true
163 })
164 .map(|(id, dist)| {
165 let vector_sim = 1.0 - (dist / max_dist);
167
168 let salience = self.salience.get_salience(id).unwrap_or(0.5);
170
171 let ts = self.temporal.get_timestamp(id).unwrap_or(0) as f64;
173 let recency = if max_ts > 0.0 {
174 (ts / max_ts) as f32
175 } else {
176 0.0
177 };
178
179 let combined = vector_sim * 0.6 + salience * 0.3 + recency * 0.1;
180 (id, combined)
181 })
182 .collect();
183
184 scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
186 scored.truncate(k);
187 scored
188 }
189}
190
191impl Default for IndexManager {
192 fn default() -> Self {
193 Self::new(IndexManagerConfig::default())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use mentedb_core::memory::MemoryType;
201 use mentedb_core::types::AgentId;
202
203 fn make_node(
204 embedding: Vec<f32>,
205 tags: Vec<String>,
206 salience: f32,
207 created_at: u64,
208 ) -> MemoryNode {
209 let mut node = MemoryNode::new(
210 AgentId::new(),
211 MemoryType::Episodic,
212 "test".into(),
213 embedding,
214 );
215 node.tags = tags;
216 node.salience = salience;
217 node.created_at = created_at;
218 node
219 }
220
221 #[test]
222 fn test_index_and_search() {
223 let mgr = IndexManager::default();
224 let node = make_node(vec![1.0, 0.0, 0.0, 0.0], vec!["test".into()], 0.8, 1000);
225 mgr.index_memory(&node);
226
227 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], None, None, 1);
228 assert_eq!(results.len(), 1);
229 assert_eq!(results[0].0, node.id);
230 }
231
232 #[test]
233 fn test_tag_filter() {
234 let mgr = IndexManager::default();
235 let a = make_node(vec![1.0, 0.0, 0.0, 0.0], vec!["alpha".into()], 0.8, 1000);
236 let b = make_node(vec![0.9, 0.1, 0.0, 0.0], vec!["beta".into()], 0.8, 1000);
237 mgr.index_memory(&a);
238 mgr.index_memory(&b);
239
240 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], Some(&["alpha"]), None, 10);
241 assert_eq!(results.len(), 1);
242 assert_eq!(results[0].0, a.id);
243 }
244
245 #[test]
246 fn test_time_filter() {
247 let mgr = IndexManager::default();
248 let a = make_node(vec![1.0, 0.0, 0.0, 0.0], vec![], 0.8, 100);
249 let b = make_node(vec![0.9, 0.1, 0.0, 0.0], vec![], 0.8, 500);
250 mgr.index_memory(&a);
251 mgr.index_memory(&b);
252
253 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], None, Some((400, 600)), 10);
254 assert_eq!(results.len(), 1);
255 assert_eq!(results[0].0, b.id);
256 }
257
258 #[test]
259 fn test_remove_memory() {
260 let mgr = IndexManager::default();
261 let node = make_node(vec![1.0, 0.0, 0.0, 0.0], vec!["t".into()], 0.5, 100);
262 let id = node.id;
263 mgr.index_memory(&node);
264 mgr.remove_memory(id, &node);
265
266 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], None, None, 10);
267 assert!(results.is_empty());
268 }
269
270 #[test]
271 fn test_empty_search() {
272 let mgr = IndexManager::default();
273 let results = mgr.hybrid_search(&[1.0, 0.0], None, None, 5);
274 assert!(results.is_empty());
275 }
276}