use std::collections::HashMap;
use nodedb_types::{PayloadAtom, Surrogate, SurrogateBitmap, value::Value};
use crate::data::executor::core_loop::CoreLoop;
use crate::data::executor::handlers::transaction::overlay::Staged;
use crate::data::executor::response_codec::VectorSearchHit;
use crate::engine::vector::distance::DistanceMetric;
use crate::types::{DatabaseId, TenantId, TxnId};
pub(in crate::data::executor) struct VectorMergeParams<'a> {
pub txn_id: TxnId,
pub database_id: DatabaseId,
pub tid: TenantId,
pub collection: &'a str,
pub field_name: &'a str,
pub query_vector: &'a [f32],
pub metric: DistanceMetric,
pub top_k: usize,
pub filter_bitmap: Option<&'a SurrogateBitmap>,
pub payload_filters: &'a [PayloadAtom],
}
fn extract_vector_field(doc: &serde_json::Value, field: &str) -> Option<Vec<f32>> {
let arr = doc.get(field)?.as_array()?;
arr.iter().map(json_number_as_f32).collect()
}
fn json_number_as_f32(v: &serde_json::Value) -> Option<f32> {
v.as_f64()
.or_else(|| v.as_str().and_then(|s| s.parse::<f64>().ok()))
.map(|f| f as f32)
}
fn payload_atom_matches(atom: &PayloadAtom, doc: &serde_json::Value) -> bool {
match atom {
PayloadAtom::Eq(field, expected) => doc
.get(field)
.is_some_and(|actual| json_value_eq(actual, expected)),
PayloadAtom::In(field, values) => {
let Some(actual) = doc.get(field) else {
return false;
};
values.iter().any(|v| json_value_eq(actual, v))
}
PayloadAtom::Range {
field,
low,
low_inclusive,
high,
high_inclusive,
} => {
let Some(actual) = doc.get(field).and_then(serde_json::Value::as_f64) else {
return false;
};
let above_low = match low {
None => true,
Some(low) => {
let low = value_as_f64(low).unwrap_or(f64::NEG_INFINITY);
if actual > low {
true
} else {
actual == low && *low_inclusive
}
}
};
let below_high = match high {
None => true,
Some(high) => {
let high = value_as_f64(high).unwrap_or(f64::INFINITY);
if actual < high {
true
} else {
actual == high && *high_inclusive
}
}
};
above_low && below_high
}
_ => false,
}
}
fn json_value_eq(actual: &serde_json::Value, expected: &Value) -> bool {
match expected {
Value::Integer(i) => actual.as_i64() == Some(*i),
Value::Float(f) => actual.as_f64() == Some(*f),
Value::String(s) => actual.as_str() == Some(s.as_str()),
Value::Bool(b) => actual.as_bool() == Some(*b),
_ => false,
}
}
fn value_as_f64(v: &Value) -> Option<f64> {
match v {
Value::Integer(i) => Some(*i as f64),
Value::Float(f) => Some(*f),
_ => None,
}
}
fn reindex_after_removal(seen: &mut HashMap<u32, usize>, removed_idx: usize) {
for idx in seen.values_mut() {
if *idx > removed_idx {
*idx -= 1;
}
}
}
impl CoreLoop {
pub(in crate::data::executor) fn merge_vector_overlay_into_search(
&self,
params: VectorMergeParams<'_>,
hits: &mut Vec<VectorSearchHit>,
) {
let VectorMergeParams {
txn_id,
database_id,
tid,
collection,
field_name,
query_vector,
metric,
top_k,
filter_bitmap,
payload_filters,
} = params;
let coll_key = (database_id, tid, collection.to_string());
let config_key = (database_id, tid, collection.to_string());
self.touch_overlay(txn_id);
if let Some(overlay) = self.txn_overlays.get(&txn_id) {
let mut seen: HashMap<u32, usize> = hits
.iter()
.enumerate()
.map(|(idx, h)| (h.id, idx))
.collect();
for (surrogate, staged) in overlay.iter_for_collection(&coll_key) {
match staged {
Staged::Tombstone => {
if let Some(idx) = seen.remove(&surrogate) {
hits.remove(idx);
reindex_after_removal(&mut seen, idx);
}
}
Staged::Put(body) => {
let Some(doc) = self.decode_indexed_body(&config_key, body) else {
continue;
};
let Some(vector) = extract_vector_field(&doc, field_name) else {
continue;
};
if vector.len() != query_vector.len() {
continue;
}
let passes_filter_bitmap =
filter_bitmap.is_none_or(|fb| fb.contains(Surrogate::new(surrogate)));
let passes_payload = payload_filters.is_empty()
|| payload_filters
.iter()
.all(|atom| payload_atom_matches(atom, &doc));
if !passes_filter_bitmap || !passes_payload {
if let Some(idx) = seen.remove(&surrogate) {
hits.remove(idx);
reindex_after_removal(&mut seen, idx);
}
continue;
}
let dist = nodedb_vector::distance::distance(query_vector, &vector, metric);
match seen.get(&surrogate).copied() {
Some(idx) => {
hits[idx].distance = dist;
hits[idx].body = Some(body.clone());
}
None => {
seen.insert(surrogate, hits.len());
hits.push(VectorSearchHit {
id: surrogate,
distance: dist,
doc_id: None,
body: Some(body.clone()),
});
}
}
}
}
}
}
hits.sort_by(|a, b| {
a.distance
.partial_cmp(&b.distance)
.unwrap_or(std::cmp::Ordering::Equal)
});
hits.truncate(top_k);
}
}