use nitrite::collection::{Document, NitriteCollection, NitriteId};
use nitrite::errors::{ErrorKind, NitriteError, NitriteResult};
use nitrite::filter::{by_id, Filter};
use nitrite::nitrite::Nitrite;
use crate::distance::Metric;
use crate::filter::{value_to_vector, vector_to_value};
use crate::fluent::vector_field;
use crate::vector_index_options;
pub const TEXT_FIELD: &str = "text";
pub const EMBEDDING_FIELD: &str = "embedding";
const DEFAULT_OVERSAMPLE: usize = 4;
#[derive(Debug, Clone)]
pub struct SearchHit {
pub id: NitriteId,
pub text: String,
pub score: f32,
pub document: Document,
}
#[derive(Clone)]
pub struct RagStore {
collection: NitriteCollection,
metric: Metric,
}
impl RagStore {
pub fn create(db: &Nitrite, name: &str, metric: Metric) -> NitriteResult<Self> {
let collection = db.collection(name)?;
if !collection.has_index(vec![EMBEDDING_FIELD])? {
collection.create_index(vec![EMBEDDING_FIELD], &vector_index_options())?;
}
Ok(RagStore { collection, metric })
}
pub fn add(
&self,
text: impl Into<String>,
embedding: Vec<f32>,
metadata: Document,
) -> NitriteResult<NitriteId> {
let mut doc = metadata;
doc.put(TEXT_FIELD, text.into())?;
doc.put(EMBEDDING_FIELD, vector_to_value(&embedding))?;
let result = self.collection.insert(doc)?;
result
.affected_nitrite_ids()
.first()
.cloned()
.ok_or_else(|| NitriteError::new("Insert returned no id", ErrorKind::InvalidOperation))
}
pub fn add_many(
&self,
records: Vec<(String, Vec<f32>, Document)>,
) -> NitriteResult<Vec<NitriteId>> {
records
.into_iter()
.map(|(text, emb, meta)| self.add(text, emb, meta))
.collect()
}
pub fn get(&self, id: &NitriteId) -> NitriteResult<Option<Document>> {
self.collection.get_by_id(id)
}
pub fn delete(&self, id: &NitriteId) -> NitriteResult<bool> {
let result = self.collection.remove(by_id(*id), true)?;
Ok(!result.affected_nitrite_ids().is_empty())
}
pub fn len(&self) -> NitriteResult<u64> {
self.collection.size()
}
pub fn is_empty(&self) -> NitriteResult<bool> {
Ok(self.len()? == 0)
}
pub fn collection(&self) -> &NitriteCollection {
&self.collection
}
pub fn search(&self, query: Vec<f32>, k: usize) -> SearchQuery<'_> {
SearchQuery {
store: self,
query,
k,
ef: None,
min_score: None,
meta_filter: None,
oversample: DEFAULT_OVERSAMPLE,
}
}
}
pub struct SearchQuery<'a> {
store: &'a RagStore,
query: Vec<f32>,
k: usize,
ef: Option<usize>,
min_score: Option<f32>,
meta_filter: Option<Filter>,
oversample: usize,
}
impl<'a> SearchQuery<'a> {
pub fn filter(mut self, filter: Filter) -> Self {
self.meta_filter = Some(filter);
self
}
pub fn ef(mut self, ef: usize) -> Self {
self.ef = Some(ef);
self
}
pub fn min_score(mut self, min_score: f32) -> Self {
self.min_score = Some(min_score);
self
}
pub fn oversample(mut self, factor: usize) -> Self {
self.oversample = factor.max(1);
self
}
pub fn run(self) -> NitriteResult<Vec<SearchHit>> {
if self.k == 0 {
return Ok(Vec::new());
}
let metric = self.store.metric;
let fetch = if self.meta_filter.is_some() || self.min_score.is_some() {
self.k.saturating_mul(self.oversample)
} else {
self.k
};
let mut builder = vector_field(EMBEDDING_FIELD).nearest(self.query.clone(), fetch);
if let Some(ef) = self.ef {
builder = builder.ef(ef);
}
let mut cursor = self.store.collection.find(builder.build())?;
let prepared_query = metric.prepare(self.query.clone());
let mut hits: Vec<SearchHit> = Vec::new();
for entry in cursor.iter_with_id() {
let (id, document) = entry?;
if let Some(mf) = &self.meta_filter {
if !mf.apply(&document)? {
continue;
}
}
let embedding = match document.get(EMBEDDING_FIELD).ok().and_then(|v| value_to_vector(&v)) {
Some(e) if e.len() == prepared_query.len() => e,
_ => continue,
};
let prepared = metric.prepare(embedding);
let score = metric.score(metric.distance(&prepared_query, &prepared));
if let Some(min) = self.min_score {
if score < min {
continue;
}
}
let text = match document.get(TEXT_FIELD) {
Ok(nitrite::common::Value::String(s)) => s,
_ => String::new(),
};
hits.push(SearchHit { id, text, score, document });
}
hits.sort_by(|a, b| b.score.total_cmp(&a.score));
hits.truncate(self.k);
Ok(hits)
}
}