velesdb_core/config.rs
1//! `VelesDB` Configuration Module
2//!
3//! Provides configuration file support via `velesdb.toml`, environment variables,
4//! and runtime overrides.
5//!
6//! # Priority (highest to lowest)
7//!
8//! 1. Runtime overrides (API, REPL)
9//! 2. Environment variables (`VELESDB_*`)
10//! 3. Configuration file (`velesdb.toml`)
11//! 4. Default values
12
13use figment::{
14 providers::{Env, Format, Serialized, Toml},
15 Figment,
16};
17use serde::{Deserialize, Serialize};
18use std::path::Path;
19use thiserror::Error;
20
21// Re-export quantization types so existing `crate::config::Quantization*` paths work.
22pub use crate::config_quantization::{QuantizationConfig, QuantizationType};
23
24/// Configuration errors.
25#[derive(Error, Debug)]
26#[non_exhaustive]
27pub enum ConfigError {
28 /// Failed to parse configuration file.
29 #[error("Failed to parse configuration: {0}")]
30 ParseError(String),
31
32 /// Invalid configuration value.
33 #[error("Invalid configuration value for '{key}': {message}")]
34 InvalidValue {
35 /// Configuration key that failed validation.
36 key: String,
37 /// Validation error message.
38 message: String,
39 },
40
41 /// Configuration file not found.
42 #[error("Configuration file not found: {0}")]
43 FileNotFound(String),
44
45 /// IO error.
46 #[error("IO error: {0}")]
47 IoError(#[from] std::io::Error),
48}
49
50/// Search mode presets.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53#[non_exhaustive]
54pub enum SearchMode {
55 /// Fast search with `ef_search=96`, ~95% recall.
56 Fast,
57 /// Balanced search with `ef_search=160`, ~99.5% recall (default).
58 #[default]
59 Balanced,
60 /// Accurate search with `ef_search=512`, ~100% recall.
61 Accurate,
62 /// Perfect recall via **exhaustive bruteforce** (`ef_search = usize::MAX`
63 /// signals a full scan): every vector is scored, no HNSW graph traversal, so
64 /// recall is 100% by construction at O(n) cost.
65 ///
66 /// Distinct from `SearchQuality::Perfect`
67 /// (`crate::index::hnsw::SearchQuality`) despite the shared name:
68 /// `SearchMode` picks the **engine** (bruteforce here vs. the HNSW graph),
69 /// whereas `SearchQuality::Perfect` stays *on* the graph with a very high
70 /// `ef_search` (`4096.max(k*100)`) — ~1.0 recall up to ~100K, ~0.9994 at 1M,
71 /// at graph cost rather than a full scan. Pick `SearchMode::Perfect` only
72 /// when an exact guarantee is worth the linear scan.
73 Perfect,
74}
75
76impl SearchMode {
77 /// Returns the `ef_search` value for this mode.
78 #[must_use]
79 pub fn ef_search(&self) -> usize {
80 match self {
81 Self::Fast => 96,
82 Self::Balanced => 160,
83 Self::Accurate => 512,
84 Self::Perfect => usize::MAX, // Signals bruteforce
85 }
86 }
87}
88
89/// Search configuration section.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(default)]
92pub struct SearchConfig {
93 /// Default search mode.
94 pub default_mode: SearchMode,
95 /// Override `ef_search` (if set, overrides mode).
96 pub ef_search: Option<usize>,
97 /// Maximum results per query.
98 pub max_results: usize,
99 /// Query timeout in milliseconds.
100 pub query_timeout_ms: u64,
101}
102
103impl Default for SearchConfig {
104 fn default() -> Self {
105 Self {
106 default_mode: SearchMode::Balanced,
107 ef_search: None,
108 max_results: 1000,
109 query_timeout_ms: 30000,
110 }
111 }
112}
113
114/// HNSW index configuration section.
115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116#[serde(default)]
117pub struct HnswConfig {
118 /// Number of connections per node (M parameter).
119 /// `None` = auto based on dimension.
120 pub m: Option<usize>,
121 /// Size of the candidate pool during construction.
122 /// `None` = auto based on dimension.
123 pub ef_construction: Option<usize>,
124 /// Maximum number of layers (0 = auto).
125 pub max_layers: usize,
126}
127
128/// Server-layer configuration types (HTTP transport, logging, storage paths).
129///
130/// These types are intentionally separated from the core engine configuration
131/// (`SearchConfig`, `HnswConfig`, `LimitsConfig`) to enforce layer boundaries.
132/// Import via `config::server::ServerConfig` or use the crate-root re-exports.
133pub mod server {
134 use serde::{Deserialize, Serialize};
135
136 /// Storage configuration section.
137 #[derive(Debug, Clone, Serialize, Deserialize)]
138 #[serde(default)]
139 pub struct StorageConfig {
140 /// Data directory path.
141 pub data_dir: String,
142 /// Storage mode: `"mmap"` or `"memory"`.
143 pub storage_mode: String,
144 /// Mmap cache size in megabytes.
145 pub mmap_cache_mb: usize,
146 /// Vector alignment in bytes.
147 pub vector_alignment: usize,
148 }
149
150 impl Default for StorageConfig {
151 fn default() -> Self {
152 Self {
153 data_dir: "./velesdb_data".to_string(),
154 storage_mode: "mmap".to_string(),
155 mmap_cache_mb: 1024,
156 vector_alignment: 64,
157 }
158 }
159 }
160
161 /// Server configuration section.
162 #[derive(Debug, Clone, Serialize, Deserialize)]
163 #[serde(default)]
164 pub struct ServerConfig {
165 /// Host address.
166 pub host: String,
167 /// Port number.
168 pub port: u16,
169 /// Number of worker threads (0 = auto).
170 pub workers: usize,
171 /// Maximum HTTP body size in bytes.
172 pub max_body_size: usize,
173 /// Enable CORS.
174 pub cors_enabled: bool,
175 /// CORS allowed origins.
176 pub cors_origins: Vec<String>,
177 }
178
179 impl Default for ServerConfig {
180 fn default() -> Self {
181 Self {
182 host: "127.0.0.1".to_string(),
183 port: 8080,
184 workers: 0,
185 max_body_size: 104_857_600,
186 cors_enabled: false,
187 cors_origins: vec!["*".to_string()],
188 }
189 }
190 }
191
192 /// Logging configuration section.
193 #[derive(Debug, Clone, Serialize, Deserialize)]
194 #[serde(default)]
195 pub struct LoggingConfig {
196 /// Log level: `error`, `warn`, `info`, `debug`, `trace`.
197 pub level: String,
198 /// Log format: `text` or `json`.
199 pub format: String,
200 /// Log file path (empty = stdout).
201 pub file: String,
202 }
203
204 impl Default for LoggingConfig {
205 fn default() -> Self {
206 Self {
207 level: "info".to_string(),
208 format: "text".to_string(),
209 file: String::new(),
210 }
211 }
212 }
213}
214
215// Backward-compatible re-exports at module level.
216pub use server::{LoggingConfig, ServerConfig, StorageConfig};
217
218/// Limits configuration section.
219///
220/// `#[non_exhaustive]`: build from [`LimitsConfig::default`] and adjust fields
221/// so future limits stay backward compatible for downstream crates.
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[serde(default)]
224#[non_exhaustive]
225pub struct LimitsConfig {
226 /// Maximum vector dimensions.
227 pub max_dimensions: usize,
228 /// Maximum vectors per collection.
229 pub max_vectors_per_collection: usize,
230 /// Maximum number of collections.
231 pub max_collections: usize,
232 /// Maximum payload size in bytes.
233 pub max_payload_size: usize,
234 /// Maximum vectors for perfect mode (bruteforce).
235 pub max_perfect_mode_vectors: usize,
236}
237
238impl Default for LimitsConfig {
239 fn default() -> Self {
240 Self {
241 max_dimensions: 4096,
242 max_vectors_per_collection: 100_000_000,
243 max_collections: 1000,
244 max_payload_size: 1_048_576, // 1 MB
245 max_perfect_mode_vectors: 500_000,
246 }
247 }
248}
249
250// ---------------------------------------------------------------------------
251// WAL batch commit configuration
252// ---------------------------------------------------------------------------
253
254/// Default commit delay in microseconds for WAL group commit.
255const fn default_commit_delay_us() -> u64 {
256 100
257}
258
259/// Default maximum entries per WAL batch.
260const fn default_max_batch_size() -> usize {
261 128
262}
263
264/// Configuration for WAL group commit batching.
265///
266/// When enabled, multiple concurrent writes are batched into a single
267/// `sync_all()` call, amortizing the fsync cost across the batch.
268///
269/// # Example (TOML)
270///
271/// ```toml
272/// [wal_batch]
273/// enabled = true
274/// commit_delay_us = 200
275/// max_batch_size = 256
276/// ```
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct WalBatchConfig {
279 /// Whether group commit is enabled. Default: `false`.
280 #[serde(default)]
281 pub enabled: bool,
282 /// Maximum delay in microseconds before flushing a batch. Default: `100`.
283 #[serde(default = "default_commit_delay_us")]
284 pub commit_delay_us: u64,
285 /// Maximum number of entries per batch. Default: `128`.
286 #[serde(default = "default_max_batch_size")]
287 pub max_batch_size: usize,
288}
289
290impl Default for WalBatchConfig {
291 fn default() -> Self {
292 Self {
293 enabled: false,
294 commit_delay_us: 100,
295 max_batch_size: 128,
296 }
297 }
298}
299
300/// Main `VelesDB` configuration structure.
301#[derive(Debug, Clone, Serialize, Deserialize, Default)]
302#[serde(default)]
303pub struct VelesConfig {
304 /// Search configuration.
305 pub search: SearchConfig,
306 /// HNSW index configuration.
307 pub hnsw: HnswConfig,
308 /// Storage configuration.
309 pub storage: StorageConfig,
310 /// Limits configuration.
311 pub limits: LimitsConfig,
312 /// Server configuration.
313 pub server: ServerConfig,
314 /// Logging configuration.
315 pub logging: LoggingConfig,
316 /// Quantization configuration.
317 pub quantization: QuantizationConfig,
318 /// WAL group commit batching configuration.
319 pub wal_batch: WalBatchConfig,
320}
321
322impl VelesConfig {
323 /// Loads configuration from default sources.
324 ///
325 /// Priority: defaults < file < environment variables.
326 ///
327 /// # Errors
328 ///
329 /// Returns `ConfigError` if the configuration file is malformed or
330 /// environment variables contain invalid values.
331 pub fn load() -> Result<Self, ConfigError> {
332 Self::load_from_path("velesdb.toml")
333 }
334
335 /// Loads configuration from a specific file path.
336 ///
337 /// # Arguments
338 ///
339 /// * `path` - Path to the configuration file.
340 ///
341 /// # Errors
342 ///
343 /// Returns an error if configuration parsing fails.
344 pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
345 let figment = Figment::new()
346 .merge(Serialized::defaults(Self::default()))
347 .merge(Toml::file(path.as_ref()))
348 .merge(Env::prefixed("VELESDB_").split("_").lowercase(false));
349
350 let config: Self = figment
351 .extract()
352 .map_err(|e| ConfigError::ParseError(e.to_string()))?;
353 config.validate()?;
354 Ok(config)
355 }
356
357 /// Creates a configuration from a TOML string.
358 ///
359 /// # Arguments
360 ///
361 /// * `toml_str` - TOML configuration string.
362 ///
363 /// # Errors
364 ///
365 /// Returns an error if parsing fails.
366 pub fn from_toml(toml_str: &str) -> Result<Self, ConfigError> {
367 let figment = Figment::new()
368 .merge(Serialized::defaults(Self::default()))
369 .merge(Toml::string(toml_str));
370
371 let config: Self = figment
372 .extract()
373 .map_err(|e| ConfigError::ParseError(e.to_string()))?;
374 config.validate()?;
375 Ok(config)
376 }
377
378 // Validation is in config_validation.rs
379
380 /// Returns the effective `ef_search` value.
381 #[must_use]
382 pub fn effective_ef_search(&self) -> usize {
383 self.search
384 .ef_search
385 .unwrap_or_else(|| self.search.default_mode.ef_search())
386 }
387
388 /// Serializes the configuration to TOML.
389 ///
390 /// # Errors
391 ///
392 /// Returns an error if serialization fails.
393 pub fn to_toml(&self) -> Result<String, ConfigError> {
394 toml::to_string_pretty(self).map_err(|e| ConfigError::ParseError(e.to_string()))
395 }
396}