use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum MemoryConstraint {
Minimal,
#[default]
Balanced,
Unlimited,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum LatencyRequirement {
UltraLow,
#[default]
Low,
Medium,
Relaxed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum AccuracyRequirement {
Exact,
VeryHigh,
#[default]
High,
Moderate,
Relaxed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataCharacteristics {
pub num_vectors: usize,
pub dimensions: usize,
pub frequent_updates: bool,
pub frequent_deletes: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IndexRequirements {
pub memory: MemoryConstraint,
pub latency: LatencyRequirement,
pub accuracy: AccuracyRequirement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RecommendedIndex {
Flat,
Hnsw,
Ivf,
IvfPq,
IvfSq,
HnswSq,
SpFresh,
}
impl std::fmt::Display for RecommendedIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RecommendedIndex::Flat => write!(f, "Flat (Brute Force)"),
RecommendedIndex::Hnsw => write!(f, "HNSW"),
RecommendedIndex::Ivf => write!(f, "IVF"),
RecommendedIndex::IvfPq => write!(f, "IVF-PQ"),
RecommendedIndex::IvfSq => write!(f, "IVF-SQ"),
RecommendedIndex::HnswSq => write!(f, "HNSW-SQ"),
RecommendedIndex::SpFresh => write!(f, "SPFresh"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexRecommendation {
pub primary: RecommendedIndex,
pub alternatives: Vec<RecommendedIndex>,
pub reasoning: String,
pub estimated_memory_bytes: usize,
pub estimated_latency_ms: f32,
pub estimated_recall: f32,
pub suggested_params: IndexParams,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IndexParams {
pub hnsw_m: Option<usize>,
pub hnsw_ef_construction: Option<usize>,
pub hnsw_ef_search: Option<usize>,
pub ivf_num_clusters: Option<usize>,
pub ivf_num_probes: Option<usize>,
pub pq_num_subquantizers: Option<usize>,
pub pq_bits_per_subquantizer: Option<usize>,
pub sq_bits: Option<usize>,
}
pub struct AutoIndexSelector;
impl AutoIndexSelector {
pub fn select(
data: &DataCharacteristics,
requirements: &IndexRequirements,
) -> IndexRecommendation {
let num_vectors = data.num_vectors;
let _dimensions = data.dimensions;
let (primary, alternatives, base_reasoning) = if num_vectors < 10_000 {
if requirements.accuracy == AccuracyRequirement::Exact {
(
RecommendedIndex::Flat,
vec![RecommendedIndex::Hnsw],
"Small dataset (<10K vectors) - flat index provides exact results efficiently",
)
} else {
(
RecommendedIndex::Hnsw,
vec![RecommendedIndex::Flat, RecommendedIndex::Ivf],
"Small dataset (<10K vectors) - HNSW provides fast approximate search",
)
}
} else if num_vectors < 100_000 {
match requirements.memory {
MemoryConstraint::Minimal => {
(
RecommendedIndex::IvfSq,
vec![RecommendedIndex::IvfPq, RecommendedIndex::Ivf],
"Medium dataset (10K-100K) with memory constraints - IVF-SQ balances memory and accuracy",
)
}
MemoryConstraint::Balanced => {
match requirements.latency {
LatencyRequirement::UltraLow | LatencyRequirement::Low => {
(
RecommendedIndex::Hnsw,
vec![RecommendedIndex::HnswSq, RecommendedIndex::Ivf],
"Medium dataset (10K-100K) with low latency requirement - HNSW provides fast queries",
)
}
_ => {
(
RecommendedIndex::Ivf,
vec![RecommendedIndex::Hnsw, RecommendedIndex::IvfPq],
"Medium dataset (10K-100K) - IVF provides good balance of speed and memory",
)
}
}
}
MemoryConstraint::Unlimited => {
(
RecommendedIndex::Hnsw,
vec![RecommendedIndex::Ivf],
"Medium dataset (10K-100K) with no memory constraints - HNSW provides best query performance",
)
}
}
} else if num_vectors < 1_000_000 {
match requirements.memory {
MemoryConstraint::Minimal => {
(
RecommendedIndex::IvfPq,
vec![RecommendedIndex::IvfSq, RecommendedIndex::SpFresh],
"Large dataset (100K-1M) with memory constraints - IVF-PQ provides best compression",
)
}
MemoryConstraint::Balanced => {
if data.frequent_updates || data.frequent_deletes {
(
RecommendedIndex::SpFresh,
vec![RecommendedIndex::Hnsw, RecommendedIndex::Ivf],
"Large dataset (100K-1M) with frequent updates - SPFresh handles streaming updates well",
)
} else {
match requirements.latency {
LatencyRequirement::UltraLow | LatencyRequirement::Low => {
(
RecommendedIndex::HnswSq,
vec![RecommendedIndex::Hnsw, RecommendedIndex::IvfPq],
"Large dataset (100K-1M) with low latency requirement - HNSW-SQ balances speed and memory",
)
}
_ => {
(
RecommendedIndex::IvfPq,
vec![RecommendedIndex::IvfSq, RecommendedIndex::Hnsw],
"Large dataset (100K-1M) - IVF-PQ provides good compression with acceptable latency",
)
}
}
}
}
MemoryConstraint::Unlimited => {
(
RecommendedIndex::Hnsw,
vec![RecommendedIndex::HnswSq],
"Large dataset (100K-1M) with no memory constraints - HNSW provides best query performance",
)
}
}
} else {
match requirements.memory {
MemoryConstraint::Minimal => {
(
RecommendedIndex::IvfPq,
vec![RecommendedIndex::SpFresh],
"Very large dataset (>1M) with memory constraints - IVF-PQ is essential for memory efficiency",
)
}
MemoryConstraint::Balanced => {
if data.frequent_updates || data.frequent_deletes {
(
RecommendedIndex::SpFresh,
vec![RecommendedIndex::IvfPq],
"Very large dataset (>1M) with frequent updates - SPFresh handles streaming workloads",
)
} else {
(
RecommendedIndex::IvfPq,
vec![RecommendedIndex::HnswSq, RecommendedIndex::SpFresh],
"Very large dataset (>1M) - IVF-PQ provides best memory/performance tradeoff",
)
}
}
MemoryConstraint::Unlimited => {
(
RecommendedIndex::HnswSq,
vec![RecommendedIndex::Hnsw, RecommendedIndex::IvfPq],
"Very large dataset (>1M) with no memory constraints - HNSW-SQ provides fast queries with reduced memory",
)
}
}
};
let (estimated_memory, estimated_latency, estimated_recall) =
Self::estimate_performance(&primary, data, requirements);
let suggested_params = Self::suggest_params(&primary, data, requirements);
IndexRecommendation {
primary,
alternatives,
reasoning: base_reasoning.to_string(),
estimated_memory_bytes: estimated_memory,
estimated_latency_ms: estimated_latency,
estimated_recall,
suggested_params,
}
}
fn estimate_performance(
index_type: &RecommendedIndex,
data: &DataCharacteristics,
requirements: &IndexRequirements,
) -> (usize, f32, f32) {
let n = data.num_vectors;
let d = data.dimensions;
let base_vector_size = n * d * 4;
match index_type {
RecommendedIndex::Flat => {
let memory = base_vector_size;
let latency = (n as f32 / 100_000.0) * 10.0; (memory, latency, 1.0) }
RecommendedIndex::Hnsw => {
let memory = base_vector_size * 2;
let latency = ((n as f32).log2() * 0.1).max(0.5); let recall = match requirements.accuracy {
AccuracyRequirement::Exact => 0.999,
AccuracyRequirement::VeryHigh => 0.995,
_ => 0.98,
};
(memory, latency, recall)
}
RecommendedIndex::Ivf => {
let num_clusters = (n as f32).sqrt() as usize;
let memory = base_vector_size + num_clusters * d * 4;
let latency = (n as f32 / num_clusters as f32 / 10_000.0) * 5.0;
(memory, latency.max(1.0), 0.95)
}
RecommendedIndex::IvfPq => {
let compression_ratio = 32; let memory = base_vector_size / compression_ratio;
let latency = 5.0 + (n as f32 / 1_000_000.0) * 2.0;
(memory, latency, 0.90)
}
RecommendedIndex::IvfSq => {
let memory = base_vector_size / 4;
let latency = 3.0 + (n as f32 / 500_000.0) * 2.0;
(memory, latency, 0.95)
}
RecommendedIndex::HnswSq => {
let memory = (base_vector_size / 4) * 2; let latency = ((n as f32).log2() * 0.15).max(0.5);
(memory, latency, 0.96)
}
RecommendedIndex::SpFresh => {
let memory = base_vector_size / 2; let latency = 10.0 + (n as f32 / 100_000.0) * 1.0;
(memory, latency, 0.92)
}
}
}
fn suggest_params(
index_type: &RecommendedIndex,
data: &DataCharacteristics,
requirements: &IndexRequirements,
) -> IndexParams {
let mut params = IndexParams::default();
let n = data.num_vectors;
let d = data.dimensions;
match index_type {
RecommendedIndex::Hnsw | RecommendedIndex::HnswSq => {
params.hnsw_m = Some(match requirements.accuracy {
AccuracyRequirement::Exact | AccuracyRequirement::VeryHigh => 32,
AccuracyRequirement::High => 16,
_ => 12,
});
params.hnsw_ef_construction = Some(params.hnsw_m.unwrap_or(16) * 10);
params.hnsw_ef_search = Some(match requirements.latency {
LatencyRequirement::UltraLow => 50,
LatencyRequirement::Low => 100,
LatencyRequirement::Medium => 200,
LatencyRequirement::Relaxed => 400,
});
if matches!(index_type, RecommendedIndex::HnswSq) {
params.sq_bits = Some(8);
}
}
RecommendedIndex::Ivf | RecommendedIndex::IvfPq | RecommendedIndex::IvfSq => {
let num_clusters = ((n as f32).sqrt() as usize).clamp(16, 65536);
params.ivf_num_clusters = Some(num_clusters);
params.ivf_num_probes = Some(
match requirements.accuracy {
AccuracyRequirement::Exact | AccuracyRequirement::VeryHigh => {
num_clusters / 4
}
AccuracyRequirement::High => num_clusters / 8,
AccuracyRequirement::Moderate => num_clusters / 16,
AccuracyRequirement::Relaxed => num_clusters / 32,
}
.max(1),
);
if matches!(index_type, RecommendedIndex::IvfPq) {
let num_subquantizers = (d / 4).clamp(1, 64);
params.pq_num_subquantizers = Some(num_subquantizers);
params.pq_bits_per_subquantizer = Some(8);
}
if matches!(index_type, RecommendedIndex::IvfSq) {
params.sq_bits = Some(8);
}
}
RecommendedIndex::SpFresh => {
let num_clusters = ((n as f32).sqrt() as usize).clamp(16, 4096);
params.ivf_num_clusters = Some(num_clusters);
params.ivf_num_probes = Some((num_clusters / 10).max(1));
}
RecommendedIndex::Flat => {
}
}
params
}
pub fn quick_select(num_vectors: usize, dimensions: usize) -> RecommendedIndex {
let data = DataCharacteristics {
num_vectors,
dimensions,
frequent_updates: false,
frequent_deletes: false,
};
let requirements = IndexRequirements::default();
Self::select(&data, &requirements).primary
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_small_dataset() {
let data = DataCharacteristics {
num_vectors: 1000,
dimensions: 128,
frequent_updates: false,
frequent_deletes: false,
};
let requirements = IndexRequirements::default();
let rec = AutoIndexSelector::select(&data, &requirements);
assert!(matches!(
rec.primary,
RecommendedIndex::Hnsw | RecommendedIndex::Flat
));
}
#[test]
fn test_large_dataset_memory_constrained() {
let data = DataCharacteristics {
num_vectors: 5_000_000,
dimensions: 768,
frequent_updates: false,
frequent_deletes: false,
};
let requirements = IndexRequirements {
memory: MemoryConstraint::Minimal,
latency: LatencyRequirement::Medium,
accuracy: AccuracyRequirement::Moderate,
};
let rec = AutoIndexSelector::select(&data, &requirements);
assert_eq!(rec.primary, RecommendedIndex::IvfPq);
assert!(rec.estimated_memory_bytes < data.num_vectors * data.dimensions * 4 / 10);
}
#[test]
fn test_streaming_workload() {
let data = DataCharacteristics {
num_vectors: 500_000,
dimensions: 256,
frequent_updates: true,
frequent_deletes: true,
};
let requirements = IndexRequirements {
memory: MemoryConstraint::Balanced,
latency: LatencyRequirement::Medium,
accuracy: AccuracyRequirement::High,
};
let rec = AutoIndexSelector::select(&data, &requirements);
assert_eq!(rec.primary, RecommendedIndex::SpFresh);
}
#[test]
fn test_exact_search() {
let data = DataCharacteristics {
num_vectors: 5000,
dimensions: 64,
frequent_updates: false,
frequent_deletes: false,
};
let requirements = IndexRequirements {
memory: MemoryConstraint::Unlimited,
latency: LatencyRequirement::Relaxed,
accuracy: AccuracyRequirement::Exact,
};
let rec = AutoIndexSelector::select(&data, &requirements);
assert_eq!(rec.primary, RecommendedIndex::Flat);
assert_eq!(rec.estimated_recall, 1.0);
}
#[test]
fn test_quick_select() {
let small = AutoIndexSelector::quick_select(5000, 128);
assert!(matches!(
small,
RecommendedIndex::Hnsw | RecommendedIndex::Flat
));
let medium = AutoIndexSelector::quick_select(50_000, 256);
assert!(matches!(
medium,
RecommendedIndex::Hnsw | RecommendedIndex::Ivf
));
let large = AutoIndexSelector::quick_select(2_000_000, 512);
assert!(matches!(
large,
RecommendedIndex::IvfPq | RecommendedIndex::HnswSq
));
}
#[test]
fn test_suggested_params() {
let data = DataCharacteristics {
num_vectors: 100_000,
dimensions: 128,
frequent_updates: false,
frequent_deletes: false,
};
let requirements = IndexRequirements::default();
let rec = AutoIndexSelector::select(&data, &requirements);
if matches!(rec.primary, RecommendedIndex::Hnsw) {
assert!(rec.suggested_params.hnsw_m.is_some());
assert!(rec.suggested_params.hnsw_ef_construction.is_some());
assert!(rec.suggested_params.hnsw_ef_search.is_some());
}
}
}