use super::facts::{FactValue, TypedFacts};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct AlphaMemoryIndex {
facts: Vec<TypedFacts>,
indexes: HashMap<String, HashMap<String, Vec<usize>>>,
stats: IndexStats,
}
#[derive(Debug, Clone, Default)]
pub struct IndexStats {
query_counts: HashMap<String, usize>,
total_queries: usize,
indexed_lookups: usize,
linear_scans: usize,
}
impl AlphaMemoryIndex {
pub fn new() -> Self {
Self {
facts: Vec::new(),
indexes: HashMap::new(),
stats: IndexStats::default(),
}
}
pub fn insert(&mut self, fact: TypedFacts) -> usize {
let idx = self.facts.len();
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
}
pub fn filter(&self, field: &str, value: &FactValue) -> Vec<&TypedFacts> {
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();
}
}
self.facts
.iter()
.filter(|f| f.get(field) == Some(value))
.collect()
}
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;
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();
}
}
self.stats.linear_scans += 1;
self.facts
.iter()
.filter(|f| f.get(field) == Some(value))
.collect()
}
pub fn create_index(&mut self, field: String) {
if self.indexes.contains_key(&field) {
return; }
let mut index = HashMap::new();
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);
}
pub fn drop_index(&mut self, field: &str) {
self.indexes.remove(field);
}
pub fn get_all(&self) -> &[TypedFacts] {
&self.facts
}
pub fn get(&self, idx: usize) -> Option<&TypedFacts> {
self.facts.get(idx)
}
pub fn len(&self) -> usize {
self.facts.len()
}
pub fn is_empty(&self) -> bool {
self.facts.is_empty()
}
pub fn indexed_fields(&self) -> Vec<&String> {
self.indexes.keys().collect()
}
pub fn stats(&self) -> &IndexStats {
&self.stats
}
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);
}
}
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 {
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
}
}
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());
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);
}
let active = mem.filter("status", &FactValue::String("active".to_string()));
assert_eq!(active.len(), 10); }
#[test]
fn test_filter_without_index() {
let mut mem = AlphaMemoryIndex::new();
for i in 0..50 {
let mut fact = TypedFacts::new();
fact.set("score", i as i64);
mem.insert(fact);
}
let results = mem.filter("score", &FactValue::Integer(25));
assert_eq!(results.len(), 1);
}
#[test]
fn test_auto_tune() {
let mut mem = AlphaMemoryIndex::new();
for i in 0..100 {
let mut fact = TypedFacts::new();
fact.set("category", if i % 5 == 0 { "A" } else { "B" });
mem.insert(fact);
}
for _ in 0..60 {
let _ = mem.filter_tracked("category", &FactValue::String("A".to_string()));
}
assert_eq!(mem.indexed_fields().len(), 0);
mem.auto_tune();
assert_eq!(mem.indexed_fields().len(), 1);
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); }
}