1use std::time::Instant;
14
15use uqa_core::{GeneralizedPostingList, PostingList};
16use uqa_graph::GraphPostingList;
17use uqa_operators::{DeepFusionLayer, OperatorTree};
18
19#[derive(Debug, Clone, Default)]
21pub struct ExecutionStats {
22 pub operator_name: String,
23 pub elapsed_ms: f64,
24 pub result_count: usize,
25 pub children: Vec<ExecutionStats>,
26}
27
28#[derive(Debug, Clone, PartialEq)]
37pub enum OperatorOutput {
38 Posting(PostingList),
39 Graph(GraphPostingList),
40 Generalized(GeneralizedPostingList),
41}
42
43impl OperatorOutput {
44 #[must_use]
45 pub fn len(&self) -> usize {
46 match self {
47 Self::Posting(result) => result.len(),
48 Self::Graph(result) => result.len(),
49 Self::Generalized(result) => result.len(),
50 }
51 }
52
53 #[must_use]
54 pub fn is_empty(&self) -> bool {
55 match self {
56 Self::Posting(result) => result.is_empty(),
57 Self::Graph(result) => result.is_empty(),
58 Self::Generalized(result) => result.is_empty(),
59 }
60 }
61
62 #[must_use]
63 pub fn as_posting(&self) -> Option<&PostingList> {
64 match self {
65 Self::Posting(result) => Some(result),
66 Self::Graph(_) | Self::Generalized(_) => None,
67 }
68 }
69
70 #[must_use]
71 pub fn as_graph(&self) -> Option<&GraphPostingList> {
72 match self {
73 Self::Graph(result) => Some(result),
74 Self::Posting(_) | Self::Generalized(_) => None,
75 }
76 }
77
78 #[must_use]
79 pub fn as_generalized(&self) -> Option<&GeneralizedPostingList> {
80 match self {
81 Self::Posting(_) | Self::Graph(_) => None,
82 Self::Generalized(result) => Some(result),
83 }
84 }
85}
86
87impl From<PostingList> for OperatorOutput {
88 fn from(value: PostingList) -> Self {
89 Self::Posting(value)
90 }
91}
92
93impl From<GraphPostingList> for OperatorOutput {
94 fn from(value: GraphPostingList) -> Self {
95 Self::Graph(value)
96 }
97}
98
99impl From<GeneralizedPostingList> for OperatorOutput {
100 fn from(value: GeneralizedPostingList) -> Self {
101 Self::Generalized(value)
102 }
103}
104
105impl ExecutionStats {
106 pub fn new(name: impl Into<String>) -> Self {
107 Self {
108 operator_name: name.into(),
109 elapsed_ms: 0.0,
110 result_count: 0,
111 children: Vec::new(),
112 }
113 }
114}
115
116pub trait OperatorTreeDriver {
121 type Error;
124
125 fn execute_node(&self, op: &OperatorTree) -> Result<OperatorOutput, Self::Error>;
126}
127
128pub struct PlanExecutor<'d, D: OperatorTreeDriver> {
130 pub driver: &'d D,
131 last_stats: Option<ExecutionStats>,
132}
133
134impl<'d, D: OperatorTreeDriver> PlanExecutor<'d, D> {
135 pub fn new(driver: &'d D) -> Self {
136 Self {
137 driver,
138 last_stats: None,
139 }
140 }
141
142 pub fn execute(&mut self, op: &OperatorTree) -> Result<OperatorOutput, D::Error> {
146 self.last_stats = None;
147 let start = Instant::now();
148 let result = self.driver.execute_node(op)?;
149 let mut stats = ExecutionStats::new(operator_name(op));
150 stats.elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
151 stats.result_count = result.len();
152 self.last_stats = Some(stats);
153 Ok(result)
154 }
155
156 pub fn last_stats(&self) -> Option<&ExecutionStats> {
157 self.last_stats.as_ref()
158 }
159
160 pub fn explain(&self, op: &OperatorTree) -> String {
161 let mut lines: Vec<String> = Vec::new();
162 explain_recursive(op, &mut lines, 0);
163 lines.join("\n")
164 }
165}
166
167pub fn operator_name(op: &OperatorTree) -> String {
169 match op {
170 OperatorTree::Empty => "EmptyOp",
171 OperatorTree::Term {
172 top_k:
173 Some(uqa_operators::TextTopKPlan {
174 strategy: uqa_operators::TextTopKStrategy::Wand,
175 ..
176 }),
177 ..
178 } => "WANDTopKOp",
179 OperatorTree::Term {
180 top_k:
181 Some(uqa_operators::TextTopKPlan {
182 strategy: uqa_operators::TextTopKStrategy::BlockMaxWand,
183 ..
184 }),
185 ..
186 } => "BlockMaxWANDTopKOp",
187 OperatorTree::Term { .. } => "TermOp",
188 OperatorTree::Filter { .. } => "FilterOp",
189 OperatorTree::Facet { .. } => "FacetOp",
190 OperatorTree::Score { .. } => "ScoreOp",
191 OperatorTree::BayesianScore { .. } => "BayesianScoreQuery",
192 OperatorTree::BayesianMatchWithPrior { .. } => "BayesianMatchWithPriorOp",
193 OperatorTree::Intersect(_) => "IntersectOp",
194 OperatorTree::Union(_) => "UnionOp",
195 OperatorTree::Complement(_) => "ComplementOp",
196 OperatorTree::Composed(_) => "ComposedOp",
197 OperatorTree::EncodeGraphPosting { .. } => "EncodeGraphPostingOp",
198 OperatorTree::VectorSimilarity { .. } => "VectorSimOp",
199 OperatorTree::KNN { .. } => "KNNOp",
200 OperatorTree::CalibratedVectorMatch { .. } => "CalibratedVectorMatchOp",
201 OperatorTree::CosineProbability(_) => "CosineProbabilityOp",
202 OperatorTree::BayesianEvidenceFusion { .. } => "BayesianEvidenceFusion",
203 OperatorTree::RobustPositiveEvidencePool { .. } => "RobustPositiveEvidencePool",
204 OperatorTree::ProbBoolFusion { .. } => "ProbBoolFusion",
205 OperatorTree::ProbNot { .. } => "ProbNot",
206 OperatorTree::AttentionFusion { .. } => "AttentionFusion",
207 OperatorTree::LearnedFusion { .. } => "LearnedFusion",
208 OperatorTree::SparseThreshold { .. } => "SparseThreshold",
209 OperatorTree::Traverse { .. } => "TraverseOp",
210 OperatorTree::GraphNeighbors { .. } => "GraphNeighborsOp",
211 OperatorTree::GraphEdges { .. } => "GraphEdgesOp",
212 OperatorTree::PatternMatch { .. } => "PatternMatchOp",
213 OperatorTree::RegularPathQuery { .. } => "RPQOp",
214 OperatorTree::GraphJoin { .. } => "GraphJoinOp",
215 OperatorTree::IndexScan { .. } => "IndexScanOp",
216 OperatorTree::Aggregate { .. } => "AggregateOp",
217 OperatorTree::GroupBy { .. } => "GroupByOp",
218 OperatorTree::MultiStage { .. } => "MultiStage",
219 OperatorTree::MultiFieldSearch { .. } => "MultiFieldSearchOp",
220 OperatorTree::HybridTextVector { .. } => "HybridTextVectorOp",
221 OperatorTree::SemanticFilter { .. } => "SemanticFilterOp",
222 OperatorTree::VectorExclusion { .. } => "VectorExclusionOp",
223 OperatorTree::FacetVector { .. } => "FacetVectorOp",
224 OperatorTree::VertexAggregation { .. } => "VertexAggregationOp",
225 OperatorTree::WeightedPathQuery { .. } => "WeightedPathQueryOp",
226 OperatorTree::MessagePassing { .. } => "MessagePassingOp",
227 OperatorTree::GraphEmbedding { .. } => "GraphEmbeddingOp",
228 OperatorTree::PageRank { .. } => "PageRankOp",
229 OperatorTree::HITS { .. } => "HITSOp",
230 OperatorTree::BetweennessCentrality { .. } => "BetweennessCentralityOp",
231 OperatorTree::TextSimilarityJoin { .. } => "TextSimilarityJoinOp",
232 OperatorTree::VectorSimilarityJoin { .. } => "VectorSimilarityJoinOp",
233 OperatorTree::HybridJoin { .. } => "HybridJoinOp",
234 OperatorTree::CrossParadigmJoin { .. } => "CrossParadigmJoinOp",
235 OperatorTree::TemporalTraverse { .. } => "TemporalTraverseOp",
236 OperatorTree::TemporalPatternMatch { .. } => "TemporalPatternMatchOp",
237 OperatorTree::ProgressiveFusion { .. } => "ProgressiveFusionOp",
238 OperatorTree::DeepFusion { .. } => "DeepFusion",
239 OperatorTree::DeepPredict { .. } => "DeepPredictOp",
240 OperatorTree::Opaque { kind, .. } => return kind.clone(),
241 }
242 .to_string()
243}
244
245#[expect(
246 clippy::too_many_lines,
247 reason = "executor exhaustively maps every operator variant and output carrier"
248)]
249fn explain_recursive(op: &OperatorTree, lines: &mut Vec<String>, indent: usize) {
250 let prefix = " ".repeat(indent);
251 match op {
252 OperatorTree::Term {
253 query,
254 field,
255 scoring,
256 top_k,
257 } => {
258 lines.push(format!(
259 "{prefix}TermOp(term={query:?}, field={field:?}, scoring={scoring:?}, top_k={top_k:?})"
260 ));
261 }
262 OperatorTree::VectorSimilarity {
263 threshold, field, ..
264 } => {
265 lines.push(format!(
266 "{prefix}VectorSimOp(threshold={threshold}, field={field:?})"
267 ));
268 }
269 OperatorTree::KNN { k, field, .. } => {
270 lines.push(format!("{prefix}KNNOp(k={k}, field={field:?})"));
271 }
272 OperatorTree::IndexScan {
273 field, index_name, ..
274 } => {
275 lines.push(format!(
276 "{prefix}IndexScanOp(field={field:?}, index={index_name:?})"
277 ));
278 }
279 OperatorTree::Score {
280 query_terms,
281 field,
282 source,
283 ..
284 } => {
285 lines.push(format!(
286 "{prefix}ScoreOp(scorer=Scorer, terms={query_terms:?}, field={field:?})"
287 ));
288 explain_recursive(source, lines, indent + 1);
289 }
290 OperatorTree::BayesianScore { source, field } => {
291 lines.push(format!("{prefix}BayesianScoreQuery(field={field:?})"));
292 explain_recursive(source, lines, indent + 1);
293 }
294 OperatorTree::Filter { field, source, .. } => {
295 lines.push(format!("{prefix}FilterOp(field={field:?})"));
296 if let Some(src) = source {
297 explain_recursive(src, lines, indent + 1);
298 }
299 }
300 OperatorTree::BayesianEvidenceFusion { signals, base_rate } => {
301 lines.push(format!(
302 "{prefix}BayesianEvidenceFusion(base_rate={base_rate:?}, signals={})",
303 signals.len()
304 ));
305 for signal in signals {
306 explain_recursive(signal, lines, indent + 1);
307 }
308 }
309 OperatorTree::RobustPositiveEvidencePool { signals, alpha, .. } => {
310 lines.push(format!(
311 "{prefix}RobustPositiveEvidencePool(alpha={alpha}, signals={})",
312 signals.len()
313 ));
314 for sig in signals {
315 explain_recursive(sig, lines, indent + 1);
316 }
317 }
318 OperatorTree::ProbBoolFusion { signals, mode } => {
319 lines.push(format!(
320 "{prefix}ProbBoolFusion(mode={mode:?}, signals={})",
321 signals.len()
322 ));
323 for sig in signals {
324 explain_recursive(sig, lines, indent + 1);
325 }
326 }
327 OperatorTree::ProbNot { signal, .. } => {
328 lines.push(format!("{prefix}ProbNot"));
329 explain_recursive(signal, lines, indent + 1);
330 }
331 OperatorTree::AttentionFusion { signals, .. } => {
332 lines.push(format!(
333 "{prefix}AttentionFusion(signals={})",
334 signals.len()
335 ));
336 for sig in signals {
337 explain_recursive(sig, lines, indent + 1);
338 }
339 }
340 OperatorTree::LearnedFusion { signals, .. } => {
341 lines.push(format!("{prefix}LearnedFusion(signals={})", signals.len()));
342 for sig in signals {
343 explain_recursive(sig, lines, indent + 1);
344 }
345 }
346 OperatorTree::Traverse {
347 start_vertex,
348 label,
349 max_hops,
350 ..
351 } => {
352 lines.push(format!(
353 "{prefix}TraverseOp(start={start_vertex}, label={label:?}, hops={max_hops})"
354 ));
355 }
356 OperatorTree::PatternMatch { pattern, .. } => {
357 lines.push(format!(
358 "{prefix}PatternMatchOp(vertices={}, edges={})",
359 pattern.vertex_patterns.len(),
360 pattern.edge_patterns.len()
361 ));
362 }
363 OperatorTree::RegularPathQuery { start_vertex, .. } => {
364 lines.push(format!("{prefix}RPQOp(start={start_vertex})"));
365 }
366 OperatorTree::Intersect(ops) => {
367 lines.push(format!("{prefix}Intersect"));
368 for child in ops {
369 explain_recursive(child, lines, indent + 1);
370 }
371 }
372 OperatorTree::Union(ops) => {
373 lines.push(format!("{prefix}Union"));
374 for child in ops {
375 explain_recursive(child, lines, indent + 1);
376 }
377 }
378 OperatorTree::Complement(inner) => {
379 lines.push(format!("{prefix}Complement"));
380 explain_recursive(inner, lines, indent + 1);
381 }
382 OperatorTree::Composed(ops) => {
383 lines.push(format!("{prefix}Composed"));
384 for child in ops {
385 explain_recursive(child, lines, indent + 1);
386 }
387 }
388 OperatorTree::EncodeGraphPosting { source } => {
389 lines.push(format!("{prefix}EncodeGraphPosting"));
390 explain_recursive(source, lines, indent + 1);
391 }
392 OperatorTree::SparseThreshold { source, threshold } => {
393 lines.push(format!("{prefix}SparseThreshold(threshold={threshold})"));
394 explain_recursive(source, lines, indent + 1);
395 }
396 OperatorTree::MessagePassing { source } => {
397 lines.push(format!("{prefix}MessagePassingOp"));
398 explain_recursive(source, lines, indent + 1);
399 }
400 OperatorTree::GraphEmbedding { source } => {
401 lines.push(format!("{prefix}GraphEmbeddingOp"));
402 explain_recursive(source, lines, indent + 1);
403 }
404 OperatorTree::MultiStage { stages } => {
405 lines.push(format!("{prefix}MultiStage(stages={})", stages.len()));
406 for (i, entry) in stages.iter().enumerate() {
407 lines.push(format!("{prefix} Stage {i} (cutoff={:?}):", entry.cutoff));
408 explain_recursive(&entry.child, lines, indent + 2);
409 }
410 }
411 OperatorTree::DeepFusion {
412 layers,
413 alpha,
414 gating,
415 } => {
416 lines.push(format!(
417 "{prefix}DeepFusion(layers={}, alpha={alpha}, gating={gating:?})",
418 layers.len()
419 ));
420 for (i, layer) in layers.iter().enumerate() {
421 match layer {
422 DeepFusionLayer::Signal { signals } => {
423 lines.push(format!("{prefix} Layer {i} (signals={}):", signals.len()));
424 for sig in signals {
425 explain_recursive(sig, lines, indent + 2);
426 }
427 }
428 DeepFusionLayer::Propagate {
429 edge_label,
430 aggregation,
431 direction,
432 } => {
433 lines.push(format!(
434 "{prefix} Layer {i} (propagate={edge_label:?}, aggregation={aggregation:?}, direction={direction:?}):"
435 ));
436 }
437 DeepFusionLayer::Conv {
438 edge_label,
439 hop_weights,
440 direction,
441 } => {
442 lines.push(format!(
443 "{prefix} Layer {i} (convolve={edge_label:?}, hop_weights={hop_weights:?}, direction={direction:?}):"
444 ));
445 }
446 DeepFusionLayer::Pool {
447 edge_label,
448 pool_size,
449 method,
450 direction,
451 } => {
452 lines.push(format!(
453 "{prefix} Layer {i} (pool={edge_label:?}, size={pool_size}, method={method:?}, direction={direction:?}):"
454 ));
455 }
456 DeepFusionLayer::Flatten => {
457 lines.push(format!("{prefix} Layer {i} (flatten):"));
458 }
459 DeepFusionLayer::Softmax => {
460 lines.push(format!("{prefix} Layer {i} (softmax):"));
461 }
462 DeepFusionLayer::Dense {
463 output_channels,
464 input_channels,
465 ..
466 } => {
467 lines.push(format!(
468 "{prefix} Layer {i} (dense, input={input_channels}, output={output_channels}):"
469 ));
470 }
471 DeepFusionLayer::BatchNorm { epsilon } => {
472 lines.push(format!(
473 "{prefix} Layer {i} (batch_norm, epsilon={epsilon}):"
474 ));
475 }
476 DeepFusionLayer::Dropout { probability } => {
477 lines.push(format!(
478 "{prefix} Layer {i} (dropout, probability={probability}):"
479 ));
480 }
481 }
482 }
483 }
484 OperatorTree::Opaque { kind, children, .. } => {
485 lines.push(format!("{prefix}{kind}"));
486 for child in children {
487 explain_recursive(child, lines, indent + 1);
488 }
489 }
490 other => {
491 lines.push(format!("{prefix}{}", operator_name(other)));
492 }
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use std::sync::atomic::{AtomicUsize, Ordering};
500
501 struct EmptyDriver;
502 impl OperatorTreeDriver for EmptyDriver {
503 type Error = std::convert::Infallible;
504
505 fn execute_node(&self, _op: &OperatorTree) -> Result<OperatorOutput, Self::Error> {
506 Ok(PostingList::new().into())
507 }
508 }
509
510 #[test]
511 fn explain_renders_intersect_tree() {
512 let op = OperatorTree::Intersect(vec![
513 OperatorTree::Term {
514 query: "rust".into(),
515 field: Some("body".into()),
516 scoring: None,
517 top_k: None,
518 },
519 OperatorTree::Filter {
520 field: "year".into(),
521 predicate: uqa_core::Predicate::Equals(uqa_core::Value::Int(2026)),
522 source: None,
523 },
524 ]);
525 let driver = EmptyDriver;
526 let executor = PlanExecutor::new(&driver);
527 let text = executor.explain(&op);
528 assert!(text.contains("Intersect"));
529 assert!(text.contains("TermOp"));
530 assert!(text.contains("FilterOp"));
531 }
532
533 #[test]
534 fn execute_collects_timing_stats() {
535 let op = OperatorTree::Term {
536 query: "x".into(),
537 field: Some("body".into()),
538 scoring: None,
539 top_k: None,
540 };
541 let driver = EmptyDriver;
542 let mut executor = PlanExecutor::new(&driver);
543 let _ = executor.execute(&op).expect("infallible driver");
544 let stats = executor.last_stats().expect("stats");
545 assert_eq!(stats.operator_name, "TermOp");
546 assert_eq!(stats.result_count, 0);
547 assert!(stats.elapsed_ms >= 0.0);
548 }
549
550 #[test]
551 fn execute_delegates_to_driver_once() {
552 struct CountingDriver {
553 calls: AtomicUsize,
554 }
555
556 impl OperatorTreeDriver for CountingDriver {
557 type Error = std::convert::Infallible;
558
559 fn execute_node(&self, _op: &OperatorTree) -> Result<OperatorOutput, Self::Error> {
560 self.calls.fetch_add(1, Ordering::SeqCst);
561 Ok(PostingList::new().into())
562 }
563 }
564
565 let op = OperatorTree::Intersect(vec![
566 OperatorTree::Term {
567 query: "rust".into(),
568 field: Some("body".into()),
569 scoring: None,
570 top_k: None,
571 },
572 OperatorTree::Term {
573 query: "search".into(),
574 field: Some("body".into()),
575 scoring: None,
576 top_k: None,
577 },
578 ]);
579 let driver = CountingDriver {
580 calls: AtomicUsize::new(0),
581 };
582 let mut executor = PlanExecutor::new(&driver);
583
584 let _ = executor.execute(&op).expect("infallible driver");
585
586 assert_eq!(driver.calls.load(Ordering::SeqCst), 1);
587 }
588
589 #[test]
590 fn execute_propagates_driver_errors() {
591 struct FailingDriver;
592
593 impl OperatorTreeDriver for FailingDriver {
594 type Error = &'static str;
595
596 fn execute_node(&self, _op: &OperatorTree) -> Result<OperatorOutput, Self::Error> {
597 Err("storage failure")
598 }
599 }
600
601 let driver = FailingDriver;
602 let mut executor = PlanExecutor::new(&driver);
603 let error = executor
604 .execute(&OperatorTree::Empty)
605 .expect_err("driver failure must be returned");
606
607 assert_eq!(error, "storage failure");
608 assert!(executor.last_stats().is_none());
609 }
610}