上一篇确立了 Data View 作为查询字段基础的角色——它把一组索引的 mapping 抽象成带格式化和运行时字段的字段表,Kibana 的所有查询都从这张表出发。这一篇进入查询翻译层。查询翻译容易被误解成"KQL 就是 Elasticsearch 的查询语言"。更准确的说法是:KQL 是 Kibana 自己定义的语法,它在客户端被解析成 AST,再翻译成 Elasticsearch Query DSL 的 JSON,然后通过 Search Source 这个内部查询构建器统一组装后发给 ES。本文只抓一个问题:三种查询语法怎么翻译到底层 DSL,过滤器 pin 与 negate 发生在哪一层,查询上下文与过滤上下文的区别对最终结果有什么影响。

翻译管道全局模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
用户输入层
├── KQL (Kibana Query Language) e.g. status:200 AND method:GET
├── Lucene query string e.g. status:200 AND method:GET (相同写法, 不同解析器)
└── 过滤器 pills 点击字段值产生, 可 pin / negate / disable

│ 解析

AST (抽象语法树, 仅 KQL 路径)
├── Comparison node { field: status, op: eq, value: 200 }
└── Comparison node { field: method, op: eq, value: GET }

│ 翻译

Query DSL 片段
├── KQL 路径 → bool.filter[].term / match_phrase / range
├── Lucene 路径 → bool.must[].query_string { query: "..." }
└── 过滤器 pills → bool.filter[] 各自独立

│ 组装 (Search Source)

Search Source 对象 (内部状态)
├── query { query: DSL片段, language: kql | lucene }
├── filters[] 过滤器数组, 每条带 meta { negate, disabled, pinned }
├── aggs 可选聚合定义
└── _source / fields / sort / size / ...

│ 序列化

ES /_search 请求体 (JSON)
bool
├── must/filter 来自 query + filters
├── must_not 来自 negate 过滤器
└── should (极少, 仅特殊查询模式)

KQL 语法与 AST

KQL 是自 Kibana 6.3 起引入的 Kibana 专有语法,目标是在不暴露 Query DSL 细节的前提下提供常用过滤能力。

基本字段匹配语法:

1
2
3
4
5
status:200                    → term query on status
status:2* → wildcard query
message:"connection refused" → match_phrase query
bytes:>1024 → range query (gt: 1024)
bytes:[1024 TO 2048] → range query (gte: 1024, lte: 2048)

逻辑组合(AND / OR / NOT 均大写,是 KQL 关键字):

1
2
3
4
status:200 AND method:GET
status:200 OR status:201
NOT status:5*
(status:200 OR status:201) AND method:POST

嵌套字段(针对 object / nested 类型字段):

1
2
user.name:alice
tags:{ label:error AND severity:high } → nested query with path

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
2
3
4
5
6
7
8
9
10
11
12
SearchSource {
index Data View 对象 (决定查哪个索引)
query { query: <kql/lucene string>, language: 'kuery'|'lucene' }
filters[] [ FilterMeta + DSL片段 ]
aggs ES aggregations 定义
size 返回文档数
sort 排序字段
fields[] 返回字段列表
_source _source include/exclude
searchAfter 翻页游标 (search_after)
trackTotalHits 是否精确计数
}

Search Source 支持父子继承:一个子 Search Source 可以 inherit 父对象的字段,自身只覆盖差异部分。Discover 里主查询和直方图子查询之间就用这种继承关系共享 Data View、query、filters,只在 aggs 和 size 上各自独立。

过滤器 pills:pin、negate、disable

Discover 搜索栏下方的每一个过滤器 pill 不只是 DSL 片段,还附带一段 meta 控制它如何参与查询:

1
2
3
4
5
6
7
8
9
10
11
12
Filter {
meta {
index Data View id (过滤器绑定到哪个 Data View)
negate false / true (正向 / 取反,映射到 bool.filter / bool.must_not)
disabled false / true (是否暂时跳过)
pinned false / true (是否固定到所有 Data View,而非当前 Data View)
type 'phrase' | 'phrases' | 'range' | 'exists' | 'query_string' | 'custom'
key 字段名 (用于展示 pill 标签)
value 展示用的值字符串
}
query { 实际 DSL 片段 }
}

negate 影响 bool 子句的槽位:普通过滤器进 bool.filter,negate 过滤器进 bool.must_not

pin 的语义:pinned 过滤器跟随用户在 Data View 之间切换,不会随切换消失;非 pinned 过滤器绑定当前 Data View,切走后自动清除。

disable 的语义:disabled 过滤器在序列化时被跳过,但保留在 UI 状态里,随时可以重新启用。

查询上下文 vs 过滤上下文

这个区别在 ES 层面,但直接影响 KQL 的翻译结果。

查询上下文(bool.mustbool.should):ES 计算相关性评分(_score),文档按分值排序,用于全文检索场景。

过滤上下文(bool.filterbool.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
2
3
4
5
6
7
8
9
{
"range": {
"@timestamp": {
"gte": "2026-08-05T00:00:00Z",
"lte": "2026-08-06T00:00:00Z",
"format": "strict_date_optional_time"
}
}
}

这条 filter 和过滤器 pills 里的其他 filter 一起进入 bool.filter。时间范围作为 filter 而非 must,是 KQL 翻译策略的延伸——时间是典型的结构化过滤,不应影响相关性评分,也适合 filter cache。

实验:用 Inspect 面板捕获 Query DSL

以下步骤在任意 Kibana 实例(自 Kibana 7.x 起有效,8.x 默认启用 Inspect)上可执行:

  1. 打开 Discover,确认已选择一个 Data View。

  2. 在搜索栏输入 KQL 查询:

    1
    status:200 AND method:GET
  3. 点击搜索栏右侧的 Inspect 按钮(放大镜图标),打开 Inspect 面板,切换到 “Requests” 标签。

  4. 找到名为 “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": []
    }
    }
    }
  5. 在过滤器 pills 区域右键一个 pill,选择 “Exclude results”(即 negate),再次查看 Inspect 请求体,确认该 pill 对应的 DSL 片段从 filter 移入了 must_not

  6. 切换搜索语言为 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
2
3
4
5
6
7
8
模式:在用户输入层和存储查询层之间插入查询语言 + 翻译层

- 用户面对的是领域友好的语法 (KQL / Lucene)
- 底层统一用存储的原生查询语言 (ES Query DSL)
- 翻译层做结构保证:确保所有过滤走 filter context,享受 cache 红利
- 过滤器 pill 的元信息 (negate / pin / disable) 是查询状态机的一部分,
不是 UI 装饰,它们直接决定 DSL 的 bool 子句分配
- Search Source 是查询状态的单一出口,防止翻译逻辑散落各处

工程迁移表

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 面板里。

练习

  1. 在 Discover 里分别用 KQL 和 Lucene 写同一个条件(如 status:200),打开 Inspect 对比两条请求的 query.bool 结构——确认 KQL 结果在 filter[],Lucene 结果在 must[]

  2. 手动添加一个过滤器 pill(点击字段值),再把它设为 negate,再设为 disable,每次操作后打开 Inspect 观察请求体变化——验证三种状态对应的 DSL 位置分别是 filtermust_not、缺席。

  3. 思考题: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 演进、生态与对比 后续阶段

参考资料