1use std::collections::{HashMap, 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::bm25::Bm25Index;
12use crate::hnsw::{HnswConfig, HnswIndex};
13use crate::salience::SalienceIndex;
14use crate::temporal::TemporalIndex;
15
16#[derive(Default)]
18pub struct IndexManagerConfig {
19 pub hnsw: HnswConfig,
21}
22
23pub struct IndexManager {
25 pub hnsw: HnswIndex,
27 pub bm25: Bm25Index,
29 pub bitmap: BitmapIndex,
31 pub temporal: TemporalIndex,
33 pub salience: SalienceIndex,
35}
36
37impl IndexManager {
38 pub fn new(config: IndexManagerConfig) -> Self {
40 Self {
41 hnsw: HnswIndex::new(config.hnsw),
42 bm25: Bm25Index::new(),
43 bitmap: BitmapIndex::new(),
44 temporal: TemporalIndex::new(),
45 salience: SalienceIndex::new(),
46 }
47 }
48
49 pub fn save(&self, dir: &Path) -> MenteResult<()> {
51 std::fs::create_dir_all(dir)?;
52 self.hnsw.save(&dir.join("hnsw.json"))?;
53 self.bm25.save(&dir.join("bm25.json"))?;
54 self.bitmap.save(&dir.join("bitmap.json"))?;
55 self.temporal.save(&dir.join("temporal.json"))?;
56 self.salience.save(&dir.join("salience.json"))?;
57 Ok(())
58 }
59
60 pub fn load(dir: &Path) -> MenteResult<Self> {
62 let hnsw = HnswIndex::load(&dir.join("hnsw.json"), HnswConfig::default().ef_search)?;
63 let bm25_path = dir.join("bm25.json");
64 let bm25 = if bm25_path.exists() {
65 Bm25Index::load(&bm25_path)?
66 } else {
67 Bm25Index::new()
68 };
69 let bitmap = BitmapIndex::load(&dir.join("bitmap.json"))?;
70 let temporal = TemporalIndex::load(&dir.join("temporal.json"))?;
71 let salience = SalienceIndex::load(&dir.join("salience.json"))?;
72 Ok(Self {
73 hnsw,
74 bm25,
75 bitmap,
76 temporal,
77 salience,
78 })
79 }
80
81 pub fn index_memory(&self, node: &MemoryNode) {
83 if !node.embedding.is_empty() {
85 let _ = self.hnsw.insert(node.id, &node.embedding);
86 }
87
88 if !node.content.is_empty() {
90 self.bm25.insert(node.id, &node.content);
91 }
92
93 for tag in &node.tags {
95 self.bitmap.add_tag(node.id, tag);
96 }
97
98 self.temporal.insert(node.id, node.created_at);
100
101 self.salience.insert(node.id, node.salience);
103 }
104
105 pub fn remove_memory(&self, id: MemoryId, node: &MemoryNode) {
107 let _ = self.hnsw.remove(id);
108 self.bm25.remove(id);
109 self.bitmap.remove_all(id);
110 self.temporal.remove(id, node.created_at);
111 self.salience.remove(id, node.salience);
112 }
113
114 pub fn hybrid_search(
125 &self,
126 query_embedding: &[f32],
127 tags: Option<&[&str]>,
128 time_range: Option<(Timestamp, Timestamp)>,
129 k: usize,
130 ) -> Vec<(MemoryId, f32)> {
131 self.hybrid_search_with_query(query_embedding, None, tags, time_range, k)
132 }
133
134 pub fn hybrid_search_with_query(
139 &self,
140 query_embedding: &[f32],
141 query_text: Option<&str>,
142 tags: Option<&[&str]>,
143 time_range: Option<(Timestamp, Timestamp)>,
144 k: usize,
145 ) -> Vec<(MemoryId, f32)> {
146 if k == 0 {
147 return Vec::new();
148 }
149
150 let fetch_k = k * 4;
151 let rrf_k: f32 = 60.0;
152
153 let vector_candidates = self.hnsw.search(query_embedding, fetch_k);
155
156 let bm25_candidates = match query_text {
158 Some(qt) if !self.bm25.is_empty() => self.bm25.search(qt, fetch_k),
159 _ => Vec::new(),
160 };
161
162 if vector_candidates.is_empty() && bm25_candidates.is_empty() {
163 return Vec::new();
164 }
165
166 let mut rrf_scores: HashMap<MemoryId, f32> = HashMap::new();
168
169 for (rank, (id, _)) in vector_candidates.iter().enumerate() {
170 *rrf_scores.entry(*id).or_insert(0.0) += 1.0 / (rrf_k + rank as f32);
171 }
172 for (rank, (id, _)) in bm25_candidates.iter().enumerate() {
173 *rrf_scores.entry(*id).or_insert(0.0) += 1.0 / (rrf_k + rank as f32);
174 }
175
176 let tag_filter: Option<HashSet<MemoryId>> = tags.map(|t| {
178 if t.is_empty() {
179 HashSet::new()
180 } else {
181 self.bitmap.query_tags_and(t).into_iter().collect()
182 }
183 });
184
185 let time_filter: Option<HashSet<MemoryId>> =
187 time_range.map(|(start, end)| self.temporal.range(start, end).into_iter().collect());
188
189 let max_ts = rrf_scores
191 .keys()
192 .filter_map(|id| self.temporal.get_timestamp(*id))
193 .max()
194 .unwrap_or(1) as f64;
195
196 let mut scored: Vec<(MemoryId, f32)> = rrf_scores
197 .into_iter()
198 .filter(|(id, _)| {
199 if let Some(ref tf) = tag_filter
200 && !tf.contains(id)
201 {
202 return false;
203 }
204 if let Some(ref trf) = time_filter
205 && !trf.contains(id)
206 {
207 return false;
208 }
209 true
210 })
211 .map(|(id, rrf_score)| {
212 let salience = self.salience.get_salience(id).unwrap_or(0.5);
213 let ts = self.temporal.get_timestamp(id).unwrap_or(0) as f64;
214 let recency = if max_ts > 0.0 {
215 (ts / max_ts) as f32
216 } else {
217 0.0
218 };
219
220 let combined = rrf_score * 0.7 + salience * 0.05 + recency * 0.02;
222 (id, combined)
223 })
224 .collect();
225
226 scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
228 scored.truncate(k);
229 scored
230 }
231}
232
233impl Default for IndexManager {
234 fn default() -> Self {
235 Self::new(IndexManagerConfig::default())
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use mentedb_core::memory::MemoryType;
243 use mentedb_core::types::AgentId;
244
245 fn make_node(
246 embedding: Vec<f32>,
247 tags: Vec<String>,
248 salience: f32,
249 created_at: u64,
250 ) -> MemoryNode {
251 let mut node = MemoryNode::new(
252 AgentId::new(),
253 MemoryType::Episodic,
254 "test".into(),
255 embedding,
256 );
257 node.tags = tags;
258 node.salience = salience;
259 node.created_at = created_at;
260 node
261 }
262
263 #[test]
264 fn test_index_and_search() {
265 let mgr = IndexManager::default();
266 let node = make_node(vec![1.0, 0.0, 0.0, 0.0], vec!["test".into()], 0.8, 1000);
267 mgr.index_memory(&node);
268
269 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], None, None, 1);
270 assert_eq!(results.len(), 1);
271 assert_eq!(results[0].0, node.id);
272 }
273
274 #[test]
275 fn test_tag_filter() {
276 let mgr = IndexManager::default();
277 let a = make_node(vec![1.0, 0.0, 0.0, 0.0], vec!["alpha".into()], 0.8, 1000);
278 let b = make_node(vec![0.9, 0.1, 0.0, 0.0], vec!["beta".into()], 0.8, 1000);
279 mgr.index_memory(&a);
280 mgr.index_memory(&b);
281
282 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], Some(&["alpha"]), None, 10);
283 assert_eq!(results.len(), 1);
284 assert_eq!(results[0].0, a.id);
285 }
286
287 #[test]
288 fn test_time_filter() {
289 let mgr = IndexManager::default();
290 let a = make_node(vec![1.0, 0.0, 0.0, 0.0], vec![], 0.8, 100);
291 let b = make_node(vec![0.9, 0.1, 0.0, 0.0], vec![], 0.8, 500);
292 mgr.index_memory(&a);
293 mgr.index_memory(&b);
294
295 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], None, Some((400, 600)), 10);
296 assert_eq!(results.len(), 1);
297 assert_eq!(results[0].0, b.id);
298 }
299
300 #[test]
301 fn test_remove_memory() {
302 let mgr = IndexManager::default();
303 let node = make_node(vec![1.0, 0.0, 0.0, 0.0], vec!["t".into()], 0.5, 100);
304 let id = node.id;
305 mgr.index_memory(&node);
306 mgr.remove_memory(id, &node);
307
308 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], None, None, 10);
309 assert!(results.is_empty());
310 }
311
312 #[test]
313 fn test_empty_search() {
314 let mgr = IndexManager::default();
315 let results = mgr.hybrid_search(&[1.0, 0.0], None, None, 5);
316 assert!(results.is_empty());
317 }
318}