Skip to main content

claw_vector/
config.rs

1// config.rs — VectorConfig with full builder pattern, defaults, validation, and env loading.
2use serde::{Deserialize, Serialize};
3use std::{num::NonZeroUsize, path::PathBuf};
4
5use crate::error::{VectorError, VectorResult};
6
7/// Runtime configuration for the claw-vector engine.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct VectorConfig {
10    /// Path to the SQLite database file for vector metadata.
11    pub db_path: PathBuf,
12    /// Directory for HNSW index files and mmap vector files.
13    pub index_dir: PathBuf,
14    /// gRPC endpoint of the Python embedding service (e.g. `"http://localhost:50051"`).
15    pub embedding_service_url: String,
16    /// Default embedding dimensionality (384 = all-MiniLM-L6-v2).
17    pub default_dimensions: usize,
18    /// HNSW `ef_construction` build parameter (higher → better recall, slower build).
19    pub ef_construction: usize,
20    /// HNSW `M` connections parameter (higher → better recall, more memory).
21    pub m_connections: usize,
22    /// HNSW `ef` search parameter (higher → better recall, slower search).
23    pub ef_search: usize,
24    /// Maximum number of vectors per index.
25    pub max_elements: usize,
26    /// Number of embedding LRU cache entries.
27    pub cache_size: usize,
28    /// Maximum number of texts per embedding gRPC call.
29    pub batch_size: usize,
30    /// Timeout for embedding gRPC calls in milliseconds.
31    pub embedding_timeout_ms: u64,
32    /// Number of rayon threads for parallel index operations.
33    pub num_threads: usize,
34    /// Default workspace id used when callers do not provide one explicitly.
35    pub default_workspace_id: String,
36    /// Require callers to provide workspace ids explicitly.
37    pub require_workspace_id: bool,
38    /// SQLite path for API key storage.
39    pub api_key_store_path: PathBuf,
40    /// Default request budget per workspace in requests/second.
41    pub rate_limit_rps: u32,
42    /// Require authentication for inbound API requests.
43    pub require_auth: bool,
44}
45
46impl Default for VectorConfig {
47    fn default() -> Self {
48        VectorConfig {
49            db_path: PathBuf::from("claw_vector.db"),
50            index_dir: PathBuf::from("claw_vector_indices"),
51            embedding_service_url: "http://localhost:50051".into(),
52            default_dimensions: 384,
53            ef_construction: 200,
54            m_connections: 16,
55            ef_search: 50,
56            max_elements: 1_000_000,
57            cache_size: 10_000,
58            batch_size: 64,
59            embedding_timeout_ms: 5_000,
60            num_threads: std::thread::available_parallelism()
61                .unwrap_or(NonZeroUsize::new(4).unwrap())
62                .get(),
63            default_workspace_id: "default".into(),
64            require_workspace_id: !cfg!(test),
65            api_key_store_path: PathBuf::from("claw_vector_auth.db"),
66            rate_limit_rps: 100,
67            require_auth: !cfg!(test),
68        }
69    }
70}
71
72impl VectorConfig {
73    /// Return a new builder initialised with the default configuration.
74    pub fn builder() -> VectorConfigBuilder {
75        VectorConfigBuilder::default()
76    }
77
78    /// Load configuration from environment variables, falling back to defaults.
79    ///
80    /// Recognised variables:
81    /// - `CLAW_VECTOR_DB_PATH`
82    /// - `CLAW_VECTOR_INDEX_DIR`
83    /// - `CLAW_EMBEDDING_URL`
84    /// - `CLAW_DEFAULT_WORKSPACE_ID`
85    /// - `CLAW_REQUIRE_WORKSPACE_ID`
86    /// - `CLAW_API_KEY_STORE_PATH`
87    /// - `CLAW_RATE_LIMIT_RPS`
88    /// - `CLAW_REQUIRE_AUTH`
89    pub fn from_env() -> Self {
90        let mut cfg = VectorConfig::default();
91        if let Ok(v) = std::env::var("CLAW_VECTOR_DB_PATH") {
92            cfg.db_path = PathBuf::from(v);
93        }
94        if let Ok(v) = std::env::var("CLAW_VECTOR_INDEX_DIR") {
95            cfg.index_dir = PathBuf::from(v);
96        }
97        if let Ok(v) = std::env::var("CLAW_EMBEDDING_URL") {
98            cfg.embedding_service_url = v;
99        }
100        if let Ok(v) = std::env::var("CLAW_DEFAULT_WORKSPACE_ID") {
101            cfg.default_workspace_id = v;
102        }
103        if let Ok(v) = std::env::var("CLAW_REQUIRE_WORKSPACE_ID") {
104            cfg.require_workspace_id =
105                matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
106        }
107        if let Ok(v) = std::env::var("CLAW_API_KEY_STORE_PATH") {
108            cfg.api_key_store_path = PathBuf::from(v);
109        }
110        if let Ok(v) = std::env::var("CLAW_RATE_LIMIT_RPS") {
111            if let Ok(parsed) = v.parse::<u32>() {
112                cfg.rate_limit_rps = parsed.max(1);
113            }
114        }
115        if let Ok(v) = std::env::var("CLAW_REQUIRE_AUTH") {
116            cfg.require_auth = matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
117        }
118        cfg
119    }
120}
121
122// ─── Builder ─────────────────────────────────────────────────────────────────
123
124/// Fluent builder for [`VectorConfig`].
125#[derive(Debug, Clone, Default)]
126pub struct VectorConfigBuilder {
127    inner: VectorConfig,
128}
129
130impl VectorConfigBuilder {
131    /// Set the SQLite database path.
132    pub fn db_path(mut self, path: impl Into<PathBuf>) -> Self {
133        self.inner.db_path = path.into();
134        self
135    }
136
137    /// Set the index directory.
138    pub fn index_dir(mut self, dir: impl Into<PathBuf>) -> Self {
139        self.inner.index_dir = dir.into();
140        self
141    }
142
143    /// Set the embedding service gRPC URL.
144    pub fn embedding_service_url(mut self, url: impl Into<String>) -> Self {
145        self.inner.embedding_service_url = url.into();
146        self
147    }
148
149    /// Set the default embedding dimensionality.
150    pub fn default_dimensions(mut self, dims: usize) -> Self {
151        self.inner.default_dimensions = dims;
152        self
153    }
154
155    /// Set the HNSW `ef_construction` parameter.
156    pub fn ef_construction(mut self, ef: usize) -> Self {
157        self.inner.ef_construction = ef;
158        self
159    }
160
161    /// Set the HNSW `M` connections parameter.
162    pub fn m_connections(mut self, m: usize) -> Self {
163        self.inner.m_connections = m;
164        self
165    }
166
167    /// Set the HNSW `ef_search` parameter.
168    pub fn ef_search(mut self, ef: usize) -> Self {
169        self.inner.ef_search = ef;
170        self
171    }
172
173    /// Set the maximum number of vectors per index.
174    pub fn max_elements(mut self, n: usize) -> Self {
175        self.inner.max_elements = n;
176        self
177    }
178
179    /// Set the LRU embedding cache capacity.
180    pub fn cache_size(mut self, n: usize) -> Self {
181        self.inner.cache_size = n;
182        self
183    }
184
185    /// Set the maximum batch size for embedding calls.
186    pub fn batch_size(mut self, n: usize) -> Self {
187        self.inner.batch_size = n;
188        self
189    }
190
191    /// Set the embedding gRPC call timeout in milliseconds.
192    pub fn embedding_timeout_ms(mut self, ms: u64) -> Self {
193        self.inner.embedding_timeout_ms = ms;
194        self
195    }
196
197    /// Set the number of rayon threads.
198    pub fn num_threads(mut self, n: usize) -> Self {
199        self.inner.num_threads = n;
200        self
201    }
202
203    /// Set the default workspace id.
204    pub fn default_workspace_id(mut self, workspace_id: impl Into<String>) -> Self {
205        self.inner.default_workspace_id = workspace_id.into();
206        self
207    }
208
209    /// Set whether workspace id is mandatory in API calls.
210    pub fn require_workspace_id(mut self, require_workspace_id: bool) -> Self {
211        self.inner.require_workspace_id = require_workspace_id;
212        self
213    }
214
215    /// Set the API key store path.
216    pub fn api_key_store_path(mut self, path: impl Into<PathBuf>) -> Self {
217        self.inner.api_key_store_path = path.into();
218        self
219    }
220
221    /// Set the workspace rate limit in requests per second.
222    pub fn rate_limit_rps(mut self, rps: u32) -> Self {
223        self.inner.rate_limit_rps = rps.max(1);
224        self
225    }
226
227    /// Set whether request authentication is required.
228    pub fn require_auth(mut self, require_auth: bool) -> Self {
229        self.inner.require_auth = require_auth;
230        self
231    }
232
233    /// Validate and return the completed [`VectorConfig`].
234    ///
235    /// # Errors
236    /// - `dimensions` must be ≥ 1
237    /// - `ef_construction` must be ≥ `m_connections`
238    /// - `m_connections` must be ≥ 2
239    pub fn build(self) -> VectorResult<VectorConfig> {
240        let cfg = self.inner;
241        if cfg.default_dimensions < 1 {
242            return Err(VectorError::Config(
243                "default_dimensions must be >= 1".into(),
244            ));
245        }
246        if cfg.m_connections < 2 {
247            return Err(VectorError::Config("m_connections must be >= 2".into()));
248        }
249        if cfg.ef_construction < cfg.m_connections {
250            return Err(VectorError::Config(
251                "ef_construction must be >= m_connections".into(),
252            ));
253        }
254        if cfg.default_workspace_id.trim().is_empty() {
255            return Err(VectorError::Config(
256                "default_workspace_id must not be empty".into(),
257            ));
258        }
259        if cfg.rate_limit_rps == 0 {
260            return Err(VectorError::Config("rate_limit_rps must be > 0".into()));
261        }
262        Ok(cfg)
263    }
264}