1use std::collections::HashSet;
6
7use loro::LoroValue;
8
9use nodedb_types::document::Document;
10use nodedb_types::error::{NodeDbError, NodeDbResult};
11use nodedb_types::filter::MetadataFilter;
12use nodedb_types::result::SearchResult;
13
14use crate::engine::vector::nodes::{encode_sidecar, unbind_node, upsert_node};
15use crate::engine::vector::resident::{check_insert_widths, lock_resident};
16use crate::engine::vector::row::{
17 EMBEDDING_DIM_FIELD, VECTOR_FIELD_TAG, VECTOR_ROW_FIELDS, VectorSlot, detach_vector_row,
18 is_vector_primary,
19};
20use crate::engine::vector::search::{
21 AnnTuning, SearchFilter, VectorSearchRequest, run_vector_search,
22};
23use crate::nodedb::LockExt;
24use crate::nodedb::NodeDbLite;
25use crate::nodedb::convert::value_to_loro;
26use crate::storage::engine::StorageEngine;
27
28pub(super) const INTERNAL_FIELDS_BASE: &[&str] = &[EMBEDDING_DIM_FIELD];
30pub(super) const INTERNAL_FIELDS_NAMED: &[&str] = VECTOR_ROW_FIELDS;
33
34pub(super) struct TraitVectorSearch<'a> {
40 pub index_key: &'a str,
41 pub collection: &'a str,
42 pub query: &'a [f32],
43 pub k: usize,
44 pub filter: Option<&'a MetadataFilter>,
45 pub exclude_fields: &'a [&'a str],
46 pub allowed_ids: Option<&'a HashSet<String>>,
47}
48
49impl<S: StorageEngine> NodeDbLite<S> {
50 pub(super) async fn vector_search_internal(
57 &self,
58 search: TraitVectorSearch<'_>,
59 ) -> NodeDbResult<Vec<SearchResult>> {
60 let TraitVectorSearch {
61 index_key,
62 collection,
63 query,
64 k,
65 filter,
66 exclude_fields,
67 allowed_ids,
68 } = search;
69 let prefilter = allowed_ids.map(|ids| {
70 let id_map = self.vector_state.vector_id_map.lock_or_recover();
71 let mut bm = roaring::RoaringBitmap::new();
72 if let Some(index_ids) = id_map.index(index_key) {
73 bm.extend(ids.iter().filter_map(|doc_id| index_ids.node(doc_id)));
74 }
75 bm
76 });
77 run_vector_search(
78 &self.vector_state,
79 &self.crdt,
80 VectorSearchRequest {
81 index_key,
82 collection,
83 query,
84 k,
85 filter: SearchFilter {
86 metadata: filter,
87 exclude_fields,
88 prefilter_bitmap: prefilter.as_ref(),
89 },
90 tuning: AnnTuning::default(),
91 skip_payload_fetch: false,
92 },
93 )
94 .await
95 }
96
97 pub(super) async fn vector_insert_impl(
102 &self,
103 collection: &str,
104 id: &str,
105 embedding: &[f32],
106 metadata: Option<Document>,
107 ) -> NodeDbResult<()> {
108 if self.governor.worst_engine_pressure() == nodedb_mem::PressureLevel::Emergency {
109 return Err(nodedb_types::error::NodeDbError::storage(
110 crate::error::LiteError::Backpressure {
111 detail: "vector insert rejected: memory governor is at Emergency pressure"
112 .into(),
113 },
114 ));
115 }
116
117 check_insert_widths(&self.vector_state, collection, [embedding.len()])
120 .await
121 .map_err(NodeDbError::from)?;
122
123 if !embedding.is_empty() {
129 let op = crate::engine::vector::durable::put_op(collection, id, embedding);
130 self.storage
131 .batch_write(std::slice::from_ref(&op))
132 .await
133 .map_err(NodeDbError::storage)?;
134 }
135
136 let internal_id = upsert_node(&self.vector_state, collection, id, embedding)
138 .await
139 .map_err(NodeDbError::from)?;
140 encode_sidecar(&self.vector_state, collection, internal_id, embedding)
142 .map_err(|e| NodeDbError::bad_request(e.to_string()))?;
143
144 {
145 let mut crdt = self.crdt.lock_or_recover();
146 let mut fields = vec![(EMBEDDING_DIM_FIELD, LoroValue::I64(embedding.len() as i64))];
147 if let Some(meta) = &metadata {
148 for (k, v) in &meta.fields {
149 fields.push((k.as_str(), value_to_loro(v)));
150 }
151 }
152 crdt.set_fields(collection, id, &fields)
154 .map_err(NodeDbError::from)?;
155 }
156
157 #[cfg(not(target_arch = "wasm32"))]
159 if let Some(q) = &self.vector_outbound {
160 crate::sync::reconcile_outbound_enqueue(
161 q.enqueue_insert(collection, id, embedding.to_vec(), embedding.len(), "")
162 .await,
163 "vector insert",
164 collection,
165 id,
166 )
167 .map_err(nodedb_types::error::NodeDbError::storage)?;
168 }
169
170 self.update_memory_stats();
171 Ok(())
172 }
173
174 pub(super) async fn vector_delete_impl(&self, collection: &str, id: &str) -> NodeDbResult<()> {
177 self.vector_delete_slot(collection, "", id).await
178 }
179
180 pub async fn vector_delete_field(
184 &self,
185 collection: &str,
186 field_name: &str,
187 id: &str,
188 ) -> NodeDbResult<()> {
189 self.vector_delete_slot(collection, field_name, id).await
190 }
191
192 async fn vector_delete_slot(
199 &self,
200 collection: &str,
201 field_name: &str,
202 id: &str,
203 ) -> NodeDbResult<()> {
204 let (index_key, slot) = if field_name.is_empty() {
205 (collection.to_string(), VectorSlot::Base)
206 } else {
207 (
208 format!("{collection}:{field_name}"),
209 VectorSlot::Named(field_name),
210 )
211 };
212
213 crate::engine::vector::durable::remove(&*self.storage, &index_key, id)
220 .await
221 .map_err(NodeDbError::from)?;
222
223 let (internal_id, remaining) = {
226 let mut indices = lock_resident(&self.vector_state, &index_key)
227 .await
228 .map_err(NodeDbError::from)?;
229 let node = unbind_node(
230 &self.vector_state,
231 indices.get_mut(&index_key),
232 &index_key,
233 id,
234 );
235 let remaining = self
236 .vector_state
237 .vector_id_map
238 .lock_or_recover()
239 .attached_vectors(collection, id);
240 (node, remaining)
241 };
242
243 if internal_id.is_some() {
244 if let Err(e) =
250 crate::engine::vector::sidecar::persist_sidecar(&self.vector_state, &index_key)
251 .await
252 {
253 tracing::warn!(
254 error = %e,
255 index_key,
256 "sidecar persist after delete failed; in-memory sidecar still valid"
257 );
258 }
259 }
260
261 let vector_primary = is_vector_primary(&*self.storage, collection)
262 .await
263 .map_err(NodeDbError::from)?;
264 {
265 let mut crdt = self.crdt.lock_or_recover();
266 detach_vector_row(&mut crdt, collection, id, slot, &remaining, vector_primary)
267 .map_err(NodeDbError::storage)?;
268 }
269
270 #[cfg(not(target_arch = "wasm32"))]
272 if let Some(q) = &self.vector_outbound {
273 crate::sync::reconcile_outbound_enqueue(
274 q.enqueue_delete(collection, id, field_name).await,
275 "vector delete",
276 collection,
277 id,
278 )
279 .map_err(nodedb_types::error::NodeDbError::storage)?;
280 }
281
282 Ok(())
283 }
284
285 pub(super) async fn vector_insert_field_impl(
294 &self,
295 collection: &str,
296 field_name: &str,
297 id: &str,
298 embedding: &[f32],
299 metadata: Option<Document>,
300 ) -> NodeDbResult<()> {
301 let index_key = if field_name.is_empty() {
302 collection.to_string()
303 } else {
304 format!("{collection}:{field_name}")
305 };
306
307 check_insert_widths(&self.vector_state, &index_key, [embedding.len()])
308 .await
309 .map_err(NodeDbError::from)?;
310
311 if !embedding.is_empty() {
314 let op = crate::engine::vector::durable::put_op(&index_key, id, embedding);
315 self.storage
316 .batch_write(std::slice::from_ref(&op))
317 .await
318 .map_err(NodeDbError::storage)?;
319 }
320
321 let internal_id = upsert_node(&self.vector_state, &index_key, id, embedding)
323 .await
324 .map_err(NodeDbError::from)?;
325 encode_sidecar(&self.vector_state, &index_key, internal_id, embedding)
326 .map_err(|e| NodeDbError::bad_request(e.to_string()))?;
327
328 {
329 let mut crdt = self.crdt.lock_or_recover();
330 let mut fields = vec![
331 (EMBEDDING_DIM_FIELD, LoroValue::I64(embedding.len() as i64)),
332 (VECTOR_FIELD_TAG, LoroValue::String(field_name.into())),
333 ];
334 if let Some(meta) = &metadata {
335 for (k, v) in &meta.fields {
336 fields.push((k.as_str(), value_to_loro(v)));
337 }
338 }
339 crdt.set_fields(collection, id, &fields)
341 .map_err(NodeDbError::from)?;
342 }
343
344 #[cfg(not(target_arch = "wasm32"))]
346 if let Some(q) = &self.vector_outbound {
347 crate::sync::reconcile_outbound_enqueue(
348 q.enqueue_insert(
349 collection,
350 id,
351 embedding.to_vec(),
352 embedding.len(),
353 field_name,
354 )
355 .await,
356 "vector field insert",
357 collection,
358 id,
359 )
360 .map_err(nodedb_types::error::NodeDbError::storage)?;
361 }
362
363 self.update_memory_stats();
364 Ok(())
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use nodedb_client::NodeDb;
375 use nodedb_types::collection::CollectionType;
376 use nodedb_types::collection_config::{PartitionStrategy, PrimaryEngine};
377 use nodedb_types::document::Document;
378 use nodedb_types::id::DatabaseId;
379 use nodedb_types::sync::wire::CollectionDescriptor;
380 use nodedb_types::value::Value;
381
382 use crate::PagedbStorageMem;
383 use crate::engine::vector::row::is_vector_primary;
384 use crate::nodedb::NodeDbLite;
385 use crate::nodedb::collection::CollectionMeta;
386 use crate::storage::engine::StorageEngine;
387
388 async fn open_db() -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
389 let storage = PagedbStorageMem::open_in_memory()
390 .await
391 .expect("in-memory storage");
392 NodeDbLite::open(storage).await.expect("open")
393 }
394
395 async fn declare_vector_primary(db: &NodeDbLite<PagedbStorageMem>, name: &str) {
398 let descriptor = CollectionDescriptor {
399 tenant_id: 1,
400 database_id: DatabaseId::DEFAULT,
401 name: name.into(),
402 collection_type: CollectionType::document(),
403 bitemporal: false,
404 crdt: false,
405 fields: Vec::new(),
406 primary: PrimaryEngine::Vector,
407 vector_primary: None,
408 partition_strategy: PartitionStrategy::default(),
409 declared_primary_key: None,
410 descriptor_version: 1,
411 };
412 let meta = CollectionMeta {
413 name: name.into(),
414 collection_type: "document".into(),
415 created_at_ms: 0,
416 fields: Vec::new(),
417 config_json: None,
418 descriptor_json: Some(sonic_rs::to_string(&descriptor).expect("descriptor json")),
419 bitemporal: false,
420 crdt: false,
421 };
422 db.storage
423 .put(
424 nodedb_types::Namespace::Meta,
425 format!("collection:{name}").as_bytes(),
426 &sonic_rs::to_vec(&meta).expect("meta json"),
427 )
428 .await
429 .expect("put meta");
430 }
431
432 #[tokio::test]
433 async fn vector_delete_on_vector_collection_removes_row() {
434 let db = open_db().await;
435 declare_vector_primary(&db, "vp").await;
436 assert!(
437 is_vector_primary(&*db.storage, "vp")
438 .await
439 .expect("read meta")
440 );
441
442 let mut payload = Document::new("v1");
443 payload.set("title", Value::String("hello".into()));
444 db.vector_insert("vp", "v1", &[1.0, 0.0], Some(payload))
445 .await
446 .expect("vector_insert");
447 db.vector_delete("vp", "v1").await.expect("vector_delete");
448
449 assert!(
450 db.document_get("vp", "v1")
451 .await
452 .expect("document_get")
453 .is_none(),
454 "a vector-primary row goes with its vector"
455 );
456 }
457
458 #[tokio::test]
459 async fn undeclared_collection_is_not_vector_primary() {
460 let db = open_db().await;
461 assert!(
462 !is_vector_primary(&*db.storage, "plain")
463 .await
464 .expect("read meta")
465 );
466 }
467}