1use std::sync::Arc;
10use uqa_core::{memory::Budgeted, DocId, PostingList};
11
12use super::codec::encode_value;
13use super::codec::{other_error, vector_field_prefix};
14use super::hnsw_persistence::{self, metadata_from_graph, PersistedHNSWNode};
15use super::index_keys::{hnsw_metadata_key, hnsw_node_key, hnsw_node_prefix};
16use super::{KeyValueBatch, KeyValueRead, KeyValueStore, KeyValueVectorIndex};
17use crate::hnsw_index::{HNSWCanonicalBuilder, HNSWGraphDelta, HNSWIndex, HNSWMutation};
18use crate::vector_index::{HNSWIndexParams, VectorIndex};
19use crate::{StorageBackendError, StorageBackendResult};
20
21use super::index_view::{read_view, IndexState, IndexView};
22
23#[cfg(test)]
24mod tests;
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 require_persisted: bool,
34 view: IndexView<HNSWIndex>,
35}
36
37impl KeyValueHNSWIndex {
38 pub fn create(
39 store: Arc<dyn KeyValueStore>,
40 table: impl Into<String>,
41 field: impl Into<String>,
42 dimensions: u32,
43 params: HNSWIndexParams,
44 ) -> StorageBackendResult<Self> {
45 let index = Self::new(store, table.into(), field.into(), dimensions, params, false)?;
46 index.read_graph()?;
47 Ok(index)
48 }
49
50 pub fn restore(
51 store: Arc<dyn KeyValueStore>,
52 table: impl Into<String>,
53 field: impl Into<String>,
54 dimensions: u32,
55 params: HNSWIndexParams,
56 ) -> StorageBackendResult<Self> {
57 let index = Self::new(store, table.into(), field.into(), dimensions, params, true)?;
58 index.read_graph()?;
59 Ok(index)
60 }
61
62 fn new(
63 store: Arc<dyn KeyValueStore>,
64 table: String,
65 field: String,
66 dimensions: u32,
67 params: HNSWIndexParams,
68 require_persisted: bool,
69 ) -> StorageBackendResult<Self> {
70 Ok(Self {
71 raw: KeyValueVectorIndex::new(Arc::clone(&store), &table, &field, dimensions),
72 store,
73 table,
74 field,
75 dimensions,
76 params: params.validate()?,
77 require_persisted,
78 view: IndexView::new(!require_persisted),
79 })
80 }
81
82 fn graph_at(&self, read: &dyn KeyValueRead) -> StorageBackendResult<IndexState<HNSWIndex>> {
83 self.view.load(
84 read,
85 &[
86 &hnsw_metadata_key(&self.table, &self.field)?,
87 &hnsw_node_prefix(&self.table, &self.field)?,
88 &vector_field_prefix(&self.table, &self.field)?,
89 ],
90 |creating| {
91 let revision = hnsw_persistence::load_revision(read, &self.table, &self.field)?;
92 let graph = if creating || (revision.is_none() && !self.require_persisted) {
93 self.build_from_canonical(read)?
94 } else {
95 hnsw_persistence::restore_graph(
96 read,
97 &self.raw,
98 &self.table,
99 &self.field,
100 self.dimensions,
101 self.params,
102 )?
103 .0
104 };
105 Ok((graph, revision))
106 },
107 )
108 }
109
110 fn read_graph(&self) -> StorageBackendResult<IndexState<HNSWIndex>> {
111 read_view(self.store.as_ref(), |read| self.graph_at(read))
112 }
113
114 fn mutate_graph(
115 &self,
116 mutation: HNSWMutation<'_>,
117 canonical: impl FnOnce(&mut dyn KeyValueBatch) -> StorageBackendResult<()>,
118 ) -> StorageBackendResult<()> {
119 let metadata = hnsw_metadata_key(&self.table, &self.field)?;
120 let nodes = hnsw_node_prefix(&self.table, &self.field)?;
121 let vectors = vector_field_prefix(&self.table, &self.field)?;
122 self.view.evaluate_candidate(
123 self.store.as_ref(),
124 &[&metadata, &nodes, &vectors],
125 |read, batch| {
126 let cached = self.graph_at(read)?;
127 let preview = !cached.definition_candidate
128 && cached.revision.is_some()
129 && !matches!(mutation, HNSWMutation::Clear);
130 if preview {
131 batch.hnsw_mutation(&hnsw_metadata_key(&self.table, &self.field)?, mutation)?;
133 }
134 let delta = cached.value.prepare_delta(mutation, read.control())?;
135 canonical(batch)?;
136 let revision = next_revision(cached.revision)?;
137 self.stage_delta(batch, &delta, revision, preview)?;
138 let (delta, memory) = delta.into_parts();
139 Ok((Budgeted::new(delta.into_graph(), memory), revision))
140 },
141 )
142 }
143
144 fn rebuild_graph(&self) -> StorageBackendResult<()> {
145 self.view.evaluate(self.store.as_ref(), |read, batch| {
146 let revision = hnsw_persistence::load_revision(read, &self.table, &self.field)?;
147 if revision.is_none() && self.require_persisted {
148 return Err(other_error(format!(
149 "missing persisted HNSW metadata for {}.{}",
150 self.table, self.field
151 )));
152 }
153 let delta = self.canonical_builder(read)?.finish_delta()?;
154 self.stage_delta(batch, &delta, next_revision(revision)?, false)
155 })
156 }
157
158 fn build_from_canonical(
159 &self,
160 read: &dyn KeyValueRead,
161 ) -> StorageBackendResult<Budgeted<HNSWIndex>> {
162 self.canonical_builder(read)?.finish()
163 }
164
165 fn canonical_builder(
166 &self,
167 read: &dyn KeyValueRead,
168 ) -> StorageBackendResult<HNSWCanonicalBuilder> {
169 let mut builder = HNSWCanonicalBuilder::new(self.dimensions, self.params, read.control())?;
170 self.raw
171 .visit_canonical_from(read, |document, ordinal, vector| {
172 builder.push(document, ordinal, vector)
173 })?;
174 Ok(builder)
175 }
176
177 fn stage_delta(
178 &self,
179 batch: &mut dyn KeyValueBatch,
180 delta: &HNSWGraphDelta,
181 revision: u64,
182 preview: bool,
183 ) -> StorageBackendResult<()> {
184 let metadata = hnsw_metadata_key(&self.table, &self.field)?;
185 let value = encode_value(&metadata_from_graph(
186 self.dimensions,
187 self.params,
188 delta.meta,
189 revision,
190 )?)?;
191 if preview {
192 batch.preview_hnsw_record(&metadata, Some(&value))?;
193 } else {
194 batch.fence_hnsw_prefix(&metadata)?;
195 batch.put(&metadata, &value)?;
196 }
197 if delta.full_rewrite {
198 let prefix = hnsw_node_prefix(&self.table, &self.field)?;
199 if preview {
200 batch.preview_hnsw_prefix(&prefix)?;
201 } else {
202 batch.delete_prefix(&prefix)?;
203 }
204 }
205 for node in delta.nodes() {
206 let node = node?;
207 let key = hnsw_node_key(&self.table, &self.field, node.node_id)?;
208 let value = encode_value(&PersistedHNSWNode::try_from(&*node)?)?;
209 if preview {
210 batch.preview_hnsw_record(&key, Some(&value))?;
211 } else {
212 batch.put(&key, &value)?;
213 }
214 }
215 Ok(())
216 }
217}
218
219impl VectorIndex for KeyValueHNSWIndex {
220 fn contains_document(&self, doc_id: DocId) -> StorageBackendResult<bool> {
221 self.raw.contains_document(doc_id)
222 }
223
224 fn dimensions(&self) -> u32 {
225 self.dimensions
226 }
227 fn index_kind(&self) -> &'static str {
228 "hnsw"
229 }
230 fn add(&mut self, doc_id: DocId, vector: Vec<f32>) -> StorageBackendResult<()> {
231 self.add_many(doc_id, vec![vector])
232 }
233 fn add_many(&mut self, doc_id: DocId, vectors: Vec<Vec<f32>>) -> StorageBackendResult<()> {
234 self.mutate_graph(
235 HNSWMutation::Replace {
236 document: doc_id,
237 vectors: &vectors,
238 },
239 |batch| self.raw.stage_replace(batch, doc_id, &vectors),
240 )
241 }
242 fn delete(&mut self, doc_id: DocId) -> StorageBackendResult<()> {
243 self.mutate_graph(HNSWMutation::Delete(doc_id), |batch| {
244 self.raw.stage_replace(batch, doc_id, &[])
245 })
246 }
247 fn clear(&mut self) -> StorageBackendResult<()> {
248 self.mutate_graph(HNSWMutation::Clear, |batch| self.raw.stage_clear(batch))
249 }
250 fn search_knn(&self, query: &[f32], k: usize) -> StorageBackendResult<PostingList> {
251 self.read_graph()?.value.search_knn(query, k)
252 }
253 fn search_threshold(&self, query: &[f32], threshold: f32) -> StorageBackendResult<PostingList> {
254 self.read_graph()?.value.search_threshold(query, threshold)
255 }
256 fn count(&self) -> StorageBackendResult<usize> {
257 self.read_graph()?.value.count()
258 }
259 fn initialize(&mut self) -> StorageBackendResult<()> {
260 self.rebuild_graph()
261 }
262 fn snapshot(&self) -> StorageBackendResult<Arc<dyn VectorIndex>> {
263 Ok(self.read_graph()?.snapshot)
264 }
265 fn vector_read_snapshot(
266 &self,
267 control: &crate::read_control::StorageReadControl,
268 ) -> StorageBackendResult<Option<crate::vector_index::VectorReadSnapshot>> {
269 self.raw.vector_read_snapshot(control)
270 }
271}
272
273fn next_revision(revision: Option<u64>) -> StorageBackendResult<u64> {
274 revision
275 .unwrap_or(0)
276 .checked_add(1)
277 .ok_or_else(|| StorageBackendError::Other("HNSW metadata revision space exhausted".into()))
278}