use std::sync::{Arc, Mutex};
use nodedb_types::error::NodeDbResult;
use nodedb_types::filter::MetadataFilter;
use nodedb_types::result::SearchResult;
use nodedb_vector::rerank::{Candidate, IndexShape, recall_scale, validate_options};
use crate::engine::crdt::CrdtEngine;
use crate::engine::vector::sidecar::install_sidecar_for_index;
use crate::engine::vector::{IndexIdMap, VectorState};
use crate::error::LiteError;
use crate::nodedb::lock_ext::LockExt;
use crate::storage::engine::StorageEngine;
use super::filter::allowed_nodes;
use super::hydrate::Hydration;
use super::lazy_load;
#[derive(Clone, Copy, Default)]
pub(crate) struct SearchFilter<'a> {
pub metadata: Option<&'a MetadataFilter>,
pub exclude_fields: &'a [&'a str],
pub prefilter_bitmap: Option<&'a roaring::RoaringBitmap>,
}
#[derive(Clone, Copy, Default)]
pub(crate) struct AnnTuning<'a> {
pub options: Option<&'a nodedb_types::VectorAnnOptions>,
pub metric: Option<nodedb_types::vector_distance::DistanceMetric>,
pub ef_search: Option<usize>,
}
pub(crate) struct VectorSearchRequest<'a> {
pub index_key: &'a str,
pub collection: &'a str,
pub query: &'a [f32],
pub k: usize,
pub filter: SearchFilter<'a>,
pub tuning: AnnTuning<'a>,
pub skip_payload_fetch: bool,
}
pub(crate) async fn run_vector_search<S>(
vector_state: &Arc<VectorState<S>>,
crdt: &Arc<Mutex<CrdtEngine>>,
request: VectorSearchRequest<'_>,
) -> NodeDbResult<Vec<SearchResult>>
where
S: StorageEngine,
{
let VectorSearchRequest {
index_key,
collection,
query,
k,
filter:
SearchFilter {
metadata: filter,
exclude_fields,
prefilter_bitmap,
},
tuning:
AnnTuning {
options: ann_options,
metric,
ef_search: ef_search_caller,
},
skip_payload_fetch,
} = request;
let default_opts;
let opts: &nodedb_types::VectorAnnOptions = match ann_options {
Some(o) => o,
None => {
default_opts = nodedb_types::VectorAnnOptions::default();
&default_opts
}
};
let collection_quant = {
let configs = vector_state
.per_index_config
.lock()
.map_err(|_| LiteError::LockPoisoned)?;
configs
.get(index_key)
.map(|cfg| cfg.quantization)
.unwrap_or(nodedb_types::VectorQuantization::None)
};
let rerank_codec =
validate_options(opts, IndexShape::SingleVector, collection_quant).map_err(|e| {
LiteError::BadRequest {
detail: e.to_string(),
}
})?;
if let Some(codec_name) = rerank_codec {
lazy_load::ensure_index_loaded(vector_state, index_key).await?;
install_sidecar_for_index(vector_state, index_key, codec_name)?;
}
let (scaled_ef, scaled_oversample) = recall_scale(
opts.target_recall,
vector_state.search_ef,
opts.oversample.unwrap_or(1).max(1),
)
.map_err(|e| LiteError::BadRequest {
detail: e.to_string(),
})?;
let ef_search = opts
.ef_search_override
.or(ef_search_caller)
.unwrap_or(scaled_ef);
let indices = crate::engine::vector::resident::lock_resident(vector_state, index_key).await?;
let Some(index) = indices.get(index_key) else {
return Ok(Vec::new());
};
nodedb_vector::error::check_dim(index.dim(), query.len()).map_err(LiteError::from)?;
let id_map = vector_state.vector_id_map.lock_or_recover();
let ids = id_map.index(index_key);
let crdt_guard = crdt.lock_or_recover();
let needs_filter = filter.is_some() || prefilter_bitmap.is_some();
let oversample = scaled_oversample.max(1) as usize;
let fetch_k = if needs_filter {
k.saturating_mul(oversample).saturating_mul(3)
} else {
k.saturating_mul(oversample)
};
let collection_size = ids.map_or(0, IndexIdMap::len);
let searched = if let Some(f) = filter
&& collection_size <= 10_000
{
let mut allowed = allowed_nodes(ids, &crdt_guard, collection, f, exclude_fields);
if let Some(pre) = prefilter_bitmap {
allowed &= pre;
}
if allowed.is_empty() {
return Ok(Vec::new());
}
index.search_filtered(query, k, ef_search, &allowed)
} else if let Some(pre) = prefilter_bitmap {
if pre.is_empty() {
return Ok(Vec::new());
}
index.search_filtered(query, k, ef_search, pre)
} else {
index.search(query, fetch_k, ef_search)
};
let raw_results = searched.map_err(LiteError::from)?;
let candidates: Vec<Candidate> = raw_results
.into_iter()
.filter(|r| !index.is_deleted(r.id))
.map(|r| Candidate {
id: r.id,
index_distance: r.distance,
})
.collect();
let sidecars = vector_state
.codec_sidecars
.lock()
.map_err(|_| LiteError::LockPoisoned)?;
let sidecar = sidecars.get(index_key);
let ranked =
super::rerank::rank_candidates(index, candidates, query, metric, k, opts, sidecar)?;
let hydration = Hydration {
ids,
crdt: &crdt_guard,
collection,
filter,
exclude_fields,
skip_payload_fetch,
};
Ok(hydration.results(ranked))
}
#[cfg(test)]
mod tests {
use nodedb_types::VectorAnnOptions;
use nodedb_vector::rerank::{IndexShape, recall_scale, validate_options};
#[test]
fn oversample_4_no_filter_fetch_k_is_k_times_4() {
let k = 10_usize;
let oversample: usize = 4;
let fetch_k = k.saturating_mul(oversample);
assert_eq!(fetch_k, 40);
}
#[test]
fn oversample_4_with_filter_fetch_k_is_k_times_12() {
let k = 10_usize;
let oversample: usize = 4;
let fetch_k = k.saturating_mul(oversample).saturating_mul(3);
assert_eq!(fetch_k, 120);
}
#[test]
fn target_recall_095_scales_ef_and_oversample() {
let base_ef = 50_usize;
let base_oversample: u8 = 1;
let (scaled_ef, scaled_oversample) =
recall_scale(Some(0.95), base_ef, base_oversample).unwrap();
assert_eq!(scaled_ef, 200);
assert_eq!(scaled_oversample, 2);
}
#[test]
fn target_recall_none_returns_base_unchanged() {
let (ef, os) = recall_scale(None, 100, 1).unwrap();
assert_eq!(ef, 100);
assert_eq!(os, 1);
}
#[test]
fn target_recall_invalid_returns_bad_input() {
let result = recall_scale(Some(1.5), 100, 1);
assert!(result.is_err());
}
#[test]
fn sq8_quantization_returns_bad_request_via_validate() {
use nodedb_types::vector_ann::VectorQuantization;
let opts = VectorAnnOptions {
quantization: Some(VectorQuantization::Sq8),
..Default::default()
};
let rerank_codec =
validate_options(&opts, IndexShape::SingleVector, VectorQuantization::Sq8).unwrap();
assert!(
rerank_codec.is_some(),
"Sq8 should produce a Some(CodecName)"
);
}
#[test]
fn meta_token_budget_returns_bad_input_from_validate() {
let opts = VectorAnnOptions {
meta_token_budget: Some(8),
..Default::default()
};
let result = validate_options(
&opts,
IndexShape::SingleVector,
nodedb_types::VectorQuantization::None,
);
assert!(
result.is_err(),
"meta_token_budget on single-vector should be a BadInput error"
);
}
}