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