Skip to main content

mentedb_index/
bitmap.rs

1//! Roaring bitmap tag index for fast set-based tag filtering.
2
3use std::io::Cursor;
4
5use ahash::HashMap;
6use parking_lot::RwLock;
7use roaring::RoaringBitmap;
8use serde::{Deserialize, Serialize};
9
10use mentedb_core::error::{MenteError, MenteResult};
11use mentedb_core::types::MemoryId;
12
13/// Roaring-bitmap-backed tag index.
14///
15/// Maps tag strings to roaring bitmaps for fast AND/OR/NOT set operations.
16/// Uses an internal u32 offset for each MemoryId.
17pub struct BitmapIndex {
18    inner: RwLock<BitmapInner>,
19}
20
21struct BitmapInner {
22    /// Tag name → bitmap of u32 offsets.
23    tag_bitmaps: HashMap<String, RoaringBitmap>,
24    /// MemoryId → internal u32 offset.
25    id_to_offset: HashMap<MemoryId, u32>,
26    /// Reverse: u32 offset → MemoryId.
27    offset_to_id: Vec<MemoryId>,
28}
29
30impl BitmapIndex {
31    /// Creates a new empty bitmap index.
32    pub fn new() -> Self {
33        Self {
34            inner: RwLock::new(BitmapInner {
35                tag_bitmaps: HashMap::default(),
36                id_to_offset: HashMap::default(),
37                offset_to_id: Vec::new(),
38            }),
39        }
40    }
41
42    /// Get or create the internal u32 offset for a MemoryId.
43    fn ensure_offset(inner: &mut BitmapInner, id: MemoryId) -> u32 {
44        if let Some(&offset) = inner.id_to_offset.get(&id) {
45            return offset;
46        }
47        let offset = inner.offset_to_id.len() as u32;
48        inner.id_to_offset.insert(id, offset);
49        inner.offset_to_id.push(id);
50        offset
51    }
52
53    /// Add a tag for the given memory id.
54    pub fn add_tag(&self, id: MemoryId, tag: &str) {
55        let mut inner = self.inner.write();
56        let offset = Self::ensure_offset(&mut inner, id);
57        inner
58            .tag_bitmaps
59            .entry(tag.to_string())
60            .or_default()
61            .insert(offset);
62    }
63
64    /// Remove a tag for the given memory id.
65    pub fn remove_tag(&self, id: MemoryId, tag: &str) {
66        let mut inner = self.inner.write();
67        if let Some(&offset) = inner.id_to_offset.get(&id)
68            && let Some(bm) = inner.tag_bitmaps.get_mut(tag)
69        {
70            bm.remove(offset);
71        }
72    }
73
74    /// Get all memory ids that have the given tag.
75    pub fn query_tag(&self, tag: &str) -> Vec<MemoryId> {
76        let inner = self.inner.read();
77        match inner.tag_bitmaps.get(tag) {
78            Some(bm) => bm
79                .iter()
80                .filter_map(|offset| inner.offset_to_id.get(offset as usize).copied())
81                .collect(),
82            None => Vec::new(),
83        }
84    }
85
86    /// All tags currently indexed that start with the given prefix. Cheap:
87    /// one pass over the tag key set, used to enumerate small controlled
88    /// vocabularies like the `trigger:` namespace.
89    pub fn tags_with_prefix(&self, prefix: &str) -> Vec<String> {
90        let inner = self.inner.read();
91        inner
92            .tag_bitmaps
93            .keys()
94            .filter(|t| t.starts_with(prefix))
95            .cloned()
96            .collect()
97    }
98
99    /// Get memory ids that have ALL given tags (intersection).
100    pub fn query_tags_and(&self, tags: &[&str]) -> Vec<MemoryId> {
101        let inner = self.inner.read();
102        if tags.is_empty() {
103            return Vec::new();
104        }
105
106        let mut result: Option<RoaringBitmap> = None;
107        for tag in tags {
108            match inner.tag_bitmaps.get(*tag) {
109                Some(bm) => {
110                    result = Some(match result {
111                        Some(r) => r & bm,
112                        None => bm.clone(),
113                    });
114                }
115                None => return Vec::new(),
116            }
117        }
118
119        match result {
120            Some(bm) => bm
121                .iter()
122                .filter_map(|offset| inner.offset_to_id.get(offset as usize).copied())
123                .collect(),
124            None => Vec::new(),
125        }
126    }
127
128    /// Get memory ids that have ANY of the given tags (union).
129    pub fn query_tags_or(&self, tags: &[&str]) -> Vec<MemoryId> {
130        let inner = self.inner.read();
131        if tags.is_empty() {
132            return Vec::new();
133        }
134
135        let mut result = RoaringBitmap::new();
136        for tag in tags {
137            if let Some(bm) = inner.tag_bitmaps.get(*tag) {
138                result |= bm;
139            }
140        }
141
142        result
143            .iter()
144            .filter_map(|offset| inner.offset_to_id.get(offset as usize).copied())
145            .collect()
146    }
147
148    /// Remove all tags for a given memory id.
149    pub fn remove_all(&self, id: MemoryId) {
150        let mut inner = self.inner.write();
151        if let Some(&offset) = inner.id_to_offset.get(&id) {
152            for bm in inner.tag_bitmaps.values_mut() {
153                bm.remove(offset);
154            }
155        }
156    }
157}
158
159/// Serializable snapshot of the bitmap index data.
160#[derive(Serialize, Deserialize)]
161struct BitmapSnapshot {
162    /// Tag name → serialized RoaringBitmap bytes.
163    tag_bitmaps: Vec<(String, Vec<u8>)>,
164    id_to_offset: Vec<(MemoryId, u32)>,
165    offset_to_id: Vec<MemoryId>,
166}
167
168impl BitmapIndex {
169    /// Save the bitmap index to a binary file.
170    pub fn save(&self, path: &std::path::Path) -> MenteResult<()> {
171        let inner = self.inner.read();
172        let mut tag_bitmaps = Vec::new();
173        for (tag, bm) in &inner.tag_bitmaps {
174            let mut buf = Vec::new();
175            bm.serialize_into(&mut buf)
176                .map_err(|e| MenteError::Serialization(e.to_string()))?;
177            tag_bitmaps.push((tag.clone(), buf));
178        }
179        let snapshot = BitmapSnapshot {
180            tag_bitmaps,
181            id_to_offset: inner.id_to_offset.iter().map(|(&k, &v)| (k, v)).collect(),
182            offset_to_id: inner.offset_to_id.clone(),
183        };
184        let data =
185            bincode::serialize(&snapshot).map_err(|e| MenteError::Serialization(e.to_string()))?;
186        std::fs::write(path, data)?;
187        Ok(())
188    }
189
190    /// Load the bitmap index from a file (bincode, with JSON fallback for migration).
191    pub fn load(path: &std::path::Path) -> MenteResult<Self> {
192        let data = std::fs::read(path)?;
193        let snapshot: BitmapSnapshot = bincode::deserialize(&data)
194            .or_else(|_| serde_json::from_slice(&data))
195            .map_err(|e| MenteError::Serialization(e.to_string()))?;
196
197        let mut tag_bitmaps = HashMap::default();
198        for (tag, bytes) in snapshot.tag_bitmaps {
199            let bm = RoaringBitmap::deserialize_from(&mut Cursor::new(bytes))
200                .map_err(|e| MenteError::Serialization(e.to_string()))?;
201            tag_bitmaps.insert(tag, bm);
202        }
203
204        let mut id_to_offset = HashMap::default();
205        for (id, offset) in snapshot.id_to_offset {
206            id_to_offset.insert(id, offset);
207        }
208
209        Ok(Self {
210            inner: RwLock::new(BitmapInner {
211                tag_bitmaps,
212                id_to_offset,
213                offset_to_id: snapshot.offset_to_id,
214            }),
215        })
216    }
217}
218
219impl Default for BitmapIndex {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_add_and_query_tag() {
231        let idx = BitmapIndex::new();
232        let id = MemoryId::new();
233        idx.add_tag(id, "important");
234
235        let results = idx.query_tag("important");
236        assert_eq!(results, vec![id]);
237    }
238
239    #[test]
240    fn test_query_empty_tag() {
241        let idx = BitmapIndex::new();
242        assert!(idx.query_tag("nonexistent").is_empty());
243    }
244
245    #[test]
246    fn test_remove_tag() {
247        let idx = BitmapIndex::new();
248        let id = MemoryId::new();
249        idx.add_tag(id, "foo");
250        idx.remove_tag(id, "foo");
251
252        assert!(idx.query_tag("foo").is_empty());
253    }
254
255    #[test]
256    fn test_tags_and() {
257        let idx = BitmapIndex::new();
258        let a = MemoryId::new();
259        let b = MemoryId::new();
260
261        idx.add_tag(a, "x");
262        idx.add_tag(a, "y");
263        idx.add_tag(b, "x");
264
265        let both = idx.query_tags_and(&["x", "y"]);
266        assert_eq!(both, vec![a]);
267    }
268
269    #[test]
270    fn test_tags_or() {
271        let idx = BitmapIndex::new();
272        let a = MemoryId::new();
273        let b = MemoryId::new();
274
275        idx.add_tag(a, "x");
276        idx.add_tag(b, "y");
277
278        let either = idx.query_tags_or(&["x", "y"]);
279        assert_eq!(either.len(), 2);
280    }
281
282    #[test]
283    fn test_remove_all() {
284        let idx = BitmapIndex::new();
285        let id = MemoryId::new();
286        idx.add_tag(id, "a");
287        idx.add_tag(id, "b");
288        idx.remove_all(id);
289
290        assert!(idx.query_tag("a").is_empty());
291        assert!(idx.query_tag("b").is_empty());
292    }
293}