use anyhow::Result;
use async_trait::async_trait;
use rocksdb::{DB, WriteBatch};
use std::cmp::{Ordering, Reverse};
use std::collections::BinaryHeap;
use tracing::debug;
use crate::traits::VectorStorage;
use post_cortex_embeddings::{SearchMatch, VectorMetadata, cosine_with_norms, l2_norm};
use super::RealRocksDBStorage;
use super::core::{blocking, keys_with_prefix, scan_prefix};
use super::types::StoredEmbedding;
const EMBEDDING_PREFIX: &str = "embedding:";
fn embedding_key(session_id: &str, content_id: &str) -> String {
format!("embedding:{}:{}", session_id, content_id)
}
fn decode_embedding(value: &[u8]) -> Option<StoredEmbedding> {
bincode::serde::decode_from_slice::<StoredEmbedding, _>(value, bincode::config::standard())
.ok()
.map(|(e, _)| e)
}
fn put_embedding(batch: &mut WriteBatch, embedding: &StoredEmbedding) -> Result<()> {
let data = bincode::serde::encode_to_vec(embedding, bincode::config::standard())
.map_err(|e| anyhow::anyhow!("Failed to serialize embedding: {}", e))?;
batch.put(
embedding_key(&embedding.session_id, &embedding.content_id).as_bytes(),
&data,
);
Ok(())
}
fn delete_by_content_id(db: &DB, content_id: &str) -> Result<bool> {
let mut batch = WriteBatch::default();
let mut found = false;
for key in keys_with_prefix(db, EMBEDDING_PREFIX.as_bytes())? {
let rest = &key[EMBEDDING_PREFIX.len()..];
if let Some(pos) = rest.iter().position(|b| *b == b':')
&& &rest[pos + 1..] == content_id.as_bytes()
{
batch.delete(&key);
found = true;
}
}
db.write(batch)?;
Ok(found)
}
struct Scored {
similarity: f32,
ordinal: u32,
embedding: StoredEmbedding,
}
impl PartialEq for Scored {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Scored {}
impl PartialOrd for Scored {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Scored {
fn cmp(&self, other: &Self) -> Ordering {
self.similarity
.total_cmp(&other.similarity)
.then_with(|| other.ordinal.cmp(&self.ordinal))
}
}
fn scan_search(
db: &DB,
prefix: &str,
query: &[f32],
k: usize,
filter: impl Fn(&StoredEmbedding) -> bool,
) -> Result<Vec<SearchMatch>> {
if k == 0 {
return Ok(Vec::new());
}
let query_norm = l2_norm(query);
let mut heap: BinaryHeap<Reverse<Scored>> = BinaryHeap::with_capacity(k + 1);
for (ordinal, item) in scan_prefix(db, prefix.as_bytes()).enumerate() {
let (_, value) = item?;
let Some(embedding) = decode_embedding(&value) else {
continue;
};
if embedding.vector.len() != query.len() || !filter(&embedding) {
continue;
}
let similarity = cosine_with_norms(
query,
&embedding.vector,
query_norm,
l2_norm(&embedding.vector),
);
let ordinal = u32::try_from(ordinal).unwrap_or(u32::MAX);
if heap.len() < k {
heap.push(Reverse(Scored {
similarity,
ordinal,
embedding,
}));
} else if let Some(Reverse(worst)) = heap.peek()
&& similarity > worst.similarity
{
heap.pop();
heap.push(Reverse(Scored {
similarity,
ordinal,
embedding,
}));
}
}
Ok(heap
.into_sorted_vec()
.into_iter()
.map(|Reverse(s)| SearchMatch {
vector_id: s.ordinal,
similarity: s.similarity,
metadata: s.embedding.to_metadata(),
})
.collect())
}
impl RealRocksDBStorage {
fn validate_vector(&self, vector: &[f32]) -> Result<()> {
match self.vector_dimension {
Some(dim) if vector.len() != dim => Err(anyhow::anyhow!(
"Invalid embedding dimension: expected {}, got {}",
dim,
vector.len()
)),
None if vector.is_empty() => {
Err(anyhow::anyhow!("Invalid embedding dimension: empty vector"))
}
_ => Ok(()),
}
}
pub async fn save_embedding(&self, embedding: &StoredEmbedding) -> Result<()> {
self.batch_save_embeddings(std::slice::from_ref(embedding))
.await
}
pub async fn load_session_embeddings(&self, session_id: &str) -> Result<Vec<StoredEmbedding>> {
self.load_embeddings_with_prefix(format!("embedding:{}:", session_id))
.await
}
pub async fn load_all_embeddings(&self) -> Result<Vec<StoredEmbedding>> {
self.load_embeddings_with_prefix(EMBEDDING_PREFIX.to_string())
.await
}
async fn load_embeddings_with_prefix(&self, prefix: String) -> Result<Vec<StoredEmbedding>> {
let db = self.db.clone();
blocking(move || {
let mut embeddings = Vec::new();
for item in scan_prefix(&db, prefix.as_bytes()) {
let (_, value) = item?;
if let Some(embedding) = decode_embedding(&value) {
embeddings.push(embedding);
}
}
Ok(embeddings)
})
.await
}
pub async fn delete_embedding(&self, session_id: &str, content_id: &str) -> Result<bool> {
let db = self.db.clone();
let key = embedding_key(session_id, content_id);
blocking(move || {
let existed = db.get(key.as_bytes())?.is_some();
db.delete(key.as_bytes())?;
Ok(existed)
})
.await
}
pub async fn count_embeddings(&self, session_id: &str) -> usize {
let db = self.db.clone();
let prefix = format!("embedding:{}:", session_id);
blocking(move || Ok(keys_with_prefix(&db, prefix.as_bytes())?.len()))
.await
.unwrap_or(0)
}
pub async fn batch_save_embeddings(&self, embeddings: &[StoredEmbedding]) -> Result<()> {
if embeddings.is_empty() {
return Ok(());
}
let mut batch = WriteBatch::default();
for embedding in embeddings {
put_embedding(&mut batch, embedding)?;
}
let count = embeddings.len();
let db = self.db.clone();
blocking(move || {
db.write(batch)?;
debug!("Batch saved {} embeddings", count);
Ok(())
})
.await
}
}
#[async_trait]
impl VectorStorage for RealRocksDBStorage {
async fn add_vector(&self, vector: Vec<f32>, metadata: VectorMetadata) -> Result<String> {
self.validate_vector(&vector)?;
let id = metadata.id.clone();
self.save_embedding(&StoredEmbedding::new(vector, metadata))
.await?;
Ok(id)
}
async fn add_vectors_batch(
&self,
vectors: Vec<(Vec<f32>, VectorMetadata)>,
) -> Result<Vec<String>> {
for (vector, _) in &vectors {
self.validate_vector(vector)?;
}
let ids = vectors.iter().map(|(_, m)| m.id.clone()).collect();
let embeddings: Vec<StoredEmbedding> = vectors
.into_iter()
.map(|(vector, metadata)| StoredEmbedding::new(vector, metadata))
.collect();
self.batch_save_embeddings(&embeddings).await?;
Ok(ids)
}
async fn search(&self, query: &[f32], k: usize) -> Result<Vec<SearchMatch>> {
let db = self.db.clone();
let query = query.to_vec();
blocking(move || scan_search(&db, EMBEDDING_PREFIX, &query, k, |_| true)).await
}
async fn search_in_session(
&self,
query: &[f32],
k: usize,
session_id: &str,
) -> Result<Vec<SearchMatch>> {
let db = self.db.clone();
let query = query.to_vec();
let prefix = format!("embedding:{}:", session_id);
blocking(move || scan_search(&db, &prefix, &query, k, |_| true)).await
}
async fn search_by_content_type(
&self,
query: &[f32],
k: usize,
content_type: &str,
) -> Result<Vec<SearchMatch>> {
let db = self.db.clone();
let query = query.to_vec();
let content_type = content_type.to_string();
blocking(move || {
scan_search(&db, EMBEDDING_PREFIX, &query, k, |e| {
e.content_type == content_type
})
})
.await
}
async fn remove_vector(&self, id: &str) -> Result<bool> {
let db = self.db.clone();
let content_id = id.to_string();
blocking(move || delete_by_content_id(&db, &content_id)).await
}
async fn remove_session_vector(&self, session_id: &str, content_id: &str) -> Result<bool> {
self.delete_embedding(session_id, content_id).await
}
async fn has_session_embeddings(&self, session_id: &str) -> bool {
let db = self.db.clone();
let prefix = format!("embedding:{}:", session_id);
blocking(move || Ok(scan_prefix(&db, prefix.as_bytes()).next().is_some()))
.await
.unwrap_or(false)
}
async fn count_session_embeddings(&self, session_id: &str) -> usize {
self.count_embeddings(session_id).await
}
async fn total_count(&self) -> usize {
let db = self.db.clone();
blocking(move || Ok(keys_with_prefix(&db, EMBEDDING_PREFIX.as_bytes())?.len()))
.await
.unwrap_or(0)
}
async fn get_session_vectors(
&self,
session_id: &str,
) -> Result<Vec<(Vec<f32>, VectorMetadata)>> {
let embeddings = self.load_session_embeddings(session_id).await?;
Ok(embeddings
.into_iter()
.map(|e| {
let metadata = e.to_metadata();
(e.vector, metadata)
})
.collect())
}
async fn get_all_vectors(&self) -> Result<Vec<(Vec<f32>, VectorMetadata)>> {
let embeddings = self.load_all_embeddings().await?;
Ok(embeddings
.into_iter()
.map(|e| {
let metadata = e.to_metadata();
(e.vector, metadata)
})
.collect())
}
}