Skip to main content

rust_rule_engine/rete/
alpha_memory_index.rs

1//! Alpha Memory Indexing
2//!
3//! Provides O(1) hash-based indexing for alpha node fact filtering.
4//! Complements Beta Memory Indexing for complete RETE optimization.
5//!
6//! **Performance**: 10-100x speedup vs linear scan
7
8use super::facts::{FactValue, TypedFacts};
9use std::collections::HashMap;
10
11/// Alpha Memory with automatic indexing for O(1) fact lookups
12#[derive(Debug, Clone)]
13pub struct AlphaMemoryIndex {
14    /// All facts stored sequentially
15    facts: Vec<TypedFacts>,
16
17    /// Indexes: field → (value → [fact indices])
18    /// Example: "status" → { "active" → [0, 5, 12], "pending" → [1, 3] }
19    indexes: HashMap<String, HashMap<String, Vec<usize>>>,
20
21    /// Statistics for index effectiveness
22    stats: IndexStats,
23}
24
25/// Statistics for index effectiveness and auto-tuning
26#[derive(Debug, Clone, Default)]
27pub struct IndexStats {
28    /// How many times each field was queried
29    query_counts: HashMap<String, usize>,
30
31    /// Total queries performed
32    total_queries: usize,
33
34    /// Total indexed lookups (O(1))
35    indexed_lookups: usize,
36
37    /// Total linear scans (O(n))
38    linear_scans: usize,
39}
40
41impl AlphaMemoryIndex {
42    /// Create new alpha memory index
43    pub fn new() -> Self {
44        Self {
45            facts: Vec::new(),
46            indexes: HashMap::new(),
47            stats: IndexStats::default(),
48        }
49    }
50
51    /// Insert fact and update indexes
52    pub fn insert(&mut self, fact: TypedFacts) -> usize {
53        let idx = self.facts.len();
54
55        // Update all existing indexes
56        for (field_name, index) in &mut self.indexes {
57            if let Some(value) = fact.get(field_name) {
58                let key = format!("{:?}", value);
59                index.entry(key).or_insert_with(Vec::new).push(idx);
60            }
61        }
62
63        self.facts.push(fact);
64        idx
65    }
66
67    /// Filter facts by field value
68    /// Uses index if available (O(1)), otherwise falls back to linear scan (O(n))
69    pub fn filter(&self, field: &str, value: &FactValue) -> Vec<&TypedFacts> {
70        // Try index lookup first
71        if let Some(index) = self.indexes.get(field) {
72            let key = format!("{:?}", value);
73
74            if let Some(indices) = index.get(&key) {
75                return indices.iter().map(|&i| &self.facts[i]).collect();
76            } else {
77                return Vec::new();
78            }
79        }
80
81        // Fallback to linear scan
82        self.facts
83            .iter()
84            .filter(|f| f.get(field) == Some(value))
85            .collect()
86    }
87
88    /// Filter facts and update statistics (mutable version for tracking)
89    pub fn filter_tracked(&mut self, field: &str, value: &FactValue) -> Vec<&TypedFacts> {
90        self.stats.total_queries += 1;
91        *self
92            .stats
93            .query_counts
94            .entry(field.to_string())
95            .or_insert(0) += 1;
96
97        // Try index lookup first
98        if let Some(index) = self.indexes.get(field) {
99            let key = format!("{:?}", value);
100            self.stats.indexed_lookups += 1;
101
102            if let Some(indices) = index.get(&key) {
103                return indices.iter().map(|&i| &self.facts[i]).collect();
104            } else {
105                return Vec::new();
106            }
107        }
108
109        // Fallback to linear scan
110        self.stats.linear_scans += 1;
111        self.facts
112            .iter()
113            .filter(|f| f.get(field) == Some(value))
114            .collect()
115    }
116
117    /// Create index on a field
118    pub fn create_index(&mut self, field: String) {
119        if self.indexes.contains_key(&field) {
120            return; // Already indexed
121        }
122
123        let mut index = HashMap::new();
124
125        // Build index from existing facts
126        for (idx, fact) in self.facts.iter().enumerate() {
127            if let Some(value) = fact.get(&field) {
128                let key = format!("{:?}", value);
129                index.entry(key).or_insert_with(Vec::new).push(idx);
130            }
131        }
132
133        self.indexes.insert(field, index);
134    }
135
136    /// Remove index from a field
137    pub fn drop_index(&mut self, field: &str) {
138        self.indexes.remove(field);
139    }
140
141    /// Get all facts
142    pub fn get_all(&self) -> &[TypedFacts] {
143        &self.facts
144    }
145
146    /// Get fact by index
147    pub fn get(&self, idx: usize) -> Option<&TypedFacts> {
148        self.facts.get(idx)
149    }
150
151    /// Get number of facts
152    pub fn len(&self) -> usize {
153        self.facts.len()
154    }
155
156    /// Check if empty
157    pub fn is_empty(&self) -> bool {
158        self.facts.is_empty()
159    }
160
161    /// Get indexed fields
162    pub fn indexed_fields(&self) -> Vec<&String> {
163        self.indexes.keys().collect()
164    }
165
166    /// Get statistics
167    pub fn stats(&self) -> &IndexStats {
168        &self.stats
169    }
170
171    /// Auto-detect which fields to index based on query patterns
172    /// Indexes fields that are:
173    /// 1. Frequently queried (>50 queries)
174    /// 2. Not already indexed
175    pub fn auto_tune(&mut self) {
176        let fields_to_index: Vec<String> = self
177            .stats
178            .query_counts
179            .iter()
180            .filter(|(field, &count)| count > 50 && !self.indexes.contains_key(*field))
181            .map(|(field, _)| field.clone())
182            .collect();
183
184        for field in fields_to_index {
185            self.create_index(field);
186        }
187    }
188
189    /// Clear all facts and indexes
190    pub fn clear(&mut self) {
191        self.facts.clear();
192        self.indexes.clear();
193        self.stats = IndexStats::default();
194    }
195}
196
197impl Default for AlphaMemoryIndex {
198    fn default() -> Self {
199        Self::new()
200    }
201}
202
203impl IndexStats {
204    /// Get speedup ratio (indexed vs linear)
205    pub fn speedup_ratio(&self) -> f64 {
206        if self.linear_scans == 0 {
207            return 1.0;
208        }
209
210        let indexed_ratio = self.indexed_lookups as f64 / self.total_queries as f64;
211        let linear_ratio = self.linear_scans as f64 / self.total_queries as f64;
212
213        if linear_ratio > 0.0 {
214            indexed_ratio / linear_ratio
215        } else {
216            1.0
217        }
218    }
219
220    /// Get percentage of queries using index
221    pub fn index_hit_rate(&self) -> f64 {
222        if self.total_queries == 0 {
223            return 0.0;
224        }
225        (self.indexed_lookups as f64 / self.total_queries as f64) * 100.0
226    }
227}
228
229impl std::fmt::Display for IndexStats {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        write!(
232            f,
233            "Alpha Index Stats:\n\
234             - Total queries: {}\n\
235             - Indexed lookups: {} ({:.1}%)\n\
236             - Linear scans: {} ({:.1}%)\n\
237             - Most queried fields: {:?}",
238            self.total_queries,
239            self.indexed_lookups,
240            self.index_hit_rate(),
241            self.linear_scans,
242            (100.0 - self.index_hit_rate()),
243            self.query_counts
244                .iter()
245                .take(5)
246                .map(|(k, v)| format!("{}({})", k, v))
247                .collect::<Vec<_>>()
248        )
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_basic_index() {
258        let mut mem = AlphaMemoryIndex::new();
259        mem.create_index("status".to_string());
260
261        // Insert 100 facts
262        for i in 0..100 {
263            let mut fact = TypedFacts::new();
264            fact.set("id", i as i64);
265            fact.set("status", if i % 10 == 0 { "active" } else { "pending" });
266            mem.insert(fact);
267        }
268
269        // Query: should use index
270        let active = mem.filter("status", &FactValue::String("active".to_string()));
271        assert_eq!(active.len(), 10); // 10% are active
272    }
273
274    #[test]
275    fn test_filter_without_index() {
276        let mut mem = AlphaMemoryIndex::new();
277
278        // Insert facts without creating index
279        for i in 0..50 {
280            let mut fact = TypedFacts::new();
281            fact.set("score", i as i64);
282            mem.insert(fact);
283        }
284
285        // Query without index - should use linear scan
286        let results = mem.filter("score", &FactValue::Integer(25));
287        assert_eq!(results.len(), 1);
288    }
289
290    #[test]
291    fn test_auto_tune() {
292        let mut mem = AlphaMemoryIndex::new();
293
294        // Insert facts
295        for i in 0..100 {
296            let mut fact = TypedFacts::new();
297            fact.set("category", if i % 5 == 0 { "A" } else { "B" });
298            mem.insert(fact);
299        }
300
301        // Query many times (>50) to trigger auto-tuning
302        for _ in 0..60 {
303            let _ = mem.filter_tracked("category", &FactValue::String("A".to_string()));
304        }
305
306        // Should still be using linear scan
307        assert_eq!(mem.indexed_fields().len(), 0);
308
309        // Auto-tune should create index
310        mem.auto_tune();
311        assert_eq!(mem.indexed_fields().len(), 1);
312
313        // Next query should use index
314        let _ = mem.filter("category", &FactValue::String("A".to_string()));
315    }
316
317    #[test]
318    fn test_multiple_indexes() {
319        let mut mem = AlphaMemoryIndex::new();
320        mem.create_index("status".to_string());
321        mem.create_index("priority".to_string());
322
323        for i in 0..50 {
324            let mut fact = TypedFacts::new();
325            fact.set("status", if i % 2 == 0 { "active" } else { "inactive" });
326            fact.set("priority", if i % 3 == 0 { "high" } else { "low" });
327            mem.insert(fact);
328        }
329
330        let active = mem.filter("status", &FactValue::String("active".to_string()));
331        assert_eq!(active.len(), 25);
332
333        let high_priority = mem.filter("priority", &FactValue::String("high".to_string()));
334        assert_eq!(high_priority.len(), 17); // 50/3 rounded up
335    }
336}