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}