1use alloc::{format, string::String, vec::Vec};
9
10use plugmem_arena::{Arena, ArenaCfg, ShardMode};
11
12use crate::error::Error;
13use crate::id::NONE_U32;
14use crate::index::hnsw::{HnswGraph, HnswScratch};
15use crate::index::vecpool::VecPool;
16use crate::model::{FactRecord, fact_flags};
17use crate::snapshot::SnapshotSink;
18use crate::storage::Scratch;
19
20use super::Memory;
21use super::persist::Sections;
22use super::shards::ShardLayout;
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct ReembedReport {
28 pub previous_space: Option<String>,
30 pub new_space: String,
32 pub previous_dim: usize,
34 pub new_dim: usize,
36 pub embedded: usize,
38 pub tombstones_skipped: usize,
40 pub vector_bytes: u64,
42 pub hnsw_indexed: u32,
44}
45
46#[derive(Debug)]
48pub enum ReembedError<E> {
49 Engine(Error),
51 Embedder(E),
53}
54
55fn engine<E>(error: Error) -> ReembedError<E> {
56 ReembedError::Engine(error)
57}
58
59#[allow(clippy::too_many_arguments)]
60fn flush_batch<V, E, F>(
61 encoder: &VecPool<'_>,
62 vec_scratch: &mut V,
63 facts: &mut Arena<'static, FactRecord>,
64 records: &mut Vec<FactRecord>,
65 texts: &mut Vec<&str>,
66 slot_buf: &mut Vec<u8>,
67 next_slot: &mut u32,
68 embed: &mut F,
69) -> Result<(), ReembedError<E>>
70where
71 V: Scratch,
72 F: FnMut(&[&str]) -> Result<Vec<Vec<f32>>, E>,
73{
74 if records.is_empty() {
75 return Ok(());
76 }
77 let vectors = embed(texts).map_err(ReembedError::Embedder)?;
78 if vectors.len() != records.len() {
79 return Err(engine(Error::Invalid(
80 "embedder returned the wrong number of vectors",
81 )));
82 }
83 for (mut record, vector) in records.drain(..).zip(vectors) {
84 encoder
85 .encode_slot_into(record.id, &vector, slot_buf)
86 .map_err(engine)?;
87 vec_scratch
88 .write(slot_buf)
89 .map_err(|e| engine(Error::Storage(format!("{e:?}"))))?;
90 record.vector = *next_slot;
91 record.flags |= fact_flags::HAS_VECTOR;
92 facts.insert(&record).map_err(Error::from).map_err(engine)?;
93 *next_slot = next_slot.checked_add(1).ok_or_else(|| {
94 engine(Error::CapacityExceeded {
95 what: "vector slots",
96 })
97 })?;
98 }
99 texts.clear();
100 Ok(())
101}
102
103impl Memory<'_> {
104 #[allow(clippy::too_many_arguments)]
119 pub fn write_reembedded_snapshot<V, Sk, E, F>(
120 &self,
121 created_at: u64,
122 target_dim: usize,
123 target_space: &str,
124 batch_size: usize,
125 vec_scratch: &mut V,
126 sink: Sk,
127 mut embed: F,
128 ) -> Result<ReembedReport, ReembedError<E>>
129 where
130 V: Scratch,
131 Sk: SnapshotSink,
132 F: FnMut(&[&str]) -> Result<Vec<Vec<f32>>, E>,
133 {
134 Self::validate_vector_space(target_space).map_err(engine)?;
135 if target_dim == 0 {
136 return Err(engine(Error::Invalid(
137 "reembed target dimension must be nonzero",
138 )));
139 }
140 if batch_size == 0 {
141 return Err(engine(Error::Invalid("reembed batch size must be nonzero")));
142 }
143 let mut target_cfg = self.cfg.clone();
144 target_cfg.dim = target_dim;
145 target_cfg.validate().map_err(engine)?;
146
147 let arena = ArenaCfg::new(target_cfg.shards_facts, ShardMode::Uniform)
148 .with_max_bytes(target_cfg.max_bytes);
149 let mut facts = Arena::new(arena).map_err(Error::from).map_err(engine)?;
150 let encoder = VecPool::new(target_dim, target_cfg.max_bytes);
151 let mut records = Vec::with_capacity(batch_size);
152 let mut texts = Vec::with_capacity(batch_size);
153 let mut slot_buf = Vec::with_capacity(encoder.stride());
154 let mut next_slot = 0u32;
155 let mut tombstones_skipped = 0usize;
156
157 for fid in self.fact_ids_ascending() {
158 let Some(mut record) = self.facts.get(&fid.to_be_bytes()) else {
159 continue;
160 };
161 if record.is_tombstone() {
162 record.flags &= !fact_flags::HAS_VECTOR;
163 record.vector = NONE_U32;
164 facts.insert(&record).map_err(Error::from).map_err(engine)?;
165 tombstones_skipped += 1;
166 continue;
167 }
168 let text = core::str::from_utf8(self.texts.get(record.text))
169 .map_err(|_| engine(Error::Corrupt("reembed: fact text is not UTF-8")))?;
170 records.push(record);
171 texts.push(text);
172 if records.len() == batch_size {
173 flush_batch(
174 &encoder,
175 vec_scratch,
176 &mut facts,
177 &mut records,
178 &mut texts,
179 &mut slot_buf,
180 &mut next_slot,
181 &mut embed,
182 )?;
183 }
184 }
185 flush_batch(
186 &encoder,
187 vec_scratch,
188 &mut facts,
189 &mut records,
190 &mut texts,
191 &mut slot_buf,
192 &mut next_slot,
193 &mut embed,
194 )?;
195
196 if next_slot == 0 {
197 let probe = embed(&[""]).map_err(ReembedError::Embedder)?;
198 if probe.len() != 1 {
199 return Err(engine(Error::Invalid(
200 "embedder returned the wrong number of probe vectors",
201 )));
202 }
203 encoder
204 .encode_slot_into(crate::FactId(0), &probe[0], &mut slot_buf)
205 .map_err(engine)?;
206 }
207
208 let vector_bytes = vec_scratch.len();
209 let vec_bytes = vec_scratch
210 .freeze()
211 .map_err(|e| engine(Error::Storage(format!("{e:?}"))))?;
212 let vecs = VecPool::from_parts_borrowed(target_dim, target_cfg.max_bytes, vec_bytes)
213 .map_err(engine)?;
214 debug_assert_eq!(vecs.len(), next_slot as usize);
215
216 let mut hnsw = HnswGraph::new(target_cfg.hnsw_m, target_cfg.hnsw_m0, target_cfg.max_bytes)
217 .map_err(engine)?;
218 if vecs.len() >= target_cfg.flat_to_hnsw {
219 let mut scratch = HnswScratch::default();
220 hnsw.insert_bulk(
221 &vecs,
222 next_slot,
223 target_cfg.hnsw_ef_construction,
224 &mut scratch,
225 )
226 .map_err(engine)?;
227 }
228
229 let sections = Sections {
230 facts: &facts,
231 fact_aux: &self.fact_aux,
232 entities: &self.entities,
233 by_name: &self.by_name,
234 temporal: &self.temporal,
235 texts: &self.texts,
236 metas: &self.metas,
237 tag_lists: &self.tag_lists,
238 bm25: &self.bm25,
239 tags_idx: &self.tags_idx,
240 entity_facts: &self.entity_facts,
241 vecs: &vecs,
242 hnsw: &hnsw,
243 edges_out: &self.edges_out,
244 edges_in: &self.edges_in,
245 edges_hist_out: &self.edges_hist_out,
246 edges_hist_in: &self.edges_hist_in,
247 layout: ShardLayout::of_config(&self.cfg),
248 };
249 self.write_snapshot_reconfigured(
250 §ions,
251 &target_cfg,
252 Some(target_space),
253 created_at,
254 sink,
255 )
256 .map_err(engine)?;
257
258 Ok(ReembedReport {
259 previous_space: self.vector_space.clone(),
260 new_space: target_space.into(),
261 previous_dim: self.cfg.dim,
262 new_dim: target_dim,
263 embedded: next_slot as usize,
264 tombstones_skipped,
265 vector_bytes,
266 hnsw_indexed: hnsw.indexed(),
267 })
268 }
269}