use figment::{
providers::{Env, Format, Serialized, Toml},
Figment,
};
use serde::{Deserialize, Serialize};
use std::path::Path;
use thiserror::Error;
pub use crate::config_quantization::{QuantizationConfig, QuantizationType};
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ConfigError {
#[error("Failed to parse configuration: {0}")]
ParseError(String),
#[error("Invalid configuration value for '{key}': {message}")]
InvalidValue {
key: String,
message: String,
},
#[error("Configuration file not found: {0}")]
FileNotFound(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SearchMode {
Fast,
#[default]
Balanced,
Accurate,
Perfect,
}
impl SearchMode {
#[must_use]
pub fn ef_search(&self) -> usize {
match self {
Self::Fast => 96,
Self::Balanced => 160,
Self::Accurate => 512,
Self::Perfect => usize::MAX, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SearchConfig {
pub default_mode: SearchMode,
pub ef_search: Option<usize>,
pub max_results: usize,
pub query_timeout_ms: u64,
}
impl Default for SearchConfig {
fn default() -> Self {
Self {
default_mode: SearchMode::Balanced,
ef_search: None,
max_results: 1000,
query_timeout_ms: 30000,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct HnswConfig {
pub m: Option<usize>,
pub ef_construction: Option<usize>,
pub max_layers: usize,
}
pub mod server {
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct StorageConfig {
pub data_dir: String,
pub storage_mode: String,
pub mmap_cache_mb: usize,
pub vector_alignment: usize,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
data_dir: "./velesdb_data".to_string(),
storage_mode: "mmap".to_string(),
mmap_cache_mb: 1024,
vector_alignment: 64,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub workers: usize,
pub max_body_size: usize,
pub cors_enabled: bool,
pub cors_origins: Vec<String>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8080,
workers: 0,
max_body_size: 104_857_600,
cors_enabled: false,
cors_origins: vec!["*".to_string()],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct LoggingConfig {
pub level: String,
pub format: String,
pub file: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
format: "text".to_string(),
file: String::new(),
}
}
}
}
pub use server::{LoggingConfig, ServerConfig, StorageConfig};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[non_exhaustive]
pub struct LimitsConfig {
pub max_dimensions: usize,
pub max_vectors_per_collection: usize,
pub max_collections: usize,
pub max_payload_size: usize,
pub max_perfect_mode_vectors: usize,
}
impl Default for LimitsConfig {
fn default() -> Self {
Self {
max_dimensions: 4096,
max_vectors_per_collection: 100_000_000,
max_collections: 1000,
max_payload_size: 1_048_576, max_perfect_mode_vectors: 500_000,
}
}
}
const fn default_commit_delay_us() -> u64 {
100
}
const fn default_max_batch_size() -> usize {
128
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalBatchConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_commit_delay_us")]
pub commit_delay_us: u64,
#[serde(default = "default_max_batch_size")]
pub max_batch_size: usize,
}
impl Default for WalBatchConfig {
fn default() -> Self {
Self {
enabled: false,
commit_delay_us: 100,
max_batch_size: 128,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct VelesConfig {
pub search: SearchConfig,
pub hnsw: HnswConfig,
pub storage: StorageConfig,
pub limits: LimitsConfig,
pub server: ServerConfig,
pub logging: LoggingConfig,
pub quantization: QuantizationConfig,
pub wal_batch: WalBatchConfig,
}
impl VelesConfig {
pub fn load() -> Result<Self, ConfigError> {
Self::load_from_path("velesdb.toml")
}
pub fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
let figment = Figment::new()
.merge(Serialized::defaults(Self::default()))
.merge(Toml::file(path.as_ref()))
.merge(Env::prefixed("VELESDB_").split("_").lowercase(false));
Self::finish(&figment)
}
pub fn from_toml(toml_str: &str) -> Result<Self, ConfigError> {
let figment = Figment::new()
.merge(Serialized::defaults(Self::default()))
.merge(Toml::string(toml_str));
Self::finish(&figment)
}
const ENGINE_SECTIONS: &'static [&'static str] = &[
"search",
"hnsw",
"storage",
"limits",
"quantization",
"wal_batch",
];
fn filter_to_engine_sections(raw: &str) -> Result<String, ConfigError> {
let mut doc: toml::Value =
toml::from_str(raw).map_err(|e| ConfigError::ParseError(e.to_string()))?;
if let Some(table) = doc.as_table_mut() {
table.retain(|k, _| Self::ENGINE_SECTIONS.contains(&k));
}
toml::to_string(&doc).map_err(|e| ConfigError::ParseError(e.to_string()))
}
pub fn load_from_path_engine_only<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
let raw = std::fs::read_to_string(path.as_ref())?;
let filtered = Self::filter_to_engine_sections(&raw)?;
let figment = Figment::new()
.merge(Serialized::defaults(Self::default()))
.merge(Toml::string(&filtered))
.merge(Env::prefixed("VELESDB_").split("_").lowercase(false));
Self::finish(&figment)
}
fn finish(figment: &Figment) -> Result<Self, ConfigError> {
let config: Self = figment
.extract()
.map_err(|e| ConfigError::ParseError(e.to_string()))?;
config.validate()?;
Ok(config)
}
pub fn from_toml_engine_only(toml_str: &str) -> Result<Self, ConfigError> {
let filtered = Self::filter_to_engine_sections(toml_str)?;
Self::from_toml(&filtered)
}
#[deprecated(
since = "5.2.0",
note = "never read by the engine — [search] is not applied (issue #2087); \
query-time WITH (ef_search = N) is the working override"
)]
#[must_use]
pub fn effective_ef_search(&self) -> usize {
self.search
.ef_search
.unwrap_or_else(|| self.search.default_mode.ef_search())
}
pub fn to_toml(&self) -> Result<String, ConfigError> {
toml::to_string_pretty(self).map_err(|e| ConfigError::ParseError(e.to_string()))
}
}
#[cfg(test)]
#[path = "shared_toml_tests.rs"]
mod shared_toml_tests;