nodedb_lite/engine/vector/
resident.rs1use std::collections::HashMap;
20use std::sync::{Arc, MutexGuard};
21
22use nodedb_types::hnsw::HnswParams;
23use nodedb_types::vector_dtype::VectorStorageDtype;
24
25use crate::engine::vector::HnswIndex;
26use crate::engine::vector::VectorState;
27use crate::engine::vector::search::lazy_load::ensure_index_loaded;
28use crate::error::LiteError;
29use crate::nodedb::lock_ext::LockExt;
30use crate::storage::engine::StorageEngine;
31
32pub(crate) type IndexMap<'a> = MutexGuard<'a, HashMap<String, HnswIndex>>;
34
35pub(crate) async fn lock_resident<'a, S: StorageEngine>(
38 state: &'a Arc<VectorState<S>>,
39 index_key: &str,
40) -> Result<IndexMap<'a>, LiteError> {
41 loop {
42 ensure_index_loaded(state, index_key).await?;
43 let indices = state.hnsw_indices.lock_or_recover();
44 if indices.contains_key(index_key) || !state.evicted.lock_or_recover().contains(index_key) {
45 return Ok(indices);
46 }
47 }
48}
49
50pub(crate) struct ResidentIndex<'a> {
52 indices: IndexMap<'a>,
53 key: String,
54 dim: usize,
55 dtype: VectorStorageDtype,
56}
57
58impl ResidentIndex<'_> {
59 pub(crate) fn index(&mut self) -> &mut HnswIndex {
61 get_or_create(&mut self.indices, &self.key, self.dim, self.dtype)
62 }
63}
64
65pub(crate) async fn lock_resident_or_create<'a, S: StorageEngine>(
70 state: &'a Arc<VectorState<S>>,
71 index_key: &str,
72 dim: usize,
73) -> Result<ResidentIndex<'a>, LiteError> {
74 let dtype = state
75 .per_index_config
76 .lock_or_recover()
77 .get(index_key)
78 .map(|cfg| cfg.storage_dtype)
79 .unwrap_or(VectorStorageDtype::F32);
80 let indices = lock_resident(state, index_key).await?;
81 Ok(ResidentIndex {
82 indices,
83 key: index_key.to_string(),
84 dim,
85 dtype,
86 })
87}
88
89pub(crate) async fn check_insert_widths<S: StorageEngine>(
95 state: &Arc<VectorState<S>>,
96 index_key: &str,
97 widths: impl IntoIterator<Item = usize>,
98) -> Result<(), LiteError> {
99 let mut expected = lock_resident(state, index_key)
100 .await?
101 .get(index_key)
102 .map(HnswIndex::dim);
103 for got in widths {
104 match expected {
105 Some(dim) => nodedb_vector::error::check_dim(dim, got)?,
106 None => expected = Some(got),
107 }
108 }
109 Ok(())
110}
111
112fn get_or_create<'a>(
115 indices: &'a mut HashMap<String, HnswIndex>,
116 index_key: &str,
117 dim: usize,
118 dtype: VectorStorageDtype,
119) -> &'a mut HnswIndex {
120 indices.entry(index_key.to_string()).or_insert_with(|| {
121 HnswIndex::new(
122 dim,
123 HnswParams {
124 dtype,
125 ..HnswParams::default()
126 },
127 )
128 })
129}
130
131#[cfg(test)]
132mod tests {
133 use nodedb_types::Namespace;
134
135 use super::*;
136 use crate::query::engine::{test_governor, test_scoped_memory};
137 use crate::storage::pagedb_storage::PagedbStorageMem;
138
139 async fn state() -> Arc<VectorState<PagedbStorageMem>> {
140 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
141 let memory = test_scoped_memory(&test_governor(), nodedb_mem::EngineId::Vector);
142 Arc::new(VectorState::new(storage, 64, memory))
143 }
144
145 #[test]
146 fn get_or_create_uses_the_dtype_only_at_creation() {
147 let mut indices: HashMap<String, HnswIndex> = HashMap::new();
148 get_or_create(&mut indices, "col", 4, VectorStorageDtype::BF16);
149 assert_eq!(
150 indices.get("col").map(|i| i.params().dtype),
151 Some(VectorStorageDtype::BF16)
152 );
153 get_or_create(&mut indices, "col", 4, VectorStorageDtype::F32);
154 assert_eq!(
155 indices.get("col").map(|i| i.params().dtype),
156 Some(VectorStorageDtype::BF16),
157 "dtype is fixed at index-creation time"
158 );
159 }
160
161 #[tokio::test]
164 async fn an_evicted_index_is_loaded_not_recreated() {
165 let state = state().await;
166 state
169 .storage
170 .batch_write(&[crate::engine::vector::durable::put_op(
171 "docs",
172 "a",
173 &[1.0, 0.0, 0.0],
174 )])
175 .await
176 .unwrap();
177 {
178 let mut resident = lock_resident_or_create(&state, "docs", 3).await.unwrap();
179 resident.index().insert(vec![1.0, 0.0, 0.0]).unwrap();
180 }
181 let blob = state
183 .hnsw_indices
184 .lock_or_recover()
185 .get("docs")
186 .unwrap()
187 .checkpoint_to_bytes()
188 .unwrap();
189 state
190 .storage
191 .put(
192 Namespace::Vector,
193 b"hnsw:docs",
194 &crate::storage::checksum::wrap(&blob),
195 )
196 .await
197 .unwrap();
198 {
199 let mut indices = state.hnsw_indices.lock_or_recover();
200 state.evicted.lock_or_recover().insert("docs".into());
201 indices.remove("docs");
202 }
203
204 let mut resident = lock_resident_or_create(&state, "docs", 3).await.unwrap();
205 assert_eq!(resident.index().len(), 1, "the evicted vector is back");
206 assert!(!state.evicted.lock_or_recover().contains("docs"));
207 }
208
209 #[tokio::test]
211 async fn a_new_collection_gets_an_empty_index() {
212 let state = state().await;
213 let mut resident = lock_resident_or_create(&state, "fresh", 2).await.unwrap();
214 assert_eq!(resident.index().dim(), 2);
215 assert_eq!(resident.index().len(), 0);
216 }
217}