use alloc::format;
use alloc::vec::Vec;
use plugmem_arena::{
Arena, ArenaCfg, BlobHeap, BlobHeapBuilder, BlobHeapCfg, BlobId, ChunkPool, ChunkPoolCfg,
ListHandle, ShardMode,
};
use crate::error::Error;
use crate::id::{FactId, NONE_U32};
use crate::index::IdListIndex;
use crate::index::bm25::Bm25Index;
use crate::index::hnsw::{HnswGraph, HnswScratch};
use crate::index::vecpool::VecPool;
use crate::journal::Op;
use crate::memory::persist::Sections;
use crate::model::{EntityRecord, FactAux, FactRecord, TemporalSlot};
use crate::snapshot::SnapshotSink;
use crate::storage::{Scratch, Storage};
use crate::tokenizer::Tokenizer;
use super::Memory;
fn scratch_err<E: core::fmt::Debug>(e: E) -> Error {
Error::Storage(format!("{e:?}"))
}
trait PoolSink {
fn push_text(&mut self, bytes: &[u8]) -> Result<BlobId, Error>;
fn push_vector(&mut self, src: &VecPool<'_>, slot: u32) -> Result<u32, Error>;
}
struct OwnedPools {
texts: BlobHeap<'static>,
vecs: VecPool<'static>,
}
impl PoolSink for OwnedPools {
fn push_text(&mut self, bytes: &[u8]) -> Result<BlobId, Error> {
Ok(self.texts.push(bytes)?)
}
fn push_vector(&mut self, src: &VecPool<'_>, slot: u32) -> Result<u32, Error> {
Ok(self.vecs.copy_slot(src, slot))
}
}
struct StreamPools<'s, T: Scratch, V: Scratch> {
text_scratch: &'s mut T,
text_index: BlobHeapBuilder,
vec_scratch: &'s mut V,
vec_count: u32,
}
impl<T: Scratch, V: Scratch> PoolSink for StreamPools<'_, T, V> {
fn push_text(&mut self, bytes: &[u8]) -> Result<BlobId, Error> {
self.text_scratch.write(bytes).map_err(scratch_err)?;
Ok(self.text_index.push_len(bytes.len())?)
}
fn push_vector(&mut self, src: &VecPool<'_>, slot: u32) -> Result<u32, Error> {
self.vec_scratch
.write(src.slot_bytes(slot as usize))
.map_err(scratch_err)?;
let new = self.vec_count;
self.vec_count += 1;
Ok(new)
}
}
struct RebuildMeta {
facts: Arena<'static, FactRecord>,
fact_aux: Arena<'static, FactAux>,
entities: Arena<'static, EntityRecord>,
temporal: Arena<'static, TemporalSlot>,
tag_lists: ChunkPool<'static>,
metas: BlobHeap<'static>,
bm25: Bm25Index<'static>,
tags_idx: IdListIndex<'static>,
entity_facts: IdListIndex<'static>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MaintainReport {
pub purged: usize,
pub bytes_before: usize,
pub bytes_after: usize,
}
struct Rebuilt {
facts: Arena<'static, FactRecord>,
entities: Arena<'static, EntityRecord>,
fact_aux: Arena<'static, FactAux>,
texts: BlobHeap<'static>,
metas: BlobHeap<'static>,
tag_lists: ChunkPool<'static>,
bm25: Bm25Index<'static>,
tags_idx: IdListIndex<'static>,
entity_facts: IdListIndex<'static>,
temporal: Arena<'static, TemporalSlot>,
vecs: VecPool<'static>,
hnsw: HnswGraph<'static>,
}
impl Memory<'_> {
pub fn maintain<S: Storage>(
&mut self,
store: &mut S,
now: u64,
) -> Result<MaintainReport, Error> {
let bytes_before = self.satellite_bytes();
let (rebuilt, purged) = self.rebuild()?;
let mut entry = Vec::new();
Op::Maintain { now }.encode(&mut entry);
store
.append_journal(&entry)
.map_err(|e| Error::Storage(format!("{e:?}")))?;
self.install(rebuilt);
Ok(MaintainReport {
purged,
bytes_before,
bytes_after: self.satellite_bytes(),
})
}
pub(super) fn replay_maintain(&mut self) -> Result<(), Error> {
let (rebuilt, _) = self.rebuild()?;
self.install(rebuilt);
Ok(())
}
fn satellite_bytes(&self) -> usize {
self.facts.pool_bytes()
+ self.fact_aux.pool_bytes()
+ self.entities.pool_bytes()
+ self.hnsw.pool_bytes()
+ self.texts.pool_bytes()
+ self.metas.pool_bytes()
+ self.tag_lists.pool_bytes()
+ self.bm25.pool_bytes()
+ self.tags_idx.pool_bytes()
+ self.entity_facts.pool_bytes()
+ self.temporal.pool_bytes()
+ self.vecs.pool_bytes()
}
fn rebuild(&self) -> Result<(Rebuilt, usize), Error> {
let cfg = &self.cfg;
let blob = BlobHeapCfg::new()
.with_max_bytes(cfg.max_bytes)
.with_max_blob(cfg.max_blob);
let mut pools = OwnedPools {
texts: BlobHeap::new(blob),
vecs: VecPool::new(cfg.dim, cfg.max_bytes),
};
let (m, vec_map, purged) = self.rebuild_parts(&mut pools)?;
let hnsw = self.rebuild_graph(&vec_map, &pools.vecs)?;
Ok((
Rebuilt {
facts: m.facts,
entities: m.entities,
fact_aux: m.fact_aux,
texts: pools.texts,
metas: m.metas,
tag_lists: m.tag_lists,
bm25: m.bm25,
tags_idx: m.tags_idx,
entity_facts: m.entity_facts,
temporal: m.temporal,
vecs: pools.vecs,
hnsw,
},
purged,
))
}
fn rebuild_parts<P: PoolSink>(
&self,
pools: &mut P,
) -> Result<(RebuildMeta, alloc::vec::Vec<u32>, usize), Error> {
let cfg = &self.cfg;
let uni =
|shards: usize| ArenaCfg::new(shards, ShardMode::Uniform).with_max_bytes(cfg.max_bytes);
let ord =
|shards: usize| ArenaCfg::new(shards, ShardMode::Ordered).with_max_bytes(cfg.max_bytes);
let mut entities = Arena::new(uni(cfg.shards_entities))?;
let mut facts = Arena::new(uni(cfg.shards_facts))?;
let mut fact_aux = Arena::new(uni(cfg.shards_facts))?;
let mut tag_lists = ChunkPool::new(ChunkPoolCfg::new().with_max_bytes(cfg.max_bytes));
let mut bm25 = Bm25Index::new(cfg.shards_postings, cfg.max_bytes)?;
let mut tags_idx = IdListIndex::new(cfg.shards_postings, cfg.max_bytes)?;
let mut entity_facts = IdListIndex::new(cfg.shards_entities, cfg.max_bytes)?;
let mut temporal = Arena::new(ord(cfg.shards_temporal))?;
let mut metas = BlobHeap::new(
BlobHeapCfg::new()
.with_max_bytes(cfg.max_bytes)
.with_max_blob(cfg.max_blob),
);
for eid in 0..self.next_entity {
let rec = self
.entities
.get(&eid.to_be_bytes())
.ok_or(Error::Corrupt("maintain: entity id gap"))?;
let name_id = pools.push_text(self.texts.get(rec.name))?;
entities.insert(&EntityRecord {
name: name_id,
..rec
})?;
}
let mut tokenizer = Tokenizer::new();
let mut tf: Vec<(u32, u8)> = Vec::new();
let mut vec_map = alloc::vec![NONE_U32; self.vecs.len()];
let mut purged = 0usize;
for fid in 0..self.next_fact {
let id = FactId(fid);
let Some(rec) = self.facts.get(&fid.to_be_bytes()) else {
continue;
};
if rec.is_tombstone() {
purged += 1;
continue;
}
let text_bytes = self.texts.get(rec.text);
let text_id = pools.push_text(text_bytes)?;
let text = core::str::from_utf8(text_bytes)
.map_err(|_| Error::Corrupt("maintain: fact text is not UTF-8"))?;
tf.clear();
let terms = &self.terms;
let tf_ref = &mut tf;
tokenizer.tokenize(text, &mut |token| {
if let Some(term) = terms.lookup(token) {
match tf_ref.iter_mut().find(|(t, _)| *t == term.0) {
Some((_, c)) => *c = c.saturating_add(1),
None => tf_ref.push((term.0, 1)),
}
}
});
bm25.index_doc(id, &tf)?;
let aux = self
.fact_aux
.get(&fid.to_be_bytes())
.ok_or(Error::Corrupt("maintain: fact aux gap"))?;
let mut tags = ListHandle::EMPTY;
for chunk in self.tag_lists.iter(&aux.tags) {
for raw in chunk.chunks_exact(4) {
let term = u32::from_be_bytes(raw.try_into().unwrap());
tag_lists.push(&mut tags, &term.to_be_bytes())?;
tags_idx.push(term, id, 0)?;
}
}
let meta = if aux.meta.0 == NONE_U32 {
BlobId(NONE_U32)
} else {
metas.push(self.metas.get(aux.meta))?
};
fact_aux.insert(&FactAux { id, tags, meta })?;
if let Some(entity) = rec.entity.some() {
entity_facts.push(entity.0, id, 0)?;
}
temporal.insert(&TemporalSlot {
recorded_at: rec.recorded_at,
fact: id,
})?;
let vector = if rec.has_vector() {
let new_slot = pools.push_vector(&self.vecs, rec.vector)?;
vec_map[rec.vector as usize] = new_slot;
new_slot
} else {
NONE_U32
};
facts.insert(&FactRecord {
text: text_id,
vector,
..rec
})?;
}
Ok((
RebuildMeta {
facts,
fact_aux,
entities,
temporal,
tag_lists,
metas,
bm25,
tags_idx,
entity_facts,
},
vec_map,
purged,
))
}
pub fn snapshot_disk_first<T: Scratch, V: Scratch, Sk: SnapshotSink>(
&self,
created_at: u64,
text_scratch: &mut T,
vec_scratch: &mut V,
sink: Sk,
) -> Result<usize, Error> {
let cfg = &self.cfg;
let blob = BlobHeapCfg::new()
.with_max_bytes(cfg.max_bytes)
.with_max_blob(cfg.max_blob);
let mut pools = StreamPools {
text_scratch,
text_index: BlobHeapBuilder::new(blob),
vec_scratch,
vec_count: 0,
};
let (m, vec_map, purged) = self.rebuild_parts(&mut pools)?;
let StreamPools {
text_scratch,
text_index,
vec_scratch,
..
} = pools;
let mut text_index_bytes = Vec::new();
text_index.dump_index(&mut text_index_bytes);
let text_pool = text_scratch.freeze().map_err(scratch_err)?;
let vec_pool = vec_scratch.freeze().map_err(scratch_err)?;
let texts = BlobHeap::load_borrowed(blob, &text_index_bytes, text_pool)?;
let vecs = VecPool::from_parts_borrowed(cfg.dim, cfg.max_bytes, vec_pool)?;
let hnsw = self.rebuild_graph(&vec_map, &vecs)?;
let sections = Sections {
facts: &m.facts,
fact_aux: &m.fact_aux,
entities: &m.entities,
temporal: &m.temporal,
texts: &texts,
metas: &m.metas,
tag_lists: &m.tag_lists,
bm25: &m.bm25,
tags_idx: &m.tags_idx,
entity_facts: &m.entity_facts,
vecs: &vecs,
hnsw: &hnsw,
};
self.write_snapshot_with(§ions, created_at, sink)?;
Ok(purged)
}
fn rebuild_graph(
&self,
vec_map: &[u32],
pool: &VecPool<'_>,
) -> Result<HnswGraph<'static>, Error> {
let cfg = &self.cfg;
let mut graph: HnswGraph<'static> = HnswGraph::new(cfg.hnsw_m, cfg.hnsw_m0, cfg.max_bytes)?;
let total = pool.len() as u32;
if cfg.dim == 0 || (total as usize) < cfg.flat_to_hnsw {
return Ok(graph);
}
let old_indexed = self.hnsw.indexed() as usize;
let dead = vec_map[..old_indexed]
.iter()
.filter(|&&m| m == NONE_U32)
.count();
let mut scratch = HnswScratch::default();
if old_indexed > 0 && dead * 10 <= old_indexed {
graph = self.hnsw.remapped(vec_map, pool, cfg.max_bytes)?;
}
graph.insert_bulk(pool, total, cfg.hnsw_ef_construction, &mut scratch)?;
Ok(graph)
}
fn install(&mut self, r: Rebuilt) {
self.facts = r.facts;
self.entities = r.entities;
self.fact_aux = r.fact_aux;
self.texts = r.texts;
self.metas = r.metas;
self.tag_lists = r.tag_lists;
self.bm25 = r.bm25;
self.tags_idx = r.tags_idx;
self.entity_facts = r.entity_facts;
self.temporal = r.temporal;
self.vecs = r.vecs;
self.hnsw = r.hnsw;
}
}