Skip to main content

symbi_runtime/context/
vector_db_factory.rs

1//! Vector backend factory.
2//!
3//! Resolves which vector backend to use from env vars / config,
4//! then constructs and returns `Arc<dyn VectorDb>`.
5
6#[cfg(feature = "vector-lancedb")]
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use crate::context::types::ContextError;
11use crate::context::vector_db::NoOpVectorDatabase;
12#[cfg(feature = "vector-lancedb")]
13use crate::context::vector_db_lance::{LanceDbBackend, LanceDbConfig};
14#[cfg(feature = "vector-lancedb")]
15use crate::context::vector_db_trait::DistanceMetric;
16use crate::context::vector_db_trait::VectorDb;
17
18/// Backend selection config.
19#[derive(Debug, Clone)]
20pub enum VectorBackendConfig {
21    #[cfg(feature = "vector-lancedb")]
22    LanceDb(LanceDbConfig),
23    #[cfg(feature = "vector-qdrant")]
24    Qdrant(crate::context::vector_db::QdrantConfig),
25    NoOp,
26}
27
28#[allow(clippy::derivable_impls)] // Conditional default based on feature flags
29impl Default for VectorBackendConfig {
30    fn default() -> Self {
31        #[cfg(feature = "vector-lancedb")]
32        {
33            Self::LanceDb(LanceDbConfig::default())
34        }
35        #[cfg(not(feature = "vector-lancedb"))]
36        {
37            Self::NoOp
38        }
39    }
40}
41
42/// Build the appropriate vector backend from config.
43pub async fn create_vector_backend(
44    config: VectorBackendConfig,
45) -> Result<Arc<dyn VectorDb>, ContextError> {
46    match config {
47        #[cfg(feature = "vector-lancedb")]
48        VectorBackendConfig::LanceDb(cfg) => {
49            let backend = LanceDbBackend::new(cfg).await?;
50            Ok(Arc::new(backend))
51        }
52        #[cfg(feature = "vector-qdrant")]
53        VectorBackendConfig::Qdrant(cfg) => {
54            let backend = crate::context::vector_db::QdrantClientWrapper::new(cfg);
55            Ok(Arc::new(backend))
56        }
57        VectorBackendConfig::NoOp => Ok(Arc::new(NoOpVectorDatabase)),
58    }
59}
60
61/// Resolve vector backend config from environment variables.
62///
63/// Resolution order:
64/// 1. `SYMBIONT_VECTOR_BACKEND` env var (`lancedb` | `qdrant` | `noop`)
65/// 2. Default: `lancedb`
66///
67/// Additional env vars:
68/// - `SYMBIONT_VECTOR_DATA_PATH` — LanceDB data directory (default: `./data/vector_db`)
69/// - `SYMBIONT_VECTOR_HOST` — Qdrant host (default: `localhost`)
70/// - `SYMBIONT_VECTOR_PORT` — Qdrant port (default: `6333`)
71/// - `SYMBIONT_VECTOR_API_KEY` — Qdrant API key (optional)
72/// - `SYMBIONT_VECTOR_COLLECTION` — Collection name (default: `symbiont_context`)
73/// - `SYMBIONT_VECTOR_DIMENSION` — Vector dimension (default: `384`)
74pub fn resolve_vector_config() -> VectorBackendConfig {
75    let backend =
76        std::env::var("SYMBIONT_VECTOR_BACKEND").unwrap_or_else(|_| "lancedb".to_string());
77
78    match backend.to_lowercase().as_str() {
79        #[cfg(feature = "vector-qdrant")]
80        "qdrant" => {
81            let dimension: usize = std::env::var("SYMBIONT_VECTOR_DIMENSION")
82                .ok()
83                .and_then(|s| s.parse().ok())
84                .unwrap_or(384);
85            let collection = std::env::var("SYMBIONT_VECTOR_COLLECTION")
86                .unwrap_or_else(|_| "symbiont_context".to_string());
87            let host =
88                std::env::var("SYMBIONT_VECTOR_HOST").unwrap_or_else(|_| "localhost".to_string());
89            let port = std::env::var("SYMBIONT_VECTOR_PORT").unwrap_or_else(|_| "6333".to_string());
90            let api_key = std::env::var("SYMBIONT_VECTOR_API_KEY").ok();
91            VectorBackendConfig::Qdrant(crate::context::vector_db::QdrantConfig {
92                url: format!("http://{}:{}", host, port),
93                api_key,
94                collection_name: collection,
95                vector_dimension: dimension,
96                ..Default::default()
97            })
98        }
99        "noop" | "none" => VectorBackendConfig::NoOp,
100        #[cfg(feature = "vector-lancedb")]
101        _ => {
102            let dimension: usize = std::env::var("SYMBIONT_VECTOR_DIMENSION")
103                .ok()
104                .and_then(|s| s.parse().ok())
105                .unwrap_or(384);
106            let collection = std::env::var("SYMBIONT_VECTOR_COLLECTION")
107                .unwrap_or_else(|_| "symbiont_context".to_string());
108            let path = std::env::var("SYMBIONT_VECTOR_DATA_PATH")
109                .unwrap_or_else(|_| "./data/vector_db".to_string());
110            VectorBackendConfig::LanceDb(LanceDbConfig {
111                data_path: PathBuf::from(path),
112                collection_name: collection,
113                vector_dimension: dimension,
114                distance_metric: DistanceMetric::Cosine,
115            })
116        }
117        #[cfg(not(feature = "vector-lancedb"))]
118        _ => VectorBackendConfig::NoOp,
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[cfg(feature = "vector-lancedb")]
127    #[test]
128    fn test_resolve_defaults_to_lancedb() {
129        std::env::remove_var("SYMBIONT_VECTOR_BACKEND");
130        let config = resolve_vector_config();
131        assert!(matches!(config, VectorBackendConfig::LanceDb(_)));
132    }
133
134    #[cfg(feature = "vector-lancedb")]
135    #[test]
136    fn test_resolve_lancedb_explicit() {
137        std::env::set_var("SYMBIONT_VECTOR_BACKEND", "lancedb");
138        let config = resolve_vector_config();
139        assert!(matches!(config, VectorBackendConfig::LanceDb(_)));
140        std::env::remove_var("SYMBIONT_VECTOR_BACKEND");
141    }
142
143    #[cfg(feature = "vector-lancedb")]
144    #[test]
145    fn test_resolve_custom_data_path() {
146        std::env::set_var("SYMBIONT_VECTOR_BACKEND", "lancedb");
147        std::env::set_var("SYMBIONT_VECTOR_DATA_PATH", "/tmp/custom_vectors");
148        let config = resolve_vector_config();
149        match config {
150            VectorBackendConfig::LanceDb(cfg) => {
151                assert_eq!(cfg.data_path, PathBuf::from("/tmp/custom_vectors"));
152            }
153            #[allow(unreachable_patterns)]
154            _ => panic!("Expected LanceDb config"),
155        }
156        std::env::remove_var("SYMBIONT_VECTOR_BACKEND");
157        std::env::remove_var("SYMBIONT_VECTOR_DATA_PATH");
158    }
159
160    #[cfg(feature = "vector-lancedb")]
161    #[tokio::test]
162    async fn test_create_lance_backend() {
163        let tmp = tempfile::TempDir::new().unwrap();
164        let config = VectorBackendConfig::LanceDb(LanceDbConfig {
165            data_path: tmp.path().to_path_buf(),
166            ..Default::default()
167        });
168        let backend = create_vector_backend(config).await;
169        assert!(backend.is_ok());
170    }
171
172    #[tokio::test]
173    async fn test_create_noop_backend() {
174        let backend = create_vector_backend(VectorBackendConfig::NoOp).await;
175        assert!(backend.is_ok());
176    }
177}