深入 Kibana 04 - Search Source 与查询翻译:KQL、Lucene 与 Query DSL
在 Kibana 查询栏输入的 KQL 并不会原样交给 Elasticsearch。KQL、Lucene query string 和过滤器 pills 会先在 Kibana 内部转换,再由 Search Source 组装成 Query DSL 请求。查询语法、过滤器状态以及 query/filter context 的差异,都在这条翻译链上汇合。
本文讨论的是 Discover 的经典查询模式。切换到 ES|QL 模式后,查询直接面向索引运行,不要求先选择 Data View,也不经过下面这条 KQL/Lucene 翻译链。
翻译管道全局模型
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 请求体。
下面的字段表是概念模型,不是稳定的公开接口清单。当前源码可以确认 setField()、getField()、getFields() 和 fetch$();fetch() 仍在但已标记为 deprecated。插件代码若依赖内部形状而不是这些现有接口,升级时更容易被 Kibana 的内部重构打断。
对象的核心字段:
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 参数)?
系列导航
参考资料
- 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/platform/plugins/shared/data/common/search/search_source
- Discover 中使用 ES|QL:https://www.elastic.co/docs/explore-analyze/discover/try-esql
- EQL(Event Query Language,与 KQL 的区别):https://www.elastic.co/guide/en/elasticsearch/reference/current/eql.html
