Skip to main content

mentedb_index/
temporal.rs

1//! BTreeMap-based temporal index for timestamp range queries.
2
3use std::collections::BTreeMap;
4
5use ahash::HashMap;
6use parking_lot::RwLock;
7use serde::{Deserialize, Serialize};
8
9use mentedb_core::error::{MenteError, MenteResult};
10use mentedb_core::types::{MemoryId, Timestamp};
11
12/// Temporal index using a BTreeMap for efficient range queries on timestamps.
13pub struct TemporalIndex {
14    inner: RwLock<TemporalInner>,
15}
16
17struct TemporalInner {
18    /// Timestamp → set of MemoryIds at that timestamp.
19    tree: BTreeMap<Timestamp, Vec<MemoryId>>,
20    /// Reverse lookup for fast removal.
21    id_to_ts: HashMap<MemoryId, Timestamp>,
22}
23
24impl TemporalIndex {
25    pub fn new() -> Self {
26        Self {
27            inner: RwLock::new(TemporalInner {
28                tree: BTreeMap::new(),
29                id_to_ts: HashMap::default(),
30            }),
31        }
32    }
33
34    /// Insert a memory with its timestamp.
35    pub fn insert(&self, id: MemoryId, timestamp: Timestamp) {
36        let mut inner = self.inner.write();
37        inner.tree.entry(timestamp).or_default().push(id);
38        inner.id_to_ts.insert(id, timestamp);
39    }
40
41    /// Query memories within [start, end] inclusive.
42    pub fn range(&self, start: Timestamp, end: Timestamp) -> Vec<MemoryId> {
43        let inner = self.inner.read();
44        inner
45            .tree
46            .range(start..=end)
47            .flat_map(|(_, ids)| ids.iter().copied())
48            .collect()
49    }
50
51    /// Get the `n` most recent memories (by timestamp, descending).
52    pub fn latest(&self, n: usize) -> Vec<MemoryId> {
53        let inner = self.inner.read();
54        let mut results = Vec::with_capacity(n);
55        for (_, ids) in inner.tree.iter().rev() {
56            for &id in ids.iter().rev() {
57                results.push(id);
58                if results.len() >= n {
59                    return results;
60                }
61            }
62        }
63        results
64    }
65
66    /// Remove a memory by id and its known timestamp.
67    pub fn remove(&self, id: MemoryId, timestamp: Timestamp) {
68        let mut inner = self.inner.write();
69        if let Some(ids) = inner.tree.get_mut(&timestamp) {
70            ids.retain(|&i| i != id);
71            if ids.is_empty() {
72                inner.tree.remove(&timestamp);
73            }
74        }
75        inner.id_to_ts.remove(&id);
76    }
77
78    /// Remove a memory by id (looks up the timestamp internally).
79    pub fn remove_by_id(&self, id: MemoryId) {
80        let mut inner = self.inner.write();
81        if let Some(ts) = inner.id_to_ts.remove(&id)
82            && let Some(ids) = inner.tree.get_mut(&ts)
83        {
84            ids.retain(|&i| i != id);
85            if ids.is_empty() {
86                inner.tree.remove(&ts);
87            }
88        }
89    }
90
91    /// Get the timestamp for a given memory id (if indexed).
92    pub fn get_timestamp(&self, id: MemoryId) -> Option<Timestamp> {
93        let inner = self.inner.read();
94        inner.id_to_ts.get(&id).copied()
95    }
96}
97
98/// Serializable snapshot of the temporal index data.
99#[derive(Serialize, Deserialize)]
100struct TemporalSnapshot {
101    tree: Vec<(Timestamp, Vec<MemoryId>)>,
102    id_to_ts: Vec<(MemoryId, Timestamp)>,
103}
104
105impl TemporalIndex {
106    /// Save the temporal index to a JSON file.
107    pub fn save(&self, path: &std::path::Path) -> MenteResult<()> {
108        let inner = self.inner.read();
109        let snapshot = TemporalSnapshot {
110            tree: inner.tree.iter().map(|(&k, v)| (k, v.clone())).collect(),
111            id_to_ts: inner.id_to_ts.iter().map(|(&k, &v)| (k, v)).collect(),
112        };
113        let data =
114            serde_json::to_vec(&snapshot).map_err(|e| MenteError::Serialization(e.to_string()))?;
115        std::fs::write(path, data)?;
116        Ok(())
117    }
118
119    /// Load the temporal index from a JSON file.
120    pub fn load(path: &std::path::Path) -> MenteResult<Self> {
121        let data = std::fs::read(path)?;
122        let snapshot: TemporalSnapshot =
123            serde_json::from_slice(&data).map_err(|e| MenteError::Serialization(e.to_string()))?;
124
125        let mut tree = BTreeMap::new();
126        for (ts, ids) in snapshot.tree {
127            tree.insert(ts, ids);
128        }
129
130        let mut id_to_ts = HashMap::default();
131        for (id, ts) in snapshot.id_to_ts {
132            id_to_ts.insert(id, ts);
133        }
134
135        Ok(Self {
136            inner: RwLock::new(TemporalInner { tree, id_to_ts }),
137        })
138    }
139}
140
141impl Default for TemporalIndex {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use uuid::Uuid;
151
152    #[test]
153    fn test_insert_and_range() {
154        let idx = TemporalIndex::new();
155        let a = Uuid::new_v4();
156        let b = Uuid::new_v4();
157        let c = Uuid::new_v4();
158
159        idx.insert(a, 100);
160        idx.insert(b, 200);
161        idx.insert(c, 300);
162
163        let result = idx.range(100, 200);
164        assert_eq!(result.len(), 2);
165        assert!(result.contains(&a));
166        assert!(result.contains(&b));
167    }
168
169    #[test]
170    fn test_latest() {
171        let idx = TemporalIndex::new();
172        let a = Uuid::new_v4();
173        let b = Uuid::new_v4();
174        let c = Uuid::new_v4();
175
176        idx.insert(a, 100);
177        idx.insert(b, 200);
178        idx.insert(c, 300);
179
180        let latest = idx.latest(2);
181        assert_eq!(latest.len(), 2);
182        assert_eq!(latest[0], c);
183        assert_eq!(latest[1], b);
184    }
185
186    #[test]
187    fn test_remove() {
188        let idx = TemporalIndex::new();
189        let a = Uuid::new_v4();
190        idx.insert(a, 100);
191        idx.remove(a, 100);
192
193        assert!(idx.range(0, 1000).is_empty());
194    }
195
196    #[test]
197    fn test_remove_by_id() {
198        let idx = TemporalIndex::new();
199        let a = Uuid::new_v4();
200        idx.insert(a, 500);
201        idx.remove_by_id(a);
202
203        assert!(idx.range(0, 1000).is_empty());
204    }
205
206    #[test]
207    fn test_empty_range() {
208        let idx = TemporalIndex::new();
209        assert!(idx.range(0, 100).is_empty());
210    }
211}