1use crate::config::Config;
7use crate::reader::MicroscopeReader;
8use crate::{content_coords_blended, read_append_log, LAYER_NAMES};
9use std::path::Path;
10
11#[derive(Clone)]
13pub struct FederatedResult {
14 pub text: String,
15 pub depth: u8,
16 pub layer: String,
17 pub score: f32,
18 pub source_index: String,
19 pub is_append: bool,
20}
21
22pub struct FederatedSearch {
24 indices: Vec<(String, Config, f32)>,
26}
27
28impl FederatedSearch {
29 pub fn from_config(config: &Config) -> Result<Self, String> {
31 let mut indices = Vec::new();
32
33 for entry in &config.federation.indices {
34 let idx_config = Config::load(&entry.config_path).map_err(|e| {
35 format!(
36 "Failed to load federated index '{}' from '{}': {}",
37 entry.name, entry.config_path, e
38 )
39 })?;
40 indices.push((entry.name.clone(), idx_config, entry.weight));
41 }
42
43 if indices.is_empty() {
44 return Err("No federated indices configured".to_string());
45 }
46
47 Ok(Self { indices })
48 }
49
50 pub fn recall(&self, query: &str, k: usize) -> Vec<FederatedResult> {
52 let results: Vec<Vec<FederatedResult>> = std::thread::scope(|s| {
53 let handles: Vec<_> = self
54 .indices
55 .iter()
56 .map(|(name, config, weight)| {
57 let name = name.clone();
58 let weight = *weight;
59 s.spawn(move || recall_single(&name, config, query, k, weight))
60 })
61 .collect();
62
63 handles.into_iter().filter_map(|h| h.join().ok()).collect()
64 });
65
66 merge_results(results, k)
67 }
68
69 pub fn find_text(&self, query: &str, k: usize) -> Vec<FederatedResult> {
71 let results: Vec<Vec<FederatedResult>> = std::thread::scope(|s| {
72 let handles: Vec<_> = self
73 .indices
74 .iter()
75 .map(|(name, config, weight)| {
76 let name = name.clone();
77 let weight = *weight;
78 s.spawn(move || find_single(&name, config, query, k, weight))
79 })
80 .collect();
81
82 handles.into_iter().filter_map(|h| h.join().ok()).collect()
83 });
84
85 merge_results(results, k)
86 }
87
88 pub fn mql_query(&self, mql: &str, k: usize) -> Vec<FederatedResult> {
90 let results: Vec<Vec<FederatedResult>> = std::thread::scope(|s| {
91 let handles: Vec<_> = self
92 .indices
93 .iter()
94 .map(|(name, config, weight)| {
95 let name = name.clone();
96 let weight = *weight;
97 s.spawn(move || mql_single(&name, config, mql, k, weight))
98 })
99 .collect();
100
101 handles.into_iter().filter_map(|h| h.join().ok()).collect()
102 });
103
104 merge_results(results, k)
105 }
106
107 pub fn status(&self) -> Vec<(String, Result<usize, String>)> {
109 self.indices
110 .iter()
111 .map(|(name, config, _)| {
112 let result = MicroscopeReader::open(config).map(|r| r.block_count);
113 (name.clone(), result)
114 })
115 .collect()
116 }
117}
118
119fn recall_single(
121 name: &str,
122 config: &Config,
123 query: &str,
124 k: usize,
125 weight: f32,
126) -> Vec<FederatedResult> {
127 let reader = match MicroscopeReader::open(config) {
128 Ok(r) => r,
129 Err(_) => return Vec::new(),
130 };
131
132 let (qx, qy, qz) = content_coords_blended(query, "long_term", config.search.semantic_weight);
133 let relevance_query = crate::relevance::RelevanceQuery::new(query);
134
135 let (zoom_lo, zoom_hi) = match query.len() {
136 0..=10 => (0u8, 3u8),
137 11..=40 => (3, 6),
138 _ => (6, 8),
139 };
140
141 let mut results: Vec<(f32, FederatedResult)> = Vec::new();
142
143 for zoom in zoom_lo..=zoom_hi {
144 let (start, count) = reader.depth_ranges[zoom as usize];
145 let (start, count) = (start as usize, count as usize);
146 for i in start..(start + count) {
147 let text = reader.text(i);
148 let lexical = relevance_query.lexical_score(text);
149 if lexical > 0.0 {
150 let h = reader.header(i);
151 let dx = h.x - qx;
152 let dy = h.y - qy;
153 let dz = h.z - qz;
154 let dist = dx * dx + dy * dy + dz * dz;
155 let score = crate::relevance::rank_distance_from_score(
156 lexical,
157 dist,
158 config.search.keyword_boost,
159 h.importance,
160 ) / weight.max(0.01);
161 results.push((
162 score,
163 FederatedResult {
164 text: text.to_string(),
165 depth: h.depth,
166 layer: LAYER_NAMES
167 .get(h.layer_id as usize)
168 .unwrap_or(&"?")
169 .to_string(),
170 score,
171 source_index: name.to_string(),
172 is_append: false,
173 },
174 ));
175 }
176 }
177 }
178
179 let append_path = Path::new(&config.paths.output_dir).join("append.bin");
181 let appended = read_append_log(&append_path);
182 for entry in &appended {
183 let dx = entry.x - qx;
184 let dy = entry.y - qy;
185 let dz = entry.z - qz;
186 let dist = dx * dx + dy * dy + dz * dz;
187 let lexical = relevance_query.lexical_score(&entry.text);
188 if dist < 0.1 || lexical > 0.0 {
189 let score = crate::relevance::rank_distance_from_score(
190 lexical,
191 dist,
192 config.search.keyword_boost,
193 entry.importance,
194 ) / weight.max(0.01);
195 results.push((
196 score,
197 FederatedResult {
198 text: entry.text.clone(),
199 depth: entry.depth,
200 layer: LAYER_NAMES
201 .get(entry.layer_id as usize)
202 .unwrap_or(&"?")
203 .to_string(),
204 score,
205 source_index: name.to_string(),
206 is_append: true,
207 },
208 ));
209 }
210 }
211
212 results.sort_by(|a, b| a.0.total_cmp(&b.0));
213 results.truncate(k);
214 results.into_iter().map(|(_, r)| r).collect()
215}
216
217fn find_single(
219 name: &str,
220 config: &Config,
221 query: &str,
222 k: usize,
223 weight: f32,
224) -> Vec<FederatedResult> {
225 let reader = match MicroscopeReader::open(config) {
226 Ok(r) => r,
227 Err(_) => return Vec::new(),
228 };
229
230 let append_path = Path::new(&config.paths.output_dir).join("append.bin");
231 let appended = read_append_log(&append_path);
232 let results = reader.find_text_all(config, query, k);
233 results
234 .iter()
235 .enumerate()
236 .filter_map(|(rank, &(depth, idx, is_main))| {
237 let (text, layer) = if is_main {
238 let h = reader.header(idx);
239 (
240 reader.text(idx).to_string(),
241 LAYER_NAMES
242 .get(h.layer_id as usize)
243 .unwrap_or(&"?")
244 .to_string(),
245 )
246 } else {
247 let entry = appended.get(idx.saturating_sub(1_000_000))?;
248 (
249 entry.text.clone(),
250 LAYER_NAMES
251 .get(entry.layer_id as usize)
252 .unwrap_or(&"?")
253 .to_string(),
254 )
255 };
256 Some(FederatedResult {
257 text,
258 depth,
259 layer,
260 score: rank as f32 / weight,
261 source_index: name.to_string(),
262 is_append: !is_main,
263 })
264 })
265 .collect()
266}
267
268fn mql_single(
270 name: &str,
271 config: &Config,
272 mql: &str,
273 k: usize,
274 weight: f32,
275) -> Vec<FederatedResult> {
276 let reader = match MicroscopeReader::open(config) {
277 Ok(r) => r,
278 Err(_) => return Vec::new(),
279 };
280
281 let append_path = Path::new(&config.paths.output_dir).join("append.bin");
282 let appended = read_append_log(&append_path);
283 let q = crate::query::parse(mql);
284 let mut results = crate::query::execute(&q, &reader, &appended);
285 results.truncate(k);
286
287 results
288 .iter()
289 .map(|r| {
290 if r.is_main {
291 let h = reader.header(r.block_idx);
292 FederatedResult {
293 text: reader.text(r.block_idx).to_string(),
294 depth: h.depth,
295 layer: LAYER_NAMES
296 .get(h.layer_id as usize)
297 .unwrap_or(&"?")
298 .to_string(),
299 score: r.score / weight,
300 source_index: name.to_string(),
301 is_append: false,
302 }
303 } else {
304 let entry = &appended[r.block_idx];
305 FederatedResult {
306 text: entry.text.clone(),
307 depth: entry.depth,
308 layer: LAYER_NAMES
309 .get(entry.layer_id as usize)
310 .unwrap_or(&"?")
311 .to_string(),
312 score: r.score / weight,
313 source_index: name.to_string(),
314 is_append: true,
315 }
316 }
317 })
318 .collect()
319}
320
321fn merge_results(all: Vec<Vec<FederatedResult>>, k: usize) -> Vec<FederatedResult> {
323 let mut merged: Vec<FederatedResult> = all.into_iter().flatten().collect();
324 merged.sort_by(|a, b| a.score.total_cmp(&b.score));
325 merged.truncate(k);
326 merged
327}
328
329pub fn exchange_pulses(config: &Config) -> Result<usize, String> {
335 use crate::resonance::ResonanceState;
336
337 let output_dir = Path::new(&config.paths.output_dir);
338 let mut local = ResonanceState::load_or_init(output_dir);
339
340 let our_pulses = local.export_pulses();
342 let mut total_exchanged = 0usize;
343
344 let reader = MicroscopeReader::open(config).map_err(|e| format!("open reader: {}", e))?;
346 let local_headers: Vec<(f32, f32, f32)> = (0..reader.block_count)
347 .map(|i| {
348 let h = reader.header(i);
349 (h.x, h.y, h.z)
350 })
351 .collect();
352
353 for idx_config in &config.federation.indices {
355 let idx_cfg =
356 Config::load(&idx_config.config_path).map_err(|e| format!("load config: {}", e))?;
357 let idx_dir = Path::new(&idx_cfg.paths.output_dir);
358
359 let mut other = ResonanceState::load_or_init(idx_dir);
361
362 let their_headers: Vec<(f32, f32, f32)> = {
364 if let Ok(r) = MicroscopeReader::open(&idx_cfg) {
365 (0..r.block_count)
366 .map(|i| {
367 let h = r.header(i);
368 (h.x, h.y, h.z)
369 })
370 .collect()
371 } else {
372 continue;
373 }
374 };
375
376 let our_decoded = ResonanceState::import_pulses(&our_pulses);
377 for pulse in our_decoded {
378 other.receive_pulse(pulse, &their_headers, 0.05);
379 total_exchanged += 1;
380 }
381
382 let their_pulses = other.export_pulses();
384 let their_decoded = ResonanceState::import_pulses(&their_pulses);
385 for pulse in their_decoded {
386 local.receive_pulse(pulse, &local_headers, 0.05);
387 total_exchanged += 1;
388 }
389
390 let _ = other.save(idx_dir);
392 }
393
394 local
396 .save(output_dir)
397 .map_err(|e| format!("save resonance: {}", e))?;
398
399 Ok(total_exchanged)
400}
401
402pub fn exchange_patterns(config: &Config) -> Result<usize, String> {
407 use crate::predictive_cache::PredictiveCache;
408 use crate::thought_graph::ThoughtGraphState;
409
410 let output_dir = Path::new(&config.paths.output_dir);
411 let mut local_tg = ThoughtGraphState::load_or_init(output_dir);
412 let mut local_pc = PredictiveCache::load_or_init(output_dir);
413
414 let our_patterns: Vec<_> = local_tg.export_patterns().into_iter().cloned().collect();
415 let (_our_preds, _our_hits, _our_misses, our_hit_rate) = local_pc.export_stats();
416
417 let mut total_exchanged = 0usize;
418
419 for idx_config in &config.federation.indices {
420 let idx_cfg =
421 Config::load(&idx_config.config_path).map_err(|e| format!("load config: {}", e))?;
422 let idx_dir = Path::new(&idx_cfg.paths.output_dir);
423
424 let mut other_tg = ThoughtGraphState::load_or_init(idx_dir);
425 let mut other_pc = PredictiveCache::load_or_init(idx_dir);
426
427 let their_patterns: Vec<_> = other_tg.export_patterns().into_iter().cloned().collect();
428 let (their_preds, their_hits, their_misses, their_hit_rate) = other_pc.export_stats();
429
430 let trust_for_them = our_hit_rate * idx_config.weight;
432 other_tg.import_patterns(&our_patterns, trust_for_them);
433 total_exchanged += our_patterns.len();
434
435 let trust_for_us = their_hit_rate * idx_config.weight;
437 local_tg.import_patterns(&their_patterns, trust_for_us);
438 total_exchanged += their_patterns.len();
439
440 local_pc.merge_stats(their_preds, their_hits, their_misses);
442 other_pc.merge_stats(_our_preds, _our_hits, _our_misses);
443
444 let _ = other_tg.save(idx_dir);
445 let _ = other_pc.save(idx_dir);
446 }
447
448 local_tg
449 .save(output_dir)
450 .map_err(|e| format!("save thought_graph: {}", e))?;
451 local_pc
452 .save(output_dir)
453 .map_err(|e| format!("save predictive_cache: {}", e))?;
454
455 Ok(total_exchanged)
456}