Skip to main content

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}