1use std::sync::Arc;
10
11use parking_lot::Mutex;
12use uqa_core::{DocId, PostingList};
13
14use super::codec::other_error;
15use super::hnsw_persistence;
16use super::{KeyValueStore, KeyValueVectorIndex};
17use crate::hnsw_index::{HNSWIndex, HNSWPersistenceDelta};
18use crate::vector_index::{HNSWIndexParams, VectorIndex};
19use crate::{StorageBackendError, StorageBackendResult};
20
21struct CachedHNSW {
22 graph: HNSWIndex,
23 revision: Option<u64>,
24}
25
26pub struct KeyValueHNSWIndex {
27 store: Arc<dyn KeyValueStore>,
28 raw: KeyValueVectorIndex,
29 table: String,
30 field: String,
31 dimensions: u32,
32 params: HNSWIndexParams,
33 cached: Mutex<CachedHNSW>,
34}
35
36impl KeyValueHNSWIndex {
37 pub fn create(
38 store: Arc<dyn KeyValueStore>,
39 table: impl Into<String>,
40 field: impl Into<String>,
41 dimensions: u32,
42 params: HNSWIndexParams,
43 ) -> StorageBackendResult<Self> {
44 let params = params.validate()?;
45 let table = table.into();
46 let field = field.into();
47 let raw = KeyValueVectorIndex::new(Arc::clone(&store), &table, &field, dimensions);
48 let graph = build_from_canonical(&raw, dimensions, params)?;
49 Ok(Self {
50 store,
51 raw,
52 table,
53 field,
54 dimensions,
55 params,
56 cached: Mutex::new(CachedHNSW {
57 graph,
58 revision: None,
59 }),
60 })
61 }
62
63 pub fn restore(
64 store: Arc<dyn KeyValueStore>,
65 table: impl Into<String>,
66 field: impl Into<String>,
67 dimensions: u32,
68 params: HNSWIndexParams,
69 ) -> StorageBackendResult<Self> {
70 let params = params.validate()?;
71 let table = table.into();
72 let field = field.into();
73 let raw = KeyValueVectorIndex::new(Arc::clone(&store), &table, &field, dimensions);
74 let (graph, revision) = hnsw_persistence::restore_graph(
75 store.as_ref(),
76 &raw,
77 &table,
78 &field,
79 dimensions,
80 params,
81 )?;
82 Ok(Self {
83 store,
84 raw,
85 table,
86 field,
87 dimensions,
88 params,
89 cached: Mutex::new(CachedHNSW {
90 graph,
91 revision: Some(revision),
92 }),
93 })
94 }
95
96 fn replace_document(&self, doc_id: DocId, vectors: &[Vec<f32>]) -> StorageBackendResult<()> {
97 let mut cached = self.cached.lock();
98 self.verify_revision(cached.revision)?;
99 let mut graph = cached.graph.clone();
100 graph.add_many(doc_id, vectors.to_vec())?;
101 let delta = graph.take_persistence_delta();
102 let revision = next_revision(cached.revision)?;
103 let mut batch = self.store.batch();
104 self.raw.stage_replace(batch.as_mut(), doc_id, vectors)?;
105 self.stage_delta(batch.as_mut(), &delta, revision)?;
106 batch.commit()?;
107 *cached = CachedHNSW {
108 graph,
109 revision: Some(revision),
110 };
111 Ok(())
112 }
113
114 fn delete_document(&self, doc_id: DocId) -> StorageBackendResult<()> {
115 let mut cached = self.cached.lock();
116 self.verify_revision(cached.revision)?;
117 let mut graph = cached.graph.clone();
118 graph.delete(doc_id)?;
119 let delta = graph.take_persistence_delta();
120 let revision = next_revision(cached.revision)?;
121 let mut batch = self.store.batch();
122 self.raw.stage_replace(batch.as_mut(), doc_id, &[])?;
123 self.stage_delta(batch.as_mut(), &delta, revision)?;
124 batch.commit()?;
125 *cached = CachedHNSW {
126 graph,
127 revision: Some(revision),
128 };
129 Ok(())
130 }
131
132 fn clear_graph(&self) -> StorageBackendResult<()> {
133 let mut cached = self.cached.lock();
134 self.verify_revision(cached.revision)?;
135 let mut graph = cached.graph.clone();
136 graph.clear()?;
137 let delta = graph.take_persistence_delta();
138 let revision = next_revision(cached.revision)?;
139 let mut batch = self.store.batch();
140 self.raw.stage_clear(batch.as_mut())?;
141 self.stage_delta(batch.as_mut(), &delta, revision)?;
142 batch.commit()?;
143 *cached = CachedHNSW {
144 graph,
145 revision: Some(revision),
146 };
147 Ok(())
148 }
149
150 fn rebuild_graph(&self) -> StorageBackendResult<()> {
151 let mut cached = self.cached.lock();
152 self.verify_revision(cached.revision)?;
153 let mut graph = build_from_canonical(&self.raw, self.dimensions, self.params)?;
154 let delta = graph.take_persistence_delta();
155 let revision = next_revision(cached.revision)?;
156 let mut batch = self.store.batch();
157 self.stage_delta(batch.as_mut(), &delta, revision)?;
158 batch.commit()?;
159 *cached = CachedHNSW {
160 graph,
161 revision: Some(revision),
162 };
163 Ok(())
164 }
165
166 fn verify_revision(&self, expected: Option<u64>) -> StorageBackendResult<()> {
167 let Some(expected) = expected else {
168 return Ok(());
169 };
170 let actual =
171 hnsw_persistence::load_revision(self.store.as_ref(), &self.table, &self.field)?
172 .ok_or_else(|| {
173 other_error(format!(
174 "missing persisted HNSW metadata for {}.{}",
175 self.table, self.field
176 ))
177 })?;
178 if actual != expected {
179 return Err(other_error(format!(
180 "concurrent HNSW metadata change for {}.{}: expected revision {expected}, found {actual}",
181 self.table, self.field
182 )));
183 }
184 Ok(())
185 }
186
187 fn stage_delta(
188 &self,
189 batch: &mut dyn super::KeyValueBatch,
190 delta: &HNSWPersistenceDelta,
191 revision: u64,
192 ) -> StorageBackendResult<()> {
193 hnsw_persistence::stage_delta(
194 batch,
195 &self.table,
196 &self.field,
197 self.dimensions,
198 self.params,
199 delta,
200 revision,
201 )
202 }
203}
204
205impl VectorIndex for KeyValueHNSWIndex {
206 fn dimensions(&self) -> u32 {
207 self.dimensions
208 }
209
210 fn index_kind(&self) -> &'static str {
211 "hnsw"
212 }
213
214 fn add(&mut self, doc_id: DocId, vector: Vec<f32>) -> StorageBackendResult<()> {
215 self.replace_document(doc_id, &[vector])
216 }
217
218 fn add_many(&mut self, doc_id: DocId, vectors: Vec<Vec<f32>>) -> StorageBackendResult<()> {
219 self.replace_document(doc_id, &vectors)
220 }
221
222 fn delete(&mut self, doc_id: DocId) -> StorageBackendResult<()> {
223 self.delete_document(doc_id)
224 }
225
226 fn clear(&mut self) -> StorageBackendResult<()> {
227 self.clear_graph()
228 }
229
230 fn search_knn(&self, query: &[f32], k: usize) -> StorageBackendResult<PostingList> {
231 self.cached.lock().graph.search_knn(query, k)
232 }
233
234 fn search_threshold(&self, query: &[f32], threshold: f32) -> StorageBackendResult<PostingList> {
235 self.cached.lock().graph.search_threshold(query, threshold)
236 }
237
238 fn count(&self) -> StorageBackendResult<usize> {
239 self.cached.lock().graph.count()
240 }
241
242 fn initialize(&mut self) -> StorageBackendResult<()> {
243 self.rebuild_graph()
244 }
245
246 fn snapshot(&self) -> StorageBackendResult<Arc<dyn VectorIndex>> {
247 Ok(Arc::new(self.cached.lock().graph.clone()))
248 }
249}
250
251fn build_from_canonical(
252 raw: &KeyValueVectorIndex,
253 dimensions: u32,
254 params: HNSWIndexParams,
255) -> StorageBackendResult<HNSWIndex> {
256 let mut graph = HNSWIndex::with_params(dimensions, params)?;
257 for (doc_id, vectors) in raw.load_by_document()? {
258 graph.add_many(doc_id, vectors)?;
259 }
260 Ok(graph)
261}
262
263fn next_revision(revision: Option<u64>) -> StorageBackendResult<u64> {
264 revision
265 .unwrap_or(0)
266 .checked_add(1)
267 .ok_or_else(|| StorageBackendError::Other("HNSW metadata revision space exhausted".into()))
268}