Skip to content

Commit 3709436

Browse files
feat(query): add KQL (Kibana Query Language) support
Adds optional KQL parsing as a thin translation layer at the REST entry point. KQL inputs are parsed and lowered to QueryAst using only existing variants (BoolQuery, FullTextQuery, RangeQuery, FieldPresenceQuery, WildcardQuery, MatchAll, UserInputQuery) — the core enum, visitor traits, tag pruning, and root-search remain unchanged. Wire surface: * Native REST: `?kql=<expr>` on /api/v1/{index}/search, mutually exclusive with the existing `query=` parameter. * Elastic-compat JSON: `{"query": {"kql": {"query": "...", ...}}}` on /api/v1/_elastic/{index}/_search. Documented as a Quickwit-only extension since real Elasticsearch returns parsing_exception. Supported grammar (matches the public Kibana KQL reference): field-value, quoted phrases, bare default-field terms, `*` (match-all fast path), `?`/`*` wildcards, `field:*` exists, `field:>=N` / `>` / `<=` / `<` ranges (numeric literals coerced to JsonLiteral::Number, non-numeric falls back to String), boolean and/or/not (case-insensitive), juxtaposition as implicit AND, parens, `field:(a or b)` value groups with proper field distribution, escape semantics (`\and`, `\:`, `\+`). Safety rails (all return HTTP 400 with specific error messages): * Max KQL input length: 16 KiB (REST layer) * Max parser nesting depth: 64 * Max bare-token length: 1 KiB * Max quoted-phrase length: 4 KiB * `{...}` nested-field syntax rejected (Quickwit has no nested type) * Nested field qualifier inside value group rejected * `query` and `kql` mutually exclusive Observability: * quickwit_kql_parse_total counter * quickwit_kql_parse_failures_total counter * quickwit_kql_parse_duration_seconds histogram * Structured `kql=<bool>`, `tantivy_grammar=<bool>` fields on search log lines so SRE can split KQL vs Tantivy-grammar traffic without parsing raw query strings. OpenAPI: * `query` is now `#[serde(default)]` (semantically optional at the wire layer); utoipa override exposes both `query` and `kql` as `Option<String>` so generated SDK clients no longer encode the obsolete `required: ["query"]` contract. Tests: * 246 unit tests in quickwit-query covering lexer, parser (recursive-descent with depth guard), lowering (with Tantivy-grammar escape handling for default-field deferral), metrics wiring, JSON DSL deserialization, and proptest fuzz (~6k cases) confirming the parser never panics on arbitrary input. * Kibana conformance corpus pinning expected ASTs for each documented KQL idiom + explicit notes on intentional divergences. * REST handler unit tests for `kql`/`query` mutual exclusion, whitespace handling, size caps, and search_fields propagation. * Integration scenarios under rest-api-tests/scenarii/kql_search/ asserting exact hit counts against a known dataset. * Concurrent load harness (load_test.py) mixing happy-path and adversarial shapes; multi-node docker-compose template for distributed root→leaf testing. Line coverage on KQL production code: 95-99% per file; the remaining gaps are defensive code, test panic-guards in let-else patterns, and lazy_counter!/lazy_histogram! macro internals the coverage tool cannot introspect. Files modified outside the new kql/ module: 5 (Cargo.lock, quickwit-cli/src/tool.rs, quickwit-query/Cargo.toml, quickwit-query/src/elastic_query_dsl/mod.rs, quickwit-query/src/lib.rs, quickwit-serve/src/search_api/rest_handler.rs). The core QueryAst enum, QueryAstVisitor, QueryAstTransformer, tag_pruning, and root-search are untouched.
1 parent 9c5a2f2 commit 3709436

24 files changed

Lines changed: 3635 additions & 7 deletions

quickwit/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quickwit/quickwit-cli/src/tool.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ pub async fn local_search_cli(args: LocalSearchArgs) -> anyhow::Result<()> {
542542
let sort_by: SortBy = args.sort_by_field.map(SortBy::from).unwrap_or_default();
543543
let search_request_query_string = SearchRequestQueryString {
544544
query: args.query,
545+
kql: None,
545546
start_offset: args.start_offset as u64,
546547
max_hits: args.max_hits as u64,
547548
search_fields: args.search_fields,

quickwit/quickwit-query/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ rustc-hash = { workspace = true }
2929

3030
quickwit-common = { workspace = true }
3131
quickwit-datetime = { workspace = true }
32+
quickwit-metrics = { workspace = true }
3233
quickwit-proto = { workspace = true }
3334

3435
[dev-dependencies]

quickwit/quickwit-query/src/kql/README.md

Lines changed: 315 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2021-Present Datadog, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/// Parsed KQL expression. This is an internal representation that lowers to
16+
/// `QueryAst` via `lower_kql_ast`.
17+
#[derive(Debug, Clone, PartialEq, Eq)]
18+
pub(crate) enum KqlAst {
19+
/// Conjunction of subqueries. Empty vector is not produced by the parser.
20+
And(Vec<KqlAst>),
21+
/// Disjunction of subqueries.
22+
Or(Vec<KqlAst>),
23+
/// Negation of a subquery.
24+
Not(Box<KqlAst>),
25+
/// `field:value` clause.
26+
FieldValue { field: String, value: KqlValue },
27+
/// `field:<op><value>` numeric/datetime range bound.
28+
FieldRange {
29+
field: String,
30+
op: RangeOp,
31+
value: String,
32+
},
33+
/// `field:*` — checks whether the field is present on the document.
34+
FieldExists { field: String },
35+
/// Bare value with no field qualifier — matches against default fields.
36+
DefaultValue(KqlValue),
37+
}
38+
39+
#[derive(Debug, Clone, PartialEq, Eq)]
40+
pub(crate) enum KqlValue {
41+
/// Unquoted token. May contain `*` and `?` wildcards.
42+
Literal(String),
43+
/// Double-quoted phrase. Wildcards inside are treated literally.
44+
Phrase(String),
45+
}
46+
47+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48+
pub(crate) enum RangeOp {
49+
Gt,
50+
Gte,
51+
Lt,
52+
Lte,
53+
}

0 commit comments

Comments
 (0)