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    pub fn new() -> Self {
32        Self {
33            inner: RwLock::new(BitmapInner {
34                tag_bitmaps: HashMap::default(),
35                id_to_offset: HashMap::default(),
36                offset_to_id: Vec::new(),
37            }),
38        }
39    }
40
41    /// Get or create the internal u32 offset for a MemoryId.
42    fn ensure_offset(inner: &mut BitmapInner, id: MemoryId) -> u32 {
43        if let Some(&offset) = inner.id_to_offset.get(&id) {
44            return offset;
45        }
46        let offset = inner.offset_to_id.len() as u32;
47        inner.id_to_offset.insert(id, offset);
48        inner.offset_to_id.push(id);
49        offset
50    }
51
52    /// Add a tag for the given memory id.
53    pub fn add_tag(&self, id: MemoryId, tag: &str) {
54        let mut inner = self.inner.write();
55        let offset = Self::ensure_offset(&mut inner, id);
56        inner
57            .tag_bitmaps
58            .entry(tag.to_string())
59            .or_default()
60            .insert(offset);
61    }
62
63    /// Remove a tag for the given memory id.
64    pub fn remove_tag(&self, id: MemoryId, tag: &str) {
65        let mut inner = self.inner.write();
66        if let Some(&offset) = inner.id_to_offset.get(&id)
67            && let Some(bm) = inner.tag_bitmaps.get_mut(tag)
68        {
69            bm.remove(offset);
70        }
71    }
72
73    /// Get all memory ids that have the given tag.
74    pub fn query_tag(&self, tag: &str) -> Vec<MemoryId> {
75        let inner = self.inner.read();
76        match inner.tag_bitmaps.get(tag) {
77            Some(bm) => bm
78                .iter()
79                .filter_map(|offset| inner.offset_to_id.get(offset as usize).copied())
80                .collect(),
81            None => Vec::new(),
82        }
83    }
84
85    /// Get memory ids that have ALL given tags (intersection).
86    pub fn query_tags_and(&self, tags: &[&str]) -> Vec<MemoryId> {
87        let inner = self.inner.read();
88        if tags.is_empty() {
89            return Vec::new();
90        }
91
92        let mut result: Option<RoaringBitmap> = None;
93        for tag in tags {
94            match inner.tag_bitmaps.get(*tag) {
95                Some(bm) => {
96                    result = Some(match result {
97                        Some(r) => r & bm,
98                        None => bm.clone(),
99                    });
100                }
101                None => return Vec::new(),
102            }
103        }
104
105        match result {
106            Some(bm) => bm
107                .iter()
108                .filter_map(|offset| inner.offset_to_id.get(offset as usize).copied())
109                .collect(),
110            None => Vec::new(),
111        }
112    }
113
114    /// Get memory ids that have ANY of the given tags (union).
115    pub fn query_tags_or(&self, tags: &[&str]) -> Vec<MemoryId> {
116        let inner = self.inner.read();
117        if tags.is_empty() {
118            return Vec::new();
119        }
120
121        let mut result = RoaringBitmap::new();
122        for tag in tags {
123            if let Some(bm) = inner.tag_bitmaps.get(*tag) {
124                result |= bm;
125            }
126        }
127
128        result
129            .iter()
130            .filter_map(|offset| inner.offset_to_id.get(offset as usize).copied())
131            .collect()
132    }
133
134    /// Remove all tags for a given memory id.
135    pub fn remove_all(&self, id: MemoryId) {
136        let mut inner = self.inner.write();
137        if let Some(&offset) = inner.id_to_offset.get(&id) {
138            for bm in inner.tag_bitmaps.values_mut() {
139                bm.remove(offset);
140            }
141        }
142    }
143}
144
145/// Serializable snapshot of the bitmap index data.
146#[derive(Serialize, Deserialize)]
147struct BitmapSnapshot {
148    /// Tag name → serialized RoaringBitmap bytes.
149    tag_bitmaps: Vec<(String, Vec<u8>)>,
150    id_to_offset: Vec<(MemoryId, u32)>,
151    offset_to_id: Vec<MemoryId>,
152}
153
154impl BitmapIndex {
155    /// Save the bitmap index to a JSON file.
156    pub fn save(&self, path: &std::path::Path) -> MenteResult<()> {
157        let inner = self.inner.read();
158        let mut tag_bitmaps = Vec::new();
159        for (tag, bm) in &inner.tag_bitmaps {
160            let mut buf = Vec::new();
161            bm.serialize_into(&mut buf)
162                .map_err(|e| MenteError::Serialization(e.to_string()))?;
163            tag_bitmaps.push((tag.clone(), buf));
164        }
165        let snapshot = BitmapSnapshot {
166            tag_bitmaps,
167            id_to_offset: inner.id_to_offset.iter().map(|(&k, &v)| (k, v)).collect(),
168            offset_to_id: inner.offset_to_id.clone(),
169        };
170        let data =
171            serde_json::to_vec(&snapshot).map_err(|e| MenteError::Serialization(e.to_string()))?;
172        std::fs::write(path, data)?;
173        Ok(())
174    }
175
176    /// Load the bitmap index from a JSON file.
177    pub fn load(path: &std::path::Path) -> MenteResult<Self> {
178        let data = std::fs::read(path)?;
179        let snapshot: BitmapSnapshot =
180            serde_json::from_slice(&data).map_err(|e| MenteError::Serialization(e.to_string()))?;
181
182        let mut tag_bitmaps = HashMap::default();
183        for (tag, bytes) in snapshot.tag_bitmaps {
184            let bm = RoaringBitmap::deserialize_from(&mut Cursor::new(bytes))
185                .map_err(|e| MenteError::Serialization(e.to_string()))?;
186            tag_bitmaps.insert(tag, bm);
187        }
188
189        let mut id_to_offset = HashMap::default();
190        for (id, offset) in snapshot.id_to_offset {
191            id_to_offset.insert(id, offset);
192        }
193
194        Ok(Self {
195            inner: RwLock::new(BitmapInner {
196                tag_bitmaps,
197                id_to_offset,
198                offset_to_id: snapshot.offset_to_id,
199            }),
200        })
201    }
202}
203
204impl Default for BitmapIndex {
205    fn default() -> Self {
206        Self::new()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use uuid::Uuid;
214
215    #[test]
216    fn test_add_and_query_tag() {
217        let idx = BitmapIndex::new();
218        let id = Uuid::new_v4();
219        idx.add_tag(id, "important");
220
221        let results = idx.query_tag("important");
222        assert_eq!(results, vec![id]);
223    }
224
225    #[test]
226    fn test_query_empty_tag() {
227        let idx = BitmapIndex::new();
228        assert!(idx.query_tag("nonexistent").is_empty());
229    }
230
231    #[test]
232    fn test_remove_tag() {
233        let idx = BitmapIndex::new();
234        let id = Uuid::new_v4();
235        idx.add_tag(id, "foo");
236        idx.remove_tag(id, "foo");
237
238        assert!(idx.query_tag("foo").is_empty());
239    }
240
241    #[test]
242    fn test_tags_and() {
243        let idx = BitmapIndex::new();
244        let a = Uuid::new_v4();
245        let b = Uuid::new_v4();
246
247        idx.add_tag(a, "x");
248        idx.add_tag(a, "y");
249        idx.add_tag(b, "x");
250
251        let both = idx.query_tags_and(&["x", "y"]);
252        assert_eq!(both, vec![a]);
253    }
254
255    #[test]
256    fn test_tags_or() {
257        let idx = BitmapIndex::new();
258        let a = Uuid::new_v4();
259        let b = Uuid::new_v4();
260
261        idx.add_tag(a, "x");
262        idx.add_tag(b, "y");
263
264        let either = idx.query_tags_or(&["x", "y"]);
265        assert_eq!(either.len(), 2);
266    }
267
268    #[test]
269    fn test_remove_all() {
270        let idx = BitmapIndex::new();
271        let id = Uuid::new_v4();
272        idx.add_tag(id, "a");
273        idx.add_tag(id, "b");
274        idx.remove_all(id);
275
276        assert!(idx.query_tag("a").is_empty());
277        assert!(idx.query_tag("b").is_empty());
278    }
279}