Skip to main content

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}