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.bin"))?;
53 self.bm25.save(&dir.join("bm25.bin"))?;
54 self.bitmap.save(&dir.join("bitmap.bin"))?;
55 self.temporal.save(&dir.join("temporal.bin"))?;
56 self.salience.save(&dir.join("salience.bin"))?;
57 Ok(())
58 }
59
60 pub fn load(dir: &Path) -> MenteResult<Self> {
62 let hnsw_path = Self::resolve_path(dir, "hnsw");
63 let hnsw = HnswIndex::load(&hnsw_path, HnswConfig::default().ef_search)?;
64
65 let bm25_bin = dir.join("bm25.bin");
66 let bm25_json = dir.join("bm25.json");
67 let bm25 = if bm25_bin.exists() {
68 Bm25Index::load(&bm25_bin)?
69 } else if bm25_json.exists() {
70 Bm25Index::load(&bm25_json)?
71 } else {
72 Bm25Index::new()
73 };
74
75 let bitmap = BitmapIndex::load(&Self::resolve_path(dir, "bitmap"))?;
76 let temporal = TemporalIndex::load(&Self::resolve_path(dir, "temporal"))?;
77 let salience = SalienceIndex::load(&Self::resolve_path(dir, "salience"))?;
78 Ok(Self {
79 hnsw,
80 bm25,
81 bitmap,
82 temporal,
83 salience,
84 })
85 }
86
87 fn resolve_path(dir: &Path, name: &str) -> std::path::PathBuf {
89 let bin = dir.join(format!("{name}.bin"));
90 if bin.exists() {
91 bin
92 } else {
93 dir.join(format!("{name}.json"))
94 }
95 }
96
97 pub fn index_memory(&self, node: &MemoryNode) {
99 if !node.embedding.is_empty() {
101 let _ = self.hnsw.insert(node.id, &node.embedding);
102 }
103
104 let indexed = node.indexed_text();
107 if !indexed.is_empty() {
108 self.bm25.insert(node.id, &indexed);
109 }
110
111 self.bitmap.remove_all(node.id);
117 for tag in &node.tags {
118 self.bitmap.add_tag(node.id, tag);
119 }
120
121 self.temporal.insert(node.id, node.created_at);
123
124 self.salience.insert(node.id, node.salience);
126 }
127
128 pub fn contains_vector(&self, id: MemoryId) -> bool {
130 self.hnsw.contains(id)
131 }
132
133 pub fn vector_ids(&self) -> Vec<MemoryId> {
135 self.hnsw.ids()
136 }
137
138 pub fn remove_vector_only(&self, id: MemoryId) {
141 let _ = self.hnsw.remove(id);
142 self.bm25.remove(id);
143 }
144
145 pub fn remove_memory(&self, id: MemoryId, node: &MemoryNode) {
147 let _ = self.hnsw.remove(id);
148 self.bm25.remove(id);
149 self.bitmap.remove_all(id);
150 self.temporal.remove(id, node.created_at);
151 self.salience.remove(id, node.salience);
152 }
153
154 pub fn hybrid_search(
165 &self,
166 query_embedding: &[f32],
167 tags: Option<&[&str]>,
168 time_range: Option<(Timestamp, Timestamp)>,
169 k: usize,
170 ) -> Vec<(MemoryId, f32)> {
171 self.hybrid_search_with_query(query_embedding, None, tags, time_range, k)
172 }
173
174 pub fn hybrid_search_with_query(
179 &self,
180 query_embedding: &[f32],
181 query_text: Option<&str>,
182 tags: Option<&[&str]>,
183 time_range: Option<(Timestamp, Timestamp)>,
184 k: usize,
185 ) -> Vec<(MemoryId, f32)> {
186 self.hybrid_search_with_query_mode(query_embedding, query_text, tags, false, time_range, k)
187 }
188
189 pub fn hybrid_search_with_query_mode(
191 &self,
192 query_embedding: &[f32],
193 query_text: Option<&str>,
194 tags: Option<&[&str]>,
195 tags_or: bool,
196 time_range: Option<(Timestamp, Timestamp)>,
197 k: usize,
198 ) -> Vec<(MemoryId, f32)> {
199 if k == 0 {
200 return Vec::new();
201 }
202
203 let tag_filter: Option<HashSet<MemoryId>> = tags.map(|t| {
205 if t.is_empty() {
206 HashSet::new()
207 } else if tags_or {
208 self.bitmap.query_tags_or(t).into_iter().collect()
209 } else {
210 self.bitmap.query_tags_and(t).into_iter().collect()
211 }
212 });
213
214 let time_filter: Option<HashSet<MemoryId>> =
216 time_range.map(|(start, end)| self.temporal.range(start, end).into_iter().collect());
217
218 let candidate_set: Option<HashSet<MemoryId>> = match (&tag_filter, &time_filter) {
220 (Some(tf), Some(trf)) => Some(tf.intersection(trf).copied().collect()),
221 (Some(tf), None) => Some(tf.clone()),
222 (None, Some(trf)) => Some(trf.clone()),
223 (None, None) => None,
224 };
225
226 let use_prefilter = candidate_set.as_ref().is_some_and(|cs| {
230 let cs_len = cs.len();
231 cs_len > 0 && cs_len <= 500_000
234 });
235
236 let fetch_k = k * 4;
237 let rrf_k: f32 = 60.0;
238
239 let (vector_candidates, bm25_candidates) = if use_prefilter {
240 let cs = candidate_set.as_ref().unwrap();
241 let vc = self.hnsw.search_filtered(query_embedding, cs, fetch_k);
242 let bc = match query_text {
243 Some(qt) if !self.bm25.is_empty() => self.bm25.search_filtered(qt, fetch_k, cs),
244 _ => Vec::new(),
245 };
246 (vc, bc)
247 } else {
248 let vc = self.hnsw.search(query_embedding, fetch_k);
249 let bc = match query_text {
250 Some(qt) if !self.bm25.is_empty() => self.bm25.search(qt, fetch_k),
251 _ => Vec::new(),
252 };
253 (vc, bc)
254 };
255
256 if vector_candidates.is_empty() && bm25_candidates.is_empty() {
257 return Vec::new();
258 }
259
260 let mut rrf_scores: HashMap<MemoryId, f32> = HashMap::new();
262
263 for (rank, (id, _)) in vector_candidates.iter().enumerate() {
264 *rrf_scores.entry(*id).or_insert(0.0) += 1.0 / (rrf_k + rank as f32);
265 }
266 for (rank, (id, _)) in bm25_candidates.iter().enumerate() {
267 *rrf_scores.entry(*id).or_insert(0.0) += 1.0 / (rrf_k + rank as f32);
268 }
269
270 let mut scored: Vec<(MemoryId, f32)> = rrf_scores
272 .into_iter()
273 .filter(|(id, _)| {
274 if !use_prefilter {
275 if let Some(ref tf) = tag_filter
276 && !tf.contains(id)
277 {
278 return false;
279 }
280 if let Some(ref trf) = time_filter
281 && !trf.contains(id)
282 {
283 return false;
284 }
285 }
286 true
287 })
288 .map(|(id, rrf_score)| {
289 let salience = self.salience.get_salience(id).unwrap_or(0.5);
290 let recency = 0.5f32;
291
292 let combined = rrf_score * 0.7 + salience * 0.05 + recency * 0.02;
293 (id, combined)
294 })
295 .collect();
296
297 scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
298 scored.truncate(k);
299 scored
300 }
301}
302
303impl Default for IndexManager {
304 fn default() -> Self {
305 Self::new(IndexManagerConfig::default())
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use mentedb_core::memory::MemoryType;
313 use mentedb_core::types::AgentId;
314
315 fn make_node(
316 embedding: Vec<f32>,
317 tags: Vec<String>,
318 salience: f32,
319 created_at: u64,
320 ) -> MemoryNode {
321 let mut node = MemoryNode::new(
322 AgentId::new(),
323 MemoryType::Episodic,
324 "test".into(),
325 embedding,
326 );
327 node.tags = tags;
328 node.salience = salience;
329 node.created_at = created_at;
330 node
331 }
332
333 #[test]
334 fn test_index_and_search() {
335 let mgr = IndexManager::default();
336 let node = make_node(vec![1.0, 0.0, 0.0, 0.0], vec!["test".into()], 0.8, 1000);
337 mgr.index_memory(&node);
338
339 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], None, None, 1);
340 assert_eq!(results.len(), 1);
341 assert_eq!(results[0].0, node.id);
342 }
343
344 #[test]
345 fn test_tag_filter() {
346 let mgr = IndexManager::default();
347 let a = make_node(vec![1.0, 0.0, 0.0, 0.0], vec!["alpha".into()], 0.8, 1000);
348 let b = make_node(vec![0.9, 0.1, 0.0, 0.0], vec!["beta".into()], 0.8, 1000);
349 mgr.index_memory(&a);
350 mgr.index_memory(&b);
351
352 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], Some(&["alpha"]), None, 10);
353 assert_eq!(results.len(), 1);
354 assert_eq!(results[0].0, a.id);
355 }
356
357 #[test]
358 fn test_time_filter() {
359 let mgr = IndexManager::default();
360 let a = make_node(vec![1.0, 0.0, 0.0, 0.0], vec![], 0.8, 100);
361 let b = make_node(vec![0.9, 0.1, 0.0, 0.0], vec![], 0.8, 500);
362 mgr.index_memory(&a);
363 mgr.index_memory(&b);
364
365 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], None, Some((400, 600)), 10);
366 assert_eq!(results.len(), 1);
367 assert_eq!(results[0].0, b.id);
368 }
369
370 #[test]
371 fn test_remove_memory() {
372 let mgr = IndexManager::default();
373 let node = make_node(vec![1.0, 0.0, 0.0, 0.0], vec!["t".into()], 0.5, 100);
374 let id = node.id;
375 mgr.index_memory(&node);
376 mgr.remove_memory(id, &node);
377
378 let results = mgr.hybrid_search(&[1.0, 0.0, 0.0, 0.0], None, None, 10);
379 assert!(results.is_empty());
380 }
381
382 #[test]
383 fn test_empty_search() {
384 let mgr = IndexManager::default();
385 let results = mgr.hybrid_search(&[1.0, 0.0], None, None, 5);
386 assert!(results.is_empty());
387 }
388}