writerslogic_warm/core/engine/
query.rs1use std::fmt::Write as _;
5
6use super::WarmCore;
7use crate::core::entangled::EntangledHVec;
8use crate::core::hopfield;
9use crate::core::types::{IndexStatus, QueryExplanation, RetrievalResult, StorageHealth};
10use rayon::prelude::*;
11
12impl WarmCore {
13 pub fn query(&self, query_vec: &EntangledHVec, k: u32) -> Vec<RetrievalResult> {
15 if query_vec.dim != self.dimensions || k == 0 {
16 return Vec::new();
17 }
18 if self
23 .documents
24 .try_read()
25 .is_some_and(|store| store.pending_vector_count() > 0)
26 {
27 if let Err(error) = self.encode_pending_documents() {
28 tracing::warn!(%error, "deferred document encoding failed; pending chunks stay outside this query");
29 }
30 }
31
32 let _transaction = self.mutation_gate.read();
33 self.shards
34 .read()
35 .query(query_vec, k.min(10000), self.dimensions)
36 }
37
38 #[cfg(feature = "provenance")]
40 pub fn agent_registry(
41 &self,
42 ) -> &parking_lot::RwLock<crate::core::provenance::access::AgentRegistry> {
43 &self.agents
44 }
45
46 #[cfg(feature = "provenance")]
49 pub fn query_as(
50 &self,
51 agent_did: &str,
52 query_vec: &EntangledHVec,
53 k: u32,
54 ) -> anyhow::Result<Vec<RetrievalResult>> {
55 self.agents
56 .read()
57 .authorize(agent_did, crate::core::provenance::access::Permission::Read)?;
58 Ok(self.query(query_vec, k))
59 }
60
61 pub fn explain_query(&self, query_vec: &EntangledHVec, k: u32) -> QueryExplanation {
62 let mut explanation = self
63 .shards
64 .read()
65 .explain_query(query_vec, k, self.dimensions);
66 let (pending, lexical) = self
67 .documents
68 .try_read()
69 .map(|store| (store.pending_vector_count(), store.lexical_vector_count()))
70 .unwrap_or((0, 0));
71 if pending > 0 || lexical > 0 {
72 let _ = write!(
73 explanation.rationale,
74 " {pending} document chunk(s) await deferred encoding (a query, \
75 encode_pending_documents or maintain_indices installs them); {lexical} \
76 lexical-only chunk(s) are outside vector search."
77 );
78 }
79 explanation
80 }
81
82 pub fn index_status(&self) -> IndexStatus {
83 let count = self.vector_count().min(u32::MAX as u64) as u32;
84 let nsg = self.nsg_trained();
85 let ivf = self.ivf_trained();
86 let nsg_recommended = !nsg
87 && count > 0
88 && self.config.nsg.auto_threshold > 0
89 && count as usize >= self.config.nsg.auto_threshold;
90 let ivf_recommended = self.config.ivf.enabled
91 && !ivf
92 && count > 0
93 && self.config.ivf.auto_threshold > 0
94 && count as usize >= self.config.ivf.auto_threshold;
95 let recommendation = if nsg_recommended && ivf_recommended {
96 "train NSG and IVF indices"
97 } else if nsg_recommended {
98 "train the NSG index"
99 } else if ivf_recommended {
100 "train the IVF index"
101 } else {
102 "indices are current for the configured thresholds"
103 };
104 IndexStatus {
105 vector_count: count,
106 nsg_trained: nsg,
107 ivf_trained: ivf,
108 nsg_training_recommended: nsg_recommended,
109 ivf_training_recommended: ivf_recommended,
110 recommendation: recommendation.to_string(),
111 }
112 }
113
114 pub fn storage_health(&self) -> StorageHealth {
115 let stats = self.arena.stats();
116 StorageHealth {
117 format_version: stats.format_version,
118 segment_count: stats.segment_count.min(u32::MAX as usize) as u32,
119 used_bytes: stats.used_bytes as f64,
120 capacity_bytes: stats.capacity_bytes as f64,
121 }
122 }
123
124 pub fn maintain_indices(&self) -> anyhow::Result<IndexStatus> {
127 self.maybe_auto_shard(self.vector_count())?;
128 self.encode_pending_documents()?;
129 let before = self.index_status();
130 if before.ivf_training_recommended {
131 self.train_ivf()?;
132 }
133 if before.nsg_training_recommended {
134 self.train_nsg()?;
135 }
136 let document_training_recommended = self.documents().dense_index_recommended();
137 if document_training_recommended {
138 self.train_document_index()?;
139 }
140 self.maintain_vector_index(crate::core::documents::DEFAULT_GRAPH_MIN_ROWS)?;
141 Ok(self.index_status())
142 }
143
144 pub fn maintain_indices_background(
146 self: &std::sync::Arc<Self>,
147 ) -> std::thread::JoinHandle<anyhow::Result<IndexStatus>> {
148 let core = std::sync::Arc::clone(self);
149 std::thread::spawn(move || core.maintain_indices())
150 }
151
152 pub fn flush(&self) -> anyhow::Result<()> {
154 {
155 let _transaction = self.mutation_gate.write();
156 self.arena.flush()?;
157 self.persist_indices(&self.shards.read())?;
158 }
159 self.checkpoint_if_due()
160 }
161
162 pub fn query_hopfield(
169 &self,
170 query_vec: &EntangledHVec,
171 max_results: u32,
172 ) -> Vec<RetrievalResult> {
173 let config = &self.config.hopfield;
174 self.shards.read().with_patterns(|patterns| {
175 if config.max_iter > 1 {
176 hopfield::hopfield_query_iterative(
177 query_vec,
178 patterns,
179 config,
180 max_results as usize,
181 )
182 } else {
183 hopfield::hopfield_query(query_vec, patterns, config, max_results as usize)
184 }
185 })
186 }
187
188 pub fn query_batch(&self, queries: &[EntangledHVec], k: u32) -> Vec<Vec<RetrievalResult>> {
190 queries.par_iter().map(|q| self.query(q, k)).collect()
191 }
192
193 pub fn analyze_components(&self, vector: &EntangledHVec) -> Vec<RetrievalResult> {
196 let cfg = &self.config.query;
197 let neighbors = self.query(vector, cfg.component_max_neighbors);
198 neighbors
199 .into_iter()
200 .filter(|r| r.similarity > cfg.component_similarity_threshold)
201 .collect()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn diagnostics_report_route_lifecycle_and_storage() -> anyhow::Result<()> {
211 let directory = tempfile::tempdir()?;
212 let warm = WarmCore::new(
213 1024,
214 Some(directory.path().to_string_lossy().into_owned()),
215 None,
216 )?;
217 let query = EntangledHVec::new_deterministic(1024, 1);
218 let explanation = warm.explain_query(&query, 5);
219 assert_eq!(explanation.route, "brute_force");
220 assert_eq!(warm.index_status().vector_count, 0);
221 assert_eq!(
222 warm.storage_health().format_version,
223 crate::core::storage::STORAGE_FORMAT_VERSION
224 );
225 warm.flush()?;
226 Ok(())
227 }
228}