use std::collections::HashMap;
use std::sync::{Arc, MutexGuard};
use nodedb_types::hnsw::HnswParams;
use nodedb_types::vector_dtype::VectorStorageDtype;
use crate::engine::vector::HnswIndex;
use crate::engine::vector::VectorState;
use crate::engine::vector::search::lazy_load::ensure_index_loaded;
use crate::error::LiteError;
use crate::nodedb::lock_ext::LockExt;
use crate::storage::engine::StorageEngine;
pub(crate) type IndexMap<'a> = MutexGuard<'a, HashMap<String, HnswIndex>>;
pub(crate) async fn lock_resident<'a, S: StorageEngine>(
state: &'a Arc<VectorState<S>>,
index_key: &str,
) -> Result<IndexMap<'a>, LiteError> {
loop {
ensure_index_loaded(state, index_key).await?;
let indices = state.hnsw_indices.lock_or_recover();
if indices.contains_key(index_key) || !state.evicted.lock_or_recover().contains(index_key) {
return Ok(indices);
}
}
}
pub(crate) struct ResidentIndex<'a> {
indices: IndexMap<'a>,
key: String,
dim: usize,
dtype: VectorStorageDtype,
}
impl ResidentIndex<'_> {
pub(crate) fn index(&mut self) -> &mut HnswIndex {
get_or_create(&mut self.indices, &self.key, self.dim, self.dtype)
}
}
pub(crate) async fn lock_resident_or_create<'a, S: StorageEngine>(
state: &'a Arc<VectorState<S>>,
index_key: &str,
dim: usize,
) -> Result<ResidentIndex<'a>, LiteError> {
let dtype = state
.per_index_config
.lock_or_recover()
.get(index_key)
.map(|cfg| cfg.storage_dtype)
.unwrap_or(VectorStorageDtype::F32);
let indices = lock_resident(state, index_key).await?;
Ok(ResidentIndex {
indices,
key: index_key.to_string(),
dim,
dtype,
})
}
pub(crate) async fn check_insert_widths<S: StorageEngine>(
state: &Arc<VectorState<S>>,
index_key: &str,
widths: impl IntoIterator<Item = usize>,
) -> Result<(), LiteError> {
let mut expected = lock_resident(state, index_key)
.await?
.get(index_key)
.map(HnswIndex::dim);
for got in widths {
match expected {
Some(dim) => nodedb_vector::error::check_dim(dim, got)?,
None => expected = Some(got),
}
}
Ok(())
}
fn get_or_create<'a>(
indices: &'a mut HashMap<String, HnswIndex>,
index_key: &str,
dim: usize,
dtype: VectorStorageDtype,
) -> &'a mut HnswIndex {
indices.entry(index_key.to_string()).or_insert_with(|| {
HnswIndex::new(
dim,
HnswParams {
dtype,
..HnswParams::default()
},
)
})
}
#[cfg(test)]
mod tests {
use nodedb_types::Namespace;
use super::*;
use crate::query::engine::{test_governor, test_scoped_memory};
use crate::storage::pagedb_storage::PagedbStorageMem;
async fn state() -> Arc<VectorState<PagedbStorageMem>> {
let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
let memory = test_scoped_memory(&test_governor(), nodedb_mem::EngineId::Vector);
Arc::new(VectorState::new(storage, 64, memory))
}
#[test]
fn get_or_create_uses_the_dtype_only_at_creation() {
let mut indices: HashMap<String, HnswIndex> = HashMap::new();
get_or_create(&mut indices, "col", 4, VectorStorageDtype::BF16);
assert_eq!(
indices.get("col").map(|i| i.params().dtype),
Some(VectorStorageDtype::BF16)
);
get_or_create(&mut indices, "col", 4, VectorStorageDtype::F32);
assert_eq!(
indices.get("col").map(|i| i.params().dtype),
Some(VectorStorageDtype::BF16),
"dtype is fixed at index-creation time"
);
}
#[tokio::test]
async fn an_evicted_index_is_loaded_not_recreated() {
let state = state().await;
state
.storage
.batch_write(&[crate::engine::vector::durable::put_op(
"docs",
"a",
&[1.0, 0.0, 0.0],
)])
.await
.unwrap();
{
let mut resident = lock_resident_or_create(&state, "docs", 3).await.unwrap();
resident.index().insert(vec![1.0, 0.0, 0.0]).unwrap();
}
let blob = state
.hnsw_indices
.lock_or_recover()
.get("docs")
.unwrap()
.checkpoint_to_bytes()
.unwrap();
state
.storage
.put(
Namespace::Vector,
b"hnsw:docs",
&crate::storage::checksum::wrap(&blob),
)
.await
.unwrap();
{
let mut indices = state.hnsw_indices.lock_or_recover();
state.evicted.lock_or_recover().insert("docs".into());
indices.remove("docs");
}
let mut resident = lock_resident_or_create(&state, "docs", 3).await.unwrap();
assert_eq!(resident.index().len(), 1, "the evicted vector is back");
assert!(!state.evicted.lock_or_recover().contains("docs"));
}
#[tokio::test]
async fn a_new_collection_gets_an_empty_index() {
let state = state().await;
let mut resident = lock_resident_or_create(&state, "fresh", 2).await.unwrap();
assert_eq!(resident.index().dim(), 2);
assert_eq!(resident.index().len(), 0);
}
}