use crate::core::entangled::EntangledHVec;
const BRUTE_FORCE_THRESHOLD: usize = 1000;
const HOPFIELD_MAX_PATTERNS: usize = 10_000;
const SPARSE_INDEX_THRESHOLD: usize = 4;
const EF_SEARCH_MIN: usize = 128;
const EF_SEARCH_MAX: usize = 512;
const N_PROBE_MIN: usize = 4;
const N_PROBE_MAX: usize = 64;
const INVERTED_SPARSITY_DENOM: usize = 32;
#[derive(Debug, PartialEq, Clone, Copy)]
#[allow(clippy::upper_case_acronyms)]
pub(crate) enum IndexRoute {
NSG,
Inverted,
IVF,
Hopfield,
BruteForce,
}
pub(crate) struct QueryPlan {
pub route: IndexRoute,
pub ef_search: usize,
pub n_probe: usize,
pub rationale: &'static str,
}
impl IndexRoute {
pub fn as_str(self) -> &'static str {
match self {
Self::NSG => "nsg",
Self::Inverted => "inverted",
Self::IVF => "ivf",
Self::Hopfield => "hopfield",
Self::BruteForce => "brute_force",
}
}
}
pub(crate) struct QueryPlanner {
pub nsg_available: bool,
pub inverted_available: bool,
pub ivf_available: bool,
pub vector_count: usize,
pub dimensions: usize,
}
impl QueryPlanner {
pub fn new(
nsg_available: bool,
inverted_available: bool,
ivf_available: bool,
vector_count: usize,
dimensions: usize,
) -> Self {
Self {
nsg_available,
inverted_available,
ivf_available,
vector_count,
dimensions,
}
}
pub fn plan(&self, query_vec: &EntangledHVec, k: u32) -> QueryPlan {
let n = self.vector_count;
let s = query_vec.indices.len(); let k_idx = k as usize;
let ef_search = (k_idx * 2).clamp(EF_SEARCH_MIN, EF_SEARCH_MAX);
let n_probe = (k_idx / 8).clamp(N_PROBE_MIN, N_PROBE_MAX);
if n < BRUTE_FORCE_THRESHOLD {
return QueryPlan {
route: IndexRoute::BruteForce,
ef_search,
n_probe,
rationale: "small collections are faster with an exact scan",
};
}
if self.inverted_available && s <= SPARSE_INDEX_THRESHOLD {
return QueryPlan {
route: IndexRoute::Inverted,
ef_search,
n_probe,
rationale: "very sparse queries favor the inverted index",
};
}
if self.nsg_available {
return QueryPlan {
route: IndexRoute::NSG,
ef_search,
n_probe,
rationale: "a trained NSG index is preferred for approximate retrieval",
};
}
if self.ivf_available {
return QueryPlan {
route: IndexRoute::IVF,
ef_search,
n_probe,
rationale: "a trained IVF index is available",
};
}
if self.inverted_available && s < (self.dimensions / INVERTED_SPARSITY_DENOM) {
return QueryPlan {
route: IndexRoute::Inverted,
ef_search,
n_probe,
rationale: "the inverted index is the only applicable trained index",
};
}
if n <= HOPFIELD_MAX_PATTERNS {
return QueryPlan {
route: IndexRoute::Hopfield,
ef_search,
n_probe,
rationale: "mid-sized unindexed collections use associative retrieval",
};
}
QueryPlan {
route: IndexRoute::BruteForce,
ef_search,
n_probe,
rationale: "no trained index is available, so HMS falls back to exact search",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::entangled::EntangledHVec;
#[test]
fn plan_small_collection_uses_brute_force() {
let planner = QueryPlanner::new(true, true, true, 500, 1000);
let q = EntangledHVec::from_indices(vec![1, 2, 3], 1000);
let plan = planner.plan(&q, 10);
assert_eq!(plan.route, IndexRoute::BruteForce);
}
#[test]
fn plan_high_sparsity_uses_inverted() {
let planner = QueryPlanner::new(true, true, true, 5000, 1000);
let q = EntangledHVec::from_indices(vec![1, 2], 1000);
let plan = planner.plan(&q, 10);
assert_eq!(plan.route, IndexRoute::Inverted);
}
#[test]
fn plan_mid_collection_no_indices_uses_hopfield() {
let planner = QueryPlanner::new(false, false, false, 5000, 1000);
let q = EntangledHVec::from_indices((0..10).collect(), 1000);
let plan = planner.plan(&q, 10);
assert_eq!(plan.route, IndexRoute::Hopfield);
}
#[test]
fn plan_large_collection_no_indices_uses_brute_force() {
let planner = QueryPlanner::new(false, false, false, 20_000, 1000);
let q = EntangledHVec::from_indices((0..10).collect(), 1000);
let plan = planner.plan(&q, 10);
assert_eq!(plan.route, IndexRoute::BruteForce);
}
#[test]
fn plan_adjusts_ef_search_for_large_k() {
let planner = QueryPlanner::new(true, true, true, 5000, 1000);
let q = EntangledHVec::from_indices((0..10).collect(), 1000);
let plan = planner.plan(&q, 100);
assert!(plan.ef_search >= 100);
assert_eq!(plan.route, IndexRoute::NSG);
}
}