use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use plugmem_arena::{
Arena, ArenaCfg, BlobHeap, BlobHeapCfg, BlobId, ChunkPool, ChunkPoolCfg, Interner, ListHandle,
ShardMode, TermId, key,
};
use crate::config::Config;
use crate::error::Error;
use crate::id::{EntityId, FactId, NONE_U32};
use crate::index::IdListIndex;
use crate::index::bm25::Bm25Index;
use crate::index::hnsw::HnswGraph;
use crate::index::vecpool::VecPool;
use crate::journal::{JournalScan, Op, scan};
use crate::model::{
EdgeSlot, EntityByName, EntityRecord, FactAux, FactRecord, TemporalSlot, VALID_TO_OPEN,
fact_flags,
};
use crate::storage::Storage;
use crate::tokenizer::Tokenizer;
const SIMILAR_CANDIDATE_CAP: usize = 32;
mod maintain;
mod persist;
mod recall;
pub use maintain::MaintainReport;
pub use recall::{RecallQuery, RecallResult, RecallScratch, RecalledEdge, RecalledFact, source};
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct RememberInput<'a> {
pub now: u64,
pub text: &'a str,
pub entity: Option<&'a str>,
pub tags: &'a [&'a str],
pub links: &'a [(&'a str, &'a str)],
pub vector: Option<&'a [f32]>,
pub valid_from: Option<u64>,
pub metadata: Option<&'a [(&'a str, &'a str)]>,
}
impl<'a> RememberInput<'a> {
pub fn text(now: u64, text: &'a str) -> Self {
Self {
now,
text,
entity: None,
tags: &[],
links: &[],
vector: None,
valid_from: None,
metadata: None,
}
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct LinkInput<'a> {
pub now: u64,
pub src: &'a str,
pub rel: &'a str,
pub dst: &'a str,
pub provenance: Option<FactId>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RememberOutcome {
pub id: FactId,
pub entity: Option<EntityId>,
pub similar: Vec<Similar>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Similar {
pub id: FactId,
pub score: f32,
pub reason: SimilarReason,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SimilarReason {
LexicalOverlap,
VectorCosine,
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FactView<'a> {
pub record: FactRecord,
pub text: &'a str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FactFault {
Text,
Vector,
Metadata,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Stats {
pub facts: usize,
pub entities: usize,
pub terms: usize,
pub edges: usize,
pub vectors: usize,
pub next_fact: u32,
pub next_entity: u32,
pub db_uuid: u128,
pub pool_bytes: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenReport {
pub replayed: usize,
pub skipped: usize,
pub truncated_tail: bool,
}
pub struct Memory<'a> {
cfg: Config,
facts: Arena<'a, FactRecord>,
fact_aux: Arena<'a, FactAux>,
entities: Arena<'a, EntityRecord>,
by_name: Arena<'a, EntityByName>,
edges_out: Arena<'a, EdgeSlot>,
edges_in: Arena<'a, EdgeSlot>,
temporal: Arena<'a, TemporalSlot>,
texts: BlobHeap<'a>,
metas: BlobHeap<'a>,
terms: Interner<'a>,
tag_lists: ChunkPool<'a>,
bm25: Bm25Index<'a>,
tags_idx: IdListIndex<'a>,
entity_facts: IdListIndex<'a>,
vecs: VecPool<'a>,
hnsw: HnswGraph<'a>,
next_fact: u32,
next_entity: u32,
tokenizer: Tokenizer,
tf_scratch: Vec<(u32, u8)>,
name_scratch: String,
}
impl<'a> Memory<'a> {
pub fn new(cfg: Config) -> Result<Self, Error> {
cfg.validate()?;
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 blob = BlobHeapCfg::new()
.with_max_bytes(cfg.max_bytes)
.with_max_blob(cfg.max_blob);
Ok(Self {
facts: Arena::new(uni(cfg.shards_facts))?,
fact_aux: Arena::new(uni(cfg.shards_facts))?,
entities: Arena::new(uni(cfg.shards_entities))?,
by_name: Arena::new(ord(cfg.shards_entities))?,
edges_out: Arena::new(ord(cfg.shards_edges))?,
edges_in: Arena::new(ord(cfg.shards_edges))?,
temporal: Arena::new(ord(cfg.shards_temporal))?,
texts: BlobHeap::new(blob),
metas: BlobHeap::new(blob),
terms: Interner::new(blob),
tag_lists: ChunkPool::new(ChunkPoolCfg::new().with_max_bytes(cfg.max_bytes)),
bm25: Bm25Index::new(cfg.shards_postings, cfg.max_bytes)?,
tags_idx: IdListIndex::new(cfg.shards_postings, cfg.max_bytes)?,
entity_facts: IdListIndex::new(cfg.shards_entities, cfg.max_bytes)?,
vecs: VecPool::new(cfg.dim, cfg.max_bytes),
hnsw: HnswGraph::new(cfg.hnsw_m, cfg.hnsw_m0, cfg.max_bytes)?,
next_fact: 0,
next_entity: 0,
tokenizer: Tokenizer::new(),
tf_scratch: Vec::new(),
name_scratch: String::new(),
cfg,
})
}
pub fn open<S: Storage>(store: &mut S, cfg: Config) -> Result<(Self, OpenReport), Error> {
let snapshot = store
.read_snapshot()
.map_err(|e| Error::Storage(format!("{e:?}")))?;
let journal = store
.read_journal()
.map_err(|e| Error::Storage(format!("{e:?}")))?;
Self::from_bytes(snapshot.as_deref(), &journal, cfg)
}
pub fn from_bytes(
snapshot: Option<&[u8]>,
journal: &[u8],
cfg: Config,
) -> Result<(Self, OpenReport), Error> {
let mut mem = match snapshot {
Some(bytes) => Self::load_snapshot(bytes, cfg)?,
None => Self::new(cfg)?,
};
let report = mem.replay(journal)?;
Ok((mem, report))
}
pub fn from_bytes_borrowed(
snapshot: &'a [u8],
journal: &[u8],
cfg: Config,
) -> Result<Self, Error> {
let mem = Self::load_snapshot_borrowed(snapshot, cfg)?;
let JournalScan { entries, .. } = scan(journal)?;
if !entries.is_empty() {
return Err(Error::Invalid(
"read-only open requires a checkpointed (empty) journal",
));
}
Ok(mem)
}
pub fn from_bytes_overlay(
snapshot: &'a [u8],
journal: &[u8],
cfg: Config,
) -> Result<(Self, OpenReport), Error> {
let mut mem = Self::load_snapshot_borrowed(snapshot, cfg)?;
let report = mem.replay(journal)?;
Ok((mem, report))
}
fn replay(&mut self, journal: &[u8]) -> Result<OpenReport, Error> {
let JournalScan {
entries,
truncated_tail,
} = scan(journal)?;
let mut report = OpenReport {
truncated_tail,
..OpenReport::default()
};
for entry in entries {
let op = Op::decode(entry.op, entry.payload)?;
match op {
Op::Remember {
now,
valid_from,
entity,
text,
ref tags,
ref links,
ref vector,
ref metadata,
revises,
assigned,
} => {
if assigned.0 < self.next_fact {
report.skipped += 1;
continue;
}
if assigned.0 != self.next_fact {
return Err(Error::Corrupt("journal fact ids are not contiguous"));
}
if !vector.is_empty() && vector.len() != self.cfg.dim {
return Err(Error::Corrupt(
"journal vector dimension disagrees with dim",
));
}
if let Some(target) = revises.some() {
self.check_revisable(target)
.map_err(|_| Error::Corrupt("journal revises an unrevisable fact"))?;
}
self.apply_remember(
&RememberInput {
now,
text,
entity,
tags: &tags.to_vec(),
links: &links.to_vec(),
vector: (!vector.is_empty()).then_some(vector.as_slice()),
valid_from: Some(valid_from),
metadata: (!metadata.is_empty()).then_some(metadata.as_slice()),
},
revises,
)?;
if let Some(target) = revises.some() {
self.close_target(target, valid_from);
}
report.replayed += 1;
}
Op::Forget { fact, .. } => {
match self.apply_forget(fact) {
Ok(_) => report.replayed += 1,
Err(Error::NotFound(_)) => {
return Err(Error::Corrupt("journal forgets an unknown fact"));
}
Err(e) => return Err(e),
}
}
Op::Link {
now,
src,
rel,
dst,
provenance,
} => {
self.apply_link(now, src, rel, dst, provenance)?;
report.replayed += 1;
}
Op::Maintain { .. } => {
self.replay_maintain()?;
report.replayed += 1;
}
}
}
Ok(report)
}
pub fn remember<S: Storage>(
&mut self,
store: &mut S,
input: RememberInput<'_>,
) -> Result<RememberOutcome, Error> {
self.validate_input(&input)?;
let mut outcome = self.apply_remember(&input, FactId::NONE)?;
self.find_similar(&mut outcome);
self.journal_remember(store, &input, FactId::NONE, outcome.id)?;
Ok(outcome)
}
pub fn remember_batch<S: Storage>(
&mut self,
store: &mut S,
inputs: &[RememberInput<'_>],
skip_similar: bool,
) -> Result<Vec<RememberOutcome>, Error> {
let mut out = Vec::with_capacity(inputs.len());
for input in inputs {
self.validate_input(input)?;
let mut outcome = self.apply_remember(input, FactId::NONE)?;
if !skip_similar {
self.find_similar(&mut outcome);
}
self.journal_remember(store, input, FactId::NONE, outcome.id)?;
out.push(outcome);
}
Ok(out)
}
fn find_similar(&mut self, outcome: &mut RememberOutcome) {
let Some(entity) = outcome.entity else { return };
let new_terms: Vec<u32> = self.tf_scratch.iter().map(|&(t, _)| t).collect();
let new_vec = self
.fact(outcome.id)
.filter(|r| r.has_vector())
.map(|r| r.vector);
if new_terms.is_empty() && new_vec.is_none() {
return;
}
let mut ring = [FactId::NONE; SIMILAR_CANDIDATE_CAP];
let mut n = 0usize;
for (fact, _) in self.entity_facts.entries(entity.0) {
if fact != outcome.id {
ring[n % SIMILAR_CANDIDATE_CAP] = fact;
n += 1;
}
}
let mut cand_terms: Vec<u32> = Vec::new();
for &fact in ring.iter().take(n.min(SIMILAR_CANDIDATE_CAP)) {
let Some(record) = self.fact(fact) else {
continue;
};
if record.is_tombstone() || record.is_closed() {
continue;
}
let mut lexical = None;
if !new_terms.is_empty()
&& let Ok(text) = core::str::from_utf8(self.texts.get(record.text))
{
cand_terms.clear();
let terms = &self.terms;
let cand = &mut cand_terms;
self.tokenizer.tokenize(text, &mut |token| {
if let Some(term) = terms.lookup(token)
&& !cand.contains(&term.0)
{
cand.push(term.0);
}
});
if !cand_terms.is_empty() {
let both = cand_terms.iter().filter(|t| new_terms.contains(t)).count();
let union = cand_terms.len() + new_terms.len() - both;
let jaccard = both as f32 / union as f32;
if jaccard > self.cfg.similar_jaccard {
lexical = Some(jaccard);
}
}
}
let mut vector = None;
if let (Some(a), true) = (new_vec, record.has_vector()) {
let cos = self.vecs.cosine_slots(a, record.vector);
if cos > self.cfg.similar_cos {
vector = Some(cos);
}
}
let best = match (lexical, vector) {
(Some(l), Some(v)) if v > l => Some((v, SimilarReason::VectorCosine)),
(Some(l), _) => Some((l, SimilarReason::LexicalOverlap)),
(None, Some(v)) => Some((v, SimilarReason::VectorCosine)),
(None, None) => None,
};
if let Some((score, reason)) = best {
outcome.similar.push(Similar {
id: fact,
score,
reason,
});
}
}
outcome
.similar
.sort_unstable_by(|a, b| b.score.total_cmp(&a.score).then(a.id.cmp(&b.id)));
outcome.similar.truncate(8);
}
pub fn revise<S: Storage>(
&mut self,
store: &mut S,
target: FactId,
input: RememberInput<'_>,
) -> Result<RememberOutcome, Error> {
self.validate_input(&input)?;
self.check_revisable(target)?;
let outcome = self.apply_remember(&input, target)?;
let valid_from = input.valid_from.unwrap_or(input.now);
self.close_target(target, valid_from);
self.journal_remember(store, &input, target, outcome.id)?;
Ok(outcome)
}
pub fn forget<S: Storage>(
&mut self,
store: &mut S,
now: u64,
id: FactId,
) -> Result<bool, Error> {
let fresh = self.apply_forget(id)?;
let mut entry = Vec::new();
Op::Forget { now, fact: id }.encode(&mut entry);
store
.append_journal(&entry)
.map_err(|e| Error::Storage(format!("{e:?}")))?;
Ok(fresh)
}
pub fn link<S: Storage>(&mut self, store: &mut S, input: LinkInput<'_>) -> Result<(), Error> {
self.apply_link(
input.now,
input.src,
input.rel,
input.dst,
FactId::from_opt(input.provenance),
)?;
let mut entry = Vec::new();
Op::Link {
now: input.now,
src: input.src,
rel: input.rel,
dst: input.dst,
provenance: FactId::from_opt(input.provenance),
}
.encode(&mut entry);
store
.append_journal(&entry)
.map_err(|e| Error::Storage(format!("{e:?}")))?;
Ok(())
}
pub fn get(&self, id: FactId) -> Option<FactView<'_>> {
let record = self.fact(id)?;
if record.is_tombstone() {
return None;
}
let text = core::str::from_utf8(self.texts.get(record.text)).ok()?;
Some(FactView { record, text })
}
pub fn tags_of(&self, id: FactId, out: &mut Vec<TermId>) {
let Some(record) = self.fact(id) else { return };
if record.is_tombstone() {
return;
}
let Some(aux) = self.fact_aux.get(&id.0.to_be_bytes()) else {
return;
};
for chunk in self.tag_lists.iter(&aux.tags) {
for raw in chunk.chunks_exact(4) {
out.push(TermId(u32::from_be_bytes(raw.try_into().unwrap())));
}
}
}
pub fn metadata_of<'s>(&'s self, id: FactId, out: &mut Vec<(&'s str, &'s str)>) -> bool {
out.clear();
let Some(record) = self.fact(id) else {
return false;
};
if record.is_tombstone() {
return false;
}
let Some(aux) = self.fact_aux.get(&id.0.to_be_bytes()) else {
return false;
};
if aux.meta.0 == NONE_U32 || aux.meta.0 >= self.metas.len() as u32 {
return false;
}
crate::metadata::decode(self.metas.get(aux.meta), out).is_ok() && !out.is_empty()
}
pub fn entity(&mut self, name: &str) -> Option<EntityId> {
let mut norm = core::mem::take(&mut self.name_scratch);
normalize_name(&mut self.tokenizer, name, &mut norm);
let found = if norm.is_empty() {
None
} else {
self.lookup_entity_by_norm(&norm)
};
self.name_scratch = norm;
found
}
pub fn term(&self, id: TermId) -> &str {
self.terms.resolve(id)
}
pub fn entity_name(&self, id: EntityId) -> Option<&str> {
let record = self.entities.get(&id.0.to_be_bytes())?;
core::str::from_utf8(self.texts.get(record.name)).ok()
}
pub fn facts_len(&self) -> usize {
self.facts.len()
}
pub fn entities_len(&self) -> usize {
self.entities.len()
}
pub fn cfg(&self) -> &Config {
&self.cfg
}
pub fn stats(&self) -> Stats {
Stats {
facts: self.facts.len(),
entities: self.entities.len(),
terms: self.terms.len(),
edges: self.edges_out.len(),
vectors: self.vecs.len(),
next_fact: self.next_fact,
next_entity: self.next_entity,
db_uuid: self.cfg.db_uuid,
pool_bytes: self.facts.pool_bytes()
+ self.fact_aux.pool_bytes()
+ self.entities.pool_bytes()
+ self.by_name.pool_bytes()
+ self.edges_out.pool_bytes()
+ self.edges_in.pool_bytes()
+ self.temporal.pool_bytes()
+ self.texts.pool_bytes()
+ self.terms.pool_bytes()
+ self.tag_lists.pool_bytes()
+ self.bm25.pool_bytes()
+ self.tags_idx.pool_bytes()
+ self.entity_facts.pool_bytes()
+ self.vecs.pool_bytes()
+ self.hnsw.pool_bytes(),
}
}
fn fact(&self, id: FactId) -> Option<FactRecord> {
self.facts.get(&id.0.to_be_bytes())
}
fn validate_input(&self, input: &RememberInput<'_>) -> Result<(), Error> {
if input.text.len() > self.cfg.max_text {
return Err(Error::TooLarge {
what: "text",
len: input.text.len(),
max: self.cfg.max_text,
});
}
if input.tags.len() > 32 {
return Err(Error::TooLarge {
what: "tags",
len: input.tags.len(),
max: 32,
});
}
if input.links.len() > 16 {
return Err(Error::TooLarge {
what: "links",
len: input.links.len(),
max: 16,
});
}
if input.tags.iter().any(|t| t.is_empty()) {
return Err(Error::Invalid("empty tag"));
}
if !input.links.is_empty() && input.entity.is_none() {
return Err(Error::Invalid("links require a subject entity"));
}
if let Some(v) = input.vector {
if self.cfg.dim == 0 {
return Err(Error::Invalid("vector given but dim is 0"));
}
if v.len() != self.cfg.dim {
return Err(Error::DimMismatch {
got: v.len(),
want: self.cfg.dim,
});
}
}
Ok(())
}
fn check_revisable(&self, target: FactId) -> Result<(), Error> {
let record = self.fact(target).ok_or(Error::NotFound(target))?;
if record.is_tombstone() {
return Err(Error::NotFound(target));
}
if record.is_closed() {
return Err(Error::AlreadyClosed(target));
}
Ok(())
}
fn close_target(&mut self, target: FactId, valid_to: u64) {
let record = self.fact(target).expect("checked revisable");
let payload = self
.facts
.payload_mut(&target.0.to_be_bytes())
.expect("record fetched above");
let flags = record.flags | fact_flags::CLOSED;
payload[4..6].copy_from_slice(&flags.to_be_bytes());
payload[36..44].copy_from_slice(&valid_to.to_be_bytes());
}
fn apply_remember(
&mut self,
input: &RememberInput<'_>,
revises: FactId,
) -> Result<RememberOutcome, Error> {
let id = FactId(self.next_fact);
let entity = match input.entity {
Some(name) => Some(self.resolve_or_create_entity(name, input.now)?),
None => None,
};
let text_id = self.texts.push(input.text.as_bytes())?;
let mut tfs = core::mem::take(&mut self.tf_scratch);
tfs.clear();
let terms = &mut self.terms;
let mut intern_err = None;
self.tokenizer.tokenize(input.text, &mut |token| {
if intern_err.is_some() {
return;
}
match terms.intern(token) {
Ok(term) => match tfs.iter_mut().find(|(t, _)| *t == term.0) {
Some((_, tf)) => *tf = tf.saturating_add(1),
None => tfs.push((term.0, 1)),
},
Err(e) => intern_err = Some(e),
}
});
if let Some(e) = intern_err {
self.tf_scratch = tfs;
return Err(Error::Arena(e));
}
self.bm25.index_doc(id, &tfs)?;
self.tf_scratch = tfs;
let meta = match input.metadata {
Some(pairs) if !pairs.is_empty() => {
self.metas.push(&crate::metadata::encode(pairs)?)?
}
_ => BlobId(NONE_U32),
};
let mut aux = FactAux {
id,
tags: ListHandle::EMPTY,
meta,
};
let mut seen_tags: [u32; 32] = [NONE_U32; 32];
let mut seen_cnt = 0usize;
for tag in input.tags {
let term = self.terms.intern(tag)?;
if seen_tags[..seen_cnt].contains(&term.0) {
continue;
}
seen_tags[seen_cnt] = term.0;
seen_cnt += 1;
self.tag_lists.push(&mut aux.tags, &term.0.to_be_bytes())?;
self.tags_idx.push(term.0, id, 0)?;
}
self.fact_aux.insert(&aux)?;
if let Some(src) = entity {
for &(rel, dst_name) in input.links {
let dst = self.resolve_or_create_entity(dst_name, input.now)?;
let rel = self.terms.intern(rel)?;
self.upsert_edge(src, rel, dst, id)?;
}
self.entity_facts.push(src.0, id, 0)?;
}
let (vector, flags) = match input.vector {
Some(v) => (self.vecs.push(id, v)?, fact_flags::HAS_VECTOR),
None => (NONE_U32, 0),
};
let recorded_at = input.now;
let valid_from = input.valid_from.unwrap_or(input.now);
self.facts.insert(&FactRecord {
id,
entity: EntityId::from_opt(entity),
flags,
kind: 0,
text: text_id,
vector,
revises,
recorded_at,
valid_from,
valid_to: VALID_TO_OPEN,
})?;
self.temporal.insert(&TemporalSlot {
recorded_at,
fact: id,
})?;
self.next_fact += 1;
Ok(RememberOutcome {
id,
entity,
similar: Vec::new(),
})
}
fn apply_forget(&mut self, id: FactId) -> Result<bool, Error> {
let record = self.fact(id).ok_or(Error::NotFound(id))?;
if record.is_tombstone() {
return Ok(false);
}
let payload = self
.facts
.payload_mut(&id.0.to_be_bytes())
.expect("record fetched above");
let flags = record.flags | fact_flags::TOMBSTONE;
payload[4..6].copy_from_slice(&flags.to_be_bytes());
Ok(true)
}
fn apply_link(
&mut self,
now: u64,
src: &str,
rel: &str,
dst: &str,
provenance: FactId,
) -> Result<(), Error> {
let src = self.resolve_or_create_entity(src, now)?;
let dst = self.resolve_or_create_entity(dst, now)?;
let rel = self.terms.intern(rel)?;
self.upsert_edge(src, rel, dst, provenance)
}
fn upsert_edge(
&mut self,
src: EntityId,
rel: TermId,
dst: EntityId,
fact: FactId,
) -> Result<(), Error> {
for (arena, a, b) in [
(&mut self.edges_out, src, dst),
(&mut self.edges_in, dst, src),
] {
let slot = EdgeSlot { a, rel, b, fact };
if !arena.insert(&slot)? {
let mut kb = [0u8; 12];
key::write_u32(&mut kb, a.0);
key::write_u32(&mut kb[4..], rel.0);
key::write_u32(&mut kb[8..], b.0);
let payload = arena.payload_mut(&kb).expect("insert reported a duplicate");
payload.copy_from_slice(&fact.0.to_be_bytes());
}
}
Ok(())
}
fn lookup_entity_by_norm(&self, norm: &str) -> Option<EntityId> {
let term = self.terms.lookup(norm)?;
let mut from = [0u8; 8];
key::write_u32(&mut from, term.0);
let mut to = [0u8; 8];
key::write_u32(&mut to, term.0);
to[4..].copy_from_slice(&u32::MAX.to_be_bytes());
self.by_name.range(&from, &to).next().map(|e| e.id)
}
fn resolve_or_create_entity(&mut self, name: &str, now: u64) -> Result<EntityId, Error> {
let mut norm = core::mem::take(&mut self.name_scratch);
normalize_name(&mut self.tokenizer, name, &mut norm);
if norm.is_empty() {
self.name_scratch = norm;
return Err(Error::Invalid("entity name has no indexable characters"));
}
let result = (|| {
if let Some(found) = self.lookup_entity_by_norm(&norm) {
return Ok(found);
}
let term = self.terms.intern(&norm)?;
let id = EntityId(self.next_entity);
let name_id = self.texts.push(name.as_bytes())?;
self.entities.insert(&EntityRecord {
id,
name: name_id,
name_term: term,
created_at: now,
flags: 0,
})?;
self.by_name.insert(&EntityByName {
name_term: term,
id,
})?;
self.next_entity += 1;
Ok(id)
})();
self.name_scratch = norm;
result
}
fn journal_remember<S: Storage>(
&mut self,
store: &mut S,
input: &RememberInput<'_>,
revises: FactId,
assigned: FactId,
) -> Result<(), Error> {
let mut entry = Vec::new();
Op::Remember {
now: input.now,
valid_from: input.valid_from.unwrap_or(input.now),
entity: input.entity,
text: input.text,
tags: input.tags.to_vec(),
links: input.links.to_vec(),
vector: input.vector.map(<[f32]>::to_vec).unwrap_or_default(),
metadata: input.metadata.map(<[_]>::to_vec).unwrap_or_default(),
revises,
assigned,
}
.encode(&mut entry);
store
.append_journal(&entry)
.map_err(|e| Error::Storage(format!("{e:?}")))
}
}
impl core::fmt::Debug for Memory<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Memory")
.field("facts", &self.facts.len())
.field("entities", &self.entities.len())
.field("terms", &self.terms.len())
.finish()
}
}
fn normalize_name(tokenizer: &mut Tokenizer, name: &str, out: &mut String) {
out.clear();
tokenizer.tokenize(name, &mut |token| {
if !out.is_empty() {
out.push(' ');
}
out.push_str(token);
});
}