Skip to main content

daimon_core/
vector_store.rs

1//! Trait-based plugin system for vector stores.
2//!
3//! Implement [`VectorStore`] for your storage backend (pgvector, Qdrant,
4//! Pinecone, Chroma, Weaviate, Milvus, etc.), then compose with a knowledge
5//! base for a complete embedding-backed retrieval pipeline.
6
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use crate::document::{Document, ScoredDocument};
12use crate::error::Result;
13
14/// Low-level vector storage interface.
15///
16/// Implement this trait for your vector database. Operations deal in
17/// pre-computed embeddings — combine with an `EmbeddingModel` and knowledge
18/// base for automatic embedding computation.
19pub trait VectorStore: Send + Sync {
20    /// Inserts or updates a document with a pre-computed embedding vector.
21    fn upsert(
22        &self,
23        id: &str,
24        embedding: Vec<f32>,
25        document: Document,
26    ) -> impl Future<Output = Result<()>> + Send;
27
28    /// Inserts or updates a batch of documents with pre-computed embeddings.
29    ///
30    /// The default implementation calls [`VectorStore::upsert`] once per
31    /// item. Backends with a bulk write API (multi-row `INSERT`, `_bulk`
32    /// endpoints) should override it to collapse the batch into a single
33    /// roundtrip.
34    fn upsert_many(
35        &self,
36        items: Vec<(String, Vec<f32>, Document)>,
37    ) -> impl Future<Output = Result<()>> + Send {
38        async move {
39            for (id, embedding, document) in items {
40                self.upsert(&id, embedding, document).await?;
41            }
42            Ok(())
43        }
44    }
45
46    /// Queries the store for the `top_k` most similar documents to the
47    /// given embedding vector. Returns results sorted by descending similarity.
48    fn query(
49        &self,
50        embedding: Vec<f32>,
51        top_k: usize,
52    ) -> impl Future<Output = Result<Vec<ScoredDocument>>> + Send;
53
54    /// Deletes a document by ID. Returns `true` if the document was found
55    /// and deleted.
56    fn delete(&self, id: &str) -> impl Future<Output = Result<bool>> + Send;
57
58    /// Returns the number of documents in the store.
59    fn count(&self) -> impl Future<Output = Result<usize>> + Send;
60}
61
62/// Object-safe wrapper for [`VectorStore`].
63pub trait ErasedVectorStore: Send + Sync {
64    fn upsert_erased<'a>(
65        &'a self,
66        id: &'a str,
67        embedding: Vec<f32>,
68        document: Document,
69    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
70
71    fn upsert_many_erased(
72        &self,
73        items: Vec<(String, Vec<f32>, Document)>,
74    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
75
76    fn query_erased<'a>(
77        &'a self,
78        embedding: Vec<f32>,
79        top_k: usize,
80    ) -> Pin<Box<dyn Future<Output = Result<Vec<ScoredDocument>>> + Send + 'a>>;
81
82    fn delete_erased<'a>(
83        &'a self,
84        id: &'a str,
85    ) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'a>>;
86
87    fn count_erased(&self) -> Pin<Box<dyn Future<Output = Result<usize>> + Send + '_>>;
88}
89
90impl<T: VectorStore> ErasedVectorStore for T {
91    fn upsert_erased<'a>(
92        &'a self,
93        id: &'a str,
94        embedding: Vec<f32>,
95        document: Document,
96    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
97        Box::pin(self.upsert(id, embedding, document))
98    }
99
100    fn upsert_many_erased(
101        &self,
102        items: Vec<(String, Vec<f32>, Document)>,
103    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
104        Box::pin(self.upsert_many(items))
105    }
106
107    fn query_erased<'a>(
108        &'a self,
109        embedding: Vec<f32>,
110        top_k: usize,
111    ) -> Pin<Box<dyn Future<Output = Result<Vec<ScoredDocument>>> + Send + 'a>> {
112        Box::pin(self.query(embedding, top_k))
113    }
114
115    fn delete_erased<'a>(
116        &'a self,
117        id: &'a str,
118    ) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + 'a>> {
119        Box::pin(self.delete(id))
120    }
121
122    fn count_erased(&self) -> Pin<Box<dyn Future<Output = Result<usize>> + Send + '_>> {
123        Box::pin(self.count())
124    }
125}
126
127/// Shared ownership of a vector store.
128pub type SharedVectorStore = Arc<dyn ErasedVectorStore>;
129
130#[cfg(test)]
131mod tests {
132    use std::sync::atomic::{AtomicUsize, Ordering};
133
134    use super::*;
135
136    struct CountingStore {
137        upserts: AtomicUsize,
138    }
139
140    impl VectorStore for CountingStore {
141        async fn upsert(&self, _id: &str, _embedding: Vec<f32>, _document: Document) -> Result<()> {
142            self.upserts.fetch_add(1, Ordering::SeqCst);
143            Ok(())
144        }
145
146        async fn query(&self, _embedding: Vec<f32>, _top_k: usize) -> Result<Vec<ScoredDocument>> {
147            Ok(Vec::new())
148        }
149
150        async fn delete(&self, _id: &str) -> Result<bool> {
151            Ok(false)
152        }
153
154        async fn count(&self) -> Result<usize> {
155            Ok(self.upserts.load(Ordering::SeqCst))
156        }
157    }
158
159    #[test]
160    fn test_upsert_many_default_delegates_per_item() {
161        futures::executor::block_on(async {
162            let store = CountingStore {
163                upserts: AtomicUsize::new(0),
164            };
165            let items = (0..3)
166                .map(|i| (format!("id-{i}"), vec![0.0], Document::new("doc")))
167                .collect();
168            store.upsert_many(items).await.unwrap();
169            assert_eq!(store.upserts.load(Ordering::SeqCst), 3);
170
171            // The erased wrapper forwards to the same default implementation.
172            let shared: SharedVectorStore = Arc::new(store);
173            shared
174                .upsert_many_erased(vec![("id-3".into(), vec![0.0], Document::new("doc"))])
175                .await
176                .unwrap();
177            assert_eq!(shared.count_erased().await.unwrap(), 4);
178        });
179    }
180}