上一篇展示了 Discover 的执行模型——三类并发请求共享一个父 Search Source,文档检索与直方图各自用子 Search Source 覆盖差异。这一篇进入聚合式可视化。聚合式可视化容易被误解成"拖几个字段选一种图表类型"。更准确的说法是:Visualize 里的每一张图都是对 Elasticsearch 聚合 API 的一次声明——图表类型只是渲染方式,图形背后的数据来自一棵 ES aggregation tree;bucket 聚合决定 X 轴的分组方式,metric 聚合决定 Y 轴的数值,嵌套 bucket 就是树的多层分叉。本文只抓一个问题:bucket 嵌套如何映射到 ES 的 aggregation 树结构,以及 Visualize 作为这条旧路的设计模型——因为理解它,才能理解 Lens 为什么要重新设计。

每张图 = 一次聚合查询

Visualize 编辑器里,用户通过"Buckets"和"Metrics"两组配置描述一张图。这组配置直接对应 ES 的 aggregations 请求体:

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
34
35
用户在 Visualize 里的配置
Metrics
└── Y-axis: Average of bytes → metric agg: avg { field: bytes }

Buckets
├── X-axis: Date Histogram on @timestamp → bucket agg: date_histogram
└── Split series: Terms on status → bucket agg: terms { field: status }

│ 翻译

ES _search 请求 aggregations 段
{
"aggs": {
"date_histogram_@timestamp": {
"date_histogram": {
"field": "@timestamp",
"fixed_interval": "1h"
},
"aggs": { ← 嵌套在外层 bucket 里
"terms_status": {
"terms": {
"field": "status",
"size": 5
},
"aggs": { ← 嵌套在内层 bucket 里
"avg_bytes": {
"avg": { "field": "bytes" }
}
}
}
}
}
},
"size": 0
}

聚合树的深度等于 bucket 嵌套层数加一(metric 层)。Visualize 的"Split series"就是在当前 bucket 下再嵌一层 terms bucket;"Split chart"是在最外层再嵌一层 bucket,生成多张并排小图。

Bucket 聚合 vs Metric 聚合

ES 的聚合分两大类,Visualize 把这两类直接暴露给用户。

Bucket 聚合(分组器)——把文档集合切分成桶,每个桶是一个子文档集合:

1
2
3
4
5
6
7
date_histogram   按时间字段分桶(分钟/小时/天)
terms 按字段的 top N 个唯一值分桶
range 按数值范围分桶(自定义区间)
histogram 按数值字段等距分桶
filters 按查询条件分桶(每个 filter 一个桶)
geohash_grid 按地理位置分桶(地图可视化用)
significant_terms 统计上显著区别于背景分布的 term

Metric 聚合(度量器)——对一个桶内的文档集合计算一个数值:

1
2
3
4
5
6
7
8
count            文档数(所有桶天然自带,无需显式配置)
avg 均值
sum 求和
min / max 最小/最大值
cardinality 近似去重计数(HyperLogLog++)
percentiles 百分位数(TDigest 或 HDR 直方图)
extended_stats 方差、标准差等统计量
top_hits 桶内 top N 条文档(通常用于取代表性样本)

Pipeline 聚合(后处理器)——在已有聚合结果上做二次计算:

1
2
3
4
5
moving_avg / moving_fn   移动平均(需上层 date_histogram)
derivative 相邻桶之间的差值(变化率)
cumulative_sum 累计求和
bucket_script 对多个 metric 做自定义计算(如 A/B)
bucket_selector 按条件过滤桶(having 语义)

Visualize 的 TSVB(Time Series Visual Builder)和 Vega 可视化能使用更复杂的 pipeline 聚合;标准 Visualize 编辑器只暴露了最常用的子集。

Aggregation Tree 的映射规则

Visualize 把用户配置翻译成 aggregation tree 时遵循一套固定规则:

1
2
3
4
5
6
7
Visualize 配置顺序         ES aggregation tree 结构
─────────────────────────────────────────────────────
Buckets(从上到下排列) 外层 bucket → 内层 bucket → ... (深度优先嵌套)
Split chart 最外层 bucket(aggs 根节点)
X-axis / Rows 次外层 bucket
Split series / Columns 更内层 bucket
Metrics 最内层(叶节点),每个叶节点 metric

这意味着:Buckets 配置里的顺序决定了 ES 里的嵌套顺序,改变顺序会产生不同的聚合树,返回的数据结构也不同,即使 metric 结果"数值上相同",行列关系会变。

一个直接推论:"Split chart"和"Split series"的本质差异是 bucket 在树里的深度不同。Split chart 把变量放在外层,导致每个外层桶渲染成独立小图;Split series 把变量放在内层,导致同一个外层桶里的不同 term 渲染成同一张图里的多条线。

图表类型是渲染层

Visualize 支持的图表类型(Bar/Line/Area/Pie/Data Table 等)是对同一份聚合结果的不同渲染方式,不影响发往 ES 的聚合请求:

1
2
3
4
5
6
聚合结果(时间桶 × status 桶 × avg_bytes)

├── 渲染为 Bar chart → date_histogram 在 X 轴,status 在系列颜色
├── 渲染为 Line chart → 同上,折线代替柱状
├── 渲染为 Data Table → 每个桶 = 一行,metric = 一列
└── 渲染为 Pie chart → 只用最后一层 bucket 和 metric

这个"聚合与渲染分离"的设计意味着切换图表类型不需要重新发 ES 请求(已缓存的聚合结果直接复用),但也意味着某些图表类型对 bucket 层数有隐含约束(Pie 只展示一层 bucket,强行加多层会只显示最内层)。

Lens(Kibana 7.5 起逐步替代 Visualize 的编辑器)打破了这个绑定:Lens 允许用户先选择"维度"和"度量",编辑器自动推断合适的图表类型和聚合结构,而不是要求用户手动配置 bucket/metric 的嵌套顺序。Visualize 的这条旧路依然存在,但新建图表推荐用 Lens。

Visualize 的 Saved Object 结构

保存一张 Visualize 图,写入一个 type: visualization 的 Saved Object,关键的 attributes 字段是 visState(JSON 字符串):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
visState {
type 图表类型: "histogram" | "line" | "pie" | "table" | ...
aggs[] 聚合配置数组,每条对应一个 agg:
{
id: "1",
type: "avg",
schema: "metric", ← "metric" | "segment" | "group" | "split"
params: { field: "bytes" }
}
{
id: "2",
type: "date_histogram",
schema: "segment", ← X 轴
params: { field: "@timestamp", interval: "auto" }
}
params 渲染参数:颜色、图例位置、Y 轴标签 ...
}

schema 字段把每个 agg 映射到图表里的角色:segment = X 轴 bucket,group = Split series bucket,split = Split chart bucket,metric = Y 轴度量。ES 请求的嵌套顺序由 schema 决定,而不是 aggs 数组顺序。

这个 Saved Object 的 references 里有一条指向所用 Data View 的引用,和 Saved Search 的结构一致。

实验:用 Inspect 捕获聚合 JSON

以下步骤在 Kibana 7.x / 8.x 的 Visualize 编辑器中可执行:

  1. 在 Visualize 里新建一张 Bar chart,选择一个包含数值字段的 Data View(例如日志 Data View,含 bytesstatus 字段)。

  2. 配置:

    • Metrics → Y-axis → Aggregation: Average,Field: bytes
    • Buckets → X-axis → Aggregation: Date Histogram,Field: @timestamp
    • 点击 Add sub-buckets → Split series → Aggregation: Terms,Field: status,Size: 5
  3. 点击"Update"刷新图表,然后点击 Inspect 按钮,切换到"Requests"标签,展开请求体。

  4. 在请求体的 aggs 段,验证:

    • 外层是 date_histogram,字段是 @timestamp
    • 嵌套在 date_histogram 内的是 terms,字段是 status
    • 嵌套在 terms 内的是 avg,字段是 bytes
    • 请求 size: 0(不返回文档)
  5. 在 Buckets 配置面板里,将 Split series 上移到 X-axis 的上面(改变 bucket 顺序),再次 Update,观察聚合树是否外层变成了 terms、内层变成了 date_histogram——验证顺序决定嵌套深度。

  6. 添加一条 Pipeline 聚合:Y-axis → Add metric → Aggregation: Moving Average,选择上面的 Average metric 作为源。再次检查请求体,找到 moving_avg(或 moving_fn)出现在 pipeline_aggs 位置。

聚合嵌套示意(多层 bucket)

1
2
3
4
5
6
7
8
9
10
11
12
13
terms(status)                          ← 外层 bucket (Split chart)
├── bucket: status=200
│ ├── date_histogram(@timestamp) ← 内层 bucket (X-axis)
│ │ ├── bucket: 2026-08-06T00
│ │ │ └── avg(bytes) = 1024 ← metric (Y-axis)
│ │ ├── bucket: 2026-08-06T01
│ │ │ └── avg(bytes) = 980
│ │ └── ...
└── bucket: status=500
├── date_histogram(@timestamp)
│ ├── bucket: 2026-08-06T00
│ │ └── avg(bytes) = 256
│ └── ...

渲染为 Split chart 时:status=200 和 status=500 各生成一张独立小图,每张图 X 轴为时间,Y 轴为 avg(bytes)。

模式提炼

1
2
3
4
5
6
7
8
模式:图表 = 聚合树声明 + 渲染配置,两者解耦

- 聚合树(bucket 嵌套 + metric)是数据侧的声明,直接映射到 ES aggregation API
- 图表类型是渲染侧的配置,决定如何把聚合结果画出来
- bucket 嵌套顺序决定数据的分组维度优先级;改顺序 = 改数据结构
- size: 0 是聚合查询的标准模式:只要统计结果,不要原始文档
- pipeline 聚合是对聚合结果的后处理,相当于 SQL 里的 window function
- 把"图表配置"序列化为 Saved Object,实现图表的跨会话复用和 Dashboard 嵌入

工程迁移表

Visualize / ES 聚合概念 工程类比
bucket agg (terms / date_histogram) SQL GROUP BY field1, field2
metric agg (avg / sum / cardinality) SQL AVG() / SUM() / COUNT(DISTINCT)
嵌套 bucket = aggregation tree SQL 多级 GROUP BY / pivot 表
pipeline agg (moving_avg, derivative) SQL window function (AVG() OVER)
Split chart vs Split series SQL GROUP BY 外层维度 vs 内层维度
size: 0 SQL SELECT ... GROUP BY(无 SELECT *
visState aggs[] schema Grafana panel query 的 group by / metric 配置
Visualize Saved Object Grafana Dashboard panel JSON
电子表格 pivot 表 行/列/值 = bucket1/bucket2/metric

常见误解

误解一:“切换图表类型会重新发请求”。切换图表类型(Bar → Line → Area)只改变渲染层,聚合请求结果被缓存复用,不触发新的 ES 请求。只有改变 Buckets / Metrics 配置并点击 Update 才会重新发请求。

误解二:“terms 聚合返回所有唯一值”。terms 聚合默认只返回 top N(默认 size: 10)个桶,按文档数降序排列;其余的归入 sum_other_doc_count。如果字段基数很高(如 user_id),调大 size 会显著增加 ES 内存开销。Visualize 里的 Terms bucket 配置里的"Size"选项直接对应这个参数。

误解三:“Visualize 已经被废弃,不需要了解它”。当前(自 Kibana 8.x 起)Visualize 仍然存在,旧版 Dashboard 里的 Visualize 类型面板仍然跑在 Visualize 引擎上。Lens 是推荐的新建方式,但大量存量图表仍是 Visualize 格式。更重要的是:Visualize 的 bucket/metric 模型和 aggregation tree 的对应关系,是理解 Lens 为什么要引入"维度"抽象、为什么能自动推断图表类型的前置知识。

误解四:“cardinality 聚合是精确去重”。cardinality 使用 HyperLogLog++ 算法,是近似值,默认精度参数 precision_threshold: 3000,误差率约 5%。在基数大于 precision_threshold 时,结果是估算。需要精确去重计数时,只能对文档先做 filter 再 count,或用 composite 聚合逐页枚举。

练习

  1. 在 Visualize 里建一张 Data Table,Buckets 加两层:外层 terms on status(size:3),内层 date_histogram on @timestamp(interval: 1d);Metrics 加 sum of bytes 和 cardinality of session_id(如有该字段)。用 Inspect 确认 aggregation tree 的嵌套结构,然后在 ES Dev Tools 里手动重现同一个请求,对比两者的 JSON。

  2. 交换两个 bucket 的顺序(外层改为 date_histogram,内层改为 terms),观察 Inspect 里聚合树的变化,以及 Data Table 里行列分组的变化——验证顺序 = 树深度 = 分组语义。

  3. 思考题:Grafana 的 panel 查询模型(group by 时间 + 聚合函数)和 Visualize 的 bucket/metric 模型有什么结构上的同构?两者都能做"按时间分桶 + 按字段拆系列 + 指定 metric",但 Grafana 支持多种数据源,Visualize 深度绑定 ES。这个差异在工程上意味着什么取舍?(提示:第 16 篇会展开对比。)

系列导航

序号 主题 状态
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 Lens:字段拖拽与自动推断的新路 下一篇
08 TSVB:时序可视化的专用编辑器
09 Dashboard:Embeddable 组合与容器协议
10-12 告警、上报与自动化 后续阶段
13-15 平台、安全与扩展 后续阶段
16-17 演进、生态与对比 后续阶段

参考资料