Skip to main content

vantadb/
engine.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use parking_lot::{Mutex, RwLock};
6
7use crate::error::{Result, VantaError};
8use crate::node::{FieldValue, UnifiedNode, VectorRepresentations};
9use crate::wal::{WalReader, WalRecord, WalWriter};
10
11// ─── Query Result ──────────────────────────────────────────
12
13/// How the result was produced
14#[derive(Debug, Clone)]
15pub enum SourceType {
16    FullScan,
17    BitsetFilter,
18    VectorSearch,
19    GraphTraversal,
20    Hybrid,
21}
22
23/// Query result with exhaustivity metadata
24#[derive(Debug, Clone)]
25pub struct QueryResult {
26    pub nodes: Vec<UnifiedNode>,
27    /// true if resource limits truncated results
28    pub is_partial: bool,
29    /// 0.0-1.0 search completeness
30    pub exhaustivity: f32,
31    /// which index/scan was used
32    pub source_type: SourceType,
33}
34
35/// Engine statistics snapshot
36#[derive(Debug, Clone, Default)]
37pub struct EngineStats {
38    pub node_count: u64,
39    pub edge_count: u64,
40    pub vector_count: u64,
41    pub total_dimensions: u64,
42    pub memory_estimate_bytes: u64,
43}
44
45// ─── In-Memory Engine ──────────────────────────────────────
46
47/// Fase 1 storage engine: HashMap + optional WAL.
48///
49/// Thread-safe: RwLock for reads, Mutex for WAL writes.
50/// Fase 2: Replace HashMap with RocksDB-backed MemTable.
51pub struct InMemoryEngine {
52    nodes: RwLock<HashMap<u64, UnifiedNode>>,
53    wal: Mutex<Option<WalWriter>>,
54    next_id: AtomicU64,
55    #[allow(dead_code)]
56    wal_path: Option<PathBuf>,
57}
58
59impl InMemoryEngine {
60    /// Create engine (in-memory only, no persistence)
61    pub fn new() -> Self {
62        Self {
63            nodes: RwLock::new(HashMap::with_capacity(1024)),
64            wal: Mutex::new(None),
65            next_id: AtomicU64::new(1),
66            wal_path: None,
67        }
68    }
69
70    /// Create engine with WAL durability. Replays existing WAL on open.
71    pub fn with_wal(wal_path: impl AsRef<Path>) -> Result<Self> {
72        let path = wal_path.as_ref().to_path_buf();
73        let mut nodes_map = HashMap::with_capacity(1024);
74        let mut max_id: u64 = 0;
75
76        // Replay existing WAL
77        if path.exists() {
78            let mut reader = WalReader::open(&path)?;
79            reader.replay_all(|record| {
80                match record {
81                    WalRecord::Insert(node) => {
82                        max_id = max_id.max(node.id);
83                        nodes_map.insert(node.id, node);
84                    }
85                    WalRecord::Update { id, node } => {
86                        max_id = max_id.max(id);
87                        nodes_map.insert(id, node);
88                    }
89                    WalRecord::Delete { id } => {
90                        nodes_map.remove(&id);
91                    }
92                    WalRecord::Checkpoint { .. } => {}
93                }
94                Ok(())
95            })?;
96        }
97
98        let writer = WalWriter::open(&path, crate::config::SyncMode::Periodic)?;
99
100        Ok(Self {
101            nodes: RwLock::new(nodes_map),
102            wal: Mutex::new(Some(writer)),
103            next_id: AtomicU64::new(max_id + 1),
104            wal_path: Some(path),
105        })
106    }
107
108    /// Generate next unique node ID
109    pub fn next_id(&self) -> u64 {
110        self.next_id.fetch_add(1, Ordering::SeqCst)
111    }
112
113    /// Insert a node. Auto-assigns ID if node.id == 0.
114    pub fn insert(&self, mut node: UnifiedNode) -> Result<u64> {
115        if node.id == 0 {
116            node.id = self.next_id();
117        }
118        let id = node.id;
119
120        // WAL first (durability before visibility)
121        if let Some(ref mut wal) = *self.wal.lock() {
122            wal.append(&WalRecord::Insert(node.clone()))?;
123        }
124
125        let mut nodes = self.nodes.write();
126        if nodes.contains_key(&id) {
127            return Err(VantaError::DuplicateNode(id));
128        }
129        nodes.insert(id, node);
130        Ok(id)
131    }
132
133    /// Get a node by ID (cloned)
134    pub fn get(&self, id: u64) -> Option<UnifiedNode> {
135        self.nodes.read().get(&id).cloned()
136    }
137
138    /// Check if node exists
139    pub fn contains(&self, id: u64) -> bool {
140        self.nodes.read().contains_key(&id)
141    }
142
143    /// Update existing node
144    pub fn update(&self, id: u64, node: UnifiedNode) -> Result<()> {
145        if let Some(ref mut wal) = *self.wal.lock() {
146            wal.append(&WalRecord::Update {
147                id,
148                node: node.clone(),
149            })?;
150        }
151        let mut nodes = self.nodes.write();
152        if !nodes.contains_key(&id) {
153            return Err(VantaError::NodeNotFound(id));
154        }
155        nodes.insert(id, node);
156        Ok(())
157    }
158
159    /// Delete a node
160    pub fn delete(&self, id: u64) -> Result<()> {
161        if let Some(ref mut wal) = *self.wal.lock() {
162            wal.append(&WalRecord::Delete { id })?;
163        }
164        let mut nodes = self.nodes.write();
165        if nodes.remove(&id).is_none() {
166            return Err(VantaError::NodeNotFound(id));
167        }
168        Ok(())
169    }
170
171    /// Scan nodes matching a bitset mask (all bits in mask must be set)
172    pub fn scan_bitset(&self, mask: u128) -> Vec<u64> {
173        self.nodes
174            .read()
175            .values()
176            .filter(|n| n.is_alive() && n.matches_mask(mask))
177            .map(|n| n.id)
178            .collect()
179    }
180
181    /// Brute-force vector similarity search.
182    /// Fase 3: Replace with CP-Index HNSW for O(log n).
183    pub fn vector_search(
184        &self,
185        query: &[f32],
186        top_k: usize,
187        min_score: f32,
188        bitset_filter: Option<u128>,
189    ) -> QueryResult {
190        let query_vec = VectorRepresentations::Full(query.to_vec());
191        let nodes = self.nodes.read();
192
193        let mut scored: Vec<(u64, f32)> = nodes
194            .values()
195            .filter(|n| {
196                n.is_alive()
197                    && !n.vector.is_none()
198                    && bitset_filter.is_none_or(|m| n.matches_mask(m))
199            })
200            .filter_map(|n| {
201                n.vector
202                    .cosine_similarity(&query_vec)
203                    .filter(|&s| s >= min_score)
204                    .map(|s| (n.id, s))
205            })
206            .collect();
207
208        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
209        scored.truncate(top_k);
210
211        let result_nodes: Vec<UnifiedNode> = scored
212            .iter()
213            .filter_map(|(id, _)| nodes.get(id).cloned())
214            .collect();
215
216        QueryResult {
217            nodes: result_nodes,
218            is_partial: false,
219            exhaustivity: 1.0, // brute-force = exhaustive
220            source_type: if bitset_filter.is_some() {
221                SourceType::Hybrid
222            } else {
223                SourceType::VectorSearch
224            },
225        }
226    }
227
228    /// BFS graph traversal from start, following edges with matching label.
229    /// Returns (node_id, depth) pairs within [min_depth, max_depth].
230    pub fn traverse(
231        &self,
232        start: u64,
233        label: &str,
234        min_depth: u32,
235        max_depth: u32,
236    ) -> Result<Vec<(u64, u32)>> {
237        let nodes = self.nodes.read();
238        if !nodes.contains_key(&start) {
239            return Err(VantaError::NodeNotFound(start));
240        }
241
242        let mut visited = HashMap::new();
243        let mut queue = std::collections::VecDeque::new();
244        queue.push_back((start, 0u32));
245        visited.insert(start, 0u32);
246
247        let mut results = Vec::new();
248
249        while let Some((current_id, depth)) = queue.pop_front() {
250            if depth >= max_depth {
251                continue;
252            }
253            if let Some(node) = nodes.get(&current_id) {
254                for edge in &node.edges {
255                    if edge.label == label {
256                        if let std::collections::hash_map::Entry::Vacant(e) =
257                            visited.entry(edge.target)
258                        {
259                            let next_depth = depth + 1;
260                            e.insert(next_depth);
261                            if next_depth >= min_depth {
262                                results.push((edge.target, next_depth));
263                            }
264                            queue.push_back((edge.target, next_depth));
265                        }
266                    }
267                }
268            }
269        }
270        Ok(results)
271    }
272
273    /// Filter nodes by relational field equality
274    pub fn filter_field(&self, field: &str, value: &FieldValue) -> Vec<u64> {
275        self.nodes
276            .read()
277            .values()
278            .filter(|n| n.is_alive() && n.get_field(field) == Some(value))
279            .map(|n| n.id)
280            .collect()
281    }
282
283    /// Hybrid search: vector similarity + bitset filter + field predicates.
284    /// Evaluates filters in cost order: bitset → relational → vector.
285    pub fn hybrid_search(
286        &self,
287        query_vector: &[f32],
288        top_k: usize,
289        min_score: f32,
290        bitset_mask: Option<u128>,
291        field_filters: &[(String, FieldValue)],
292    ) -> QueryResult {
293        let query_vec = VectorRepresentations::Full(query_vector.to_vec());
294        let nodes = self.nodes.read();
295
296        let mut scored: Vec<(u64, f32)> = nodes
297            .values()
298            .filter(|n| {
299                if !n.is_alive() || n.vector.is_none() {
300                    return false;
301                }
302                // Bitset first (cheapest: single AND)
303                if let Some(mask) = bitset_mask {
304                    if !n.matches_mask(mask) {
305                        return false;
306                    }
307                }
308                // Relational second
309                for (field, value) in field_filters {
310                    if n.get_field(field) != Some(value) {
311                        return false;
312                    }
313                }
314                true
315            })
316            .filter_map(|n| {
317                n.vector
318                    .cosine_similarity(&query_vec)
319                    .filter(|&s| s >= min_score)
320                    .map(|s| (n.id, s))
321            })
322            .collect();
323
324        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
325        scored.truncate(top_k);
326
327        let result_nodes = scored
328            .iter()
329            .filter_map(|(id, _)| nodes.get(id).cloned())
330            .collect();
331
332        QueryResult {
333            nodes: result_nodes,
334            is_partial: false,
335            exhaustivity: 1.0,
336            source_type: SourceType::Hybrid,
337        }
338    }
339
340    /// Flush WAL to disk
341    pub fn flush_wal(&self) -> Result<()> {
342        if let Some(ref mut wal) = *self.wal.lock() {
343            wal.sync()?;
344        }
345        Ok(())
346    }
347
348    /// Total number of alive nodes
349    pub fn node_count(&self) -> usize {
350        self.nodes.read().values().filter(|n| n.is_alive()).count()
351    }
352
353    /// Get engine statistics
354    pub fn stats(&self) -> EngineStats {
355        let nodes = self.nodes.read();
356        let mut stats = EngineStats::default();
357        for node in nodes.values() {
358            if !node.is_alive() {
359                continue;
360            }
361            stats.node_count += 1;
362            stats.edge_count += node.edges.len() as u64;
363            if !node.vector.is_none() {
364                stats.vector_count += 1;
365                stats.total_dimensions += node.vector.dimensions() as u64;
366            }
367            stats.memory_estimate_bytes += node.memory_size() as u64;
368        }
369        stats
370    }
371}
372
373impl Default for InMemoryEngine {
374    fn default() -> Self {
375        Self::new()
376    }
377}