use async_trait::async_trait;
use entelix_core::{ExecutionContext, Result};
use serde::{Deserialize, Serialize};
use crate::namespace::Namespace;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Document {
#[serde(default)]
pub doc_id: Option<DocumentId>,
pub content: String,
#[serde(default)]
pub metadata: serde_json::Value,
#[serde(default)]
pub score: Option<f32>,
}
impl Document {
pub fn new(content: impl Into<String>) -> Self {
Self {
doc_id: None,
content: content.into(),
metadata: serde_json::Value::Null,
score: None,
}
}
#[must_use]
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub fn with_doc_id(mut self, id: impl Into<DocumentId>) -> Self {
self.doc_id = Some(id.into());
self
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EmbeddingUsage {
pub input_tokens: u32,
}
impl EmbeddingUsage {
#[must_use]
pub const fn new(input_tokens: u32) -> Self {
Self { input_tokens }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Embedding {
pub vector: Vec<f32>,
#[serde(default)]
pub usage: Option<EmbeddingUsage>,
}
impl Embedding {
#[must_use]
pub const fn new(vector: Vec<f32>) -> Self {
Self {
vector,
usage: None,
}
}
#[must_use]
pub const fn with_usage(mut self, usage: EmbeddingUsage) -> Self {
self.usage = Some(usage);
self
}
}
#[async_trait]
pub trait Embedder: Send + Sync + 'static {
fn dimension(&self) -> usize;
async fn embed(&self, text: &str, ctx: &ExecutionContext) -> Result<Embedding>;
async fn embed_batch(
&self,
texts: &[String],
ctx: &ExecutionContext,
) -> Result<Vec<Embedding>> {
let mut out = Vec::with_capacity(texts.len());
for text in texts {
if ctx.is_cancelled() {
return Err(entelix_core::Error::Cancelled);
}
out.push(self.embed(text, ctx).await?);
}
Ok(out)
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct RetrievalQuery {
pub text: String,
pub top_k: usize,
pub min_score: Option<f32>,
pub filter: Option<VectorFilter>,
}
impl RetrievalQuery {
#[must_use]
pub fn new(text: impl Into<String>, top_k: usize) -> Self {
Self {
text: text.into(),
top_k,
min_score: None,
filter: None,
}
}
#[must_use]
pub fn with_filter(mut self, filter: VectorFilter) -> Self {
self.filter = Some(filter);
self
}
#[must_use]
pub const fn with_min_score(mut self, min_score: f32) -> Self {
self.min_score = Some(min_score);
self
}
}
#[async_trait]
pub trait Retriever: Send + Sync + 'static {
async fn retrieve(
&self,
query: RetrievalQuery,
ctx: &ExecutionContext,
) -> Result<Vec<Document>>;
}
pub type DocumentId = String;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum VectorFilter {
All,
Eq {
key: String,
value: serde_json::Value,
},
Lt {
key: String,
value: serde_json::Value,
},
Lte {
key: String,
value: serde_json::Value,
},
Gt {
key: String,
value: serde_json::Value,
},
Gte {
key: String,
value: serde_json::Value,
},
Range {
key: String,
min: serde_json::Value,
max: serde_json::Value,
},
In {
key: String,
values: Vec<serde_json::Value>,
},
Exists {
key: String,
},
And(Vec<Self>),
Or(Vec<Self>),
Not(Box<Self>),
}
#[async_trait]
pub trait VectorStore: Send + Sync + 'static {
fn dimension(&self) -> usize;
async fn add(
&self,
ctx: &ExecutionContext,
ns: &Namespace,
document: Document,
vector: Vec<f32>,
) -> Result<()>;
async fn search(
&self,
ctx: &ExecutionContext,
ns: &Namespace,
query_vector: &[f32],
top_k: usize,
) -> Result<Vec<Document>>;
async fn delete(&self, _ctx: &ExecutionContext, _ns: &Namespace, _doc_id: &str) -> Result<()> {
Err(entelix_core::Error::config(
"VectorStore::delete is not supported by this backend",
))
}
async fn update(
&self,
ctx: &ExecutionContext,
ns: &Namespace,
doc_id: &str,
document: Document,
vector: Vec<f32>,
) -> Result<()> {
self.delete(ctx, ns, doc_id).await?;
self.add(ctx, ns, document, vector).await
}
async fn add_batch(
&self,
ctx: &ExecutionContext,
ns: &Namespace,
items: Vec<(Document, Vec<f32>)>,
) -> Result<()> {
for (doc, vec) in items {
if ctx.is_cancelled() {
return Err(entelix_core::Error::Cancelled);
}
self.add(ctx, ns, doc, vec).await?;
}
Ok(())
}
async fn search_filtered(
&self,
_ctx: &ExecutionContext,
_ns: &Namespace,
_query_vector: &[f32],
_top_k: usize,
_filter: &VectorFilter,
) -> Result<Vec<Document>> {
Err(entelix_core::Error::config(
"VectorStore::search_filtered is not supported by this backend; \
override the trait method to push filters down to the index",
))
}
async fn count(
&self,
_ctx: &ExecutionContext,
_ns: &Namespace,
_filter: Option<&VectorFilter>,
) -> Result<usize> {
Err(entelix_core::Error::config(
"VectorStore::count is not supported by this backend; \
override the trait method to surface index cardinality",
))
}
async fn list(
&self,
_ctx: &ExecutionContext,
_ns: &Namespace,
_filter: Option<&VectorFilter>,
_limit: usize,
_offset: usize,
) -> Result<Vec<Document>> {
Err(entelix_core::Error::config(
"VectorStore::list is not supported by this backend; \
override the trait method to enumerate documents in the index",
))
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RerankedDocument {
pub document: Document,
pub rerank_score: f32,
}
impl RerankedDocument {
pub const fn new(document: Document, rerank_score: f32) -> Self {
Self {
document,
rerank_score,
}
}
}
#[async_trait]
pub trait Reranker: Send + Sync + 'static {
async fn rerank(
&self,
query: &str,
candidates: Vec<Document>,
top_k: usize,
ctx: &ExecutionContext,
) -> Result<Vec<RerankedDocument>>;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct IdentityReranker;
#[async_trait]
impl Reranker for IdentityReranker {
async fn rerank(
&self,
_query: &str,
mut candidates: Vec<Document>,
top_k: usize,
_ctx: &ExecutionContext,
) -> Result<Vec<RerankedDocument>> {
candidates.truncate(top_k);
Ok(candidates
.into_iter()
.map(|doc| {
let score = doc.score.unwrap_or(0.0);
RerankedDocument::new(doc, score)
})
.collect())
}
}