use std::sync::Arc;
use async_trait::async_trait;
use khive_score::DeterministicScore;
use khive_storage::types::{TextQueryMode, TextSearchRequest, VectorSearchRequest};
use khive_storage::{TextSearch, VectorStore};
use uuid::Uuid;
use crate::error::{Result, RetrievalError};
use crate::hybrid::{KeywordSearch, VectorSearch};
pub(super) fn storage_err_to_retrieval(
err: khive_storage::StorageError,
context: &'static str,
) -> RetrievalError {
use khive_storage::StorageError;
match err {
StorageError::Timeout { .. } | StorageError::AdmissionTimeout { .. } => {
RetrievalError::QueryTimeout { elapsed_ms: 0 }
}
StorageError::InvalidInput { message, .. } => {
RetrievalError::InvalidQuery(format!("{context}: {message}"))
}
other if context.contains("keyword") => RetrievalError::Bm25(format!("{context}: {other}")),
other => RetrievalError::Hnsw(format!("{context}: {other}")),
}
}
pub(super) fn checked_top_k(top_k: usize) -> crate::error::Result<u32> {
u32::try_from(top_k)
.map_err(|_| RetrievalError::InvalidQuery(format!("top_k exceeds u32::MAX: {top_k}")))
}
pub struct StorageVectorSearch {
store: Arc<dyn VectorStore>,
}
impl StorageVectorSearch {
pub fn new(store: Arc<dyn VectorStore>) -> Self {
Self { store }
}
}
#[async_trait]
impl VectorSearch for StorageVectorSearch {
type Id = Uuid;
async fn vector_search(
&self,
embedding: &[f32],
top_k: usize,
) -> Result<Vec<(Uuid, DeterministicScore)>> {
let request = VectorSearchRequest {
query_vectors: vec![embedding.to_vec()],
top_k: checked_top_k(top_k)?,
namespace: None,
kind: None,
embedding_model: None,
filter: None,
backend_hints: None,
};
let hits = self
.store
.search(request)
.await
.map_err(|e| storage_err_to_retrieval(e, "vector search"))?;
Ok(hits
.into_iter()
.map(|hit| (hit.subject_id, hit.score))
.collect())
}
}
pub struct StorageKeywordSearch {
search: Arc<dyn TextSearch>,
}
impl StorageKeywordSearch {
pub fn new(search: Arc<dyn TextSearch>) -> Self {
Self { search }
}
}
#[async_trait]
impl KeywordSearch for StorageKeywordSearch {
type Id = Uuid;
async fn keyword_search(
&self,
text: &str,
top_k: usize,
) -> Result<Vec<(Uuid, DeterministicScore)>> {
let request = TextSearchRequest {
query: text.to_string(),
mode: TextQueryMode::Plain,
filter: None,
top_k: checked_top_k(top_k)?,
snippet_chars: 0, };
let hits = self
.search
.search(request)
.await
.map_err(|e| storage_err_to_retrieval(e, "keyword search"))?;
Ok(hits
.into_iter()
.map(|hit| (hit.subject_id, hit.score))
.collect())
}
}
#[cfg(test)]
#[path = "storage/tests.rs"]
mod tests;