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