Skip to main content

qql_core/ast/statement/
query.rs

1//! Typed AST for QUERY statements and expressions.
2
3use super::types::*;
4use crate::ast::{FilterExpr, FormulaExpr, Value};
5use alloc::boxed::Box;
6use alloc::string::String;
7use alloc::vec::Vec;
8
9/// Query input: embeddable text/image, pre-computed vector, or point reference.
10#[derive(Debug, Clone, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum QueryInput {
13    /// Text to embed (`TEXT '…' [MODEL '…'] [OPTIONS {…}]`).
14    Text {
15        /// The text to embed.
16        text: String,
17        /// Optional embedding model override.
18        model: Option<String>,
19        /// Parameter placeholder (`:name` or `?idx`) when the text was not provided as a literal string.
20        #[cfg_attr(
21            feature = "serde",
22            serde(default, skip_serializing_if = "Option::is_none")
23        )]
24        text_param: Option<String>,
25        /// Opaque inference options (`OPTIONS {…}`), passed to the model as-is.
26        #[cfg_attr(
27            feature = "serde",
28            serde(default, skip_serializing_if = "Vec::is_empty")
29        )]
30        options: Vec<(String, Value)>,
31    },
32    /// Image path or URL for dense embedding (CLIP vision, etc.).
33    /// Resolved to [`VectorValue::Dense`] before plan/dispatch.
34    Image {
35        /// Image path or URL.
36        source: String,
37        /// Optional embedding model override.
38        model: Option<String>,
39        /// Opaque inference options (`OPTIONS {…}`), passed to the model as-is.
40        #[cfg_attr(
41            feature = "serde",
42            serde(default, skip_serializing_if = "Vec::is_empty")
43        )]
44        options: Vec<(String, Value)>,
45    },
46    /// Custom inference object (`OBJECT {…} [MODEL '…'] [OPTIONS {…}]`).
47    /// Passed through to the backend inference service; never executed locally.
48    Object {
49        /// Arbitrary model input (usually an object).
50        object: Box<Value>,
51        /// Optional embedding model override.
52        model: Option<String>,
53        /// Opaque inference options (`OPTIONS {…}`), passed to the model as-is.
54        #[cfg_attr(
55            feature = "serde",
56            serde(default, skip_serializing_if = "Vec::is_empty")
57        )]
58        options: Vec<(String, Value)>,
59    },
60    /// Pre-computed vector value used as-is.
61    Vector(VectorValue),
62    /// Reference point — use an existing point's vector as the input.
63    Point(PointId),
64    /// Parameter placeholder (`:name`) for target query input.
65    Param(
66        String,
67        #[cfg_attr(
68            feature = "serde",
69            serde(default, skip_serializing_if = "Option::is_none")
70        )]
71        Option<alloc::boxed::Box<crate::error::Span>>,
72    ),
73    /// Positional parameter placeholder (`?`) for target query input.
74    PositionalParam(
75        usize,
76        #[cfg_attr(
77            feature = "serde",
78            serde(default, skip_serializing_if = "Option::is_none")
79        )]
80        Option<alloc::boxed::Box<crate::error::Span>>,
81    ),
82}
83
84impl QueryInput {
85    /// Construct an unlocated named parameter placeholder.
86    pub fn param(name: impl Into<String>) -> Self {
87        Self::Param(name.into(), None)
88    }
89
90    /// Construct a located named parameter placeholder.
91    pub fn param_with_span(name: impl Into<String>, span: crate::error::Span) -> Self {
92        Self::Param(name.into(), Some(alloc::boxed::Box::new(span)))
93    }
94
95    /// Construct an unlocated positional parameter placeholder.
96    pub fn positional_param(idx: usize) -> Self {
97        Self::PositionalParam(idx, None)
98    }
99
100    /// Construct a located positional parameter placeholder.
101    pub fn positional_param_with_span(idx: usize, span: crate::error::Span) -> Self {
102        Self::PositionalParam(idx, Some(alloc::boxed::Box::new(span)))
103    }
104
105    /// Extract the parameter source span if present.
106    pub fn param_span(&self) -> Option<crate::error::Span> {
107        match self {
108            Self::Param(_, span) | Self::PositionalParam(_, span) => span.as_deref().copied(),
109            _ => None,
110        }
111    }
112}
113
114/// Maximal marginal relevance settings (`MMR … DIVERSITY … CANDIDATES …`).
115#[derive(Debug, Clone, PartialEq)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117pub struct MmrConfig {
118    /// Relevance-to-diversity trade-off in `[0, 1]`.
119    pub diversity: f64,
120    /// Size of the candidate pool considered by the MMR pass.
121    pub candidates: u64,
122}
123
124/// One positive/negative example pair.
125#[derive(Debug, Clone, PartialEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct ContextPair {
128    /// Example the results should be similar to.
129    pub positive: QueryInput,
130    /// Example the results should move away from.
131    pub negative: QueryInput,
132}
133
134/// `RECOMMEND … STRATEGY` scoring strategy.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub enum RecommendStrategy {
138    /// `average_vector` — score against the averaged example vectors.
139    AverageVector,
140    /// `best_score` — score against the most similar example.
141    BestScore,
142    /// `sum_scores` — sum similarity across all examples.
143    SumScores,
144}
145
146/// One relevance feedback example with its weight.
147#[derive(Debug, Clone, PartialEq)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub struct FeedbackItem {
150    /// Feedback example input.
151    pub example: QueryInput,
152    /// Weight applied to this example's vector.
153    pub score: f64,
154}
155
156/// `STRATEGY NAIVE (a = …, b = …, c = …)` relevance feedback weights.
157#[derive(Debug, Clone, Copy, PartialEq)]
158#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
159pub struct FeedbackStrategy {
160    /// Weight of the target vector.
161    pub a: f64,
162    /// Weight of the positive feedback examples.
163    pub b: f64,
164    /// Weight of the negative feedback examples.
165    pub c: f64,
166}
167
168/// Rank/score fusion method over prefetch stages.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
171pub enum FusionMethod {
172    /// `RRF` — reciprocal rank fusion.
173    Rrf,
174    /// `DBSF` — distribution-based score fusion.
175    Dbsf,
176}
177
178/// Target collection of a query statement.
179#[derive(Debug, Clone, PartialEq)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181pub enum QueryCollection {
182    /// Collection named after `FROM`.
183    Explicit(String),
184    /// CTE without its own `FROM`; inherits the enclosing query's collection.
185    Inherited,
186}
187
188/// Where a prefetch stage draws its candidates from.
189#[derive(Debug, Clone, PartialEq)]
190#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
191pub enum PrefetchSource {
192    /// Reference to a named `WITH` CTE.
193    Cte(String),
194    /// Inline `QUERY` sub-statement.
195    Query(Box<QueryStmt>),
196}
197
198/// `LOOKUP FROM <collection> [VECTOR <name>] [SHARD <key>]` group-value join hint.
199#[derive(Debug, Clone, PartialEq)]
200#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
201pub struct LookupSpec {
202    /// Collection to read group values from.
203    pub collection: String,
204    /// Optional named vector used by the lookup.
205    pub vector: Option<String>,
206    /// Optional shard routing for the lookup collection.
207    pub shard_key: Option<ShardKey>,
208}
209
210/// One stage of the `PREFETCH (…)` pipeline.
211#[derive(Debug, Clone, PartialEq)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213pub struct Prefetch {
214    /// CTE reference or inline query.
215    pub source: PrefetchSource,
216    /// Prefetch-level `WHERE` override.
217    pub filter: Option<Box<FilterExpr>>,
218    /// Prefetch-level `SCORE THRESHOLD` override.
219    pub score_threshold: Option<f64>,
220    /// Optional cross-collection lookup for this stage.
221    pub lookup: Option<LookupSpec>,
222}
223
224/// `QUERY` expression body — the retrieval strategy and its inputs.
225#[derive(Debug, Clone, PartialEq)]
226#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
227pub enum QueryExpr {
228    /// `QUERY POINTS (ids)` — direct retrieval of the listed points.
229    Points {
230        /// Point IDs to fetch.
231        ids: Vec<PointId>,
232    },
233    /// `QUERY [NEAREST] <input> FROM <coll>` — vector nearest-neighbor search.
234    Nearest {
235        /// Embeddable input, vector, or reference point.
236        input: QueryInput,
237        /// `USING` vector target; schema-resolved when `None`.
238        using: Option<VectorTarget>,
239        /// Multi-stage `PREFETCH` pipeline.
240        prefetch: Vec<Prefetch>,
241        /// MMR re-diversification settings.
242        mmr: Option<Box<MmrConfig>>,
243    },
244    /// `QUERY RECOMMEND POSITIVE … [NEGATIVE …]` — recommend from examples.
245    Recommend {
246        /// Examples to move toward.
247        positive: Vec<QueryInput>,
248        /// Examples to move away from.
249        negative: Vec<QueryInput>,
250        /// Scoring strategy; server default when `None`.
251        strategy: Option<RecommendStrategy>,
252        /// `USING` vector target; schema-resolved when `None`.
253        using: Option<VectorTarget>,
254        /// Multi-stage `PREFETCH` pipeline.
255        prefetch: Vec<Prefetch>,
256    },
257    /// `QUERY CONTEXT (POSITIVE … NEGATIVE …)` — search guided by example pairs.
258    Context {
259        /// Positive/negative example pairs.
260        pairs: Vec<ContextPair>,
261        /// `USING` vector target; schema-resolved when `None`.
262        using: Option<VectorTarget>,
263        /// Multi-stage `PREFETCH` pipeline.
264        prefetch: Vec<Prefetch>,
265    },
266    /// `QUERY DISCOVER TARGET … CONTEXT (…)` — discovery from target plus pairs.
267    Discover {
268        /// Primary target input.
269        target: QueryInput,
270        /// Guiding positive/negative pairs.
271        context: Vec<ContextPair>,
272        /// `USING` vector target; schema-resolved when `None`.
273        using: Option<VectorTarget>,
274        /// Multi-stage `PREFETCH` pipeline.
275        prefetch: Vec<Prefetch>,
276    },
277    /// `QUERY ORDER BY field [ASC|DESC] [START FROM <value>]` — payload-value ordering.
278    OrderBy {
279        /// Payload field to sort on.
280        field: String,
281        /// Sort direction (`ASC` default).
282        direction: OrderDirection,
283        /// Optional paging origin: resume ordering from this payload value
284        /// (OpenAPI `OrderBy.start_from`: integer, float, or datetime string).
285        start_from: Option<Value>,
286    },
287    /// `QUERY SAMPLE RANDOM` — random sample of points.
288    SampleRandom,
289    /// `QUERY FUSION RRF|DBSF` — fuse results of the prefetch stages.
290    Fusion {
291        /// Fusion algorithm.
292        method: FusionMethod,
293        /// Stages whose results are fused (must be non-empty).
294        prefetch: Vec<Prefetch>,
295    },
296    /// `QUERY FORMULA <expr> [DEFAULTS (…)]` — formula-expression rescoring.
297    Formula {
298        /// Rescoring expression tree.
299        expression: Box<FormulaExpr>,
300        /// `DEFAULTS` bindings for formula variables.
301        defaults: Vec<(String, Value)>,
302        /// Candidate stages the formula rescoring applies to.
303        prefetch: Vec<Prefetch>,
304    },
305    /// `QUERY RELEVANCE FEEDBACK TARGET … FEEDBACK (…)` — naive feedback search.
306    RelevanceFeedback {
307        /// Base target input.
308        target: QueryInput,
309        /// Weighted feedback examples.
310        feedback: Vec<FeedbackItem>,
311        /// `STRATEGY NAIVE (a = …, b = …, c = …)` weights.
312        strategy: FeedbackStrategy,
313        /// `USING` vector target; schema-resolved when `None`.
314        using: Option<VectorTarget>,
315        /// Multi-stage `PREFETCH` pipeline.
316        prefetch: Vec<Prefetch>,
317    },
318    /// `QUERY HYBRID TEXT … [DENSE n] [SPARSE n] [FUSION m]` — dense+sparse fusion.
319    Hybrid {
320        /// Query text embedded for both stages.
321        text: String,
322        /// Optional embedding model override.
323        model: Option<String>,
324        /// Named dense vector; schema-resolved when `None`.
325        dense_vector: Option<String>,
326        /// Named sparse vector; schema-resolved when `None`.
327        sparse_vector: Option<String>,
328        /// Fusion method for the two stages.
329        fusion: FusionMethod,
330        /// Parameter placeholder (`:name` or `?idx`) when the text was not provided as a literal string.
331        #[cfg_attr(
332            feature = "serde",
333            serde(default, skip_serializing_if = "Option::is_none")
334        )]
335        text_param: Option<String>,
336    },
337    /// `QUERY RERANK <input> MODEL '…'` — late-interaction rerank over prefetch.
338    Rerank {
339        /// Query input embedded via `USING`.
340        input: QueryInput,
341        /// ColBERT-style late-interaction model.
342        model: String,
343        /// Dense (or multivector) target for document embeddings.
344        using: Option<VectorTarget>,
345        /// Multi-stage `PREFETCH` pipeline.
346        prefetch: Vec<Prefetch>,
347    },
348    /// Cross-encoder pair rerank: score query against PREFETCH document texts.
349    /// Not sent to Qdrant as MaxSim — executor scores client-side then reorders.
350    CrossRerank {
351        /// Query string scored against each document.
352        query: String,
353        /// Cross-encoder model id (e.g. bge-reranker-base).
354        model: String,
355        /// Payload field holding document text (default `"text"` at resolve time).
356        field: Option<String>,
357        /// Candidate stages whose documents are reranked.
358        prefetch: Vec<Prefetch>,
359        /// Parameter placeholder (`:name` or `?idx`) when the query was not provided as a literal string.
360        #[cfg_attr(
361            feature = "serde",
362            serde(default, skip_serializing_if = "Option::is_none")
363        )]
364        query_param: Option<String>,
365    },
366}
367
368/// `PARAMS (quantization = {…})` overrides for quantized index search.
369#[derive(Debug, Clone, PartialEq, Default)]
370#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
371pub struct QuantizationSearchParams {
372    /// Ignore quantized data and search the original vectors.
373    pub ignore: Option<bool>,
374    /// Rescore candidates with original vectors when available.
375    pub rescore: Option<bool>,
376    /// Oversampling factor for candidate retrieval.
377    pub oversampling: Option<f64>,
378}
379
380/// `PARAMS (…)` execution knobs for a query.
381#[derive(Debug, Clone, PartialEq, Default)]
382#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
383pub struct SearchParams {
384    /// HNSW candidate list size (`hnsw_ef`).
385    pub hnsw_ef: Option<u64>,
386    /// Force exact (brute-force) search.
387    pub exact: Option<bool>,
388    /// Enable or disable ACORN filter-aware search.
389    pub acorn: Option<bool>,
390    /// ACORN selectivity ceiling in (0, 1]. Only valid with `acorn = true`.
391    pub max_selectivity: Option<f64>,
392    /// Restrict the search to indexed points only.
393    pub indexed_only: Option<bool>,
394    /// Quantization search overrides.
395    pub quantization: Option<QuantizationSearchParams>,
396    /// RRF smoothing constant `k`.
397    pub rrf_k: Option<u64>,
398    /// Per-prefetch RRF weights.
399    pub rrf_weights: Option<Vec<f64>>,
400    /// Per-query IDF corpus for sparse vectors. `None` = collection-wide (global).
401    pub idf: Option<IdfParams>,
402    /// Request-level timeout in **seconds** (OpenAPI query param / proto field).
403    /// Not part of body `SearchParams`.
404    pub timeout: Option<u64>,
405    /// Request-level read consistency (OpenAPI query param / proto field).
406    pub consistency: Option<ReadConsistency>,
407}
408
409/// Sparse-vector IDF scope.
410///
411/// `corpus = None` is collection-wide (`PARAMS (idf = 'global')`). Otherwise
412/// IDF statistics are computed over points matching the QQL filter
413/// (`PARAMS (idf = WHERE tenant_id = 'acme')`). The planner lowers the filter
414/// to a Qdrant `Filter`; the language never takes a JSON corpus object.
415#[derive(Debug, Clone, PartialEq)]
416#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
417pub struct IdfParams {
418    /// Corpus as a QQL filter. `None` = global collection statistics.
419    pub corpus: Option<FilterExpr>,
420}
421
422/// `WITH PAYLOAD` / `WITH VECTOR` result projection of a query.
423#[derive(Debug, Clone, PartialEq, Default)]
424#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
425pub struct QueryOutput {
426    /// Payload selector; `None` defaults to all payload fields.
427    pub payload: Option<PayloadSelector>,
428    /// Vector selector; `None` returns no vectors.
429    pub vectors: Option<VectorSelector>,
430}
431
432/// `GROUP BY field [SIZE n] [LOOKUP FROM c [WITH PAYLOAD …] [WITH VECTOR …]]` settings.
433#[derive(Debug, Clone, PartialEq)]
434#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
435pub struct GroupSpec {
436    /// Payload field used as the group key.
437    pub field: String,
438    /// Source span of the group-key field token (`GROUP BY <here>`).
439    ///
440    /// Skipped by serde so AST snapshots stay span-free; the planner threads
441    /// it into `QQL-PLAN-GROUP` instead of returning `None`.
442    #[cfg_attr(feature = "serde", serde(skip))]
443    pub field_span: Option<crate::error::Span>,
444    /// Maximum hits per group.
445    pub size: Option<u64>,
446    /// Optional collection used to resolve group values.
447    pub lookup: Option<GroupLookup>,
448}
449
450/// `LOOKUP FROM <collection>` group-value resolution with result selectors
451/// (OpenAPI `WithLookup`: bare name or `{collection, with_payload, with_vectors}`).
452#[derive(Debug, Clone, PartialEq)]
453#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
454pub struct GroupLookup {
455    /// Collection providing the looked-up points.
456    pub collection: String,
457    /// Payload selector applied to looked-up points.
458    pub payload: Option<PayloadSelector>,
459    /// Vector selector applied to looked-up points.
460    pub vectors: Option<VectorSelector>,
461}
462
463/// `LIMIT` / `OFFSET` result paging.
464#[derive(Debug, Clone, PartialEq, Default)]
465#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
466pub struct PageSpec {
467    /// Maximum number of results (or groups).
468    pub limit: Option<u64>,
469    /// Number of results (or groups) to skip.
470    pub offset: Option<u64>,
471    /// Parameter name for limit (e.g. `:lim`), if unbound.
472    #[cfg_attr(
473        feature = "serde",
474        serde(default, skip_serializing_if = "Option::is_none")
475    )]
476    pub limit_param: Option<String>,
477    /// Parameter name for offset (e.g. `:off`), if unbound.
478    #[cfg_attr(
479        feature = "serde",
480        serde(default, skip_serializing_if = "Option::is_none")
481    )]
482    pub offset_param: Option<String>,
483    /// Source span of the limit parameter placeholder, if unbound.
484    #[cfg_attr(
485        feature = "serde",
486        serde(default, skip_serializing_if = "Option::is_none")
487    )]
488    pub limit_span: Option<crate::error::Span>,
489    /// Source span of the offset parameter placeholder, if unbound.
490    #[cfg_attr(
491        feature = "serde",
492        serde(default, skip_serializing_if = "Option::is_none")
493    )]
494    pub offset_span: Option<crate::error::Span>,
495}
496
497/// One named common table expression: `name AS (QUERY …)`.
498#[derive(Debug, Clone, PartialEq)]
499#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
500pub struct Cte {
501    /// CTE name, referenced case-insensitively by prefetches.
502    pub name: String,
503    /// The CTE's query body.
504    pub query: Box<QueryStmt>,
505}
506
507/// A full `QUERY` statement: CTEs, expression, clauses, and output options.
508#[derive(Debug, Clone, PartialEq)]
509#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
510pub struct QueryStmt {
511    /// Leading `WITH` definitions.
512    pub ctes: Vec<Cte>,
513    /// Target collection (explicit or inherited).
514    pub collection: QueryCollection,
515    /// Source span of the `FROM <collection>` name token, when explicit.
516    ///
517    /// `None` for inherited collections (no token exists) and for
518    /// programmatically built statements. Skipped by serde so AST snapshots
519    /// stay span-free; the planner threads it into `QQL-PLAN-COLLECTION`
520    /// instead of returning `None`.
521    #[cfg_attr(feature = "serde", serde(skip))]
522    pub collection_span: Option<crate::error::Span>,
523    /// Retrieval strategy body.
524    pub expression: QueryExpr,
525    /// `WHERE` filter.
526    pub filter: Option<Box<FilterExpr>>,
527    /// `PARAMS (…)` execution settings.
528    pub params: Option<SearchParams>,
529    /// `SCORE THRESHOLD` minimum score.
530    pub score_threshold: Option<f64>,
531    /// `GROUP BY` settings.
532    pub group: Option<GroupSpec>,
533    /// `WITH PAYLOAD` / `WITH VECTOR` projection.
534    pub output: QueryOutput,
535    /// `LIMIT` / `OFFSET` paging.
536    pub page: PageSpec,
537    /// `SHARD '<key>'` routing for tenant-partitioned collections.
538    pub shard_key: Option<super::ShardKey>,
539}