Skip to main content

subcog/services/
backend_factory.rs

1//! Backend factory for storage layer initialization.
2//!
3//! This module centralizes backend creation to:
4//! - Reduce code duplication between `for_repo` and `for_user`
5//! - Enable easier backend swapping for testing
6//! - Provide consistent error handling and graceful degradation
7//!
8//! # Architecture
9//!
10//! ```text
11//! BackendFactory
12//!   ├── create_embedder() → Arc<dyn Embedder>
13//!   ├── create_index_backend() → Option<Arc<dyn IndexBackend>>
14//!   └── create_vector_backend() → Option<Arc<dyn VectorBackend>>
15//! ```
16//!
17//! # Graceful Degradation
18//!
19//! Factory methods return `Option` for backends that may fail to initialize.
20//! This allows the service container to continue with reduced functionality.
21
22use crate::embedding::{Embedder, FastEmbedEmbedder};
23use crate::storage::index::SqliteBackend;
24use crate::storage::traits::{IndexBackend, VectorBackend};
25use crate::storage::vector::UsearchBackend;
26use std::path::Path;
27use std::sync::Arc;
28
29/// Result of backend initialization with optional components.
30///
31/// All backends are wrapped in `Arc` for shared ownership across services.
32#[derive(Default)]
33pub struct BackendSet {
34    /// Embedder for generating vector embeddings.
35    pub embedder: Option<Arc<dyn Embedder>>,
36    /// Index backend for full-text search (`SQLite` FTS5).
37    pub index: Option<Arc<dyn IndexBackend + Send + Sync>>,
38    /// Vector backend for similarity search (usearch HNSW).
39    pub vector: Option<Arc<dyn VectorBackend + Send + Sync>>,
40}
41
42impl BackendSet {
43    /// Returns true if all backends were successfully initialized.
44    #[must_use]
45    pub fn is_complete(&self) -> bool {
46        self.embedder.is_some() && self.index.is_some() && self.vector.is_some()
47    }
48
49    /// Returns true if at least the embedder is available.
50    #[must_use]
51    pub fn has_embedder(&self) -> bool {
52        self.embedder.is_some()
53    }
54
55    /// Returns true if full-text search is available.
56    #[must_use]
57    pub fn has_index(&self) -> bool {
58        self.index.is_some()
59    }
60
61    /// Returns true if vector similarity search is available.
62    #[must_use]
63    pub fn has_vector(&self) -> bool {
64        self.vector.is_some()
65    }
66}
67
68/// Factory for creating storage backends.
69///
70/// Centralizes backend initialization with consistent error handling.
71///
72/// # Example
73///
74/// ```rust,ignore
75/// use subcog::services::{BackendFactory, PathManager};
76///
77/// let paths = PathManager::for_repo("/path/to/repo");
78/// let backends = BackendFactory::create_all(&paths);
79///
80/// if backends.is_complete() {
81///     println!("All backends initialized successfully");
82/// }
83/// ```
84pub struct BackendFactory;
85
86impl BackendFactory {
87    /// Creates all backends using the provided path configuration.
88    ///
89    /// Returns a `BackendSet` with successfully initialized backends.
90    /// Failed backends are logged and set to `None`.
91    ///
92    /// # Arguments
93    ///
94    /// * `index_path` - Path for `SQLite` index database
95    /// * `vector_path` - Path for vector index files
96    ///
97    /// # Returns
98    ///
99    /// A `BackendSet` containing available backends.
100    #[must_use]
101    pub fn create_all(index_path: &Path, vector_path: &Path) -> BackendSet {
102        let embedder = Self::create_embedder();
103        let index = Self::create_index_backend(index_path);
104        let vector = Self::create_vector_backend(vector_path);
105
106        BackendSet {
107            embedder,
108            index,
109            vector,
110        }
111    }
112
113    /// Creates the embedder backend.
114    ///
115    /// Currently always returns `FastEmbedEmbedder`. In the future,
116    /// this could be configured to use different embedding models.
117    #[must_use]
118    pub fn create_embedder() -> Option<Arc<dyn Embedder>> {
119        Some(Arc::new(FastEmbedEmbedder::new()))
120    }
121
122    /// Creates the index backend (`SQLite` FTS5).
123    ///
124    /// # Arguments
125    ///
126    /// * `path` - Path to the `SQLite` database file
127    ///
128    /// # Returns
129    ///
130    /// `Some(backend)` on success, `None` if initialization fails.
131    pub fn create_index_backend(path: &Path) -> Option<Arc<dyn IndexBackend + Send + Sync>> {
132        match SqliteBackend::new(path) {
133            Ok(backend) => {
134                tracing::debug!(path = %path.display(), "Created SQLite index backend");
135                Some(Arc::new(backend))
136            },
137            Err(e) => {
138                tracing::warn!(
139                    path = %path.display(),
140                    error = %e,
141                    "Failed to create SQLite index backend"
142                );
143                None
144            },
145        }
146    }
147
148    /// Creates the vector backend (usearch HNSW).
149    ///
150    /// # Arguments
151    ///
152    /// * `path` - Path to the vector index directory
153    ///
154    /// # Returns
155    ///
156    /// `Some(backend)` on success, `None` if initialization fails.
157    pub fn create_vector_backend(path: &Path) -> Option<Arc<dyn VectorBackend + Send + Sync>> {
158        let dimensions = FastEmbedEmbedder::DEFAULT_DIMENSIONS;
159
160        #[cfg(feature = "usearch-hnsw")]
161        let result = UsearchBackend::new(path, dimensions);
162
163        #[cfg(not(feature = "usearch-hnsw"))]
164        let result: crate::Result<UsearchBackend> = Ok(UsearchBackend::new(path, dimensions));
165
166        match result {
167            Ok(backend) => {
168                // Load previously saved embeddings from disk
169                if let Err(e) = backend.load() {
170                    tracing::warn!(
171                        path = %path.display(),
172                        error = %e,
173                        "Failed to load vector index, starting with empty index"
174                    );
175                }
176                tracing::debug!(path = %path.display(), "Created usearch vector backend");
177                Some(Arc::new(backend))
178            },
179            Err(e) => {
180                tracing::warn!(
181                    path = %path.display(),
182                    error = %e,
183                    "Failed to create usearch vector backend"
184                );
185                None
186            },
187        }
188    }
189
190    /// Creates backends with custom embedder dimensions.
191    ///
192    /// Useful for testing or when using non-default embedding models.
193    ///
194    /// # Arguments
195    ///
196    /// * `index_path` - Path for `SQLite` index database
197    /// * `vector_path` - Path for vector index files
198    /// * `dimensions` - Vector embedding dimensions
199    #[must_use]
200    pub fn create_with_dimensions(
201        index_path: &Path,
202        vector_path: &Path,
203        dimensions: usize,
204    ) -> BackendSet {
205        let embedder = Self::create_embedder();
206        let index = Self::create_index_backend(index_path);
207
208        #[cfg(feature = "usearch-hnsw")]
209        let vector_result = UsearchBackend::new(vector_path, dimensions);
210        #[cfg(not(feature = "usearch-hnsw"))]
211        let vector_result: crate::Result<UsearchBackend> =
212            Ok(UsearchBackend::new(vector_path, dimensions));
213
214        let vector = match vector_result {
215            Ok(backend) => Some(Arc::new(backend) as Arc<dyn VectorBackend + Send + Sync>),
216            Err(e) => {
217                tracing::warn!(
218                    path = %vector_path.display(),
219                    dimensions,
220                    error = %e,
221                    "Failed to create usearch vector backend with custom dimensions"
222                );
223                None
224            },
225        };
226
227        BackendSet {
228            embedder,
229            index,
230            vector,
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use tempfile::TempDir;
239
240    #[test]
241    fn test_backend_set_default() {
242        let set = BackendSet::default();
243        assert!(!set.is_complete());
244        assert!(!set.has_embedder());
245        assert!(!set.has_index());
246        assert!(!set.has_vector());
247    }
248
249    #[test]
250    fn test_create_embedder() {
251        let embedder = BackendFactory::create_embedder();
252        assert!(embedder.is_some());
253    }
254
255    #[test]
256    fn test_create_index_backend() {
257        let temp_dir = TempDir::new().expect("Failed to create temp dir");
258        let index_path = temp_dir.path().join("test_index.db");
259
260        let index = BackendFactory::create_index_backend(&index_path);
261        assert!(index.is_some());
262    }
263
264    #[test]
265    fn test_create_index_backend_invalid_path() {
266        // Try to create index in non-existent deeply nested path
267        let invalid_path = std::path::Path::new("/nonexistent/deeply/nested/path/index.db");
268        let index = BackendFactory::create_index_backend(invalid_path);
269        assert!(index.is_none());
270    }
271
272    #[test]
273    fn test_create_all() {
274        let temp_dir = TempDir::new().expect("Failed to create temp dir");
275        let index_path = temp_dir.path().join("index.db");
276        let vector_path = temp_dir.path().join("vectors");
277
278        let backends = BackendFactory::create_all(&index_path, &vector_path);
279
280        assert!(backends.has_embedder());
281        assert!(backends.has_index());
282        // Vector backend may or may not succeed depending on feature flags
283    }
284
285    #[test]
286    fn test_backend_set_partial() {
287        let set = BackendSet {
288            embedder: BackendFactory::create_embedder(),
289            ..Default::default()
290        };
291
292        assert!(!set.is_complete());
293        assert!(set.has_embedder());
294        assert!(!set.has_index());
295        assert!(!set.has_vector());
296    }
297}