use std::sync::Arc;
use crate::core::registry::{IndexHandle, IndexId};
use crate::service::persistence::PersistedIndex;
use crate::service::persistence_loader::build_indexer_from_entry;
use crate::service::warm_boot::{
canonicalize_best_effort, derive_warm_boot_stages, WarmBootInputs,
};
use crate::service::SearchAppState;
pub(crate) async fn restore_index_on_demand(
state: &SearchAppState,
embedder: &Arc<dyn crate::core::Embedder>,
mut entry: PersistedIndex,
) {
let id = IndexId::new(entry.id.clone());
if state.registry.get(&id).is_some() {
return;
}
if !entry.root_path.exists() {
tracing::warn!(
"lazy-load: skipping index '{}' — root_path {} no longer exists",
entry.id,
entry.root_path.display(),
);
return;
}
let canonical_root = canonicalize_best_effort(&entry.root_path);
if canonical_root != entry.root_path {
tracing::info!(
"lazy-load: index '{}' root_path canonicalized: {} → {}",
entry.id,
entry.root_path.display(),
canonical_root.display(),
);
entry.root_path = canonical_root;
}
let mut indexer = match build_indexer_from_entry(&entry, embedder).await {
Ok(idx) => idx,
Err(e) => {
tracing::error!(
"lazy-load: index '{}' HNSW allocator failed: {e} — skipping",
entry.id
);
return;
}
};
let include_paths: Vec<std::path::PathBuf> = entry
.include_paths
.iter()
.filter(|p| !p.trim().is_empty() && p.trim() != ".")
.map(|p| entry.root_path.join(p.trim()))
.collect();
let extensions: Vec<String> = entry
.extensions
.iter()
.map(|e| e.trim_start_matches('.').to_string())
.filter(|e| !e.is_empty())
.collect();
indexer.set_domain_terms(entry.domain_terms.clone());
let indexed_head_sha = crate::core::git::head_sha(&entry.root_path);
let lexical_only = entry.lexical_only;
let skip_kg = entry.skip_kg;
let defer_embed = entry.defer_embed;
let corpus_open_failed = indexer.corpus_open_failed;
let chunk_count = indexer
.corpus_store()
.and_then(|c| c.chunk_count().ok())
.unwrap_or(0);
let hnsw_snapshot_ready = crate::service::persistence::hnsw_path_for_entry(&entry)
.map(|p| crate::service::persistence::has_persisted_hnsw(&p))
.unwrap_or(false);
let graph_node_count = indexer.snapshot_symbol_graph().await.node_count();
let stages = derive_warm_boot_stages(WarmBootInputs {
chunk_count,
hnsw_snapshot_ready,
graph_node_count,
lexical_only,
skip_kg,
corpus_open_failed,
});
tracing::info!(
"lazy-load: index '{}' restored — chunks={} hnsw_snapshot={} \
graph_nodes={} lexical_only={} skip_kg={} corpus_open_failed={}",
entry.id,
chunk_count,
hnsw_snapshot_ready,
graph_node_count,
lexical_only,
skip_kg,
corpus_open_failed,
);
let handle = IndexHandle {
id: id.clone(),
indexer: Arc::new(tokio::sync::RwLock::new(indexer)),
root_path: entry.root_path,
include_paths,
exclude_globs: entry.exclude_globs,
extensions,
domain_terms: entry.domain_terms,
include_docs: entry.include_docs,
respect_gitignore: entry.respect_gitignore,
extra_skip_dirs: entry.extra_skip_dirs,
data_file_max_bytes: crate::service::persistence::resolve_data_file_max_bytes(
entry.data_file_max_bytes,
),
path_filter: entry.path_filter,
context_embedding: Arc::new(tokio::sync::RwLock::new(None)),
context_summary: Arc::new(tokio::sync::RwLock::new(None)),
indexed_head_sha: Arc::new(tokio::sync::RwLock::new(indexed_head_sha)),
last_indexed_at: Arc::new(tokio::sync::RwLock::new(None)),
lexical_only,
skip_kg,
defer_embed,
stages: Arc::new(tokio::sync::RwLock::new(stages)),
search_pressure: Arc::new(tokio::sync::Notify::new()),
walk_diagnostics: Arc::new(tokio::sync::RwLock::new(
crate::core::registry::WalkDiagnostics::default(),
)),
};
state.registry.register(handle);
}