use std::collections::HashMap;
use std::sync::Arc;
use nodedb_fts::backend::FtsBackend;
use nodedb_fts::backend::memory::MemoryBackend;
use nodedb_fts::{FtsIndex, IndexScope};
use nodedb_mem::MemoryGovernor;
use nodedb_types::Namespace;
use nodedb_types::Surrogate;
use nodedb_types::error::{NodeDbError, NodeDbResult};
use super::format::{
COLLECTIONS_KEY, DB, FtsSurrogateState, LAYOUT_KEY, LAYOUT_PER_FIELD, META_SUBKEYS,
SURROGATES_KEY, SerPosting, TID, doclens_key, meta_key, postings_prefix, scoped_term_prefix,
ser_to_compact,
};
use crate::engine::fts::manager::resident_index;
use crate::storage::engine::StorageEngine;
#[derive(Default)]
pub(crate) struct RestoredFts {
pub indices: HashMap<String, FtsIndex<MemoryBackend>>,
pub id_to_surrogate: HashMap<String, u32>,
pub surrogate_to_id: HashMap<u32, String>,
pub next_surrogate: u32,
pub per_field_layout: bool,
}
fn decode_err(what: &str, e: impl std::fmt::Display) -> NodeDbError {
NodeDbError::serialization("msgpack", format!("fts checkpoint {what}: {e}"))
}
fn backend_err(index_key: &str, e: impl std::fmt::Display) -> NodeDbError {
NodeDbError::storage(format!("fts checkpoint restore of '{index_key}': {e}"))
}
async fn restore_postings<S: StorageEngine>(
storage: &S,
index_key: &str,
idx: &FtsIndex<MemoryBackend>,
) -> NodeDbResult<()> {
let scope = IndexScope::document(index_key);
let term_scope = scoped_term_prefix(index_key);
#[cfg(not(target_arch = "wasm32"))]
if let Some(seg_ext) = storage.as_fts_segment_ext()
&& let Some(blob) = seg_ext
.open_fts_segment(index_key)
.await
.map_err(|e| backend_err(index_key, e))?
{
let entries = zerompk::from_msgpack::<super::format::PostingsBlob>(&blob)
.map_err(|e| decode_err(&format!("segment of '{index_key}'"), e))?;
for (scoped_term, postings) in entries {
let term = scoped_term
.strip_prefix(term_scope.as_str())
.unwrap_or(&scoped_term);
for sp in postings {
idx.memtable()
.insert(DB, TID, scope, term, ser_to_compact(sp));
}
}
return Ok(());
}
let prefix = postings_prefix(index_key);
for (raw_key, value) in storage
.scan_prefix(Namespace::Fts, prefix.as_bytes())
.await?
{
let scoped_term = std::str::from_utf8(&raw_key)
.ok()
.and_then(|k| k.strip_prefix(&prefix))
.filter(|t| !t.is_empty())
.ok_or_else(|| {
decode_err(
&format!("posting key of '{index_key}'"),
"not a UTF-8 term key",
)
})?;
let Some(term) = scoped_term.strip_prefix(term_scope.as_str()) else {
continue;
};
let postings = zerompk::from_msgpack::<Vec<SerPosting>>(&value)
.map_err(|e| decode_err(&format!("postings of '{scoped_term}'"), e))?;
for sp in postings {
idx.memtable()
.insert(DB, TID, scope, term, ser_to_compact(sp));
}
}
Ok(())
}
pub(crate) async fn restore_fts<S>(
storage: &S,
governor: Arc<MemoryGovernor>,
) -> NodeDbResult<RestoredFts>
where
S: StorageEngine,
{
let Some(keys_bytes) = storage.get(Namespace::Fts, COLLECTIONS_KEY).await? else {
return Ok(RestoredFts::default());
};
let mut index_keys = zerompk::from_msgpack::<Vec<String>>(&keys_bytes)
.map_err(|e| decode_err("index key list", e))?;
let per_field_layout = storage
.get(Namespace::Fts, LAYOUT_KEY)
.await?
.is_some_and(|v| v.first().copied() == Some(LAYOUT_PER_FIELD));
if !per_field_layout {
index_keys.retain(|k| !k.ends_with(":_doc"));
}
if index_keys.is_empty() {
return Ok(RestoredFts::default());
}
let surrogate_bytes = storage
.get(Namespace::Fts, SURROGATES_KEY)
.await?
.ok_or_else(|| decode_err("surrogate maps", "missing"))?;
let state = zerompk::from_msgpack::<FtsSurrogateState>(&surrogate_bytes)
.map_err(|e| decode_err("surrogate maps", e))?;
let mut id_to_surrogate: HashMap<String, u32> =
HashMap::with_capacity(state.id_to_surrogate.len());
let mut surrogate_to_id: HashMap<u32, String> =
HashMap::with_capacity(state.id_to_surrogate.len());
for (id, s) in state.id_to_surrogate {
surrogate_to_id.insert(s, id.clone());
id_to_surrogate.insert(id, s);
}
let mut indices: HashMap<String, FtsIndex<MemoryBackend>> =
HashMap::with_capacity(index_keys.len());
for index_key in &index_keys {
let scope = IndexScope::document(index_key);
let idx = resident_index(Arc::clone(&governor));
restore_postings(storage, index_key, &idx).await?;
if let Some(data) = storage
.get(Namespace::Fts, doclens_key(index_key).as_bytes())
.await?
{
let pairs = zerompk::from_msgpack::<Vec<(u32, u32)>>(&data)
.map_err(|e| decode_err(&format!("doc lengths of '{index_key}'"), e))?;
for (s, len) in pairs {
idx.backend()
.write_doc_length(DB, TID, scope, Surrogate(s), len)
.map_err(|e| backend_err(index_key, e))?;
idx.backend()
.increment_stats(DB, TID, scope, len)
.map_err(|e| backend_err(index_key, e))?;
}
}
for &subkey in META_SUBKEYS {
if let Some(data) = storage
.get(Namespace::Fts, meta_key(index_key, subkey).as_bytes())
.await?
{
idx.backend()
.write_meta(DB, TID, scope, subkey, &data)
.map_err(|e| backend_err(index_key, e))?;
}
}
indices.insert(index_key.clone(), idx);
}
tracing::debug!(
index_count = indices.len(),
surrogate_count = id_to_surrogate.len(),
per_field_layout,
"fts checkpoint restored"
);
Ok(RestoredFts {
indices,
id_to_surrogate,
surrogate_to_id,
next_surrogate: state.next_surrogate,
per_field_layout,
})
}