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