use std::sync::Arc;
use nodedb_types::Namespace;
use crate::engine::vector::VectorState;
use crate::engine::vector::graph::HnswIndex;
use crate::engine::vector::sidecar;
use crate::error::LiteError;
use crate::nodedb::lock_ext::LockExt;
use crate::storage::engine::StorageEngine;
pub(crate) async fn ensure_index_loaded<S: StorageEngine>(
vector_state: &Arc<VectorState<S>>,
index_key: &str,
) -> Result<(), LiteError> {
let has_it = vector_state
.hnsw_indices
.lock_or_recover()
.contains_key(index_key);
if has_it {
return Ok(());
}
if vector_state
.unloadable
.lock_or_recover()
.contains(index_key)
{
return Ok(());
}
macro_rules! give_up {
() => {{
let _indices = vector_state.hnsw_indices.lock_or_recover();
vector_state
.unloadable
.lock_or_recover()
.insert(index_key.to_string());
vector_state.evicted.lock_or_recover().remove(index_key);
return Ok(());
}};
}
let key = format!("hnsw:{index_key}");
let Some(envelope) = vector_state
.storage
.get(Namespace::Vector, key.as_bytes())
.await?
else {
return Ok(());
};
let Some(checkpoint) = crate::storage::checksum::unwrap(&envelope) else {
tracing::warn!(
index_key,
"HNSW checkpoint CRC32C mismatch on lazy-load; skipping"
);
give_up!();
};
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let Ok(Some(mut index)) = HnswIndex::from_checkpoint(&checkpoint) else {
tracing::warn!(
index_key,
"HNSW checkpoint could not be deserialized on lazy-load; skipping"
);
give_up!();
};
#[cfg(not(target_arch = "wasm32"))]
if let Some(ext) = vector_state.storage.as_vector_segment_ext() {
let attached = match ext.open_vector_segment(index_key).await {
Ok(Some(backing)) => {
let attached = {
let id_map = vector_state.vector_id_map.lock_or_recover();
crate::engine::vector::segment::attach_verified(
&mut index,
backing,
id_map.index(index_key),
)
};
match attached {
Ok(_) => {
tracing::debug!(
index_key,
"lazy-load: attached pagedb vector segment backing"
);
true
}
Err(e) => {
tracing::warn!(
index_key,
error = %e,
"lazy-load: vector segment cannot serve this index \
(short payload or other node order); rebuilding from durable vectors"
);
false
}
}
}
Ok(None) => false,
Err(e) => {
tracing::warn!(
index_key,
error = %e,
"lazy-load: vector segment unreadable; rebuilding from durable vectors"
);
false
}
};
if !attached {
let shape = (index.dim(), index.params().clone());
match crate::engine::vector::durable::rebuild_index(
&*vector_state.storage,
index_key,
Some(shape),
)
.await
{
Ok(Some((rebuilt, id_map))) => {
tracing::info!(
index_key,
vectors = rebuilt.len(),
"lazy-load: HNSW rebuilt from durable vectors"
);
vector_state
.vector_id_map
.lock_or_recover()
.replace_index(index_key, id_map);
index = rebuilt;
}
Err(e) => return Err(e),
Ok(None) => {
tracing::warn!(
index_key,
"lazy-load: no durable vectors to rebuild from; \
leaving collection unloaded rather than publishing a \
vectorless index"
);
give_up!();
}
}
}
}
tracing::info!(index_key, "lazy-loaded HNSW collection from storage");
{
let mut indices = vector_state.hnsw_indices.lock_or_recover();
indices.insert(index_key.to_string(), index);
vector_state.evicted.lock_or_recover().remove(index_key);
}
match sidecar::try_restore_sidecar(vector_state, index_key).await {
Ok(true) => {
tracing::debug!(index_key, "sidecar restored from storage after lazy-load");
}
Ok(false) => {
if let Err(e) = sidecar::ensure_sidecar(vector_state, index_key) {
tracing::warn!(
index_key,
error = %e,
"sidecar rebuild after lazy-load failed; \
codec rerank will degrade to FP32 for this collection"
);
} else {
tracing::debug!(index_key, "sidecar rebuilt after lazy-load");
}
}
Err(e) => {
tracing::warn!(
index_key,
error = %e,
"sidecar restore failed; attempting rebuild via ensure_sidecar"
);
if let Err(e2) = sidecar::ensure_sidecar(vector_state, index_key) {
tracing::warn!(
index_key,
error = %e2,
"sidecar rebuild also failed; \
codec rerank will degrade to FP32 for this collection"
);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::engine::{test_governor, test_scoped_memory};
use crate::storage::pagedb_storage::PagedbStorageMem;
fn test_memory() -> nodedb_mem::ScopedMemory {
test_scoped_memory(&test_governor(), nodedb_mem::EngineId::Vector)
}
#[tokio::test]
async fn an_unloadable_collection_is_only_attempted_once() {
let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
let state = Arc::new(VectorState::new(Arc::clone(&storage), 64, test_memory()));
storage
.put(
Namespace::Vector,
b"hnsw:broken",
&crate::storage::checksum::wrap(b"not a valid hnsw checkpoint"),
)
.await
.unwrap();
ensure_index_loaded(&state, "broken").await.unwrap();
assert!(
state.unloadable.lock_or_recover().contains("broken"),
"an unloadable collection must be recorded so it is not retried"
);
assert!(
!state.hnsw_indices.lock_or_recover().contains_key("broken"),
"an unusable index must never be published"
);
ensure_index_loaded(&state, "broken").await.unwrap();
assert!(!state.hnsw_indices.lock_or_recover().contains_key("broken"));
}
#[tokio::test]
async fn a_later_insert_resolves_an_unloadable_collection() {
let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
let state = Arc::new(VectorState::new(Arc::clone(&storage), 64, test_memory()));
state.unloadable.lock_or_recover().insert("late".into());
{
let mut resident =
crate::engine::vector::resident::lock_resident_or_create(&state, "late", 3)
.await
.unwrap();
resident.index().insert(vec![1.0, 2.0, 3.0]).unwrap();
}
ensure_index_loaded(&state, "late").await.unwrap();
let indices = state.hnsw_indices.lock_or_recover();
assert_eq!(
indices.get("late").map(|i| i.len()),
Some(1),
"a collection populated by insert must stay usable despite the negative cache"
);
}
}