luci/query/mod.rs
1//! Query DSL: AST types, JSON parser, and execution framework.
2//!
3//! Implements the `Query → BoundQuery → ScorerSupplier → Scorer` pipeline
4//! from [[architecture-query-execution]]. Queries are parsed from JSON, bound to
5//! index-level statistics, then per-segment scorers are built.
6//!
7//! See [[query-dsl]] and [[architecture-query-execution|milestone-2]].
8
9pub mod ast;
10pub mod boolean;
11pub mod boost;
12pub mod boosting;
13pub mod constant_score;
14pub mod convert;
15pub mod dis_max;
16pub mod exists;
17pub mod function_score;
18pub mod fuzzy;
19pub mod match_query;
20pub mod multi_term;
21pub mod nested;
22pub mod parser;
23pub mod phrase;
24pub mod prefix;
25pub mod range;
26pub mod regex_automaton;
27pub mod regexp;
28pub mod script_score;
29pub mod span;
30pub mod term;
31pub mod wildcard;
32
33use crate::core::{Result, ScoreMode, Scorer};
34
35use crate::search::searcher::Searcher;
36use crate::segment::reader::SegmentReader;
37
38/// A parsed query that can be bound to index statistics for execution.
39///
40/// Corresponds to `Query` in [[architecture-query-execution#Core Traits]].
41/// Object-safe — used as `Box<dyn Query>` in the execution engine.
42pub(crate) trait Query: Send + Sync {
43 /// Bind this query to index-level statistics, producing a BoundQuery.
44 fn bind(&self, searcher: &Searcher, score_mode: ScoreMode) -> Result<Box<dyn BoundQuery>>;
45}
46
47/// Segment-independent query state bound to index statistics. Created
48/// once per query, then asked for per-segment scorer suppliers.
49///
50/// Corresponds to `BoundQuery` in [[architecture-query-execution#Core Traits]].
51pub(crate) trait BoundQuery: Send + Sync {
52 /// Create a scorer supplier for a segment, or `None` if this weight
53 /// cannot match any documents in the segment.
54 fn scorer_supplier(&self, reader: &SegmentReader) -> Result<Option<Box<dyn ScorerSupplier>>>;
55
56 /// Returns true if this weight matches all documents in every segment.
57 /// Used to bypass scorer iteration in agg-only paths.
58 fn is_match_all(&self) -> bool {
59 false
60 }
61
62 /// Optional: score all matching documents directly into a collector,
63 /// bypassing the doc-at-a-time Scorer interface. Returns total hits
64 /// if supported, None to fall back to doc-at-a-time.
65 fn bulk_score(
66 &self,
67 _reader: &SegmentReader,
68 _collector: &mut crate::search::collector::TopDocsCollector,
69 _segment_id: crate::core::SegmentId,
70 ) -> Result<Option<u64>> {
71 Ok(None)
72 }
73
74 /// Explain the score for a specific document.
75 ///
76 /// Default: builds a scorer, advances to the doc, returns a generic
77 /// explanation. Override for detailed BM25/boolean breakdowns.
78 /// See [[feature-search-explain]].
79 fn explain(
80 &self,
81 reader: &SegmentReader,
82 doc: crate::core::DocId,
83 ) -> Result<crate::search::Explanation> {
84 let supplier = match self.scorer_supplier(reader)? {
85 Some(s) => s,
86 None => {
87 return Ok(crate::search::Explanation::no_match(
88 "no matching docs in segment".into(),
89 ));
90 }
91 };
92 let mut scorer = supplier.scorer()?;
93 let found = scorer.advance(doc);
94 if found != doc {
95 return Ok(crate::search::Explanation::no_match(format!(
96 "doc {} not matched",
97 doc.as_u32()
98 )));
99 }
100 let score = scorer.score();
101 Ok(crate::search::Explanation::leaf(
102 score,
103 format!("score(doc={})", doc.as_u32()),
104 ))
105 }
106}
107
108/// Knows the estimated cost of scoring before a scorer is built.
109///
110/// This enables cost-based clause ordering in boolean conjunctions:
111/// the cheapest supplier becomes the lead iterator.
112///
113/// Corresponds to `ScorerSupplier` in [[architecture-query-execution#Core Traits]].
114pub trait ScorerSupplier: Send {
115 /// Estimated number of matching documents in this segment.
116 fn cost(&self) -> u64;
117
118 /// Build the actual scorer. `lead_cost` is the cost of the lead
119 /// iterator in a conjunction — implementations may use it to choose
120 /// between algorithms.
121 fn scorer(self: Box<Self>) -> Result<Box<dyn Scorer>>;
122}
123
124/// A query whose scorers expose position-level Spans, making it
125/// composable under span operators (``SpanFirst``, ``SpanNot``).
126///
127/// This is the runtime-layer counterpart to the AST's
128/// ``SpanExpression`` enum. ``SpanQuery: Query`` via trait upcasting
129/// (Rust ≥ 1.86), so a ``Box<dyn SpanQuery>`` can be used wherever a
130/// ``Box<dyn Query>`` is expected. Only the four concrete span types
131/// implement it: ``SpanTermQuery``, ``SpanNearQuery``,
132/// ``SpanNotQuery``, ``SpanFirstQuery``.
133///
134/// ``SpanFirstQuery.inner`` and ``SpanNotQuery.include/exclude`` hold
135/// ``Box<dyn SpanQuery>`` (not ``Box<dyn Query>``), so the type
136/// system prevents wrapping a non-span query in a span operator.
137pub(crate) trait SpanQuery: Query {
138 /// Bind to index stats, returning a span-typed ``BoundSpanQuery``
139 /// so span composition preserves typing through the pipeline.
140 fn bind_span(
141 &self,
142 searcher: &Searcher,
143 score_mode: ScoreMode,
144 ) -> Result<Box<dyn BoundSpanQuery>>;
145}
146
147/// Segment-independent span query. ``BoundSpanQuery: BoundQuery`` so
148/// it flows through the general pipeline, but adds the required
149/// ``span_scorer_supplier`` method used by SpanFirst to apply an end
150/// constraint via the ``FilterSpans`` wrapper.
151///
152/// Implemented only by the bound span types
153/// (``BoundSpanTermQuery`` etc.), produced by ``SpanQuery::bind_span``.
154pub(crate) trait BoundSpanQuery: BoundQuery {
155 /// Build a scorer supplier that additionally filters emitted
156 /// spans by end position. SpanFirst uses this to apply its
157 /// ``end`` constraint without downcasting. Each span type wires
158 /// its concrete ``Spans`` iterator through a ``FilterSpans``
159 /// wrapper so the constraint reaches the position level.
160 fn span_scorer_supplier(
161 &self,
162 reader: &SegmentReader,
163 max_end: u32,
164 ) -> Result<Option<Box<dyn ScorerSupplier>>>;
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 // Verify trait object safety
172 #[test]
173 fn query_is_object_safe() {
174 fn _takes_query(_q: &dyn Query) {}
175 }
176
177 #[test]
178 fn bound_query_is_object_safe() {
179 fn _takes_bound_query(_w: &dyn BoundQuery) {}
180 }
181
182 #[test]
183 fn scorer_supplier_is_object_safe() {
184 fn _takes_ss(_ss: Box<dyn ScorerSupplier>) {}
185 }
186}