深入 Kibana 04 - Search Source 与查询翻译:KQL、Lucene 与 Query DSL
上一篇确立了 Data View 作为查询字段基础的角色——它把一组索引的 mapping 抽象成带格式化和运行时字段的字段表,Kibana 的所有查询都从这张表出发。这一篇进入查询翻译层。查询翻译容易被误解成"KQL 就是 Elasticsearch 的查询语言"。更准确的说法是:KQL 是 Kibana 自己定义的语法,它在客户端被解析成 AST,再翻译成 Elasticsearch Query DSL 的 JSON,然后通过 Search Source 这个内部查询构建器统一组装后发给 ES。本文只抓一个问题:三种查询语法怎么翻译到底层 DSL,过滤器 pin 与 negate 发生在哪一层,查询上下文与过滤上下文的区别对最终结果有什么影响。
翻译管道全局模型
1 | |
KQL 语法与 AST
KQL 是自 Kibana 6.3 起引入的 Kibana 专有语法,目标是在不暴露 Query DSL 细节的前提下提供常用过滤能力。
基本字段匹配语法:
1 | |
逻辑组合(AND / OR / NOT 均大写,是 KQL 关键字):
1 | |
嵌套字段(针对 object / nested 类型字段):
1 | |
KQL 对比 Lucene query string 的关键差异:KQL 是结构化解析,每一个 token 都知道自己对应的 ES 字段和操作符;Lucene query string 直接把整个字符串当成 query_string 参数传给 ES,由 ES 内部的 Lucene 解析器处理。实践效果相近,但 Lucene 路径下的 * 会扩展成所有字段的通配,行为更难预测;KQL 把这个扩展限定在已知字段范围内。
Search Source:查询构建器对象
Search Source 是 Kibana 服务端和前端共用的一个内部对象(@kbn/data-plugin 里的 SearchSource 类),作用是把若干来源的查询片段合并成一个合法的 ES 请求体。
对象的核心字段:
1 | |
Search Source 支持父子继承:一个子 Search Source 可以 inherit 父对象的字段,自身只覆盖差异部分。Discover 里主查询和直方图子查询之间就用这种继承关系共享 Data View、query、filters,只在 aggs 和 size 上各自独立。
过滤器 pills:pin、negate、disable
Discover 搜索栏下方的每一个过滤器 pill 不只是 DSL 片段,还附带一段 meta 控制它如何参与查询:
1 | |
negate 影响 bool 子句的槽位:普通过滤器进 bool.filter,negate 过滤器进 bool.must_not。
pin 的语义:pinned 过滤器跟随用户在 Data View 之间切换,不会随切换消失;非 pinned 过滤器绑定当前 Data View,切走后自动清除。
disable 的语义:disabled 过滤器在序列化时被跳过,但保留在 UI 状态里,随时可以重新启用。
查询上下文 vs 过滤上下文
这个区别在 ES 层面,但直接影响 KQL 的翻译结果。
查询上下文(bool.must,bool.should):ES 计算相关性评分(_score),文档按分值排序,用于全文检索场景。
过滤上下文(bool.filter,bool.must_not):ES 不计算评分,只做是/否判断,结果可缓存,性能更好。用于结构化过滤场景。
KQL 的翻译策略:KQL 把所有条件翻译成 bool.filter,不参与评分,这是有意为之——日志和指标场景不需要相关性排序,只需要精确过滤,filter 上下文性能更优。时间范围也作为 filter 自动附加。
Lucene query string 路径则走 bool.must[query_string],属于查询上下文,会影响评分。在 Kibana 里通常感知不到差异(因为 Kibana 默认按时间排序而非分值排序),但了解这个差异有助于调试 explain API 的输出。
时间范围的隐式过滤
Discover 和 Dashboard 上的时间选择器(Time Picker)生成的时间范围,不是通过搜索框写入的,而是 Search Source 在序列化时自动附加的一条 range filter:
1 | |
这条 filter 和过滤器 pills 里的其他 filter 一起进入 bool.filter。时间范围作为 filter 而非 must,是 KQL 翻译策略的延伸——时间是典型的结构化过滤,不应影响相关性评分,也适合 filter cache。
实验:用 Inspect 面板捕获 Query DSL
以下步骤在任意 Kibana 实例(自 Kibana 7.x 起有效,8.x 默认启用 Inspect)上可执行:
-
打开 Discover,确认已选择一个 Data View。
-
在搜索栏输入 KQL 查询:
1
status:200 AND method:GET -
点击搜索栏右侧的 Inspect 按钮(放大镜图标),打开 Inspect 面板,切换到 “Requests” 标签。
-
找到名为 “Documents” 的请求,展开 “Request” 部分,可以看到实际发往 ES 的请求体。预期结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14{
"query": {
"bool": {
"must": [],
"filter": [
{ "match_phrase": { "method": "GET" } },
{ "term": { "status": 200 } },
{ "range": { "@timestamp": { "gte": "...", "lte": "..." } } }
],
"should": [],
"must_not": []
}
}
} -
在过滤器 pills 区域右键一个 pill,选择 “Exclude results”(即 negate),再次查看 Inspect 请求体,确认该 pill 对应的 DSL 片段从
filter移入了must_not。 -
切换搜索语言为 Lucene(点击搜索栏左侧的 KQL 标签可切换),保持相同的查询文本,再次捕获请求体。对比 KQL 路径(
bool.filter[term])和 Lucene 路径(bool.must[query_string])的差异。
翻译结果对照表
| KQL 片段 | 翻译后 DSL | bool 槽位 |
|---|---|---|
status:200 |
term: { status: 200 } |
filter |
message:"timeout" |
match_phrase: { message: "timeout" } |
filter |
bytes:>1024 |
range: { bytes: { gt: 1024 } } |
filter |
status:2* |
wildcard: { status: "2*" } |
filter |
NOT status:500 (negate pill) |
term: { status: 500 } |
must_not |
| 时间范围 | range: { @timestamp: {...} } |
filter |
Lucene status:200 |
query_string: { query: "status:200" } |
must |
模式提炼
1 | |
工程迁移表
| Kibana 概念 | 工程类比 |
|---|---|
| KQL → AST → Query DSL | GraphQL → AST → SQL / 存储查询 |
| Search Source 对象 | ORM QueryBuilder (Hibernate Criteria, Django ORM) |
| 过滤器 negate | SQL NOT IN / WHERE NOT |
| 过滤器 disabled | Query builder 中临时注释掉的条件 |
| filter context vs query context | SQL WHERE (无评分) vs 全文检索评分字段 |
| Lucene query string 路径 | ORM 的 raw SQL escape-hatch |
| 时间范围自动附加 filter | 多租户系统自动附加 tenant_id filter |
| Grafana query editor → datasource query | 与 KQL → Query DSL 同构,仅目标查询语言不同 |
常见误解
误解一:“KQL 是 Elasticsearch 的原生查询语言”。KQL 是 Kibana 层定义的语法,ES 内部完全不知道 KQL 的存在,它只看到最终的 Query DSL JSON。另一种有时被混淆的是 EQL(Event Query Language),EQL 是 ES 原生支持的事件序列查询语言,针对时序事件关联分析,走 /_eql/search 端点,与 KQL 的应用场景和翻译机制完全不同。
误解二:“KQL 和 Lucene 查询结果相同,只是语法不同”。语法上很多写法相同,但翻译后的 DSL 结构不同:KQL 走 filter context,Lucene 走 query context。在纯过滤场景(日志查询)下结果集相同,但评分行为和缓存效率不同。Lucene 路径的 * 通配也更激进。
误解三:“过滤器 pill 就是搜索框里写的条件”。两者都进入最终的 bool.filter,但来源和状态机不同。搜索框内容走 KQL/Lucene 翻译管道,存在 Search Source 的 query 字段;过滤器 pill 存在 filters[] 数组,每条有独立的 meta(negate/disable/pin)。这个区别在保存 Saved Search 时很重要:两者都会保存进 Saved Object,加载时也各自还原。
误解四:“时间选择器是独立的,不影响查询”。时间选择器产生的时间范围在 Search Source 序列化时作为一条 range filter 附加进 bool.filter,和用户手动加的 pill 是同一类型的过滤器,会被计入 filter cache,也会出现在 Inspect 面板里。
练习
-
在 Discover 里分别用 KQL 和 Lucene 写同一个条件(如
status:200),打开 Inspect 对比两条请求的query.bool结构——确认 KQL 结果在filter[],Lucene 结果在must[]。 -
手动添加一个过滤器 pill(点击字段值),再把它设为 negate,再设为 disable,每次操作后打开 Inspect 观察请求体变化——验证三种状态对应的 DSL 位置分别是
filter、must_not、缺席。 -
思考题:Search Source 的父子继承模式(Discover 主查询继承到直方图子查询)和 CSS 的层叠继承、ORM 的 scope 链有什么结构上的相似?在其他查询构建器场景里,哪些条件适合放在父级(全局 filter),哪些放在子级(局部 aggs 参数)?
系列导航
| 序号 | 主题 | 状态 |
|---|---|---|
| 00 | 导读:Kibana 的状态存在 Elasticsearch 里 | 已发布 |
| 01 | Kibana 架构:浏览器、Node.js server 与 New Platform | |
| 02 | Saved Object:Kibana 一切状态的统一模型 | |
| 03 | Data View(Index Pattern):查询之前的字段抽象 | |
| 04 | Search Source 与查询翻译:KQL、Lucene 与 Query DSL | 本篇 |
| 05 | Discover:交互式检索的执行模型 | 下一篇 |
| 06 | 聚合式可视化的老路:Visualize 与 bucket/metric | |
| 07-09 | Lens / TSVB / Dashboard | 后续阶段 |
| 10-12 | 告警、上报与自动化 | 后续阶段 |
| 13-15 | 平台、安全与扩展 | 后续阶段 |
| 16-17 | 演进、生态与对比 | 后续阶段 |
参考资料
- Kibana 官方文档 KQL:https://www.elastic.co/guide/en/kibana/current/kuery-query.html
- Kibana 官方文档 Lucene query syntax:https://www.elastic.co/guide/en/kibana/current/lucene-query.html
- ES Query DSL bool query:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
- ES Query vs filter context:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html
- Kibana 源码 SearchSource:https://github.com/elastic/kibana/tree/main/src/plugins/data/common/search/search_source
- EQL(Event Query Language,与 KQL 的区别):https://www.elastic.co/guide/en/elasticsearch/reference/current/eql.html
