rust-rule-engine 1.20.1

A blazing-fast Rust rule engine with RETE algorithm, backward chaining inference, and GRL (Grule Rule Language) syntax. Features: forward/backward chaining, pattern matching, unification, O(1) rule indexing, TMS, expression evaluation, method calls, streaming with Redis state backend, watermarking, and custom functions. Production-ready for business rules, expert systems, real-time stream processing, and decision automation.
Documentation
//! Alpha Memory Indexing
//!
//! Provides O(1) hash-based indexing for alpha node fact filtering.
//! Complements Beta Memory Indexing for complete RETE optimization.
//!
//! **Performance**: 10-100x speedup vs linear scan

use super::facts::{FactValue, TypedFacts};
use std::collections::HashMap;

/// Alpha Memory with automatic indexing for O(1) fact lookups
#[derive(Debug, Clone)]
pub struct AlphaMemoryIndex {
    /// All facts stored sequentially
    facts: Vec<TypedFacts>,

    /// Indexes: field → (value → [fact indices])
    /// Example: "status" → { "active" → [0, 5, 12], "pending" → [1, 3] }
    indexes: HashMap<String, HashMap<String, Vec<usize>>>,

    /// Statistics for index effectiveness
    stats: IndexStats,
}

/// Statistics for index effectiveness and auto-tuning
#[derive(Debug, Clone, Default)]
pub struct IndexStats {
    /// How many times each field was queried
    query_counts: HashMap<String, usize>,

    /// Total queries performed
    total_queries: usize,

    /// Total indexed lookups (O(1))
    indexed_lookups: usize,

    /// Total linear scans (O(n))
    linear_scans: usize,
}

impl AlphaMemoryIndex {
    /// Create new alpha memory index
    pub fn new() -> Self {
        Self {
            facts: Vec::new(),
            indexes: HashMap::new(),
            stats: IndexStats::default(),
        }
    }

    /// Insert fact and update indexes
    pub fn insert(&mut self, fact: TypedFacts) -> usize {
        let idx = self.facts.len();

        // Update all existing indexes
        for (field_name, index) in &mut self.indexes {
            if let Some(value) = fact.get(field_name) {
                let key = format!("{:?}", value);
                index.entry(key).or_insert_with(Vec::new).push(idx);
            }
        }

        self.facts.push(fact);
        idx
    }

    /// Filter facts by field value
    /// Uses index if available (O(1)), otherwise falls back to linear scan (O(n))
    pub fn filter(&self, field: &str, value: &FactValue) -> Vec<&TypedFacts> {
        // Try index lookup first
        if let Some(index) = self.indexes.get(field) {
            let key = format!("{:?}", value);

            if let Some(indices) = index.get(&key) {
                return indices.iter().map(|&i| &self.facts[i]).collect();
            } else {
                return Vec::new();
            }
        }

        // Fallback to linear scan
        self.facts
            .iter()
            .filter(|f| f.get(field) == Some(value))
            .collect()
    }

    /// Filter facts and update statistics (mutable version for tracking)
    pub fn filter_tracked(&mut self, field: &str, value: &FactValue) -> Vec<&TypedFacts> {
        self.stats.total_queries += 1;
        *self
            .stats
            .query_counts
            .entry(field.to_string())
            .or_insert(0) += 1;

        // Try index lookup first
        if let Some(index) = self.indexes.get(field) {
            let key = format!("{:?}", value);
            self.stats.indexed_lookups += 1;

            if let Some(indices) = index.get(&key) {
                return indices.iter().map(|&i| &self.facts[i]).collect();
            } else {
                return Vec::new();
            }
        }

        // Fallback to linear scan
        self.stats.linear_scans += 1;
        self.facts
            .iter()
            .filter(|f| f.get(field) == Some(value))
            .collect()
    }

    /// Create index on a field
    pub fn create_index(&mut self, field: String) {
        if self.indexes.contains_key(&field) {
            return; // Already indexed
        }

        let mut index = HashMap::new();

        // Build index from existing facts
        for (idx, fact) in self.facts.iter().enumerate() {
            if let Some(value) = fact.get(&field) {
                let key = format!("{:?}", value);
                index.entry(key).or_insert_with(Vec::new).push(idx);
            }
        }

        self.indexes.insert(field, index);
    }

    /// Remove index from a field
    pub fn drop_index(&mut self, field: &str) {
        self.indexes.remove(field);
    }

    /// Get all facts
    pub fn get_all(&self) -> &[TypedFacts] {
        &self.facts
    }

    /// Get fact by index
    pub fn get(&self, idx: usize) -> Option<&TypedFacts> {
        self.facts.get(idx)
    }

    /// Get number of facts
    pub fn len(&self) -> usize {
        self.facts.len()
    }

    /// Check if empty
    pub fn is_empty(&self) -> bool {
        self.facts.is_empty()
    }

    /// Get indexed fields
    pub fn indexed_fields(&self) -> Vec<&String> {
        self.indexes.keys().collect()
    }

    /// Get statistics
    pub fn stats(&self) -> &IndexStats {
        &self.stats
    }

    /// Auto-detect which fields to index based on query patterns
    /// Indexes fields that are:
    /// 1. Frequently queried (>50 queries)
    /// 2. Not already indexed
    pub fn auto_tune(&mut self) {
        let fields_to_index: Vec<String> = self
            .stats
            .query_counts
            .iter()
            .filter(|(field, &count)| count > 50 && !self.indexes.contains_key(*field))
            .map(|(field, _)| field.clone())
            .collect();

        for field in fields_to_index {
            self.create_index(field);
        }
    }

    /// Clear all facts and indexes
    pub fn clear(&mut self) {
        self.facts.clear();
        self.indexes.clear();
        self.stats = IndexStats::default();
    }
}

impl Default for AlphaMemoryIndex {
    fn default() -> Self {
        Self::new()
    }
}

impl IndexStats {
    /// Get speedup ratio (indexed vs linear)
    pub fn speedup_ratio(&self) -> f64 {
        if self.linear_scans == 0 {
            return 1.0;
        }

        let indexed_ratio = self.indexed_lookups as f64 / self.total_queries as f64;
        let linear_ratio = self.linear_scans as f64 / self.total_queries as f64;

        if linear_ratio > 0.0 {
            indexed_ratio / linear_ratio
        } else {
            1.0
        }
    }

    /// Get percentage of queries using index
    pub fn index_hit_rate(&self) -> f64 {
        if self.total_queries == 0 {
            return 0.0;
        }
        (self.indexed_lookups as f64 / self.total_queries as f64) * 100.0
    }
}

impl std::fmt::Display for IndexStats {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Alpha Index Stats:\n\
             - Total queries: {}\n\
             - Indexed lookups: {} ({:.1}%)\n\
             - Linear scans: {} ({:.1}%)\n\
             - Most queried fields: {:?}",
            self.total_queries,
            self.indexed_lookups,
            self.index_hit_rate(),
            self.linear_scans,
            (100.0 - self.index_hit_rate()),
            self.query_counts
                .iter()
                .take(5)
                .map(|(k, v)| format!("{}({})", k, v))
                .collect::<Vec<_>>()
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_basic_index() {
        let mut mem = AlphaMemoryIndex::new();
        mem.create_index("status".to_string());

        // Insert 100 facts
        for i in 0..100 {
            let mut fact = TypedFacts::new();
            fact.set("id", i as i64);
            fact.set("status", if i % 10 == 0 { "active" } else { "pending" });
            mem.insert(fact);
        }

        // Query: should use index
        let active = mem.filter("status", &FactValue::String("active".to_string()));
        assert_eq!(active.len(), 10); // 10% are active
    }

    #[test]
    fn test_filter_without_index() {
        let mut mem = AlphaMemoryIndex::new();

        // Insert facts without creating index
        for i in 0..50 {
            let mut fact = TypedFacts::new();
            fact.set("score", i as i64);
            mem.insert(fact);
        }

        // Query without index - should use linear scan
        let results = mem.filter("score", &FactValue::Integer(25));
        assert_eq!(results.len(), 1);
    }

    #[test]
    fn test_auto_tune() {
        let mut mem = AlphaMemoryIndex::new();

        // Insert facts
        for i in 0..100 {
            let mut fact = TypedFacts::new();
            fact.set("category", if i % 5 == 0 { "A" } else { "B" });
            mem.insert(fact);
        }

        // Query many times (>50) to trigger auto-tuning
        for _ in 0..60 {
            let _ = mem.filter_tracked("category", &FactValue::String("A".to_string()));
        }

        // Should still be using linear scan
        assert_eq!(mem.indexed_fields().len(), 0);

        // Auto-tune should create index
        mem.auto_tune();
        assert_eq!(mem.indexed_fields().len(), 1);

        // Next query should use index
        let _ = mem.filter("category", &FactValue::String("A".to_string()));
    }

    #[test]
    fn test_multiple_indexes() {
        let mut mem = AlphaMemoryIndex::new();
        mem.create_index("status".to_string());
        mem.create_index("priority".to_string());

        for i in 0..50 {
            let mut fact = TypedFacts::new();
            fact.set("status", if i % 2 == 0 { "active" } else { "inactive" });
            fact.set("priority", if i % 3 == 0 { "high" } else { "low" });
            mem.insert(fact);
        }

        let active = mem.filter("status", &FactValue::String("active".to_string()));
        assert_eq!(active.len(), 25);

        let high_priority = mem.filter("priority", &FactValue::String("high".to_string()));
        assert_eq!(high_priority.len(), 17); // 50/3 rounded up
    }
}