use alloc::{format, string::String, vec::Vec};
use plugmem_arena::{Arena, ArenaCfg, ShardMode};
use crate::error::Error;
use crate::id::NONE_U32;
use crate::index::hnsw::{HnswGraph, HnswScratch};
use crate::index::vecpool::VecPool;
use crate::model::{FactRecord, fact_flags};
use crate::snapshot::SnapshotSink;
use crate::storage::Scratch;
use super::Memory;
use super::persist::Sections;
use super::shards::ShardLayout;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ReembedReport {
pub previous_space: Option<String>,
pub new_space: String,
pub previous_dim: usize,
pub new_dim: usize,
pub embedded: usize,
pub tombstones_skipped: usize,
pub vector_bytes: u64,
pub hnsw_indexed: u32,
}
#[derive(Debug)]
pub enum ReembedError<E> {
Engine(Error),
Embedder(E),
}
fn engine<E>(error: Error) -> ReembedError<E> {
ReembedError::Engine(error)
}
#[allow(clippy::too_many_arguments)]
fn flush_batch<V, E, F>(
encoder: &VecPool<'_>,
vec_scratch: &mut V,
facts: &mut Arena<'static, FactRecord>,
records: &mut Vec<FactRecord>,
texts: &mut Vec<&str>,
slot_buf: &mut Vec<u8>,
next_slot: &mut u32,
embed: &mut F,
) -> Result<(), ReembedError<E>>
where
V: Scratch,
F: FnMut(&[&str]) -> Result<Vec<Vec<f32>>, E>,
{
if records.is_empty() {
return Ok(());
}
let vectors = embed(texts).map_err(ReembedError::Embedder)?;
if vectors.len() != records.len() {
return Err(engine(Error::Invalid(
"embedder returned the wrong number of vectors",
)));
}
for (mut record, vector) in records.drain(..).zip(vectors) {
encoder
.encode_slot_into(record.id, &vector, slot_buf)
.map_err(engine)?;
vec_scratch
.write(slot_buf)
.map_err(|e| engine(Error::Storage(format!("{e:?}"))))?;
record.vector = *next_slot;
record.flags |= fact_flags::HAS_VECTOR;
facts.insert(&record).map_err(Error::from).map_err(engine)?;
*next_slot = next_slot.checked_add(1).ok_or_else(|| {
engine(Error::CapacityExceeded {
what: "vector slots",
})
})?;
}
texts.clear();
Ok(())
}
impl Memory<'_> {
#[allow(clippy::too_many_arguments)]
pub fn write_reembedded_snapshot<V, Sk, E, F>(
&self,
created_at: u64,
target_dim: usize,
target_space: &str,
batch_size: usize,
vec_scratch: &mut V,
sink: Sk,
mut embed: F,
) -> Result<ReembedReport, ReembedError<E>>
where
V: Scratch,
Sk: SnapshotSink,
F: FnMut(&[&str]) -> Result<Vec<Vec<f32>>, E>,
{
Self::validate_vector_space(target_space).map_err(engine)?;
if target_dim == 0 {
return Err(engine(Error::Invalid(
"reembed target dimension must be nonzero",
)));
}
if batch_size == 0 {
return Err(engine(Error::Invalid("reembed batch size must be nonzero")));
}
let mut target_cfg = self.cfg.clone();
target_cfg.dim = target_dim;
target_cfg.validate().map_err(engine)?;
let arena = ArenaCfg::new(target_cfg.shards_facts, ShardMode::Uniform)
.with_max_bytes(target_cfg.max_bytes);
let mut facts = Arena::new(arena).map_err(Error::from).map_err(engine)?;
let encoder = VecPool::new(target_dim, target_cfg.max_bytes);
let mut records = Vec::with_capacity(batch_size);
let mut texts = Vec::with_capacity(batch_size);
let mut slot_buf = Vec::with_capacity(encoder.stride());
let mut next_slot = 0u32;
let mut tombstones_skipped = 0usize;
for fid in self.fact_ids_ascending() {
let Some(mut record) = self.facts.get(&fid.to_be_bytes()) else {
continue;
};
if record.is_tombstone() {
record.flags &= !fact_flags::HAS_VECTOR;
record.vector = NONE_U32;
facts.insert(&record).map_err(Error::from).map_err(engine)?;
tombstones_skipped += 1;
continue;
}
let text = core::str::from_utf8(self.texts.get(record.text))
.map_err(|_| engine(Error::Corrupt("reembed: fact text is not UTF-8")))?;
records.push(record);
texts.push(text);
if records.len() == batch_size {
flush_batch(
&encoder,
vec_scratch,
&mut facts,
&mut records,
&mut texts,
&mut slot_buf,
&mut next_slot,
&mut embed,
)?;
}
}
flush_batch(
&encoder,
vec_scratch,
&mut facts,
&mut records,
&mut texts,
&mut slot_buf,
&mut next_slot,
&mut embed,
)?;
if next_slot == 0 {
let probe = embed(&[""]).map_err(ReembedError::Embedder)?;
if probe.len() != 1 {
return Err(engine(Error::Invalid(
"embedder returned the wrong number of probe vectors",
)));
}
encoder
.encode_slot_into(crate::FactId(0), &probe[0], &mut slot_buf)
.map_err(engine)?;
}
let vector_bytes = vec_scratch.len();
let vec_bytes = vec_scratch
.freeze()
.map_err(|e| engine(Error::Storage(format!("{e:?}"))))?;
let vecs = VecPool::from_parts_borrowed(target_dim, target_cfg.max_bytes, vec_bytes)
.map_err(engine)?;
debug_assert_eq!(vecs.len(), next_slot as usize);
let mut hnsw = HnswGraph::new(target_cfg.hnsw_m, target_cfg.hnsw_m0, target_cfg.max_bytes)
.map_err(engine)?;
if vecs.len() >= target_cfg.flat_to_hnsw {
let mut scratch = HnswScratch::default();
hnsw.insert_bulk(
&vecs,
next_slot,
target_cfg.hnsw_ef_construction,
&mut scratch,
)
.map_err(engine)?;
}
let sections = Sections {
facts: &facts,
fact_aux: &self.fact_aux,
entities: &self.entities,
by_name: &self.by_name,
temporal: &self.temporal,
texts: &self.texts,
metas: &self.metas,
tag_lists: &self.tag_lists,
bm25: &self.bm25,
tags_idx: &self.tags_idx,
entity_facts: &self.entity_facts,
vecs: &vecs,
hnsw: &hnsw,
edges_out: &self.edges_out,
edges_in: &self.edges_in,
edges_hist_out: &self.edges_hist_out,
edges_hist_in: &self.edges_hist_in,
layout: ShardLayout::of_config(&self.cfg),
};
self.write_snapshot_reconfigured(
§ions,
&target_cfg,
Some(target_space),
created_at,
sink,
)
.map_err(engine)?;
Ok(ReembedReport {
previous_space: self.vector_space.clone(),
new_space: target_space.into(),
previous_dim: self.cfg.dim,
new_dim: target_dim,
embedded: next_slot as usize,
tombstones_skipped,
vector_bytes,
hnsw_indexed: hnsw.indexed(),
})
}
}