1use anyhow::{anyhow, ensure, Result};
5use rayon::prelude::*;
6use std::sync::Arc;
7
8use super::{
9 mutation::{validate_id, Mutation},
10 ShardSet, WarmCore,
11};
12use crate::core::documents::{
13 self, DocumentInput, DocumentResult, IngestOptions, SearchOptions, VectorIndexMode,
14 VectorIndexStatus, VectorQueryOptions, VectorState,
15};
16
17const ENCODE_BATCH: usize = 1024;
20use crate::core::types::MemorizeBatchItem;
21
22impl WarmCore {
23 pub fn security_status(&self) -> serde_json::Value {
24 serde_json::json!({
25 "securityCompiled": cfg!(feature = "security"),
26 "encryptionActive": self.config.security.encryption_enabled,
27 "signingActive": self.config.security.signing_enabled,
28 "auditActive": self.config.security.audit_enabled,
29 "differentialPrivacyActive": self.config.privacy.dp_enabled,
30 "epsilon": self.config.privacy.epsilon,
31 "privacyMechanism": "clipped-full-domain-laplace-top-k",
32 "embeddingSpace": self.config.embedding_space,
33 })
34 }
35
36 pub fn memorize_document(&self, input: DocumentInput) -> Result<u32> {
37 self.memorize_document_with(input, &IngestOptions::default())
38 }
39
40 pub fn memorize_document_with(
43 &self,
44 input: DocumentInput,
45 options: &IngestOptions,
46 ) -> Result<u32> {
47 #[cfg(feature = "local-models")]
48 if let Some(stages) = self.model_stages() {
49 return self.memorize_document_with_models(input, &stages, options);
50 }
51 #[cfg(feature = "local-embed")]
52 let input = {
53 let mut input = input;
54 self.embed_document_inputs(std::slice::from_mut(&mut input))?;
55 input
56 };
57 let mut memo = self.stem_memos.take();
60 let document = {
61 let _memo = memo.install();
62 documents::prepare_with_options(
63 input,
64 self.dimensions,
65 self.config.embedding_space.as_ref().map(|s| s.dimensions),
66 None,
67 self.config.text_encoder,
68 options,
69 )?
70 };
71 drop(memo);
72 let count = document.chunks.len() as u32;
73 self.commit(&[Mutation::Document(Arc::new(document))])?;
74 Ok(count)
75 }
76
77 pub fn memorize_documents(&self, inputs: Vec<DocumentInput>) -> Result<Vec<u32>> {
78 self.memorize_documents_with(inputs, &IngestOptions::default())
79 }
80
81 pub fn memorize_documents_with(
85 &self,
86 inputs: Vec<DocumentInput>,
87 options: &IngestOptions,
88 ) -> Result<Vec<u32>> {
89 let (mutations, payload) = self.prepare_batch(inputs, options)?;
90 let counts = mutations
91 .iter()
92 .map(|mutation| match mutation {
93 Mutation::Document(document) => document.chunks.len() as u32,
94 _ => unreachable!("document preparation produced a non-document mutation"),
95 })
96 .collect();
97 self.commit_serialized(&mutations, &payload)?;
98 Ok(counts)
99 }
100
101 pub(super) fn prepare_batch(
104 &self,
105 inputs: Vec<DocumentInput>,
106 options: &IngestOptions,
107 ) -> Result<(Vec<Mutation>, Vec<u8>)> {
108 #[cfg(feature = "local-models")]
109 let stages = self.model_stages();
110 #[cfg(feature = "local-models")]
111 let inferred_dimensions = stages
112 .as_ref()
113 .and_then(|stages| stages.embedder.as_ref())
114 .map(|embedder| embedder.dimensions());
115 #[cfg(not(feature = "local-models"))]
116 let inferred_dimensions = None;
117 #[cfg(feature = "local-embed")]
118 let inferred_dimensions =
119 inferred_dimensions.or_else(|| self.text_embedder().map(|e| e.dimensions()));
120 let spans =
121 super::document_batch::validate_inputs_with(&inputs, inferred_dimensions, options)?;
122 #[cfg(feature = "local-embed")]
123 let inputs = {
124 let mut inputs = inputs;
125 self.embed_document_inputs(&mut inputs)?;
126 inputs
127 };
128 let mutations = inputs
129 .into_par_iter()
130 .zip(spans)
131 .map_init(
132 || self.stem_memos.take(),
133 |memo, (input, spans)| {
134 let _memo = memo.install();
135 #[cfg(feature = "local-models")]
136 if let Some(stages) = stages.as_deref() {
137 return self
138 .prepare_document_with_models(input, stages, options)
139 .map(|document| Mutation::Document(Arc::new(document)));
140 }
141 documents::prepare_spans(
142 input,
143 spans,
144 self.dimensions,
145 self.config.embedding_space.as_ref().map(|s| s.dimensions),
146 None,
147 self.config.text_encoder,
148 options,
149 )
150 .map(|document| Mutation::Document(Arc::new(document)))
151 },
152 )
153 .collect::<Result<Vec<_>>>();
154 self.stem_memos.trim();
156 let mutations = mutations?;
157 let payload = super::document_batch::serialize_transaction(self, &mutations)?;
158 Ok((mutations, payload))
159 }
160
161 pub fn search_documents(
162 &self,
163 text: &str,
164 options: &SearchOptions,
165 ) -> Result<Vec<DocumentResult>> {
166 #[cfg(feature = "local-models")]
168 if let Some(stages) = self.model_stages().filter(|_| !text.trim().is_empty()) {
169 return self.search_documents_with_models(text, "", options, &stages);
170 }
171 #[cfg(feature = "local-embed")]
172 let embedded = self
173 .embed_search_text(text, options)?
174 .map(|embedding| SearchOptions {
175 embedding: Some(embedding),
176 ..options.clone()
177 });
178 #[cfg(feature = "local-embed")]
179 let options = embedded.as_ref().unwrap_or(options);
180 let prepared = documents::prepare_search(
181 text,
182 options,
183 self.config.embedding_space.as_ref().map(|s| s.dimensions),
184 )?;
185 Ok(self.run_searches(vec![prepared])?.pop().expect("one query"))
186 }
187
188 fn run_searches(
194 &self,
195 prepared: Vec<documents::PreparedSearch>,
196 ) -> Result<Vec<Vec<DocumentResult>>> {
197 let mut prepared = Some(prepared);
198 let from_checkpoint = self.capture_from_checkpoint(|store| {
199 let queries = prepared.as_deref().expect("queries not taken yet");
200 documents::overlay_serves(store, queries).then(|| {
201 documents::capture_overlaid_search(store, prepared.take().expect("taken once"))
202 })
203 });
204 let captured = match from_checkpoint {
205 Some(captured) => captured?,
206 None => {
207 self.hydrate_documents()?;
208 let _transaction = self.mutation_gate.read();
209 let prepared = prepared.expect("queries not taken");
210 documents::capture_search(&self.documents.read(), prepared)
211 }
212 };
213 let ranked: Vec<_> = (0..captured.len())
214 .map(|i| documents::rank_search(&captured, i))
215 .collect();
216 {
217 let _transaction = self.mutation_gate.read();
218 let read: Option<Vec<_>> = ranked
219 .into_iter()
220 .enumerate()
221 .map(|(i, hits)| documents::read_ranked(&captured, i, hits))
222 .collect();
223 if let Some(results) = read {
224 return Ok(results);
225 }
226 }
227 self.hydrate_documents()?;
228 let _transaction = self.mutation_gate.read();
229 let captured = documents::capture_search(&self.documents.read(), captured.into_prepared());
230 Ok((0..captured.len())
231 .map(|i| documents::execute_search(&captured, i))
232 .collect())
233 }
234
235 pub fn count_documents(&self, text: &str) -> Result<usize> {
239 self.hydrate_documents()?;
240 let _transaction = self.mutation_gate.read();
241 documents::count_matches(&self.documents.read(), text)
242 }
243
244 pub fn search_lexical(
246 &self,
247 query: &documents::LexicalQuery,
248 options: &documents::LexicalOptions,
249 ) -> Result<documents::LexicalResults> {
250 self.hydrate_documents()?;
251 let _transaction = self.mutation_gate.read();
252 documents::execute_lexical(&self.documents.read(), query, options)
253 }
254
255 pub fn count_lexical(&self, query: &documents::LexicalQuery) -> Result<usize> {
257 let result = self.search_lexical(
258 query,
259 &documents::LexicalOptions {
260 k: 0,
261 count: true,
262 ..Default::default()
263 },
264 )?;
265 Ok(result.count.expect("count requested"))
266 }
267
268 pub fn document_index_trained(&self) -> bool {
269 self.documents().dense_index_trained()
270 }
271
272 pub fn document_embedding_count(&self) -> usize {
273 self.documents().embedded_count()
274 }
275
276 pub fn search_vectors(
283 &self,
284 embedding: &[f64],
285 k: usize,
286 options: &VectorQueryOptions,
287 ) -> Result<Vec<DocumentResult>> {
288 let Some(space) = &self.config.embedding_space else {
289 anyhow::bail!("configure an embedding space before vector search");
290 };
291 ensure!(
292 (1..=documents::MAX_K).contains(&k),
293 "require 1 <= k <= {}",
294 documents::MAX_K
295 );
296 ensure!(
297 (1..=documents::MAX_EF).contains(&options.ef),
298 "require 1 <= ef <= {}",
299 documents::MAX_EF
300 );
301 ensure!(
302 options.min_score.is_finite() && (-1.0..=1.0).contains(&options.min_score),
303 "minScore must be in [-1,1]"
304 );
305 let normalized = documents::normalize_embedding(embedding, space.dimensions)?;
306 let query: Vec<f32> = normalized.iter().map(|&value| value as f32).collect();
307 match self.vector_index.get() {
308 Some(snapshot) => {
309 if let Some(results) = documents::search_vector_snapshot(
310 &snapshot,
311 &self.documents,
312 &query,
313 &normalized,
314 k,
315 options,
316 )? {
317 return Ok(results);
318 }
319 }
320 None if options.mode == VectorIndexMode::Graph => {
321 return Err(anyhow::Error::new(documents::GraphUnavailable));
322 }
323 None => {
324 if self.rebuild_vector_index(documents::DEFAULT_GRAPH_MIN_ROWS)? {
328 if let Some(snapshot) = self.vector_index.get() {
329 if let Some(results) = documents::search_vector_snapshot(
330 &snapshot,
331 &self.documents,
332 &query,
333 &normalized,
334 k,
335 options,
336 )? {
337 return Ok(results);
338 }
339 }
340 }
341 }
342 }
343 let fallback = SearchOptions {
344 k: Some(k as u32),
345 candidate_limit: Some((k as u32).max(100)),
346 embedding: Some(embedding.to_vec()),
347 lexical_weight: Some(0.0),
348 semantic_weight: Some(1.0),
349 min_semantic_score: Some(options.min_score),
350 ..SearchOptions::default()
351 };
352 let mut hits = self.search_documents("", &fallback)?;
353 for hit in &mut hits {
354 hit.score = hit.semantic_score.unwrap_or(hit.score);
355 }
356 Ok(hits)
357 }
358
359 pub fn vector_index_status(&self) -> VectorIndexStatus {
360 let snapshot = self.vector_index.get();
361 documents::vector_index_status(snapshot.as_deref(), &self.documents())
362 }
363
364 pub fn rebuild_vector_index(&self, graph_min_rows: usize) -> Result<bool> {
370 if self.config.embedding_space.is_none() {
371 return Ok(false);
372 }
373 let Some(_building) = self.vector_index.try_build() else {
374 return Ok(self.vector_index.get().is_some());
375 };
376 self.rebuild_vector_index_locked(graph_min_rows)
377 }
378
379 pub(super) fn rebuild_vector_index_waiting(&self, graph_min_rows: usize) -> Result<bool> {
382 let _building = self.vector_index.build();
383 self.rebuild_vector_index_locked(graph_min_rows)
384 }
385
386 fn rebuild_vector_index_locked(&self, graph_min_rows: usize) -> Result<bool> {
387 let Some(space) = &self.config.embedding_space else {
388 return Ok(false);
389 };
390 let capture = {
391 let _transaction = self.mutation_gate.read();
392 documents::capture_vector_rows(&self.documents(), space.dimensions)?
393 };
394 let Some(capture) = capture else {
395 self.vector_index.clear();
396 return Ok(false);
397 };
398 let snapshot = documents::build_vector_snapshot(capture, graph_min_rows)?;
399 self.vector_index.publish(Arc::new(snapshot));
400 Ok(true)
401 }
402
403 pub fn maintain_vector_index(&self, graph_min_rows: usize) -> Result<bool> {
407 if self.vector_index_status().rebuild_recommended {
408 return self.rebuild_vector_index(graph_min_rows);
409 }
410 Ok(self.vector_index.get().is_some())
411 }
412
413 pub fn train_document_index(&self) -> Result<()> {
414 self.encode_pending_documents()?;
415 self.maintain_vector_index(documents::DEFAULT_GRAPH_MIN_ROWS)?;
416 self.train_dense_index()
417 }
418
419 pub(super) fn train_dense_index(&self) -> Result<()> {
421 let Some(space) = &self.config.embedding_space else {
422 return Ok(());
423 };
424 let _building = self.vector_index.try_build().ok_or_else(|| {
425 anyhow!("document index maintenance already in progress; retry maintenance")
426 })?;
427 self.hydrate_documents()?;
428 let snapshot = {
429 let _transaction = self.mutation_gate.read();
430 documents::capture_dense_index(&self.documents.read(), space.dimensions)?
431 };
432 let Some(snapshot) = snapshot else {
433 return Ok(());
434 };
435 let Some(trained) = documents::build_dense_index(snapshot)? else {
436 return Ok(());
437 };
438 let published = {
439 let _transaction = self.mutation_gate.read();
440 documents::publish_dense_index(&mut self.documents.write(), &trained)
441 };
442 ensure!(
443 published,
444 "store changed during document index training; retry maintenance"
445 );
446 Ok(())
447 }
448
449 pub fn encode_pending_documents(&self) -> Result<usize> {
456 if self.documents.read().pending_vector_count() == 0 {
459 return Ok(0);
460 }
461 let gate = self.documents.read().encode_gate();
462 let _encoding = gate.lock();
463 let encoder = self.config.text_encoder;
464 ensure!(
465 !encoder.is_embedding(),
466 "deferred encoding needs a text encoder, not {}",
467 encoder.name()
468 );
469 let pending: Vec<(Arc<documents::StoredDocument>, u64, Vec<Option<String>>)> = {
472 let _transaction = self.mutation_gate.read();
473 let store = self.documents.read();
474 if store.pending_vector_count() == 0 {
475 return Ok(0);
476 }
477 let mut pending = Vec::new();
478 for document in store.shared_values() {
479 if !document
480 .chunks
481 .iter()
482 .any(|chunk| chunk.vector_state == VectorState::Pending)
483 {
484 continue;
485 }
486 let mut texts = Vec::with_capacity(document.chunks.len());
487 for chunk in &document.chunks {
488 texts.push(if chunk.vector_state == VectorState::Pending {
489 Some(store.chunk_text(chunk).ok_or_else(|| {
490 anyhow!(
491 "document {} awaits deferred encoding but has no stored text",
492 document.id
493 )
494 })?)
495 } else {
496 None
497 });
498 }
499 let fingerprint = documents::document_fingerprint(&store, &document)
500 .ok_or_else(|| anyhow!("document {} has no stored text", document.id))?;
501 pending.push((document, fingerprint, texts));
502 }
503 pending
504 };
505 let dimensions = self.dimensions;
506 let mut encoded = 0;
507 for batch in pending.chunks(ENCODE_BATCH) {
508 let mutations: Vec<Mutation> = batch
509 .par_iter()
510 .map(|(document, fingerprint, texts)| {
511 let vectors = document
512 .chunks
513 .iter()
514 .zip(texts)
515 .map(|(chunk, text)| match text {
516 Some(text) => encoder.encode(text, dimensions),
517 None => chunk.vector.clone(),
518 })
519 .collect();
520 Mutation::EncodeDocument {
521 id: document.id.clone(),
522 version: document.version.clone(),
523 fingerprint: *fingerprint,
524 vectors,
525 }
526 })
527 .collect();
528 self.commit(&mutations)?;
529 encoded += batch.len();
530 }
531 Ok(encoded)
532 }
533
534 pub fn has_document(&self, id: &str) -> bool {
536 self.documents().contains_key(id)
537 }
538
539 pub fn set_keep_ingest_interner(&self, keep: bool) {
545 self.stem_memos.set_keep(keep);
546 }
547
548 pub fn document_count(&self) -> usize {
550 documents::count(&self.documents())
551 }
552
553 pub fn get_document(&self, id: &str) -> Option<documents::DocumentRecord> {
555 let _transaction = self.mutation_gate.read();
556 documents::get_record(&self.documents(), id)
557 }
558
559 pub fn list_documents(
562 &self,
563 from: Option<&str>,
564 limit: usize,
565 ) -> Vec<documents::DocumentRecord> {
566 let _transaction = self.mutation_gate.read();
567 documents::list_records(&self.documents(), from, limit)
568 }
569
570 pub fn delete_document(&self, id: &str) -> Result<bool> {
571 validate_id(id)?;
572 let exists = self.documents.read().contains_key(id);
574 self.commit(&[Mutation::DeleteDocument { id: id.into() }])?;
575 Ok(exists)
576 }
577
578 pub(crate) fn sync_text_segment(&self) -> Result<()> {
582 let segment = self.documents().text_segment();
583 segment.map_or(Ok(()), |segment| segment.sync())
584 }
585
586 pub(super) fn remove_document_from_memory(&self, id: &str, shards: &ShardSet) -> Result<()> {
587 let mut store = self.documents.write();
591 let removed = store.remove(id);
592 if let Some(old) = &removed {
593 for chunk in &old.chunks {
594 shards.remove(&chunk.id, self.dimensions)?;
595 }
596 }
597 store.scrub_removed(removed).map(drop)
598 }
599
600 pub fn memorize_batch(&self, items: &[MemorizeBatchItem]) -> Result<()> {
602 ensure!(
603 !items.is_empty() && items.len() <= 4096,
604 "batch requires 1..=4096 items"
605 );
606 ensure!(
607 items
608 .iter()
609 .map(|item| item.id.len() + item.text.len())
610 .sum::<usize>()
611 <= documents::MAX_DOCUMENT_BYTES,
612 "batch exceeds 8 MiB; split the input into bounded batches"
613 );
614 for item in items {
615 super::mutation::validate_public_id(&item.id)?;
616 ensure!(
617 !item.id.starts_with(documents::CHUNK_PREFIX),
618 "chunk IDs are reserved"
619 );
620 }
621 let texts: Vec<_> = items.iter().map(|item| item.text.as_str()).collect();
622 let vectors = self.try_encode_texts(&texts)?;
623 let mutations: Vec<_> = items
624 .iter()
625 .zip(vectors)
626 .map(|(item, vector)| Mutation::Vector {
627 id: item.id.clone(),
628 vector,
629 })
630 .collect();
631 self.commit(&mutations)
632 }
633
634 pub fn query_vector(&self, dense: &[f32], k: u32) -> Result<Vec<crate::RetrievalResult>> {
635 self.validate_dense(dense)?;
636 let vector = crate::EntangledHVec::from_dense(dense, self.dimensions);
637 Ok(self.query(&vector, k))
638 }
639
640 pub(super) fn validate_dense(&self, dense: &[f32]) -> Result<()> {
641 ensure!(
642 !dense.is_empty()
643 && dense.len() <= 65536
644 && dense.iter().all(|v| v.is_finite())
645 && dense.iter().any(|&v| v != 0.0),
646 "dense vectors require 1..=65536 finite values and nonzero magnitude"
647 );
648 if let Some(space) = &self.config.embedding_space {
649 ensure!(
650 dense.len() == space.dimensions,
651 "dense embedding dimensions do not match the store schema"
652 );
653 }
654 Ok(())
655 }
656}
657
658#[cfg(feature = "local-models")]
659impl WarmCore {
660 pub fn set_model_stages(
663 &self,
664 stages: Option<std::sync::Arc<crate::core::models::ModelStages>>,
665 ) -> Result<()> {
666 if let Some(stages) = &stages {
667 stages.params.validate()?;
668 }
669 if let Some(e) = stages.as_ref().and_then(|s| s.embedder.as_ref()) {
670 ensure!(
671 self.config.embedding_space.as_ref() == Some(&e.space()),
672 "the embedder's effective model identity does not match the store embedding space; reopen with the original model or re-embed original source documents into a new store"
673 );
674 }
675 *self.model_stages.write() = stages;
676 Ok(())
677 }
678
679 pub fn model_stages(&self) -> Option<std::sync::Arc<crate::core::models::ModelStages>> {
680 self.model_stages.read().clone()
681 }
682
683 fn memorize_document_with_models(
684 &self,
685 input: DocumentInput,
686 stages: &crate::core::models::ModelStages,
687 options: &IngestOptions,
688 ) -> Result<u32> {
689 let document = self.prepare_document_with_models(input, stages, options)?;
690 let count = document.chunks.len() as u32;
691 self.commit(&[Mutation::Document(Arc::new(document))])?;
692 Ok(count)
693 }
694
695 fn prepare_document_with_models(
696 &self,
697 mut input: DocumentInput,
698 stages: &crate::core::models::ModelStages,
699 options: &IngestOptions,
700 ) -> Result<documents::StoredDocument> {
701 let chunks: Vec<String> = documents::chunk_document(&input)?
702 .into_iter()
703 .map(|c| c.text)
704 .collect();
705 let keys = stages.fact_keys(&chunks)?;
706 if let (Some(embedder), None) = (&stages.embedder, &input.embeddings) {
707 let texts = keys.as_deref().unwrap_or(&chunks);
708 input.embeddings = Some(
709 embedder
710 .embed(texts, false)?
711 .into_iter()
712 .map(|e| e.into_iter().map(f64::from).collect())
713 .collect(),
714 );
715 }
716 documents::prepare_with_options(
717 input,
718 self.dimensions,
719 self.config.embedding_space.as_ref().map(|s| s.dimensions),
720 keys.as_deref(),
721 self.config.text_encoder,
722 options,
723 )
724 }
725
726 pub fn search_documents_with_models(
730 &self,
731 text: &str,
732 today: &str,
733 options: &SearchOptions,
734 stages: &crate::core::models::ModelStages,
735 ) -> Result<Vec<DocumentResult>> {
736 stages.params.validate()?;
737 let k = options.k.unwrap_or(10) as usize;
738 ensure!((1..=1000).contains(&k), "require 1 <= k <= 1000");
739 let pool = k.max(stages.params.rerank_n) as u32;
740 let variants = stages.query_variants(text, today)?;
741 let embeddings = match (&stages.embedder, &options.embedding) {
742 (Some(e), None) => {
743 let queries: Vec<&str> = variants.iter().map(|v| v.0.as_str()).collect();
744 Some(e.embed(&queries, true)?)
745 }
746 _ => None,
747 };
748 let mut prepared = Vec::with_capacity(variants.len());
749 for (i, (query, _)) in variants.iter().enumerate() {
750 let mut opts = options.clone();
751 opts.k = Some(pool);
752 opts.lexical_weight = opts.lexical_weight.or(Some(stages.params.lexical_weight));
753 opts.semantic_weight = opts.semantic_weight.or(Some(stages.params.semantic_weight));
754 if let Some(e) = &embeddings {
755 opts.embedding = Some(e[i].iter().copied().map(f64::from).collect());
756 }
757 prepared.push(documents::prepare_search(
758 query,
759 &opts,
760 self.config.embedding_space.as_ref().map(|s| s.dimensions),
761 )?);
762 }
763 let lists = variants
764 .iter()
765 .map(|(_, weight)| *weight)
766 .zip(self.run_searches(prepared)?)
767 .collect();
768 stages.fuse(text, lists, k)
769 }
770}