Skip to main content

uqa_storage/key_value/
hnsw_index.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Transactional HNSW graph persistence over the logical Key/Value backend.
8
9use 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}