1use figment::{
14 providers::{Env, Format, Serialized, Toml},
15 Figment,
16};
17use serde::{Deserialize, Serialize};
18use std::path::Path;
19use thiserror::Error;
20
21const fn default_k() -> usize {
27 256
28}
29
30#[allow(clippy::unnecessary_wraps)]
32const fn default_oversampling() -> Option<u32> {
33 Some(4)
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(tag = "type", rename_all = "lowercase")]
43pub enum QuantizationType {
44 #[default]
46 None,
47 #[serde(alias = "sq8")]
49 SQ8,
50 Binary,
52 #[serde(alias = "pq")]
54 PQ {
55 m: usize,
57 #[serde(default = "default_k")]
59 k: usize,
60 #[serde(default)]
62 opq_enabled: bool,
63 #[serde(default = "default_oversampling")]
65 oversampling: Option<u32>,
66 },
67 #[serde(alias = "rabitq")]
69 RaBitQ,
70}
71
72impl QuantizationType {
73 #[must_use]
75 pub const fn is_pq(&self) -> bool {
76 matches!(self, Self::PQ { .. })
77 }
78
79 #[must_use]
81 pub const fn is_rabitq(&self) -> bool {
82 matches!(self, Self::RaBitQ)
83 }
84}
85
86#[derive(Error, Debug)]
88pub enum ConfigError {
89 #[error("Failed to parse configuration: {0}")]
91 ParseError(String),
92
93 #[error("Invalid configuration value for '{key}': {message}")]
95 InvalidValue {
96 key: String,
98 message: String,
100 },
101
102 #[error("Configuration file not found: {0}")]
104 FileNotFound(String),
105
106 #[error("IO error: {0}")]
108 IoError(#[from] std::io::Error),
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum SearchMode {
115 Fast,
117 #[default]
119 Balanced,
120 Accurate,
122 Perfect,
124}
125
126impl SearchMode {
127 #[must_use]
129 pub fn ef_search(&self) -> usize {
130 match self {
131 Self::Fast => 64,
132 Self::Balanced => 128,
133 Self::Accurate => 512,
134 Self::Perfect => usize::MAX, }
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(default)]
142pub struct SearchConfig {
143 pub default_mode: SearchMode,
145 pub ef_search: Option<usize>,
147 pub max_results: usize,
149 pub query_timeout_ms: u64,
151}
152
153impl Default for SearchConfig {
154 fn default() -> Self {
155 Self {
156 default_mode: SearchMode::Balanced,
157 ef_search: None,
158 max_results: 1000,
159 query_timeout_ms: 30000,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Default, Serialize, Deserialize)]
166#[serde(default)]
167pub struct HnswConfig {
168 pub m: Option<usize>,
171 pub ef_construction: Option<usize>,
174 pub max_layers: usize,
176}
177
178pub mod server {
184 use serde::{Deserialize, Serialize};
185
186 #[derive(Debug, Clone, Serialize, Deserialize)]
188 #[serde(default)]
189 pub struct StorageConfig {
190 pub data_dir: String,
192 pub storage_mode: String,
194 pub mmap_cache_mb: usize,
196 pub vector_alignment: usize,
198 }
199
200 impl Default for StorageConfig {
201 fn default() -> Self {
202 Self {
203 data_dir: "./velesdb_data".to_string(),
204 storage_mode: "mmap".to_string(),
205 mmap_cache_mb: 1024,
206 vector_alignment: 64,
207 }
208 }
209 }
210
211 #[derive(Debug, Clone, Serialize, Deserialize)]
213 #[serde(default)]
214 pub struct ServerConfig {
215 pub host: String,
217 pub port: u16,
219 pub workers: usize,
221 pub max_body_size: usize,
223 pub cors_enabled: bool,
225 pub cors_origins: Vec<String>,
227 }
228
229 impl Default for ServerConfig {
230 fn default() -> Self {
231 Self {
232 host: "127.0.0.1".to_string(),
233 port: 8080,
234 workers: 0,
235 max_body_size: 104_857_600,
236 cors_enabled: false,
237 cors_origins: vec!["*".to_string()],
238 }
239 }
240 }
241
242 #[derive(Debug, Clone, Serialize, Deserialize)]
244 #[serde(default)]
245 pub struct LoggingConfig {
246 pub level: String,
248 pub format: String,
250 pub file: String,
252 }
253
254 impl Default for LoggingConfig {
255 fn default() -> Self {
256 Self {
257 level: "info".to_string(),
258 format: "text".to_string(),
259 file: String::new(),
260 }
261 }
262 }
263}
264
265pub use server::{LoggingConfig, ServerConfig, StorageConfig};
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270#[serde(default)]
271pub struct LimitsConfig {
272 pub max_dimensions: usize,
274 pub max_vectors_per_collection: usize,
276 pub max_collections: usize,
278 pub max_payload_size: usize,
280 pub max_perfect_mode_vectors: usize,
282}
283
284impl Default for LimitsConfig {
285 fn default() -> Self {
286 Self {
287 max_dimensions: 4096,
288 max_vectors_per_collection: 100_000_000,
289 max_collections: 1000,
290 max_payload_size: 1_048_576, max_perfect_mode_vectors: 500_000,
292 }
293 }
294}
295
296#[derive(Debug, Clone, Serialize)]
302pub struct QuantizationConfig {
303 pub mode: QuantizationType,
305 pub rerank_enabled: bool,
307 pub rerank_multiplier: usize,
309 pub auto_quantization: bool,
311 pub auto_quantization_threshold: usize,
313}
314
315impl Default for QuantizationConfig {
316 fn default() -> Self {
317 Self {
318 mode: QuantizationType::None,
319 rerank_enabled: true,
320 rerank_multiplier: 2,
321 auto_quantization: true,
322 auto_quantization_threshold: 10_000,
323 }
324 }
325}
326
327impl QuantizationConfig {
328 #[must_use]
330 pub const fn mode(&self) -> &QuantizationType {
331 &self.mode
332 }
333
334 #[must_use]
336 pub fn should_quantize(&self, vector_count: usize) -> bool {
337 self.auto_quantization && vector_count >= self.auto_quantization_threshold
338 }
339}
340
341impl<'de> Deserialize<'de> for QuantizationConfig {
346 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
347 where
348 D: serde::Deserializer<'de>,
349 {
350 #[derive(Deserialize)]
352 struct RawQuantizationConfig {
353 #[serde(default)]
355 mode: Option<QuantizationType>,
356 #[serde(default)]
358 default_type: Option<String>,
359 #[serde(default = "default_rerank_enabled")]
360 rerank_enabled: bool,
361 #[serde(default = "default_rerank_multiplier")]
362 rerank_multiplier: usize,
363 #[serde(default = "default_auto_quantization")]
364 auto_quantization: bool,
365 #[serde(default = "default_auto_quantization_threshold")]
366 auto_quantization_threshold: usize,
367 }
368
369 fn default_rerank_enabled() -> bool {
370 true
371 }
372 fn default_rerank_multiplier() -> usize {
373 2
374 }
375 fn default_auto_quantization() -> bool {
376 true
377 }
378 fn default_auto_quantization_threshold() -> usize {
379 10_000
380 }
381
382 let raw = RawQuantizationConfig::deserialize(deserializer)?;
383
384 let mode = if let Some(m) = raw.mode {
385 m
386 } else if let Some(ref s) = raw.default_type {
387 match s.as_str() {
388 "none" | "" => QuantizationType::None,
389 "sq8" => QuantizationType::SQ8,
390 "binary" => QuantizationType::Binary,
391 other => {
392 return Err(serde::de::Error::custom(format!(
393 "unknown quantization type: '{other}'"
394 )));
395 }
396 }
397 } else {
398 QuantizationType::None
399 };
400
401 Ok(Self {
402 mode,
403 rerank_enabled: raw.rerank_enabled,
404 rerank_multiplier: raw.rerank_multiplier,
405 auto_quantization: raw.auto_quantization,
406 auto_quantization_threshold: raw.auto_quantization_threshold,
407 })
408 }
409}
410
411const fn default_commit_delay_us() -> u64 {
417 100
418}
419
420const fn default_max_batch_size() -> usize {
422 128
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct WalBatchConfig {
440 #[serde(default)]
442 pub enabled: bool,
443 #[serde(default = "default_commit_delay_us")]
445 pub commit_delay_us: u64,
446 #[serde(default = "default_max_batch_size")]
448 pub max_batch_size: usize,
449}
450
451impl Default for WalBatchConfig {
452 fn default() -> Self {
453 Self {
454 enabled: false,
455 commit_delay_us: 100,
456 max_batch_size: 128,
457 }
458 }
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize, Default)]
463#[serde(default)]
464pub struct VelesConfig {
465 pub search: SearchConfig,
467 pub hnsw: HnswConfig,
469 pub storage: StorageConfig,
471 pub limits: LimitsConfig,
473 pub server: ServerConfig,
475 pub logging: LoggingConfig,
477 pub quantization: QuantizationConfig,
479 pub wal_batch: WalBatchConfig,
481}
482
483impl VelesConfig {
484 pub fn load() -> Result<Self, ConfigError> {
492 Self::load_from_path("velesdb.toml")
493 }
494
495 pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
505 let figment = Figment::new()
506 .merge(Serialized::defaults(Self::default()))
507 .merge(Toml::file(path.as_ref()))
508 .merge(Env::prefixed("VELESDB_").split("_").lowercase(false));
509
510 figment
511 .extract()
512 .map_err(|e| ConfigError::ParseError(e.to_string()))
513 }
514
515 pub fn from_toml(toml_str: &str) -> Result<Self, ConfigError> {
525 let figment = Figment::new()
526 .merge(Serialized::defaults(Self::default()))
527 .merge(Toml::string(toml_str));
528
529 figment
530 .extract()
531 .map_err(|e| ConfigError::ParseError(e.to_string()))
532 }
533
534 pub fn validate(&self) -> Result<(), ConfigError> {
540 if let Some(ef) = self.search.ef_search {
542 if !(16..=4096).contains(&ef) {
543 return Err(ConfigError::InvalidValue {
544 key: "search.ef_search".to_string(),
545 message: format!("value {ef} is out of range [16, 4096]"),
546 });
547 }
548 }
549
550 if self.search.max_results == 0 || self.search.max_results > 10000 {
551 return Err(ConfigError::InvalidValue {
552 key: "search.max_results".to_string(),
553 message: format!(
554 "value {} is out of range [1, 10000]",
555 self.search.max_results
556 ),
557 });
558 }
559
560 if let Some(m) = self.hnsw.m {
562 if !(4..=128).contains(&m) {
563 return Err(ConfigError::InvalidValue {
564 key: "hnsw.m".to_string(),
565 message: format!("value {m} is out of range [4, 128]"),
566 });
567 }
568 }
569
570 if let Some(ef) = self.hnsw.ef_construction {
571 if !(100..=2000).contains(&ef) {
572 return Err(ConfigError::InvalidValue {
573 key: "hnsw.ef_construction".to_string(),
574 message: format!("value {ef} is out of range [100, 2000]"),
575 });
576 }
577 }
578
579 if self.limits.max_dimensions == 0 || self.limits.max_dimensions > 65536 {
581 return Err(ConfigError::InvalidValue {
582 key: "limits.max_dimensions".to_string(),
583 message: format!(
584 "value {} is out of range [1, 65536]",
585 self.limits.max_dimensions
586 ),
587 });
588 }
589
590 if self.server.port < 1024 {
592 return Err(ConfigError::InvalidValue {
593 key: "server.port".to_string(),
594 message: format!("value {} must be >= 1024", self.server.port),
595 });
596 }
597
598 let valid_modes = ["mmap", "memory"];
600 if !valid_modes.contains(&self.storage.storage_mode.as_str()) {
601 return Err(ConfigError::InvalidValue {
602 key: "storage.storage_mode".to_string(),
603 message: format!(
604 "value '{}' is invalid, expected one of: {:?}",
605 self.storage.storage_mode, valid_modes
606 ),
607 });
608 }
609
610 let valid_levels = ["error", "warn", "info", "debug", "trace"];
612 if !valid_levels.contains(&self.logging.level.as_str()) {
613 return Err(ConfigError::InvalidValue {
614 key: "logging.level".to_string(),
615 message: format!(
616 "value '{}' is invalid, expected one of: {:?}",
617 self.logging.level, valid_levels
618 ),
619 });
620 }
621
622 Ok(())
623 }
624
625 #[must_use]
629 pub fn effective_ef_search(&self) -> usize {
630 self.search
631 .ef_search
632 .unwrap_or_else(|| self.search.default_mode.ef_search())
633 }
634
635 pub fn to_toml(&self) -> Result<String, ConfigError> {
641 toml::to_string_pretty(self).map_err(|e| ConfigError::ParseError(e.to_string()))
642 }
643}