Skip to main content

khive_storage/
vectors.rs

1//! Vector embedding storage and similarity search capability.
2
3use std::collections::HashSet;
4use std::sync::OnceLock;
5
6use async_trait::async_trait;
7use uuid::Uuid;
8
9use khive_types::SubstrateKind;
10
11use crate::capability::StorageCapability;
12use crate::error::StorageError;
13use crate::types::{
14    BatchWriteSummary, IndexRebuildScope, OrphanSweepConfig, OrphanSweepResult, StorageResult,
15    VectorMetadataFilter, VectorRecord, VectorSearchHit, VectorSearchRequest,
16    VectorStoreCapabilities, VectorStoreInfo,
17};
18
19/// Storage capability for dense vector embeddings and similarity search.
20#[async_trait]
21pub trait VectorStore: Send + Sync + 'static {
22    // --- Required methods ---
23
24    /// Store one or more dense vectors for a subject, identified by field name.
25    async fn insert(
26        &self,
27        subject_id: Uuid,
28        kind: SubstrateKind,
29        namespace: &str,
30        field: &str,
31        vectors: Vec<Vec<f32>>,
32    ) -> StorageResult<()>;
33    /// Store one or more vectors in a permanently exact-only space.
34    ///
35    /// Unlike [`Self::insert`], a backend implementation of this seam must not
36    /// emit ANN-consumer deltas. Callers may use it only for an identity whose
37    /// contract excludes later approximate-index consumption. Backends that do
38    /// not have a distinct exact-only write path reject the operation.
39    async fn insert_exact_only(
40        &self,
41        subject_id: Uuid,
42        kind: SubstrateKind,
43        namespace: &str,
44        field: &str,
45        vectors: Vec<Vec<f32>>,
46    ) -> StorageResult<()> {
47        let _ = (subject_id, kind, namespace, field, vectors);
48        Err(StorageError::Unsupported {
49            capability: StorageCapability::Vectors,
50            operation: "insert_exact_only".into(),
51            message: "backend has no exact-only write seam".into(),
52        })
53    }
54    /// Insert a batch of pre-assembled vector records in one call.
55    async fn insert_batch(&self, records: Vec<VectorRecord>) -> StorageResult<BatchWriteSummary>;
56    /// Delete all vectors associated with the given subject ID.
57    async fn delete(&self, subject_id: Uuid) -> StorageResult<bool>;
58    /// Return the total number of vector entries in this store.
59    async fn count(&self) -> StorageResult<u64>;
60    /// Run approximate nearest-neighbor search and return ranked hits.
61    async fn search(&self, request: VectorSearchRequest) -> StorageResult<Vec<VectorSearchHit>>;
62    /// Return index metadata and health statistics for this backend.
63    async fn info(&self) -> StorageResult<VectorStoreInfo>;
64    /// Rebuild the ANN index, optionally scoped to a subset of entries.
65    async fn rebuild(&self, scope: IndexRebuildScope) -> StorageResult<VectorStoreInfo>;
66
67    // --- New methods (default impls; backends opt in by overriding) ---
68
69    /// Declare what this backend supports (called at runtime policy construction).
70    ///
71    /// Default returns a conservative, backend-neutral baseline with all optional
72    /// features disabled and no advertised dimension ceiling or index kind
73    /// (STORAGE-AUD-001, ADR-044, ADR-071 Phase 1). Backends that support filter
74    /// pushdown, batch search, quantization, in-place update, or that have a known
75    /// dimension ceiling or index kind should override this and return their own
76    /// `&'static VectorStoreCapabilities`.
77    fn capabilities(&self) -> &'static VectorStoreCapabilities {
78        static BASELINE: OnceLock<VectorStoreCapabilities> = OnceLock::new();
79        BASELINE.get_or_init(|| VectorStoreCapabilities {
80            supports_filter: false,
81            supports_batch_search: false,
82            supports_quantization: false,
83            supports_update: false,
84            supports_orphan_sweep: false,
85            supports_multi_field: false,
86            // Backend-neutral baseline: unknown dimension ceiling and no
87            // advertised index kind. Backends with a concrete limit (e.g.
88            // SqliteVecStore) must override capabilities().
89            max_dimensions: None,
90            index_kinds: vec![],
91        })
92    }
93
94    /// Search with metadata pre-filter.
95    ///
96    /// Default: delegates to [`Self::search`] when the filter carries no predicates;
97    /// returns [`StorageError::Unsupported`] otherwise. Backends with native filter
98    /// pushdown should override this method and set `supports_filter = true` in their
99    /// [`VectorStoreCapabilities`].
100    ///
101    /// Callers must check `capabilities().supports_filter` before calling; the
102    /// runtime layer is responsible for post-filtering when native pushdown is absent.
103    ///
104    /// A backend that claims `supports_filter = true` but does not override this
105    /// method will trigger a `debug_assert` at runtime.
106    async fn search_with_filter(
107        &self,
108        request: &VectorSearchRequest,
109        filter: &VectorMetadataFilter,
110    ) -> StorageResult<Vec<VectorSearchHit>> {
111        if filter.is_empty() {
112            return self.search(request.clone()).await;
113        }
114        debug_assert!(
115            !self.capabilities().supports_filter,
116            "backend claims supports_filter=true but did not override search_with_filter"
117        );
118        Err(StorageError::Unsupported {
119            capability: StorageCapability::Vectors,
120            operation: "search_with_filter".into(),
121            message: "filter pushdown not supported; set supports_filter=true only when overriding this method".into(),
122        })
123    }
124
125    /// Search with N query vectors in one round-trip (HyDE fan-out, multi-query).
126    ///
127    /// Default: sequential calls to [`Self::search`], isolating per-query errors so one
128    /// bad request does not abort the batch. Backends that support native batch
129    /// search should override this and set `supports_batch_search = true`.
130    async fn search_batch(
131        &self,
132        requests: &[VectorSearchRequest],
133    ) -> StorageResult<Vec<StorageResult<Vec<VectorSearchHit>>>> {
134        let mut out = Vec::with_capacity(requests.len());
135        for req in requests {
136            out.push(self.search(req.clone()).await);
137        }
138        Ok(out)
139    }
140
141    /// Re-embed an existing entry in place.
142    ///
143    /// Default: delete then insert. Backends that support atomic in-place update
144    /// should override this and set `supports_update = true` in their
145    /// [`VectorStoreCapabilities`].
146    async fn update(
147        &self,
148        subject_id: Uuid,
149        kind: SubstrateKind,
150        namespace: &str,
151        field: &str,
152        vectors: Vec<Vec<f32>>,
153    ) -> StorageResult<()> {
154        self.delete(subject_id).await?;
155        self.insert(subject_id, kind, namespace, field, vectors)
156            .await
157    }
158
159    /// Remove vectors with no live subject (orphan sweep).
160    ///
161    /// Default returns [`StorageError::Unsupported`]. Backends that implement
162    /// deletion must set `supports_orphan_sweep = true` and override this method.
163    async fn orphan_sweep(&self, config: &OrphanSweepConfig) -> StorageResult<OrphanSweepResult> {
164        let _ = config;
165        Err(StorageError::Unsupported {
166            capability: StorageCapability::Vectors,
167            operation: "orphan_sweep".into(),
168            message: "this backend does not support orphan sweep".into(),
169        })
170    }
171
172    /// Check which of the given subject IDs already have embeddings in this store
173    /// for the specified namespace.
174    ///
175    /// Returns a [`HashSet`] of IDs that are present. IDs not in the returned set
176    /// have no embedding. Default returns [`StorageError::Unsupported`]; backends
177    /// that support fast bulk existence checks should override this method.
178    async fn batch_exists(&self, ids: &[Uuid], namespace: &str) -> StorageResult<HashSet<Uuid>> {
179        let _ = (ids, namespace);
180        Err(StorageError::Unsupported {
181            capability: StorageCapability::Vectors,
182            operation: "batch_exists".into(),
183            message: "this backend does not support batch existence checks".into(),
184        })
185    }
186
187    /// Delete all rows for the given subject IDs, regardless of their stored namespace.
188    ///
189    /// This is a namespace-agnostic sweep — it removes every vector row whose
190    /// `subject_id` matches, no matter which namespace the row was written under.
191    /// Required when the vec table's PRIMARY KEY is `subject_id` alone (not
192    /// `(subject_id, namespace)`): a row from a prior namespace would collide on
193    /// re-insert after a relabel, so the pre-insert drop must target by subject
194    /// only. Returns the number of rows deleted across all chunks. The operation
195    /// is atomic across the full input: an error must not leave a prefix of the
196    /// requested subject IDs deleted.
197    ///
198    /// Default returns [`StorageError::Unsupported`]; backends that store vectors
199    /// in a per-subject keyed table should override this method.
200    async fn delete_subjects(&self, ids: &[Uuid]) -> StorageResult<u64> {
201        let _ = ids;
202        Err(StorageError::Unsupported {
203            capability: StorageCapability::Vectors,
204            operation: "delete_subjects".into(),
205            message: "this backend does not support namespace-agnostic subject deletion".into(),
206        })
207    }
208}