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 => 256,
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
411#[derive(Debug, Clone, Serialize, Deserialize, Default)]
413#[serde(default)]
414pub struct VelesConfig {
415 pub search: SearchConfig,
417 pub hnsw: HnswConfig,
419 pub storage: StorageConfig,
421 pub limits: LimitsConfig,
423 pub server: ServerConfig,
425 pub logging: LoggingConfig,
427 pub quantization: QuantizationConfig,
429}
430
431impl VelesConfig {
432 pub fn load() -> Result<Self, ConfigError> {
440 Self::load_from_path("velesdb.toml")
441 }
442
443 pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
453 let figment = Figment::new()
454 .merge(Serialized::defaults(Self::default()))
455 .merge(Toml::file(path.as_ref()))
456 .merge(Env::prefixed("VELESDB_").split("_").lowercase(false));
457
458 figment
459 .extract()
460 .map_err(|e| ConfigError::ParseError(e.to_string()))
461 }
462
463 pub fn from_toml(toml_str: &str) -> Result<Self, ConfigError> {
473 let figment = Figment::new()
474 .merge(Serialized::defaults(Self::default()))
475 .merge(Toml::string(toml_str));
476
477 figment
478 .extract()
479 .map_err(|e| ConfigError::ParseError(e.to_string()))
480 }
481
482 pub fn validate(&self) -> Result<(), ConfigError> {
488 if let Some(ef) = self.search.ef_search {
490 if !(16..=4096).contains(&ef) {
491 return Err(ConfigError::InvalidValue {
492 key: "search.ef_search".to_string(),
493 message: format!("value {ef} is out of range [16, 4096]"),
494 });
495 }
496 }
497
498 if self.search.max_results == 0 || self.search.max_results > 10000 {
499 return Err(ConfigError::InvalidValue {
500 key: "search.max_results".to_string(),
501 message: format!(
502 "value {} is out of range [1, 10000]",
503 self.search.max_results
504 ),
505 });
506 }
507
508 if let Some(m) = self.hnsw.m {
510 if !(4..=128).contains(&m) {
511 return Err(ConfigError::InvalidValue {
512 key: "hnsw.m".to_string(),
513 message: format!("value {m} is out of range [4, 128]"),
514 });
515 }
516 }
517
518 if let Some(ef) = self.hnsw.ef_construction {
519 if !(100..=2000).contains(&ef) {
520 return Err(ConfigError::InvalidValue {
521 key: "hnsw.ef_construction".to_string(),
522 message: format!("value {ef} is out of range [100, 2000]"),
523 });
524 }
525 }
526
527 if self.limits.max_dimensions == 0 || self.limits.max_dimensions > 65536 {
529 return Err(ConfigError::InvalidValue {
530 key: "limits.max_dimensions".to_string(),
531 message: format!(
532 "value {} is out of range [1, 65536]",
533 self.limits.max_dimensions
534 ),
535 });
536 }
537
538 if self.server.port < 1024 {
540 return Err(ConfigError::InvalidValue {
541 key: "server.port".to_string(),
542 message: format!("value {} must be >= 1024", self.server.port),
543 });
544 }
545
546 let valid_modes = ["mmap", "memory"];
548 if !valid_modes.contains(&self.storage.storage_mode.as_str()) {
549 return Err(ConfigError::InvalidValue {
550 key: "storage.storage_mode".to_string(),
551 message: format!(
552 "value '{}' is invalid, expected one of: {:?}",
553 self.storage.storage_mode, valid_modes
554 ),
555 });
556 }
557
558 let valid_levels = ["error", "warn", "info", "debug", "trace"];
560 if !valid_levels.contains(&self.logging.level.as_str()) {
561 return Err(ConfigError::InvalidValue {
562 key: "logging.level".to_string(),
563 message: format!(
564 "value '{}' is invalid, expected one of: {:?}",
565 self.logging.level, valid_levels
566 ),
567 });
568 }
569
570 Ok(())
571 }
572
573 #[must_use]
577 pub fn effective_ef_search(&self) -> usize {
578 self.search
579 .ef_search
580 .unwrap_or_else(|| self.search.default_mode.ef_search())
581 }
582
583 pub fn to_toml(&self) -> Result<String, ConfigError> {
589 toml::to_string_pretty(self).map_err(|e| ConfigError::ParseError(e.to_string()))
590 }
591}