use std::any::Any;
use std::fmt::{self, Display};
use std::sync::{Arc, OnceLock};
use nitrite::collection::Document;
use nitrite::common::Value;
use nitrite::errors::{ErrorKind, NitriteError, NitriteResult};
use nitrite::filter::{Filter, FilterProvider};
pub const VECTOR_INDEX: &str = "vector";
#[derive(Clone)]
pub struct VectorNearestFilter {
inner: Arc<VectorNearestInner>,
}
struct VectorNearestInner {
field: OnceLock<String>,
query: Vec<f32>,
k: usize,
ef: Option<usize>,
min_score: Option<f32>,
}
impl VectorNearestFilter {
pub fn new(field: impl Into<String>, query: Vec<f32>, k: usize) -> Self {
let name = OnceLock::new();
let _ = name.set(field.into());
VectorNearestFilter {
inner: Arc::new(VectorNearestInner {
field: name,
query,
k,
ef: None,
min_score: None,
}),
}
}
pub fn with_ef(self, ef: usize) -> Self {
let inner = VectorNearestInner {
field: clone_once(&self.inner.field),
query: self.inner.query.clone(),
k: self.inner.k,
ef: Some(ef),
min_score: self.inner.min_score,
};
VectorNearestFilter { inner: Arc::new(inner) }
}
pub fn with_min_score(self, min_score: f32) -> Self {
let inner = VectorNearestInner {
field: clone_once(&self.inner.field),
query: self.inner.query.clone(),
k: self.inner.k,
ef: self.inner.ef,
min_score: Some(min_score),
};
VectorNearestFilter { inner: Arc::new(inner) }
}
pub fn query(&self) -> &[f32] {
&self.inner.query
}
pub fn k(&self) -> usize {
self.inner.k
}
pub fn ef(&self) -> Option<usize> {
self.inner.ef
}
pub fn min_score(&self) -> Option<f32> {
self.inner.min_score
}
}
fn clone_once(field: &OnceLock<String>) -> OnceLock<String> {
let out = OnceLock::new();
if let Some(v) = field.get() {
let _ = out.set(v.clone());
}
out
}
impl FilterProvider for VectorNearestFilter {
fn apply(&self, _entry: &Document) -> NitriteResult<bool> {
Err(NitriteError::new(
&format!(
"kNN filter on '{}' requires a vector index; create one with \
collection.create_index([field], &vector_index_options())",
self.field_name().unwrap_or_else(|_| "<unknown>".into())
),
ErrorKind::FilterError,
))
}
fn has_field(&self) -> bool {
true
}
fn get_field_name(&self) -> NitriteResult<String> {
self.field_name()
}
fn set_field_name(&self, field_name: String) -> NitriteResult<()> {
self.inner.field.get_or_init(|| field_name);
Ok(())
}
fn is_index_only_filter(&self) -> bool {
true
}
fn supported_index_type(&self) -> NitriteResult<String> {
Ok(VECTOR_INDEX.to_string())
}
fn can_be_grouped(&self, other: Filter) -> NitriteResult<bool> {
if other.as_any().downcast_ref::<VectorNearestFilter>().is_some() {
Ok(self.field_name()? == other.get_field_name()?)
} else {
Ok(false)
}
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl VectorNearestFilter {
fn field_name(&self) -> NitriteResult<String> {
self.inner
.field
.get()
.cloned()
.ok_or_else(|| NitriteError::new("Field name not set", ErrorKind::InvalidOperation))
}
}
impl Display for VectorNearestFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use std::hash::{Hash, Hasher};
let field = self.inner.field.get().map(String::as_str).unwrap_or("<unknown>");
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for x in &self.inner.query {
x.to_bits().hash(&mut hasher);
}
self.inner.k.hash(&mut hasher);
self.inner.ef.hash(&mut hasher);
self.inner.min_score.map(f32::to_bits).hash(&mut hasher);
write!(
f,
"({} nearest k={} dim={} q={:016x})",
field,
self.inner.k,
self.inner.query.len(),
hasher.finish()
)
}
}
pub fn value_to_vector(value: &Value) -> Option<Vec<f32>> {
let arr = value.as_array()?;
let mut out = Vec::with_capacity(arr.len());
for v in arr {
out.push(value_to_f32(v)?);
}
Some(out)
}
fn value_to_f32(v: &Value) -> Option<f32> {
match v {
Value::F32(x) => Some(*x),
Value::F64(x) => Some(*x as f32),
Value::I8(x) => Some(*x as f32),
Value::I16(x) => Some(*x as f32),
Value::I32(x) => Some(*x as f32),
Value::I64(x) => Some(*x as f32),
Value::U8(x) => Some(*x as f32),
Value::U16(x) => Some(*x as f32),
Value::U32(x) => Some(*x as f32),
Value::U64(x) => Some(*x as f32),
_ => None,
}
}
pub fn vector_to_value(vector: &[f32]) -> Value {
Value::Array(vector.iter().map(|x| Value::F32(*x)).collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_vector_value() {
let v = vec![0.1f32, 0.2, 0.3];
let value = vector_to_value(&v);
assert_eq!(value_to_vector(&value), Some(v));
}
#[test]
fn parses_mixed_numeric_array() {
let value = Value::Array(vec![Value::I32(1), Value::F64(2.5), Value::U8(3)]);
assert_eq!(value_to_vector(&value), Some(vec![1.0, 2.5, 3.0]));
}
#[test]
fn rejects_non_array() {
assert_eq!(value_to_vector(&Value::String("x".into())), None);
}
#[test]
fn filter_exposes_query_params() {
let f = VectorNearestFilter::new("emb", vec![1.0, 2.0], 5)
.with_ef(100)
.with_min_score(0.8);
assert_eq!(f.k(), 5);
assert_eq!(f.ef(), Some(100));
assert_eq!(f.min_score(), Some(0.8));
assert_eq!(f.query(), &[1.0, 2.0]);
assert_eq!(f.get_field_name().unwrap(), "emb");
assert!(f.is_index_only_filter());
assert_eq!(f.supported_index_type().unwrap(), VECTOR_INDEX);
}
}