use crate::quantization::StorageMode;
use serde::{Deserialize, Serialize};
const fn default_alpha() -> f32 {
1.2
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct HnswParams {
pub max_connections: usize,
pub ef_construction: usize,
pub max_elements: usize,
#[serde(default)]
pub storage_mode: StorageMode,
#[serde(default = "default_alpha")]
pub alpha: f32,
}
impl Eq for HnswParams {}
impl Default for HnswParams {
fn default() -> Self {
Self::auto(768)
}
}
impl HnswParams {
#[must_use]
pub fn auto(dimension: usize) -> Self {
match dimension {
0..=256 => Self {
max_connections: 24,
ef_construction: 300,
max_elements: 100_000,
storage_mode: StorageMode::Full,
alpha: default_alpha(),
},
_ => Self {
max_connections: 32,
ef_construction: 400,
max_elements: 100_000,
storage_mode: StorageMode::Full,
alpha: default_alpha(),
},
}
}
#[must_use]
pub fn for_dataset_size(dimension: usize, expected_vectors: usize) -> Self {
let (m_low, ef_low, m_high, ef_high, max_elems) = match expected_vectors {
0..=10_000 => (24, 200, 32, 400, 20_000),
10_001..=100_000 => (64, 800, 128, 1600, 150_000),
100_001..=500_000 => (96, 1200, 128, 2000, 750_000),
_ => (64, 800, 128, 1600, 1_500_000),
};
let (m, ef) = if dimension <= 256 {
(m_low, ef_low)
} else {
(m_high, ef_high)
};
Self {
max_connections: m,
ef_construction: ef,
max_elements: max_elems,
storage_mode: StorageMode::Full,
alpha: default_alpha(),
}
}
#[must_use]
pub fn large_dataset(dimension: usize) -> Self {
Self::for_dataset_size(dimension, 500_000)
}
#[must_use]
pub fn million_scale(dimension: usize) -> Self {
Self::for_dataset_size(dimension, 1_000_000)
}
#[must_use]
pub fn fast() -> Self {
Self {
max_connections: 16,
ef_construction: 150,
max_elements: 100_000,
storage_mode: StorageMode::Full,
alpha: default_alpha(),
}
}
#[must_use]
pub fn turbo() -> Self {
Self {
max_connections: 12,
ef_construction: 100,
max_elements: 100_000,
storage_mode: StorageMode::Full,
alpha: default_alpha(),
}
}
#[must_use]
pub fn high_recall(dimension: usize) -> Self {
let base = Self::auto(dimension);
Self {
max_connections: base.max_connections + 8,
ef_construction: base.ef_construction + 200,
..base
}
}
#[must_use]
pub fn max_recall(dimension: usize) -> Self {
match dimension {
0..=256 => Self {
max_connections: 32,
ef_construction: 500,
max_elements: 100_000,
storage_mode: StorageMode::Full,
alpha: default_alpha(),
},
257..=768 => Self {
max_connections: 48,
ef_construction: 800,
max_elements: 100_000,
storage_mode: StorageMode::Full,
alpha: default_alpha(),
},
_ => Self {
max_connections: 64,
ef_construction: 1000,
max_elements: 100_000,
storage_mode: StorageMode::Full,
alpha: default_alpha(),
},
}
}
#[must_use]
pub fn fast_indexing(dimension: usize) -> Self {
let base = Self::auto(dimension);
Self {
max_connections: (base.max_connections / 2).max(8),
ef_construction: base.ef_construction / 2,
..base
}
}
#[must_use]
pub const fn custom(
max_connections: usize,
ef_construction: usize,
max_elements: usize,
) -> Self {
Self {
max_connections,
ef_construction,
max_elements,
storage_mode: StorageMode::Full,
alpha: 1.2,
}
}
#[must_use]
pub fn with_sq8(dimension: usize) -> Self {
let mut params = Self::auto(dimension);
params.storage_mode = StorageMode::SQ8;
params
}
#[must_use]
pub fn with_binary(dimension: usize) -> Self {
let mut params = Self::auto(dimension);
params.storage_mode = StorageMode::Binary;
params
}
#[must_use]
pub const fn with_alpha(self, alpha: f32) -> Self {
Self { alpha, ..self }
}
pub fn validate(&self) -> crate::error::Result<()> {
if !self.alpha.is_finite() || self.alpha < 1.0 {
return Err(crate::error::Error::Config(format!(
"hnsw alpha must be finite and >= 1.0 (VAMANA range), got {}",
self.alpha
)));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SearchQuality {
Fast,
#[default]
Balanced,
Accurate,
Perfect,
Custom(usize),
Adaptive {
min_ef: usize,
max_ef: usize,
},
AutoTune,
}
impl SearchQuality {
#[must_use]
pub fn ef_search(&self, k: usize) -> usize {
match self {
Self::Fast => 96.max(k * 3),
Self::Balanced | Self::AutoTune => 160.max(k * 5),
Self::Accurate => 512.max(k * 16),
Self::Perfect => 4096.max(k * 100),
Self::Custom(ef) => (*ef).max(k),
Self::Adaptive { min_ef, .. } => (*min_ef).max(k),
}
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub fn ef_search_for_scale(&self, k: usize, dataset_size: usize) -> usize {
let base = self.ef_search(k);
if dataset_size <= 10_000 {
return base;
}
let ratio = dataset_size as f64 / 10_000.0;
let scale = ratio.sqrt().min(2.0);
let scaled = (base as f64 * scale) as usize;
scaled.min(base * 2)
}
#[must_use]
pub const fn is_adaptive(&self) -> bool {
matches!(self, Self::Adaptive { .. } | Self::AutoTune)
}
#[must_use]
pub const fn adaptive_max_ef(&self) -> Option<usize> {
match self {
Self::Adaptive { max_ef, .. } => Some(*max_ef),
_ => None,
}
}
}