writerslogic_warm/core/engine/
memory_access.rs1use anyhow::Result;
9use serde_json::Value;
10
11use super::{mutation::Mutation, WarmCore};
12
13pub(crate) const DELETE_BATCH: usize = 4096;
15
16pub(crate) struct DocumentParts {
19 pub metadata: Value,
20 pub text: Option<String>,
21 pub source_uri: Option<String>,
22 pub version: String,
23 pub embedding: Option<Vec<f64>>,
24}
25
26pub(crate) struct DocumentView<'a> {
29 pub id: &'a str,
30 pub version: &'a str,
31 pub metadata: &'a Value,
32 text: &'a dyn Fn() -> Option<String>,
33}
34
35impl DocumentView<'_> {
36 pub(crate) fn text(&self) -> Option<String> {
37 (self.text)()
38 }
39}
40
41impl WarmCore {
42 pub(crate) fn validate_document_search(
43 &self,
44 text: &str,
45 options: &crate::SearchOptions,
46 ) -> Result<()> {
47 crate::core::documents::validate_search(
48 text,
49 options,
50 self.config
51 .embedding_space
52 .as_ref()
53 .map(|space| space.dimensions),
54 )
55 .map(drop)
56 }
57
58 pub fn revision(&self) -> u64 {
63 self.revision.load(std::sync::atomic::Ordering::Acquire)
64 }
65
66 pub(crate) fn scan_documents(&self, mut visit: impl FnMut(DocumentView<'_>)) -> u64 {
69 self.try_scan_documents(None, |document| {
70 visit(document);
71 Ok(())
72 })
73 .expect("infallible document visitor")
74 }
75
76 pub(crate) fn try_scan_documents(
77 &self,
78 ids: Option<&std::collections::BTreeSet<String>>,
79 mut visit: impl FnMut(DocumentView<'_>) -> Result<()>,
80 ) -> Result<u64> {
81 let _transaction = self.mutation_gate.read();
82 let revision = self.revision();
83 self.hydrate_documents()?;
84 let store = self.documents.read();
85 let mut visit_document =
86 |document: std::sync::Arc<crate::core::documents::StoredDocument>| {
87 let text = || {
88 document
89 .chunks
90 .first()
91 .and_then(|chunk| store.chunk_text(chunk))
92 };
93 visit(DocumentView {
94 id: &document.id,
95 version: &document.version,
96 metadata: &document.metadata,
97 text: &text,
98 })
99 };
100 if let Some(ids) = ids {
101 for id in ids {
102 if let Some(document) = store.get(id) {
103 visit_document(std::sync::Arc::clone(document))?;
104 }
105 }
106 } else {
107 for document in store.shared_values() {
108 visit_document(document)?;
109 }
110 }
111 Ok(revision)
112 }
113
114 pub(crate) fn document_snapshot(&self, id: &str) -> Option<(String, Value, Option<String>)> {
116 self.document_parts(id)
117 .map(|parts| (parts.version, parts.metadata, parts.text))
118 }
119
120 pub(crate) fn document_parts(&self, id: &str) -> Option<DocumentParts> {
121 let _transaction = self.mutation_gate.read();
122 let store = self.documents();
123 let document = store.get(id)?;
124 let chunk = document.chunks.first();
125 Some(DocumentParts {
126 metadata: document.metadata.clone(),
127 text: chunk.and_then(|chunk| store.chunk_text(chunk)),
128 source_uri: document.source_uri.clone(),
129 version: document.version.clone(),
130 embedding: chunk.and_then(|chunk| chunk.embedding.as_ref().map(|e| e.to_f64())),
131 })
132 }
133
134 pub(crate) fn memory_revision_matches(&self, revision: u64) -> bool {
135 let _writer = self.mutation_gate.maintenance();
136 self.revision() == revision
137 }
138
139 pub(crate) fn change_memory_documents(
140 &self,
141 inputs: Vec<crate::DocumentInput>,
142 deleted: &[String],
143 revision: u64,
144 ) -> Result<bool> {
145 anyhow::ensure!(
146 inputs
147 .len()
148 .checked_add(deleted.len())
149 .is_some_and(|n| n <= DELETE_BATCH),
150 "memory transaction requires at most 4096 document changes"
151 );
152 let mut mutations = if inputs.is_empty() {
153 Vec::new()
154 } else {
155 self.prepare_batch(inputs, &Default::default())?.0
156 };
157 mutations.extend(
158 deleted
159 .iter()
160 .cloned()
161 .map(|id| Mutation::DeleteDocument { id }),
162 );
163 if mutations.is_empty() {
164 return Ok(self.memory_revision_matches(revision));
165 }
166 let payload = super::document_batch::serialize_transaction(self, &mutations)?;
167 self.commit_serialized_if_revision(&mutations, &payload, revision)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn fallible_scoped_scan_stops_at_the_first_error() -> Result<()> {
177 let directory = tempfile::tempdir()?;
178 let core = WarmCore::new(256, Some(directory.path().display().to_string()), None)?;
179 for id in ["a", "b", "c"] {
180 core.memorize_document(serde_json::from_value(
181 serde_json::json!({"id": id, "text": "evidence"}),
182 )?)?;
183 }
184 let scope = std::collections::BTreeSet::from(["b".into(), "c".into()]);
185 let mut visited = Vec::new();
186 let result = core.try_scan_documents(Some(&scope), |document| {
187 visited.push(document.id.to_owned());
188 anyhow::bail!("budget exhausted")
189 });
190 assert!(result.is_err());
191 assert_eq!(visited, ["b"]);
192 Ok(())
193 }
194}