1use nodedb_types::Namespace;
4use nodedb_types::document::Document;
5use nodedb_types::error::{NodeDbError, NodeDbResult};
6use nodedb_types::id::{EdgeId, NodeId};
7
8use crate::engine::crdt::engine::CrdtBatchOp;
9use crate::engine::graph::edge::{
10 edge_crdt_collection, edge_crdt_fields, edge_history_value, edge_id_for,
11};
12use crate::engine::graph::history;
13use crate::engine::graph::index::CsrIndex;
14use crate::engine::vector::nodes::{bind_node, encode_sidecar};
15use crate::engine::vector::resident::{check_insert_widths, lock_resident_or_create};
16use crate::engine::vector::row::EMBEDDING_DIM_FIELD;
17use crate::runtime::now_millis_i64;
18
19use super::{LockExt, NodeDbLite};
20use crate::storage::engine::StorageEngine;
21
22impl<S: StorageEngine> NodeDbLite<S> {
23 pub async fn batch_vector_insert(
31 &self,
32 collection: &str,
33 vectors: &[(&str, &[f32])],
34 ) -> NodeDbResult<()> {
35 if vectors.is_empty() {
36 return Ok(());
37 }
38
39 if self.governor.worst_engine_pressure() == nodedb_mem::PressureLevel::Emergency {
40 return Err(NodeDbError::storage(
41 crate::error::LiteError::Backpressure {
42 detail:
43 "batch vector insert rejected: memory governor is at Emergency pressure"
44 .into(),
45 },
46 ));
47 }
48
49 let dim = vectors[0].1.len();
50
51 check_insert_widths(
55 &self.vector_state,
56 collection,
57 vectors.iter().map(|(_, e)| e.len()),
58 )
59 .await
60 .map_err(NodeDbError::from)?;
61
62 {
67 let ops: Vec<_> = vectors
68 .iter()
69 .filter(|(_, embedding)| !embedding.is_empty())
70 .map(|(id, embedding)| {
71 crate::engine::vector::durable::put_op(collection, id, embedding)
72 })
73 .collect();
74 if !ops.is_empty() {
75 self.storage
76 .batch_write(&ops)
77 .await
78 .map_err(NodeDbError::storage)?;
79 }
80 }
81
82 let mut bound: std::collections::HashMap<&str, (u32, &[f32])> =
85 std::collections::HashMap::with_capacity(vectors.len());
86 {
87 let mut resident = lock_resident_or_create(&self.vector_state, collection, dim)
88 .await
89 .map_err(NodeDbError::from)?;
90 let index = resident.index();
91 for &(id, embedding) in vectors {
92 let node = bind_node(
93 &self.vector_state,
94 index,
95 collection,
96 id,
97 embedding.to_vec(),
98 )
99 .map_err(NodeDbError::from)?;
100 bound.insert(id, (node, embedding));
101 }
102 }
103 for (node, embedding) in bound.into_values() {
106 encode_sidecar(&self.vector_state, collection, node, embedding)
107 .map_err(|e| NodeDbError::bad_request(e.to_string()))?;
108 }
109
110 {
111 let mut crdt = self.crdt.lock_or_recover();
112
113 use crate::engine::crdt::{CrdtField, CrdtRowOp, CrdtRowWrite};
114
115 let fields: Vec<Vec<CrdtField<'_>>> = vectors
116 .iter()
117 .map(|&(_, emb)| {
118 vec![(EMBEDDING_DIM_FIELD, loro::LoroValue::I64(emb.len() as i64))]
119 })
120 .collect();
121
122 let ops: Vec<CrdtRowOp<'_>> = vectors
124 .iter()
125 .zip(fields.iter())
126 .map(|(&(id, _), f)| (CrdtRowWrite::SetFields, collection, id, f.as_slice()))
127 .collect();
128
129 crdt.batch_write(&ops).map_err(NodeDbError::from)?;
130 }
131
132 self.update_memory_stats();
133 Ok(())
134 }
135
136 pub async fn batch_graph_insert_edges(
150 &self,
151 collection: &str,
152 edges: &[(NodeId, NodeId, &str, Option<Document>)],
153 ) -> NodeDbResult<Vec<EdgeId>> {
154 if edges.is_empty() {
155 return Ok(Vec::new());
156 }
157
158 if self.governor.worst_engine_pressure() == nodedb_mem::PressureLevel::Emergency {
159 return Err(NodeDbError::storage(
160 crate::error::LiteError::Backpressure {
161 detail:
162 "batch graph edge insert rejected: memory governor is at Emergency pressure"
163 .into(),
164 },
165 ));
166 }
167
168 let edge_ids: Vec<EdgeId> = edges
171 .iter()
172 .map(|(from, to, edge_type, _)| edge_id_for(from, to, edge_type))
173 .collect::<NodeDbResult<Vec<_>>>()?;
174
175 {
176 let memory = self.memory_for(nodedb_mem::EngineId::Graph);
177 let mut csr_map = self.csr.lock_or_recover();
178 let csr = csr_map
179 .entry(collection.to_string())
180 .or_insert_with(|| CsrIndex::new(memory));
181 for (from, to, edge_type, _) in edges {
182 let _ = csr.add_edge(from.as_str(), edge_type, to.as_str());
183 }
184 }
185
186 let edge_coll = edge_crdt_collection(collection);
187 let edge_keys: Vec<String> = edge_ids.iter().map(|id| format!("{id}")).collect();
188 let field_lists: Vec<Vec<(&str, loro::LoroValue)>> = edges
189 .iter()
190 .map(|(from, to, edge_type, properties)| {
191 edge_crdt_fields(from, to, edge_type, properties)
192 })
193 .collect();
194
195 {
196 let mut crdt = self.crdt.lock_or_recover();
197 let ops: Vec<CrdtBatchOp<'_>> = edge_keys
198 .iter()
199 .zip(field_lists.iter())
200 .map(|(key, fields)| (edge_coll.as_str(), key.as_str(), fields.as_slice()))
201 .collect();
202 crdt.batch_upsert(&ops).map_err(NodeDbError::storage)?;
203 }
204
205 let bitemporal = history::is_bitemporal(self.storage.as_ref(), collection)
208 .await
209 .unwrap_or(false);
210 if bitemporal {
211 let system_from_ms = now_millis_i64();
212 for (i, (from, to, edge_type, properties)) in edges.iter().enumerate() {
213 let props_value = edge_history_value(from, to, edge_type, properties);
214 let _ = history::record_edge_insert(
215 self.storage.as_ref(),
216 collection,
217 &edge_keys[i],
218 &props_value,
219 system_from_ms,
220 )
221 .await;
222 }
223 }
224
225 self.update_memory_stats();
226 Ok(edge_ids)
227 }
228
229 pub fn compact_graph(&self) -> NodeDbResult<()> {
231 let mut csr_map = self.csr.lock_or_recover();
232 for (name, csr) in csr_map.iter_mut() {
233 csr.compact().map_err(|e| {
234 NodeDbError::storage(format!("graph csr compact failed for '{name}': {e}"))
235 })?;
236 }
237 Ok(())
238 }
239
240 pub async fn evict_collections(&self, max_to_evict: usize) -> NodeDbResult<usize> {
245 let mut evicted = 0;
246
247 let candidates: Vec<(String, usize)> = {
248 let indices = self.vector_state.hnsw_indices.lock_or_recover();
249 let mut sorted: Vec<(String, usize)> = indices
250 .iter()
251 .map(|(name, idx)| (name.clone(), idx.len()))
252 .collect();
253 sorted.sort_by_key(|(_, size)| *size);
254 sorted
255 };
256
257 #[cfg(not(target_arch = "wasm32"))]
259 let seg_ext = self.storage.as_vector_segment_ext();
260
261 for (name, _) in candidates.into_iter().take(max_to_evict) {
262 #[cfg(not(target_arch = "wasm32"))]
275 let (blob, segment_sources, snapshot) = {
276 let indices = self.vector_state.hnsw_indices.lock_or_recover();
277 match indices.get(&name) {
278 Some(idx) => {
279 let snapshot = (idx.len(), idx.tombstone_count());
280 if seg_ext.is_some() {
281 let graph_bytes = idx.graph_checkpoint_to_bytes().map_err(|e| {
282 NodeDbError::serialization("hnsw-graph-checkpoint", e)
283 })?;
284 let id_map = self.vector_state.vector_id_map.lock_or_recover();
287 let sources = crate::engine::vector::segment::node_sources(
288 idx,
289 id_map.index(&name),
290 );
291 (graph_bytes, sources, snapshot)
292 } else {
293 let blob = idx
294 .checkpoint_to_bytes()
295 .map_err(|e| NodeDbError::serialization("hnsw-checkpoint", e))?;
296 (blob, None, snapshot)
297 }
298 }
299 None => continue,
300 }
301 };
302 #[cfg(target_arch = "wasm32")]
303 let (blob, snapshot) = {
304 let indices = self.vector_state.hnsw_indices.lock_or_recover();
305 match indices.get(&name) {
306 Some(idx) => (
307 idx.checkpoint_to_bytes()
308 .map_err(|e| NodeDbError::serialization("hnsw-checkpoint", e))?,
309 (idx.len(), idx.tombstone_count()),
310 ),
311 None => continue,
312 }
313 };
314
315 let key = format!("hnsw:{name}");
316 self.storage
317 .put(
318 Namespace::Vector,
319 key.as_bytes(),
320 &crate::storage::checksum::wrap(&blob),
321 )
322 .await
323 .map_err(NodeDbError::storage)?;
324
325 #[cfg(not(target_arch = "wasm32"))]
327 if let Some(sources) = segment_sources
328 && let Some(ext) = seg_ext
329 {
330 match crate::engine::vector::segment::segment_payload(
331 &*self.storage,
332 &name,
333 sources,
334 )
335 .await
336 {
337 Ok(Some((dim, vectors, surrogates))) => {
338 if let Err(e) = ext
339 .write_vector_segment(&name, dim, &vectors, &surrogates)
340 .await
341 {
342 tracing::error!(
343 collection = %name,
344 error = %e,
345 "HNSW vector segment write failed during eviction; \
346 graph blob is persisted but vectors may be lost on cold restart"
347 );
348 }
349 }
350 Ok(None) => {}
353 Err(e) => tracing::error!(
354 collection = %name,
355 error = %e,
356 "reading durable vectors for the eviction segment write failed; \
357 leaving the existing segment in place"
358 ),
359 }
360 }
361
362 {
366 let mut indices = self.vector_state.hnsw_indices.lock_or_recover();
367 let unchanged = indices
368 .get(&name)
369 .is_some_and(|idx| (idx.len(), idx.tombstone_count()) == snapshot);
370 if !unchanged {
371 tracing::debug!(
372 collection = %name,
373 "HNSW collection changed during eviction; kept in memory"
374 );
375 continue;
376 }
377 self.vector_state
378 .evicted
379 .lock_or_recover()
380 .insert(name.clone());
381 self.vector_state.unloadable.lock_or_recover().remove(&name);
382 indices.remove(&name);
383 }
384
385 tracing::info!(collection = %name, "HNSW collection evicted from memory");
386 evicted += 1;
387 }
388
389 self.update_memory_stats();
390 Ok(evicted)
391 }
392
393 pub async fn check_and_evict(&self) -> NodeDbResult<usize> {
400 use nodedb_mem::PressureLevel;
401
402 self.update_memory_stats();
403 match self.governor.worst_engine_pressure() {
404 PressureLevel::Emergency => self.evict_collections(2).await,
405 PressureLevel::Critical => self.evict_collections(1).await,
406 PressureLevel::Warning => Ok(0),
407 PressureLevel::Normal => Ok(0),
408 }
409 }
410}