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 VectorIndexKind, 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 /// Insert a batch of pre-assembled vector records in one call.
34 async fn insert_batch(&self, records: Vec<VectorRecord>) -> StorageResult<BatchWriteSummary>;
35 /// Delete all vectors associated with the given subject ID.
36 async fn delete(&self, subject_id: Uuid) -> StorageResult<bool>;
37 /// Return the total number of vector entries in this store.
38 async fn count(&self) -> StorageResult<u64>;
39 /// Run approximate nearest-neighbor search and return ranked hits.
40 async fn search(&self, request: VectorSearchRequest) -> StorageResult<Vec<VectorSearchHit>>;
41 /// Return index metadata and health statistics for this backend.
42 async fn info(&self) -> StorageResult<VectorStoreInfo>;
43 /// Rebuild the ANN index, optionally scoped to a subset of entries.
44 async fn rebuild(&self, scope: IndexRebuildScope) -> StorageResult<VectorStoreInfo>;
45
46 // --- New methods (default impls; backends opt in by overriding) ---
47
48 /// Declare what this backend supports (called at runtime policy construction).
49 ///
50 /// Default returns a conservative baseline with all optional features disabled,
51 /// preserving backward compatibility for existing implementations. Backends that
52 /// support filter pushdown, batch search, quantization, or in-place update should
53 /// override this and return their own `&'static VectorStoreCapabilities`.
54 fn capabilities(&self) -> &'static VectorStoreCapabilities {
55 static BASELINE: OnceLock<VectorStoreCapabilities> = OnceLock::new();
56 BASELINE.get_or_init(|| VectorStoreCapabilities {
57 supports_filter: false,
58 supports_batch_search: false,
59 supports_quantization: false,
60 supports_update: false,
61 supports_orphan_sweep: false,
62 supports_multi_field: false,
63 // sqlite-vec 0.1.9 enforces SQLITE_VEC_VEC0_MAX_DIMENSIONS = 8192.
64 // The baseline uses the same value so generic callers that have not
65 // overridden capabilities() report the correct ceiling.
66 max_dimensions: Some(8192),
67 index_kinds: vec![VectorIndexKind::SqliteVec],
68 })
69 }
70
71 /// Search with metadata pre-filter.
72 ///
73 /// Default: delegates to [`search`] when the filter carries no predicates;
74 /// returns [`StorageError::Unsupported`] otherwise. Backends with native filter
75 /// pushdown should override this method and set `supports_filter = true` in their
76 /// [`VectorStoreCapabilities`].
77 ///
78 /// Callers must check `capabilities().supports_filter` before calling; the
79 /// runtime layer is responsible for post-filtering when native pushdown is absent.
80 ///
81 /// A backend that claims `supports_filter = true` but does not override this
82 /// method will trigger a `debug_assert` at runtime.
83 async fn search_with_filter(
84 &self,
85 request: &VectorSearchRequest,
86 filter: &VectorMetadataFilter,
87 ) -> StorageResult<Vec<VectorSearchHit>> {
88 if filter.is_empty() {
89 return self.search(request.clone()).await;
90 }
91 debug_assert!(
92 !self.capabilities().supports_filter,
93 "backend claims supports_filter=true but did not override search_with_filter"
94 );
95 Err(StorageError::Unsupported {
96 capability: StorageCapability::Vectors,
97 operation: "search_with_filter".into(),
98 message: "filter pushdown not supported; set supports_filter=true only when overriding this method".into(),
99 })
100 }
101
102 /// Search with N query vectors in one round-trip (HyDE fan-out, multi-query).
103 ///
104 /// Default: sequential calls to [`search`], isolating per-query errors so one
105 /// bad request does not abort the batch. Backends that support native batch
106 /// search should override this and set `supports_batch_search = true`.
107 async fn search_batch(
108 &self,
109 requests: &[VectorSearchRequest],
110 ) -> StorageResult<Vec<StorageResult<Vec<VectorSearchHit>>>> {
111 let mut out = Vec::with_capacity(requests.len());
112 for req in requests {
113 out.push(self.search(req.clone()).await);
114 }
115 Ok(out)
116 }
117
118 /// Re-embed an existing entry in place.
119 ///
120 /// Default: delete then insert. Backends that support atomic in-place update
121 /// should override this and set `supports_update = true` in their
122 /// [`VectorStoreCapabilities`].
123 async fn update(
124 &self,
125 subject_id: Uuid,
126 kind: SubstrateKind,
127 namespace: &str,
128 field: &str,
129 vectors: Vec<Vec<f32>>,
130 ) -> StorageResult<()> {
131 self.delete(subject_id).await?;
132 self.insert(subject_id, kind, namespace, field, vectors)
133 .await
134 }
135
136 /// Remove vectors with no live subject (orphan sweep).
137 ///
138 /// Default returns [`StorageError::Unsupported`]. Backends that implement
139 /// deletion must set `supports_orphan_sweep = true` and override this method.
140 async fn orphan_sweep(&self, config: &OrphanSweepConfig) -> StorageResult<OrphanSweepResult> {
141 let _ = config;
142 Err(StorageError::Unsupported {
143 capability: StorageCapability::Vectors,
144 operation: "orphan_sweep".into(),
145 message: "this backend does not support orphan sweep".into(),
146 })
147 }
148
149 /// Check which of the given subject IDs already have embeddings in this store
150 /// for the specified namespace.
151 ///
152 /// Returns a [`HashSet`] of IDs that are present. IDs not in the returned set
153 /// have no embedding. Default returns [`StorageError::Unsupported`]; backends
154 /// that support fast bulk existence checks should override this method.
155 async fn batch_exists(&self, ids: &[Uuid], namespace: &str) -> StorageResult<HashSet<Uuid>> {
156 let _ = (ids, namespace);
157 Err(StorageError::Unsupported {
158 capability: StorageCapability::Vectors,
159 operation: "batch_exists".into(),
160 message: "this backend does not support batch existence checks".into(),
161 })
162 }
163}