1use alloc::{string::String, vec::Vec};
19
20use plugmem_arena::{
21 Arena, ArenaCfg, BlobHeap, BlobHeapCfg, ChunkPool, ChunkPoolCfg, Interner, ShardMode, Slot,
22};
23
24use crate::config::Config;
25use crate::error::Error;
26use crate::id::{FactId, NONE_U32};
27use crate::index::IdListIndex;
28use crate::index::bm25::Bm25Index;
29use crate::index::hnsw::HnswGraph;
30use crate::index::postings::PostingStore;
31use crate::index::varint::decode_u32;
32use crate::index::vecpool::VecPool;
33use crate::memory::FactFault;
34use crate::memory::migrations::{self, STATE_LEN};
35use crate::memory::shards::ShardLayout;
36use crate::model::{
37 EdgeHistorySlot, EdgeSlot, EntityByName, EntityRecord, FactAux, FactRecord, TemporalSlot,
38 VALID_TO_OPEN, edge_history_key, edge_key,
39};
40use crate::snapshot::{Prefix, SectionMeta, Snapshot, SnapshotSink, build_prefix, pad_len};
41use xxhash_rust::xxh3::Xxh3;
42
43use super::Memory;
44
45mod kind {
48 pub const FACTS_META: u16 = 1;
49 pub const FACTS_POOL: u16 = 2;
50 pub const AUX_META: u16 = 3;
51 pub const AUX_POOL: u16 = 4;
52 pub const ENTITIES_META: u16 = 5;
53 pub const ENTITIES_POOL: u16 = 6;
54 pub const BY_NAME_META: u16 = 7;
55 pub const BY_NAME_POOL: u16 = 8;
56 pub const TEMPORAL_META: u16 = 13;
59 pub const TEMPORAL_POOL: u16 = 14;
60 pub const TEXTS_INDEX: u16 = 15;
61 pub const TEXTS_POOL: u16 = 16;
62 pub const TERMS_INDEX: u16 = 17;
63 pub const TERMS_POOL: u16 = 18;
64 pub const TERMS_TABLE: u16 = 19;
65 pub const TAG_LISTS_META: u16 = 20;
66 pub const TAG_LISTS_POOL: u16 = 21;
67 pub const BM25_HANDLES_META: u16 = 22;
68 pub const BM25_HANDLES_POOL: u16 = 23;
69 pub const BM25_CHUNKS_META: u16 = 24;
70 pub const BM25_CHUNKS_POOL: u16 = 25;
71 pub const TAGS_HANDLES_META: u16 = 28;
74 pub const TAGS_HANDLES_POOL: u16 = 29;
75 pub const TAGS_CHUNKS_META: u16 = 30;
76 pub const TAGS_CHUNKS_POOL: u16 = 31;
77 pub const ENTFACTS_HANDLES_META: u16 = 32;
78 pub const ENTFACTS_HANDLES_POOL: u16 = 33;
79 pub const ENTFACTS_CHUNKS_META: u16 = 34;
80 pub const ENTFACTS_CHUNKS_POOL: u16 = 35;
81 pub const ENGINE_STATE: u16 = 36;
82 pub const VEC_POOL: u16 = 37;
83 pub const HNSW_META: u16 = 38;
84 pub const HNSW_LEVEL0: u16 = 39;
85 pub const HNSW_UPPER_META: u16 = 40;
86 pub const HNSW_UPPER_POOL: u16 = 41;
87 pub const HNSW_LISTS_META: u16 = 42;
88 pub const HNSW_LISTS_POOL: u16 = 43;
89 pub const METAS_INDEX: u16 = 44;
90 pub const METAS_POOL: u16 = 45;
91 pub const EDGES_OUT_META: u16 = 50;
93 pub const EDGES_OUT_POOL: u16 = 51;
94 pub const EDGES_IN_META: u16 = 52;
95 pub const EDGES_IN_POOL: u16 = 53;
96 pub const EDGE_HIST_OUT_META: u16 = 54;
98 pub const EDGE_HIST_OUT_POOL: u16 = 55;
99 pub const EDGE_HIST_IN_META: u16 = 56;
100 pub const EDGE_HIST_IN_POOL: u16 = 57;
101 pub const BM25_DOCLEN_META: u16 = 58;
103 pub const BM25_DOCLEN_POOL: u16 = 59;
104 pub const TAG_CATALOG: u16 = 60;
106 pub const VECTOR_SPACE: u16 = 61;
108}
109
110type SectionFn<'f> = dyn FnMut(u16, &[&[u8]]) -> Result<(), Error> + 'f;
114
115pub(crate) struct Sections<'r, 'a> {
129 pub(crate) facts: &'r Arena<'a, FactRecord>,
130 pub(crate) fact_aux: &'r Arena<'a, FactAux>,
131 pub(crate) entities: &'r Arena<'a, EntityRecord>,
132 pub(crate) by_name: &'r Arena<'a, EntityByName>,
133 pub(crate) temporal: &'r Arena<'a, TemporalSlot>,
134 pub(crate) texts: &'r BlobHeap<'a>,
135 pub(crate) metas: &'r BlobHeap<'a>,
136 pub(crate) tag_lists: &'r ChunkPool<'a>,
137 pub(crate) bm25: &'r Bm25Index<'a>,
138 pub(crate) tags_idx: &'r IdListIndex<'a>,
139 pub(crate) entity_facts: &'r IdListIndex<'a>,
140 pub(crate) vecs: &'r VecPool<'a>,
141 pub(crate) hnsw: &'r HnswGraph<'a>,
142 pub(crate) edges_out: &'r Arena<'a, EdgeSlot>,
143 pub(crate) edges_in: &'r Arena<'a, EdgeSlot>,
144 pub(crate) edges_hist_out: &'r Arena<'a, EdgeHistorySlot>,
145 pub(crate) edges_hist_in: &'r Arena<'a, EdgeHistorySlot>,
146 pub(crate) layout: ShardLayout,
153}
154
155fn arena_sections<T: Slot>(a: &Arena<'_, T>) -> (Vec<u8>, Vec<u8>) {
157 let (mut meta, mut pool) = (Vec::new(), Vec::new());
158 a.dump_meta(&mut meta);
159 a.dump_pool(&mut pool);
160 (meta, pool)
161}
162
163fn section<'a>(snap: &Snapshot<'a>, kind: u16) -> Result<&'a [u8], Error> {
165 snap.section(kind)
166 .ok_or(Error::Corrupt("snapshot is missing a required section"))
167}
168
169fn decode_vector_space(bytes: Option<&[u8]>) -> Result<Option<String>, Error> {
173 let Some(bytes) = bytes else {
174 return Ok(None);
175 };
176 if bytes.is_empty() {
177 return Ok(None);
178 }
179 let space =
180 core::str::from_utf8(bytes).map_err(|_| Error::Corrupt("vector space is not UTF-8"))?;
181 Memory::validate_vector_space(space).map_err(|_| Error::Corrupt("vector space is invalid"))?;
182 Ok(Some(space.into()))
183}
184
185struct EdgeSections<'a> {
187 out_meta: &'a [u8],
188 out_pool: &'a [u8],
189 in_meta: &'a [u8],
190 in_pool: &'a [u8],
191 hist_out_meta: &'a [u8],
192 hist_out_pool: &'a [u8],
193 hist_in_meta: &'a [u8],
194 hist_in_pool: &'a [u8],
195}
196
197fn edge_sections<'a>(snap: &Snapshot<'a>) -> Result<Option<EdgeSections<'a>>, Error> {
202 const KINDS: [u16; 8] = [
203 kind::EDGES_OUT_META,
204 kind::EDGES_OUT_POOL,
205 kind::EDGES_IN_META,
206 kind::EDGES_IN_POOL,
207 kind::EDGE_HIST_OUT_META,
208 kind::EDGE_HIST_OUT_POOL,
209 kind::EDGE_HIST_IN_META,
210 kind::EDGE_HIST_IN_POOL,
211 ];
212 let found = KINDS.map(|k| snap.section(k));
213 if found.iter().all(Option::is_none) {
214 return Ok(None);
215 }
216 let [
217 out_meta,
218 out_pool,
219 in_meta,
220 in_pool,
221 hist_out_meta,
222 hist_out_pool,
223 hist_in_meta,
224 hist_in_pool,
225 ] = found.map(|s| s.ok_or(Error::Corrupt("snapshot has incomplete edge sections")));
226 Ok(Some(EdgeSections {
227 out_meta: out_meta?,
228 out_pool: out_pool?,
229 in_meta: in_meta?,
230 in_pool: in_pool?,
231 hist_out_meta: hist_out_meta?,
232 hist_out_pool: hist_out_pool?,
233 hist_in_meta: hist_in_meta?,
234 hist_in_pool: hist_in_pool?,
235 }))
236}
237
238impl<'a, const TF: bool> PostingStore<'a, TF> {
239 pub(crate) fn dump_sections(&self) -> [Vec<u8>; 4] {
241 let (hm, hp) = (self.handles_meta(), self.handles_pool());
242 let (cm, cp) = (self.chunks_meta(), self.chunks_pool());
243 [hm, hp, cm, cp]
244 }
245
246 pub(crate) fn load_sections(
253 shards: usize,
254 max_bytes: usize,
255 hm: &[u8],
256 hp: &[u8],
257 cm: &[u8],
258 cp: &[u8],
259 ) -> Result<Self, Error> {
260 let handles = Arena::<crate::index::postings::IdListSlot>::load(
261 ArenaCfg::new(shards, ShardMode::Uniform).with_max_bytes(max_bytes),
262 hm,
263 hp,
264 )?;
265 let pool = ChunkPool::load(ChunkPoolCfg::new().with_max_bytes(max_bytes), cm, cp)?;
266 Self::validate_lists(&handles, &pool)?;
267 Ok(Self::from_parts(handles, pool))
268 }
269
270 pub(crate) fn load_sections_borrowed(
275 shards: usize,
276 max_bytes: usize,
277 hm: &[u8],
278 hp: &'a [u8],
279 cm: &[u8],
280 cp: &'a [u8],
281 ) -> Result<Self, Error> {
282 let handles = Arena::<crate::index::postings::IdListSlot>::load_borrowed(
283 ArenaCfg::new(shards, ShardMode::Uniform).with_max_bytes(max_bytes),
284 hm,
285 hp,
286 )?;
287 let pool = ChunkPool::load_borrowed(ChunkPoolCfg::new().with_max_bytes(max_bytes), cm, cp)?;
288 Self::validate_lists(&handles, &pool)?;
289 Ok(Self::from_parts(handles, pool))
290 }
291
292 fn validate_lists(
297 handles: &Arena<'_, crate::index::postings::IdListSlot>,
298 pool: &ChunkPool<'_>,
299 ) -> Result<(), Error> {
300 let mut visited = alloc::vec![false; pool.chunks()];
301 for slot in handles.iter() {
302 pool.validate_chain(&slot.handle, &mut visited)?;
303 let mut count = 0u32;
304 let mut last = 0u32;
305 let mut first = true;
306 for chunk in pool.iter(&slot.handle) {
307 let mut cur = chunk;
308 while !cur.is_empty() {
309 let Some((delta, used)) = decode_u32(cur) else {
310 return Err(Error::Corrupt("posting entry is malformed"));
311 };
312 let mut entry_len = used;
313 if TF {
314 if cur.len() < used + 1 {
315 return Err(Error::Corrupt("posting entry is malformed"));
316 }
317 entry_len += 1;
318 }
319 cur = &cur[entry_len..];
320 let id = if first {
321 first = false;
322 delta
323 } else {
324 if delta == 0 {
325 return Err(Error::Corrupt("posting ids are not ascending"));
326 }
327 last.checked_add(delta)
328 .ok_or(Error::Corrupt("posting id overflows"))?
329 };
330 last = id;
331 count += 1;
332 }
333 }
334 if count != slot.count || (count > 0 && last != slot.last) {
335 return Err(Error::Corrupt("posting list disagrees with its handle"));
336 }
337 }
338 if pool.orphan_count(&visited) != 0 {
339 return Err(Error::Corrupt("posting pool has orphan chunks"));
340 }
341 Ok(())
342 }
343}
344
345impl<'a> Bm25Index<'a> {
346 fn dump_pairs(&self) -> [(u16, Vec<u8>); 6] {
348 let [hm, hp, cm, cp] = self.postings().dump_sections();
349 let (dm, dp) = arena_sections(self.doc_len_arena());
350 [
351 (kind::BM25_HANDLES_META, hm),
352 (kind::BM25_HANDLES_POOL, hp),
353 (kind::BM25_CHUNKS_META, cm),
354 (kind::BM25_CHUNKS_POOL, cp),
355 (kind::BM25_DOCLEN_META, dm),
356 (kind::BM25_DOCLEN_POOL, dp),
357 ]
358 }
359
360 fn load_from(snap: &Snapshot<'_>, cfg: &Config) -> Result<Self, Error> {
363 let postings = PostingStore::<true>::load_sections(
364 cfg.shards_postings,
365 cfg.max_bytes,
366 section(snap, kind::BM25_HANDLES_META)?,
367 section(snap, kind::BM25_HANDLES_POOL)?,
368 section(snap, kind::BM25_CHUNKS_META)?,
369 section(snap, kind::BM25_CHUNKS_POOL)?,
370 )?;
371 let (doc_len, migrated) = match migrations::legacy_doc_len(snap, cfg)? {
372 Some(upgraded) => (upgraded, true),
373 None => (
374 Arena::load(
375 migrations::doc_len_cfg(cfg),
376 section(snap, kind::BM25_DOCLEN_META)?,
377 section(snap, kind::BM25_DOCLEN_POOL)?,
378 )?,
379 false,
380 ),
381 };
382 Self::assemble(postings, doc_len, snap, migrated)
383 }
384
385 fn load_from_borrowed(snap: &Snapshot<'a>, cfg: &Config) -> Result<Self, Error> {
388 let postings = PostingStore::<true>::load_sections_borrowed(
389 cfg.shards_postings,
390 cfg.max_bytes,
391 section(snap, kind::BM25_HANDLES_META)?,
392 section(snap, kind::BM25_HANDLES_POOL)?,
393 section(snap, kind::BM25_CHUNKS_META)?,
394 section(snap, kind::BM25_CHUNKS_POOL)?,
395 )?;
396 let (doc_len, migrated) = match migrations::legacy_doc_len(snap, cfg)? {
399 Some(upgraded) => (upgraded, true),
400 None => (
401 Arena::load_borrowed(
402 migrations::doc_len_cfg(cfg),
403 section(snap, kind::BM25_DOCLEN_META)?,
404 section(snap, kind::BM25_DOCLEN_POOL)?,
405 )?,
406 false,
407 ),
408 };
409 Self::assemble(postings, doc_len, snap, migrated)
410 }
411
412 fn assemble(
416 postings: PostingStore<'a, true>,
417 doc_len: Arena<'a, crate::index::bm25::DocLenSlot>,
418 snap: &Snapshot<'_>,
419 migrated: bool,
420 ) -> Result<Self, Error> {
421 let state = section(snap, kind::ENGINE_STATE)?;
422 migrations::decode_engine_state(state)?;
425 let total_docs = u64::from_le_bytes(state[8..16].try_into().unwrap());
426 let total_len = u64::from_le_bytes(state[16..24].try_into().unwrap());
427 if total_docs != doc_len.len() as u64 {
428 return Err(Error::Corrupt("bm25 document total disagrees with doc_len"));
429 }
430 let mut index = Self::from_parts(postings, doc_len, total_docs, total_len);
431 if migrated {
432 index.mark_unsummarized();
433 }
434 Ok(index)
435 }
436}
437
438impl<'a> Memory<'a> {
439 pub(super) fn sections(&self) -> Sections<'_, 'a> {
442 Sections {
443 facts: &self.facts,
444 fact_aux: &self.fact_aux,
445 entities: &self.entities,
446 by_name: &self.by_name,
447 temporal: &self.temporal,
448 texts: &self.texts,
449 metas: &self.metas,
450 tag_lists: &self.tag_lists,
451 bm25: &self.bm25,
452 tags_idx: &self.tags_idx,
453 entity_facts: &self.entity_facts,
454 vecs: &self.vecs,
455 hnsw: &self.hnsw,
456 edges_out: &self.edges_out,
457 edges_in: &self.edges_in,
458 edges_hist_out: &self.edges_hist_out,
459 edges_hist_in: &self.edges_hist_in,
460 layout: ShardLayout::of_config(&self.cfg),
461 }
462 }
463
464 fn emit_sections_from(
474 &self,
475 s: &Sections<'_, '_>,
476 vector_space: Option<&str>,
477 f: &mut SectionFn<'_>,
478 ) -> Result<(), Error> {
479 for (mk, pk, arena) in [
480 (kind::FACTS_META, kind::FACTS_POOL, arena_sections(s.facts)),
481 (kind::AUX_META, kind::AUX_POOL, arena_sections(s.fact_aux)),
482 (
483 kind::ENTITIES_META,
484 kind::ENTITIES_POOL,
485 arena_sections(s.entities),
486 ),
487 (
488 kind::BY_NAME_META,
489 kind::BY_NAME_POOL,
490 arena_sections(s.by_name),
491 ),
492 (
493 kind::EDGES_OUT_META,
494 kind::EDGES_OUT_POOL,
495 arena_sections(s.edges_out),
496 ),
497 (
498 kind::EDGES_IN_META,
499 kind::EDGES_IN_POOL,
500 arena_sections(s.edges_in),
501 ),
502 (
503 kind::EDGE_HIST_OUT_META,
504 kind::EDGE_HIST_OUT_POOL,
505 arena_sections(s.edges_hist_out),
506 ),
507 (
508 kind::EDGE_HIST_IN_META,
509 kind::EDGE_HIST_IN_POOL,
510 arena_sections(s.edges_hist_in),
511 ),
512 (
513 kind::TEMPORAL_META,
514 kind::TEMPORAL_POOL,
515 arena_sections(s.temporal),
516 ),
517 ] {
518 let (m, p) = arena;
519 f(mk, &[&m])?;
520 f(pk, &[&p])?;
521 }
522 let (mut i, mut p) = (Vec::new(), Vec::new());
523 s.texts.dump_index(&mut i);
524 s.texts.dump_pool(&mut p);
525 f(kind::TEXTS_INDEX, &[&i])?;
526 f(kind::TEXTS_POOL, &[&p])?;
527 let (mut i, mut p) = (Vec::new(), Vec::new());
528 s.metas.dump_index(&mut i);
529 s.metas.dump_pool(&mut p);
530 f(kind::METAS_INDEX, &[&i])?;
531 f(kind::METAS_POOL, &[&p])?;
532 let (mut i, mut p, mut t) = (Vec::new(), Vec::new(), Vec::new());
533 self.terms.dump_index(&mut i);
534 self.terms.dump_pool(&mut p);
535 self.terms.dump_table(&mut t);
536 f(kind::TERMS_INDEX, &[&i])?;
537 f(kind::TERMS_POOL, &[&p])?;
538 f(kind::TERMS_TABLE, &[&t])?;
539 let (mut m, mut p) = (Vec::new(), Vec::new());
540 s.tag_lists.dump_meta(&mut m);
541 s.tag_lists.dump_pool(&mut p);
542 f(kind::TAG_LISTS_META, &[&m])?;
543 f(kind::TAG_LISTS_POOL, &[&p])?;
544 for (k, bytes) in s.bm25.dump_pairs() {
545 f(k, &[&bytes])?;
546 }
547 let [hm, hp, cm, cp] = s.tags_idx.dump_sections();
548 f(kind::TAGS_HANDLES_META, &[&hm])?;
549 f(kind::TAGS_HANDLES_POOL, &[&hp])?;
550 f(kind::TAGS_CHUNKS_META, &[&cm])?;
551 f(kind::TAGS_CHUNKS_POOL, &[&cp])?;
552 f(kind::TAG_CATALOG, &[&self.tag_catalog.dump(&self.terms)])?;
553 f(kind::VECTOR_SPACE, &[vector_space.unwrap_or("").as_bytes()])?;
554 let [hm, hp, cm, cp] = s.entity_facts.dump_sections();
555 f(kind::ENTFACTS_HANDLES_META, &[&hm])?;
556 f(kind::ENTFACTS_HANDLES_POOL, &[&hp])?;
557 f(kind::ENTFACTS_CHUNKS_META, &[&cm])?;
558 f(kind::ENTFACTS_CHUNKS_POOL, &[&cp])?;
559 let mut state = Vec::with_capacity(STATE_LEN);
560 state.extend_from_slice(&self.next_fact.to_le_bytes());
561 state.extend_from_slice(&self.next_entity.to_le_bytes());
562 state.extend_from_slice(&s.bm25.docs().to_le_bytes());
563 state.extend_from_slice(&s.bm25.total_len().to_le_bytes());
564 state.extend_from_slice(&self.bm25_tokenizer_version.to_le_bytes());
565 state.extend_from_slice(&0u32.to_le_bytes());
566 state.extend_from_slice(&self.next_edge.to_le_bytes());
567 state.extend_from_slice(&0u32.to_le_bytes());
568 f(kind::ENGINE_STATE, &[&state])?;
569 f(kind::VEC_POOL, &s.vecs.pieces())?;
572 f(kind::HNSW_META, &[&s.hnsw.dump_meta()])?;
575 f(kind::HNSW_LEVEL0, &[&s.hnsw.dump_level0()])?;
576 let [um, up, lm, lp] = s.hnsw.dump_upper();
577 f(kind::HNSW_UPPER_META, &[&um])?;
578 f(kind::HNSW_UPPER_POOL, &[&up])?;
579 f(kind::HNSW_LISTS_META, &[&lm])?;
580 f(kind::HNSW_LISTS_POOL, &[&lp])?;
581 Ok(())
582 }
583
584 pub fn write_snapshot_to(&self, created_at: u64, sink: impl SnapshotSink) -> Result<(), Error> {
596 self.write_snapshot_with(&self.sections(), created_at, sink)
597 }
598
599 pub(crate) fn write_snapshot_with(
606 &self,
607 s: &Sections<'_, '_>,
608 created_at: u64,
609 sink: impl SnapshotSink,
610 ) -> Result<(), Error> {
611 self.write_snapshot_reconfigured(
612 s,
613 &self.cfg,
614 self.vector_space.as_deref(),
615 created_at,
616 sink,
617 )
618 }
619
620 pub(crate) fn write_snapshot_reconfigured(
624 &self,
625 s: &Sections<'_, '_>,
626 cfg: &Config,
627 vector_space: Option<&str>,
628 created_at: u64,
629 mut sink: impl SnapshotSink,
630 ) -> Result<(), Error> {
631 let mut cfg_bytes = Vec::new();
632 let mut stored_cfg = cfg.clone();
633 s.layout.apply(&mut stored_cfg);
634 stored_cfg.encode(&mut cfg_bytes);
635 let flags = if stored_cfg.dim > 0 {
636 crate::snapshot::FLAG_VECTORS
637 } else {
638 0
639 };
640
641 let mut metas: Vec<SectionMeta> = Vec::new();
643 self.emit_sections_from(s, vector_space, &mut |kind, pieces| {
644 let mut h = Xxh3::new();
645 let mut len = 0u64;
646 for p in pieces {
647 h.update(p);
648 len += p.len() as u64;
649 }
650 metas.push(SectionMeta {
651 kind,
652 len,
653 hash: h.digest(),
654 });
655 Ok(())
656 })?;
657
658 let Prefix {
659 bytes: prefix,
660 offsets,
661 file_len: _,
662 } = build_prefix(
663 &cfg_bytes,
664 flags,
665 created_at,
666 env!("CARGO_PKG_VERSION"),
667 &metas,
668 );
669 sink.write(&prefix)?;
670 let mut file_hash = Xxh3::new();
671 file_hash.update(&prefix);
672
673 let zero = [0u8; 64]; let mut idx = 0usize;
676 self.emit_sections_from(s, vector_space, &mut |_, pieces| {
677 for p in pieces {
678 sink.write(p)?;
679 file_hash.update(p);
680 }
681 let n = pad_len(offsets[idx], metas[idx].len);
682 sink.write(&zero[..n])?;
683 file_hash.update(&zero[..n]);
684 idx += 1;
685 Ok(())
686 })?;
687
688 sink.patch(
689 crate::snapshot::FILE_HASH_OFFSET,
690 &file_hash.digest().to_le_bytes(),
691 )
692 }
693
694 pub fn snapshot_bytes(&self, created_at: u64) -> Vec<u8> {
699 let mut out = Vec::new();
700 self.write_snapshot_to(created_at, &mut out)
701 .expect("writing a snapshot into a Vec is infallible");
702 out
703 }
704
705 pub fn snapshot<S: crate::storage::Storage>(
707 &mut self,
708 store: &mut S,
709 now: u64,
710 ) -> Result<(), Error> {
711 let bytes = self.snapshot_bytes(now);
712 store
713 .write_snapshot(&bytes)
714 .map_err(|e| Error::Storage(alloc::format!("{e:?}")))?;
715 store
716 .clear_journal()
717 .map_err(|e| Error::Storage(alloc::format!("{e:?}")))?;
718 Ok(())
719 }
720
721 pub(super) fn load_snapshot(bytes: &[u8], cfg: Config) -> Result<Self, Error> {
726 cfg.validate()?;
727 let snap = Snapshot::parse(bytes)?;
728 let cfg = Self::reconcile_config(&snap, cfg)?;
729 let mut mem = Self::new(cfg)?;
730 let cfg = &mem.cfg;
731 let uni =
732 |shards: usize| ArenaCfg::new(shards, ShardMode::Uniform).with_max_bytes(cfg.max_bytes);
733 let ord =
734 |shards: usize| ArenaCfg::new(shards, ShardMode::Ordered).with_max_bytes(cfg.max_bytes);
735 let blob = BlobHeapCfg::new()
736 .with_max_bytes(cfg.max_bytes)
737 .with_max_blob(cfg.max_blob);
738 mem.facts = Arena::load(
739 uni(cfg.shards_facts),
740 section(&snap, kind::FACTS_META)?,
741 section(&snap, kind::FACTS_POOL)?,
742 )?;
743 mem.fact_aux = Arena::load(
744 uni(cfg.shards_facts),
745 section(&snap, kind::AUX_META)?,
746 section(&snap, kind::AUX_POOL)?,
747 )?;
748 mem.entities = Arena::load(
749 uni(cfg.shards_entities),
750 section(&snap, kind::ENTITIES_META)?,
751 section(&snap, kind::ENTITIES_POOL)?,
752 )?;
753 mem.by_name = Arena::load(
754 ord(cfg.shards_entities),
755 section(&snap, kind::BY_NAME_META)?,
756 section(&snap, kind::BY_NAME_POOL)?,
757 )?;
758 if let Some(edges) = edge_sections(&snap)? {
761 mem.edges_out = Arena::load(ord(cfg.shards_edges), edges.out_meta, edges.out_pool)?;
762 mem.edges_in = Arena::load(ord(cfg.shards_edges), edges.in_meta, edges.in_pool)?;
763 mem.edges_hist_out = Arena::load(
764 ord(cfg.shards_edges),
765 edges.hist_out_meta,
766 edges.hist_out_pool,
767 )?;
768 mem.edges_hist_in = Arena::load(
769 ord(cfg.shards_edges),
770 edges.hist_in_meta,
771 edges.hist_in_pool,
772 )?;
773 }
774 mem.temporal = Arena::load(
775 ord(cfg.shards_temporal),
776 section(&snap, kind::TEMPORAL_META)?,
777 section(&snap, kind::TEMPORAL_POOL)?,
778 )?;
779 mem.texts = BlobHeap::load(
780 blob,
781 section(&snap, kind::TEXTS_INDEX)?,
782 section(&snap, kind::TEXTS_POOL)?,
783 )?;
784 mem.metas = BlobHeap::load(
785 blob,
786 section(&snap, kind::METAS_INDEX)?,
787 section(&snap, kind::METAS_POOL)?,
788 )?;
789 mem.terms = Interner::load(
790 blob,
791 section(&snap, kind::TERMS_INDEX)?,
792 section(&snap, kind::TERMS_POOL)?,
793 section(&snap, kind::TERMS_TABLE)?,
794 )?;
795 mem.tag_lists = ChunkPool::load(
796 ChunkPoolCfg::new().with_max_bytes(cfg.max_bytes),
797 section(&snap, kind::TAG_LISTS_META)?,
798 section(&snap, kind::TAG_LISTS_POOL)?,
799 )?;
800 mem.bm25 = Bm25Index::load_from(&snap, cfg)?;
801 mem.tags_idx = IdListIndex::load_sections(
802 cfg.shards_postings,
803 cfg.max_bytes,
804 section(&snap, kind::TAGS_HANDLES_META)?,
805 section(&snap, kind::TAGS_HANDLES_POOL)?,
806 section(&snap, kind::TAGS_CHUNKS_META)?,
807 section(&snap, kind::TAGS_CHUNKS_POOL)?,
808 )?;
809 mem.tag_catalog = match snap.section(kind::TAG_CATALOG) {
810 Some(bytes) => super::tags::TagCatalog::load(bytes, &mem.terms)?,
811 None => super::tags::TagCatalog::new(),
812 };
813 mem.vector_space = decode_vector_space(snap.section(kind::VECTOR_SPACE))?;
814 mem.entity_facts = IdListIndex::load_sections(
815 cfg.shards_entities,
816 cfg.max_bytes,
817 section(&snap, kind::ENTFACTS_HANDLES_META)?,
818 section(&snap, kind::ENTFACTS_HANDLES_POOL)?,
819 section(&snap, kind::ENTFACTS_CHUNKS_META)?,
820 section(&snap, kind::ENTFACTS_CHUNKS_POOL)?,
821 )?;
822 mem.vecs = VecPool::from_parts(cfg.dim, cfg.max_bytes, section(&snap, kind::VEC_POOL)?)?;
823 mem.hnsw = crate::index::hnsw::HnswGraph::from_parts(
824 cfg.hnsw_m,
825 cfg.hnsw_m0,
826 cfg.max_bytes,
827 section(&snap, kind::HNSW_META)?,
828 section(&snap, kind::HNSW_LEVEL0)?,
829 section(&snap, kind::HNSW_UPPER_META)?,
830 section(&snap, kind::HNSW_UPPER_POOL)?,
831 section(&snap, kind::HNSW_LISTS_META)?,
832 section(&snap, kind::HNSW_LISTS_POOL)?,
833 )?;
834 Self::finish_load(mem, &snap)
835 }
836
837 pub(super) fn load_snapshot_borrowed(bytes: &'a [u8], cfg: Config) -> Result<Self, Error> {
845 cfg.validate()?;
846 let snap = Snapshot::parse(bytes)?;
847 let cfg = Self::reconcile_config(&snap, cfg)?;
848 let mut mem = Self::new(cfg)?;
849 let cfg = &mem.cfg;
850 let uni =
851 |shards: usize| ArenaCfg::new(shards, ShardMode::Uniform).with_max_bytes(cfg.max_bytes);
852 let ord =
853 |shards: usize| ArenaCfg::new(shards, ShardMode::Ordered).with_max_bytes(cfg.max_bytes);
854 let blob = BlobHeapCfg::new()
855 .with_max_bytes(cfg.max_bytes)
856 .with_max_blob(cfg.max_blob);
857 mem.facts = Arena::load_borrowed(
858 uni(cfg.shards_facts),
859 section(&snap, kind::FACTS_META)?,
860 section(&snap, kind::FACTS_POOL)?,
861 )?;
862 mem.fact_aux = Arena::load_borrowed(
863 uni(cfg.shards_facts),
864 section(&snap, kind::AUX_META)?,
865 section(&snap, kind::AUX_POOL)?,
866 )?;
867 mem.entities = Arena::load_borrowed(
868 uni(cfg.shards_entities),
869 section(&snap, kind::ENTITIES_META)?,
870 section(&snap, kind::ENTITIES_POOL)?,
871 )?;
872 mem.by_name = Arena::load_borrowed(
873 ord(cfg.shards_entities),
874 section(&snap, kind::BY_NAME_META)?,
875 section(&snap, kind::BY_NAME_POOL)?,
876 )?;
877 if let Some(edges) = edge_sections(&snap)? {
881 mem.edges_out =
882 Arena::load_borrowed(ord(cfg.shards_edges), edges.out_meta, edges.out_pool)?;
883 mem.edges_in =
884 Arena::load_borrowed(ord(cfg.shards_edges), edges.in_meta, edges.in_pool)?;
885 mem.edges_hist_out = Arena::load_borrowed(
886 ord(cfg.shards_edges),
887 edges.hist_out_meta,
888 edges.hist_out_pool,
889 )?;
890 mem.edges_hist_in = Arena::load_borrowed(
891 ord(cfg.shards_edges),
892 edges.hist_in_meta,
893 edges.hist_in_pool,
894 )?;
895 }
896 mem.temporal = Arena::load_borrowed(
897 ord(cfg.shards_temporal),
898 section(&snap, kind::TEMPORAL_META)?,
899 section(&snap, kind::TEMPORAL_POOL)?,
900 )?;
901 mem.texts = BlobHeap::load_borrowed(
902 blob,
903 section(&snap, kind::TEXTS_INDEX)?,
904 section(&snap, kind::TEXTS_POOL)?,
905 )?;
906 mem.metas = BlobHeap::load_borrowed(
907 blob,
908 section(&snap, kind::METAS_INDEX)?,
909 section(&snap, kind::METAS_POOL)?,
910 )?;
911 mem.terms = Interner::load_borrowed(
912 blob,
913 section(&snap, kind::TERMS_INDEX)?,
914 section(&snap, kind::TERMS_POOL)?,
915 section(&snap, kind::TERMS_TABLE)?,
916 )?;
917 mem.tag_lists = ChunkPool::load_borrowed(
918 ChunkPoolCfg::new().with_max_bytes(cfg.max_bytes),
919 section(&snap, kind::TAG_LISTS_META)?,
920 section(&snap, kind::TAG_LISTS_POOL)?,
921 )?;
922 mem.bm25 = Bm25Index::load_from_borrowed(&snap, cfg)?;
923 mem.tags_idx = IdListIndex::load_sections_borrowed(
924 cfg.shards_postings,
925 cfg.max_bytes,
926 section(&snap, kind::TAGS_HANDLES_META)?,
927 section(&snap, kind::TAGS_HANDLES_POOL)?,
928 section(&snap, kind::TAGS_CHUNKS_META)?,
929 section(&snap, kind::TAGS_CHUNKS_POOL)?,
930 )?;
931 mem.tag_catalog = match snap.section(kind::TAG_CATALOG) {
932 Some(bytes) => super::tags::TagCatalog::load(bytes, &mem.terms)?,
933 None => super::tags::TagCatalog::new(),
934 };
935 mem.vector_space = decode_vector_space(snap.section(kind::VECTOR_SPACE))?;
936 mem.entity_facts = IdListIndex::load_sections_borrowed(
937 cfg.shards_entities,
938 cfg.max_bytes,
939 section(&snap, kind::ENTFACTS_HANDLES_META)?,
940 section(&snap, kind::ENTFACTS_HANDLES_POOL)?,
941 section(&snap, kind::ENTFACTS_CHUNKS_META)?,
942 section(&snap, kind::ENTFACTS_CHUNKS_POOL)?,
943 )?;
944 mem.vecs =
945 VecPool::from_parts_borrowed(cfg.dim, cfg.max_bytes, section(&snap, kind::VEC_POOL)?)?;
946 mem.hnsw = crate::index::hnsw::HnswGraph::from_parts_borrowed(
947 cfg.hnsw_m,
948 cfg.hnsw_m0,
949 cfg.max_bytes,
950 section(&snap, kind::HNSW_META)?,
951 section(&snap, kind::HNSW_LEVEL0)?,
952 section(&snap, kind::HNSW_UPPER_META)?,
953 section(&snap, kind::HNSW_UPPER_POOL)?,
954 section(&snap, kind::HNSW_LISTS_META)?,
955 section(&snap, kind::HNSW_LISTS_POOL)?,
956 )?;
957 Self::finish_load(mem, &snap)
958 }
959
960 fn reconcile_config(snap: &Snapshot<'_>, mut cfg: Config) -> Result<Config, Error> {
965 let stored = Config::decode(snap.config())?;
966 cfg.dim = stored.dim;
973 ShardLayout::of_config(&stored).apply(&mut cfg);
982 if stored.max_bytes != cfg.max_bytes
983 || stored.max_text != cfg.max_text
984 || stored.max_blob != cfg.max_blob
985 {
986 return Err(Error::ConfigMismatch("stored size limits differ"));
987 }
988 if stored.hnsw_m != cfg.hnsw_m || stored.hnsw_m0 != cfg.hnsw_m0 {
997 return Err(Error::ConfigMismatch("stored hnsw degrees differ"));
998 }
999 if cfg.db_uuid != 0 && stored.db_uuid != cfg.db_uuid {
1003 return Err(Error::ConfigMismatch("stored db_uuid differs"));
1004 }
1005 cfg.db_uuid = stored.db_uuid;
1006 Ok(cfg)
1007 }
1008
1009 fn finish_load(mut mem: Self, snap: &Snapshot<'_>) -> Result<Self, Error> {
1018 let state = migrations::decode_engine_state(section(snap, kind::ENGINE_STATE)?)?;
1019 mem.next_fact = state.next_fact;
1020 mem.next_entity = state.next_entity;
1021 mem.bm25_tokenizer_version = state.bm25_tokenizer_version;
1022 mem.next_edge = state.next_edge;
1023 let cfg = mem.cfg.clone();
1026 if mem.edges_hist_out.is_empty() && mem.edges_out.is_empty() {
1027 mem.migrate_edges(snap, &cfg)?;
1028 }
1029 let derived_next_edge = mem
1030 .edges_hist_out
1031 .iter()
1032 .map(|edge| edge.edge.0)
1033 .max()
1034 .map(|edge| edge.saturating_add(1))
1035 .unwrap_or(0);
1036 if mem.next_edge < derived_next_edge {
1037 if !state.predates_edge_versions {
1038 return Err(Error::Corrupt("engine edge id counter below record count"));
1039 }
1040 mem.next_edge = derived_next_edge;
1041 }
1042 if (mem.next_fact as usize) < mem.facts.len()
1043 || (mem.next_entity as usize) < mem.entities.len()
1044 {
1045 return Err(Error::Corrupt("engine id counters below record counts"));
1046 }
1047 mem.tombstones = mem.facts.iter().filter(|fact| fact.is_tombstone()).count();
1048 mem.validate_references()?;
1049 mem.migrate_tag_catalog(snap.section(kind::TAG_CATALOG).is_some());
1050 Ok(mem)
1051 }
1052
1053 fn validate_references(&self) -> Result<(), Error> {
1062 let texts = self.texts.len() as u32;
1063 let terms = self.terms.len() as u32;
1064 self.hnsw.validate(&self.vecs)?;
1068 for fact in self.facts.iter() {
1069 if fact.id.0 >= self.next_fact
1070 || fact.text.0 >= texts
1071 || (fact.entity.0 != NONE_U32 && fact.entity.0 >= self.next_entity)
1072 || (fact.revises.0 != NONE_U32 && fact.revises.0 >= self.next_fact)
1073 || fact.kind != 0
1074 {
1075 return Err(Error::Corrupt("fact record references out of range"));
1076 }
1077 if !fact.has_vector() && fact.vector != NONE_U32 {
1081 return Err(Error::Corrupt("fact without a vector flag carries a slot"));
1082 }
1083 }
1084 let metas = self.metas.len() as u32;
1085 let mut visited = alloc::vec![false; self.tag_lists.chunks()];
1086 for aux in self.fact_aux.iter() {
1087 if aux.id.0 >= self.next_fact || (aux.meta.0 != NONE_U32 && aux.meta.0 >= metas) {
1088 return Err(Error::Corrupt("aux record references out of range"));
1089 }
1090 self.tag_lists.validate_chain(&aux.tags, &mut visited)?;
1091 for chunk in self.tag_lists.iter(&aux.tags) {
1092 if !chunk.len().is_multiple_of(4) {
1093 return Err(Error::Corrupt("tag list is not a term-id sequence"));
1094 }
1095 for raw in chunk.chunks_exact(4) {
1096 if u32::from_be_bytes(raw.try_into().unwrap()) >= terms {
1097 return Err(Error::Corrupt("tag term out of range"));
1098 }
1099 }
1100 }
1101 }
1102 if self.tag_lists.orphan_count(&visited) != 0 {
1103 return Err(Error::Corrupt("tag pool has orphan chunks"));
1104 }
1105 for entity in self.entities.iter() {
1106 if entity.id.0 >= self.next_entity
1107 || entity.name.0 >= texts
1108 || entity.name_term.0 >= terms
1109 {
1110 return Err(Error::Corrupt("entity record references out of range"));
1111 }
1112 }
1113 for by_name in self.by_name.iter() {
1114 if by_name.name_term.0 >= terms || !self.entities.contains(&by_name.id.0.to_be_bytes())
1115 {
1116 return Err(Error::Corrupt("by-name record references out of range"));
1117 }
1118 }
1119 for arena in [&self.edges_out, &self.edges_in] {
1127 for edge in arena.iter() {
1128 if edge.a.0 >= self.next_entity
1129 || edge.b.0 >= self.next_entity
1130 || edge.rel.0 >= terms
1131 || edge.edge.0 >= self.next_edge
1132 || (edge.fact.0 != NONE_U32 && edge.fact.0 >= self.next_fact)
1133 {
1134 return Err(Error::Corrupt("edge record references out of range"));
1135 }
1136 }
1137 }
1138 if self.edges_out.len() != self.edges_in.len()
1139 || self.edges_hist_out.len() != self.edges_hist_in.len()
1140 {
1141 return Err(Error::Corrupt("edge mirrors disagree"));
1142 }
1143 for edge in self.edges_hist_out.iter() {
1144 if edge.a.0 >= self.next_entity
1145 || edge.b.0 >= self.next_entity
1146 || edge.edge.0 >= self.next_edge
1147 || edge.rel.0 >= terms
1148 || edge.kind != 0
1149 || edge.valid_from > edge.valid_to
1150 || (edge.fact.0 != NONE_U32 && edge.fact.0 >= self.next_fact)
1151 {
1152 return Err(Error::Corrupt("edge history references out of range"));
1153 }
1154 }
1155 for slot in self.temporal.iter() {
1156 if slot.fact.0 >= self.next_fact {
1157 return Err(Error::Corrupt("temporal record references out of range"));
1158 }
1159 }
1160 Ok(())
1161 }
1162
1163 pub fn verify(&self) -> Result<(), Error> {
1193 self.verify_graph()?;
1194 self.verify_tag_catalog()?;
1195 for (_, text) in self.texts.iter() {
1198 if core::str::from_utf8(text).is_err() {
1199 return Err(Error::Corrupt("stored text is not valid UTF-8"));
1200 }
1201 }
1202 let mut pairs = Vec::new();
1207 for aux in self.fact_aux.iter() {
1208 if aux.meta.0 != NONE_U32 {
1209 crate::metadata::decode(self.metas.get(aux.meta), &mut pairs)?;
1210 }
1211 }
1212 self.vecs.validate()?;
1216 let vslots = self.vecs.len() as u32;
1217 let mut with_vec = 0u32;
1218 for fact in self.facts.iter() {
1219 if fact.has_vector() {
1220 if fact.vector >= vslots || self.vecs.slot_fact(fact.vector as usize) != fact.id.0 {
1221 return Err(Error::Corrupt(
1222 "fact vector slot is out of range or mismatched",
1223 ));
1224 }
1225 with_vec += 1;
1226 }
1227 }
1228 if with_vec != vslots {
1229 return Err(Error::Corrupt("vector pool has orphan slots"));
1230 }
1231 Ok(())
1232 }
1233
1234 fn verify_tag_catalog(&self) -> Result<(), Error> {
1238 let mut active = 0usize;
1239 for slot in self.tags_idx.slots() {
1240 let count = self
1241 .tags_idx
1242 .entries(slot.key)
1243 .filter(|(id, _)| {
1244 self.fact(*id)
1245 .is_some_and(|fact| !fact.is_tombstone() && !fact.is_closed())
1246 })
1247 .count();
1248 let count = u32::try_from(count)
1249 .map_err(|_| Error::Corrupt("tag catalog count exceeds u32"))?;
1250 if count != 0 {
1251 active += 1;
1252 }
1253 if self
1254 .tag_catalog
1255 .count(&self.terms, plugmem_arena::TermId(slot.key))
1256 != count
1257 {
1258 return Err(Error::Corrupt("tag catalog disagrees with tag postings"));
1259 }
1260 }
1261 if self.tag_catalog.dump(&self.terms).len() / 8 != active {
1262 return Err(Error::Corrupt("tag catalog contains an unindexed tag"));
1263 }
1264 Ok(())
1265 }
1266
1267 fn verify_graph(&self) -> Result<(), Error> {
1270 for arena in [&self.edges_out, &self.edges_in] {
1271 for edge in arena.iter() {
1272 if !self.entities.contains(&edge.a.0.to_be_bytes())
1273 || !self.entities.contains(&edge.b.0.to_be_bytes())
1274 {
1275 return Err(Error::Corrupt("edge names an entity that does not exist"));
1276 }
1277 }
1278 }
1279 for edge in self.edges_out.iter() {
1280 if !self.edges_in.contains(&edge_key(edge.b, edge.rel, edge.a)) {
1281 return Err(Error::Corrupt("edge mirrors disagree"));
1282 }
1283 let version = self
1286 .edges_hist_out
1287 .get(&edge_history_key(edge.a, edge.valid_from, edge.edge))
1288 .ok_or(Error::Corrupt("current edge has no history record"))?;
1289 if version.valid_to != VALID_TO_OPEN
1290 || version.rel != edge.rel
1291 || version.b != edge.b
1292 || version.fact != edge.fact
1293 {
1294 return Err(Error::Corrupt("current edge disagrees with its history"));
1295 }
1296 }
1297 for edge in self.edges_hist_out.iter() {
1298 if !self.entities.contains(&edge.a.0.to_be_bytes())
1299 || !self.entities.contains(&edge.b.0.to_be_bytes())
1300 {
1301 return Err(Error::Corrupt(
1302 "edge history names an entity that does not exist",
1303 ));
1304 }
1305 if !self
1306 .edges_hist_in
1307 .contains(&edge_history_key(edge.b, edge.valid_from, edge.edge))
1308 {
1309 return Err(Error::Corrupt("edge history mirrors disagree"));
1310 }
1311 if edge.valid_to == VALID_TO_OPEN
1315 && !self.edges_out.contains(&edge_key(edge.a, edge.rel, edge.b))
1316 {
1317 return Err(Error::Corrupt("open edge version is not a current edge"));
1318 }
1319 }
1320 Ok(())
1321 }
1322
1323 pub fn faulty_facts(&self) -> Vec<(FactId, FactFault)> {
1335 let vslots = self.vecs.len() as u32;
1336 let metas = self.metas.len() as u32;
1337 let mut pairs = Vec::new();
1338 let mut out = Vec::new();
1339 for i in self.fact_ids_ascending() {
1340 let id = FactId(i);
1341 let Some(record) = self.fact(id) else {
1342 continue; };
1344 if record.is_tombstone() {
1345 continue;
1346 }
1347 if core::str::from_utf8(self.texts.get(record.text)).is_err() {
1348 out.push((id, FactFault::Text));
1349 continue;
1350 }
1351 if record.has_vector()
1352 && (record.vector >= vslots || self.vecs.slot_fact(record.vector as usize) != id.0)
1353 {
1354 out.push((id, FactFault::Vector));
1355 continue;
1356 }
1357 if let Some(aux) = self.fact_aux.get(&id.0.to_be_bytes())
1361 && aux.meta.0 != NONE_U32
1362 && (aux.meta.0 >= metas
1363 || crate::metadata::decode(self.metas.get(aux.meta), &mut pairs).is_err())
1364 {
1365 out.push((id, FactFault::Metadata));
1366 }
1367 }
1368 out
1369 }
1370}