exo_backend_classical/
lib.rs

1//! # EXO Backend Classical
2//!
3//! Classical substrate backend consuming ruvector crates.
4//! This provides a bridge between the EXO substrate abstractions and the
5//! high-performance ruvector vector database and graph database.
6
7#![warn(missing_docs)]
8
9pub mod graph;
10pub mod vector;
11
12use exo_core::{
13    Error as ExoError, Filter, ManifoldDelta, Pattern, Result as ExoResult,
14    SearchResult, SubstrateBackend,
15};
16use parking_lot::RwLock;
17use std::sync::Arc;
18use vector::VectorIndexWrapper;
19
20pub use graph::GraphWrapper;
21
22/// Configuration for the classical backend
23#[derive(Debug, Clone)]
24pub struct ClassicalConfig {
25    /// Vector dimensions
26    pub dimensions: usize,
27    /// Distance metric
28    pub distance_metric: ruvector_core::DistanceMetric,
29}
30
31impl Default for ClassicalConfig {
32    fn default() -> Self {
33        Self {
34            dimensions: 768,
35            distance_metric: ruvector_core::DistanceMetric::Cosine,
36        }
37    }
38}
39
40/// Classical substrate backend using ruvector
41///
42/// This backend wraps ruvector-core for vector operations and ruvector-graph
43/// for hypergraph operations, providing a classical (discrete) implementation
44/// of the substrate backend trait.
45pub struct ClassicalBackend {
46    /// Vector index wrapper
47    vector_index: Arc<RwLock<VectorIndexWrapper>>,
48    /// Graph database wrapper
49    graph_db: Arc<RwLock<GraphWrapper>>,
50    /// Configuration
51    config: ClassicalConfig,
52}
53
54impl ClassicalBackend {
55    /// Create a new classical backend with the given configuration
56    pub fn new(config: ClassicalConfig) -> ExoResult<Self> {
57        let vector_index = VectorIndexWrapper::new(config.dimensions, config.distance_metric)
58            .map_err(|e| ExoError::Backend(format!("Failed to create vector index: {}", e)))?;
59
60        let graph_db = GraphWrapper::new();
61
62        Ok(Self {
63            vector_index: Arc::new(RwLock::new(vector_index)),
64            graph_db: Arc::new(RwLock::new(graph_db)),
65            config,
66        })
67    }
68
69    /// Create with default configuration
70    pub fn with_dimensions(dimensions: usize) -> ExoResult<Self> {
71        let mut config = ClassicalConfig::default();
72        config.dimensions = dimensions;
73        Self::new(config)
74    }
75
76    /// Get access to the underlying graph database (for hyperedge operations)
77    pub fn graph_db(&self) -> Arc<RwLock<GraphWrapper>> {
78        Arc::clone(&self.graph_db)
79    }
80}
81
82impl SubstrateBackend for ClassicalBackend {
83    fn similarity_search(
84        &self,
85        query: &[f32],
86        k: usize,
87        filter: Option<&Filter>,
88    ) -> ExoResult<Vec<SearchResult>> {
89        // Validate dimensions
90        if query.len() != self.config.dimensions {
91            return Err(ExoError::InvalidDimension {
92                expected: self.config.dimensions,
93                got: query.len(),
94            });
95        }
96
97        // Delegate to vector index wrapper
98        let index = self.vector_index.read();
99        index.search(query, k, filter)
100    }
101
102    fn manifold_deform(&self, pattern: &Pattern, _learning_rate: f32) -> ExoResult<ManifoldDelta> {
103        // Validate dimensions
104        if pattern.embedding.len() != self.config.dimensions {
105            return Err(ExoError::InvalidDimension {
106                expected: self.config.dimensions,
107                got: pattern.embedding.len(),
108            });
109        }
110
111        // Classical backend: discrete insert (no continuous deformation)
112        let mut index = self.vector_index.write();
113        let id = index.insert(pattern)?;
114
115        Ok(ManifoldDelta::DiscreteInsert { id })
116    }
117
118    fn dimension(&self) -> usize {
119        self.config.dimensions
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use exo_core::{Metadata, PatternId, SubstrateTime};
127
128    #[test]
129    fn test_classical_backend_creation() {
130        let backend = ClassicalBackend::with_dimensions(128).unwrap();
131        assert_eq!(backend.dimension(), 128);
132    }
133
134    #[test]
135    fn test_insert_and_search() {
136        let backend = ClassicalBackend::with_dimensions(3).unwrap();
137
138        // Create a pattern
139        let pattern = Pattern {
140            id: PatternId::new(),
141            embedding: vec![1.0, 2.0, 3.0],
142            metadata: Metadata::default(),
143            timestamp: SubstrateTime::now(),
144            antecedents: vec![],
145            salience: 1.0,
146        };
147
148        // Insert pattern
149        let result = backend.manifold_deform(&pattern, 0.0);
150        assert!(result.is_ok());
151
152        // Search
153        let query = vec![1.1, 2.1, 3.1];
154        let results = backend.similarity_search(&query, 1, None);
155        assert!(results.is_ok());
156        let results = results.unwrap();
157        assert_eq!(results.len(), 1);
158    }
159}