上一篇确立了 Saved Object 作为 Kibana 一切状态的统一持久化模型,type、attributes、references 三段结构解决了跨插件的状态管理问题。这一篇进入第一种具体且重要的 Saved Object 类型:Data View。Data View 容易被误解成"告诉 Kibana 去查哪个索引的配置项"。更准确的说法是:Data View 是一层字段语义抽象,把 ES 里裸的 mapping 信息转换为 Kibana 可以推理的字段模型——带时间轴绑定、带显示格式、带运行时扩展能力。本文只抓一个问题:时间字段绑定、字段格式化、运行时字段三个机制解决了什么,Data View 与直接查索引的本质区别在哪里。

Data View 在链路里的位置

1
2
3
4
5
6
7
8
9
ES 物理索引                Data View (SO)                 Kibana UI
────────────────── ────────────────────────── ──────────────────────
logs-app-2026.08.*title: "logs-app-*" → Discover 字段列表
logs-app-2026.07.* timeField: "@timestamp" KQL 自动补全
metrics-host-* fieldFormats: 时间轴 histogram
bytes → human readable Lens 字段选择器
runtimeFields: Dashboard 变量
hour_of_day: painless
(resolved via field_caps API)

直接对 ES 执行 _search 时,调用方要自行处理字段类型推断、时间范围注入、字段显示格式。Data View 把这些关注点提升到一个可复用的抽象层,所有使用同一 Data View 的 Discover 页、Lens 面板、Dashboard 变量共享同一套字段定义,修改一处全局生效。

命名沿革

自 Kibana 7.x 起该对象的 UI 名称是"Index Pattern",Saved Object 的 type 字段为 index-pattern。自 Kibana 8.0 起 UI 名称改为"Data View",但底层 Saved Object 的 type 字段仍保持 index-pattern 以保证向后兼容,API 路径也同时接受 /api/index_patterns/api/data_views 两个前缀(8.x 主流版本推荐使用 /api/data_views)。

时间字段绑定

每个 Data View 有一个可选的 timeFieldName 属性,指向索引里一个 date 类型字段(通常是 @timestamp)。这个绑定有三个用途:

Discover 的时间轴 histogram 依赖它做 date_histogram 聚合。没有时间字段的 Data View 在 Discover 里不显示时间轴,搜索结果按文档顺序排列而非时间顺序。

时间选择器(time picker)把用户选定的时间范围注入为 ES 查询里的 range filter,filter 的字段名就是 timeFieldName。Kibana 在构造 _search 请求时自动在 query.bool.filter 里追加这个 range 条件,调用方不需要手动拼。

数据可视化和告警规则的"过去 N 分钟"相对时间表达式(now-15m)也通过时间字段解析。告警规则执行时把 now 替换为当前服务器时间,再传给 ES 的 range query。

没有时间字段不代表 Data View 不可用,只是丧失了时间轴和时间过滤能力——适用于静态参考数据(geo lookup 表、配置表)。

字段格式化

ES 存储的 bytes 字段是一个 long,原始值是 1258291。Data View 的字段格式层可以给这个字段配置一个 bytes 格式化器,Discover 里显示为 1.2 MB,用户不需要手写 Painless 脚本。

字段格式化配置存在 Data View 的 attributes 里:

1
2
3
4
5
6
7
8
9
10
{
"fieldAttrs": {
"bytes": {
"format": { "id": "bytes", "params": { "pattern": "0.0b" } }
},
"url.original": {
"format": { "id": "url", "params": { "urlTemplate": "{{value}}", "labelTemplate": "{{value}}" } }
}
}
}

格式化在 Kibana 渲染层发生,不影响 ES 里的原始数据,也不影响查询结果的实际值。_search 返回的 bytes 仍然是 1258291,格式化是 Discover/Lens 在把值写入 DOM 之前做的最后一步转换。

运行时字段

运行时字段(Runtime Field)是 Elasticsearch 7.11 引入的特性,允许在查询时用 Painless 脚本临时计算一个字段的值,不需要重新索引。Data View 提供了在 Kibana 层定义运行时字段的界面,定义后存储在 Data View 的 runtimeFieldMap 属性里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"runtimeFieldMap": {
"hour_of_day": {
"type": "long",
"script": {
"source": "emit(doc['@timestamp'].value.getHour())"
}
},
"url_domain": {
"type": "keyword",
"script": {
"source": "def u = doc['url.original'].value; int i = u.indexOf('/', 8); emit(i > 0 ? u.substring(0, i) : u);"
}
}
}
}

Kibana 在构造 _search 请求时,把 runtimeFieldMap 里的定义注入到 ES 请求的 runtime_mappings 参数里。ES 收到请求后,在查询执行阶段为每个命中的文档临时计算这些字段的值。对 Discover 用户来说,hour_of_dayurl_domain 出现在字段列表里,行为与普通 mapping 字段完全一致。

运行时字段适合探索阶段的快速迭代:先用运行时字段验证业务逻辑,确认有用后再把计算逻辑移到 ingest pipeline 或 index mapping 里做索引时计算,换取查询性能提升。

Scripted fields(7.x 的早期实现)已在 8.x 中标记为 deprecated,推荐迁移到运行时字段。两者的本质差异:Scripted fields 用的是 Kibana 自己封装的脚本层,运行时字段直接走 ES 原生的 runtime_mappings API,后者的行为更可预测,也可以在 ES 侧直接测试。

field_caps API 与字段刷新

Data View 里的字段列表不是静态配置,而是通过 ES 的 field_caps API 动态解析的。field_caps 返回匹配 title pattern 的所有索引里每个字段的类型信息,Kibana 把结果缓存起来用于字段列表显示和 KQL 自动补全。

当索引 mapping 变化(新增字段)时,Kibana 不会自动感知,需要手动刷新 Data View(UI 里有"Refresh field list"按钮,或通过 API 触发)。自 Kibana 8.x 起,Discover 在每次搜索时会做轻量级的 mapping 变化检测,发现新字段时会在页面上提示用户刷新。

实验:通过 API 创建 Data View 并观察字段

在 Dev Tools 里通过 Data Views API 创建一个 Data View,附带运行时字段定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 创建 Data View(替换 logs-* 为实际存在的索引 pattern)
POST kbn:/api/data_views/data_view
{
"data_view": {
"title": "logs-*",
"timeFieldName": "@timestamp",
"runtimeFieldMap": {
"hour_of_day": {
"type": "long",
"script": { "source": "emit(doc['@timestamp'].value.getHour())" }
}
}
}
}
# 返回包含新 Data View 的 id
1
2
# 2. 查询这个 Data View 下所有字段(包括运行时字段)
GET kbn:/api/data_views/data_view/<id>/fields
1
2
# 3. 直接对 ES 执行 field_caps,对比 Kibana 和 ES 层面看到的字段差异
GET logs-*/_field_caps?fields=*

对比第 2 步和第 3 步的结果:field_caps 返回的是 ES mapping 里的物理字段,Data View API 返回的字段列表还包含 runtimeFieldMap 里定义的运行时字段(hour_of_day),以及 Data View 层配置的字段格式元数据。运行时字段不出现在 field_caps 里,因为它们不在 ES 的 mapping 里,只存在于 Data View 的 attributes 里,由 Kibana 在查询时动态注入。

接着验证运行时字段在实际查询里的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 4. 直接向 ES 发请求,注入运行时字段定义
POST logs-*/_search
{
"runtime_mappings": {
"hour_of_day": {
"type": "long",
"script": { "source": "emit(doc['@timestamp'].value.getHour())" }
}
},
"aggs": {
"by_hour": {
"terms": { "field": "hour_of_day", "size": 24 }
}
},
"size": 0
}

这一步复现了 Kibana 内部的行为——当 Discover 或 Lens 对包含运行时字段的 Data View 执行查询时,Kibana 就是把 runtimeFieldMap 里的定义展开成 runtime_mappings 参数,附加到 ES 请求体里。

将实验结果映射回内部对象

POST /api/data_views/data_view 的实现路径:server 端路由 → DataViewsService.createAndSave() → 调用 SavedObjectsRepository.create('index-pattern', attributes) 写入 .kibana 索引。Data View 本质上是一个 index-pattern 类型的 Saved Object,titletimeFieldNamefieldAttrsruntimeFieldMap 等都在 attributes 里。

GET /api/data_views/data_view/<id>/fields 的实现:DataViewsService.getFieldsForIndexPattern() → 向 ES 发 field_caps 请求 → 合并 runtimeFieldMap → 合并 fieldAttrs(格式化配置)→ 返回增强后的字段列表。

模式提炼

字段语义与存储分离。ES mapping 描述字段的存储类型(longkeyworddate),Data View 描述字段在 UI 层的语义(格式化方式、标签、运行时计算逻辑)。两层解耦让 ES 侧的 mapping 变化不需要同步修改 UI 配置,UI 侧的显示调整也不需要重新索引。

运行时字段把探索成本前移。传统做法是先设计好 mapping、ingest pipeline,再索引数据。运行时字段允许先索引原始数据,后期在 Data View 里追加派生字段,把"不知道需要什么字段"的探索成本从重建索引(高)降到修改 Data View(低)。确定有用后再固化到 mapping 里,是一种渐进式的 schema 演化路径。

工程迁移表

Kibana 概念 Grafana 对应 BI 语义层对应 dbt 模型对应
Data View title (wildcard) DataSource query target 语义层数据集 / dataset dbt model source
timeFieldName Time series field binding 日期维度字段 事件时间戳列
fieldAttrs (format) Unit / display override in panel 指标格式化 / display format column meta (description)
runtimeFieldMap Grafana calculated field 计算字段 / calculated measure dbt metrics / calculated column
field_caps refresh DataSource schema refresh BI 语义层元数据刷新 dbt source freshness
Data View → Discover DataSource → Explore panel 数据集 → 临时分析 dbt model → ad-hoc query

常见误解

Data View 和 ES 索引是一对一的关系。Data View 的 title 支持通配符和逗号分隔的多个 pattern(如 logs-app-*,logs-infra-*),可以跨多个物理索引。field_caps 会对所有匹配的索引做联合 schema 查询,返回的字段类型以所有索引的 union 为准(冲突的字段类型标记为 conflict)。

修改 Data View 的字段格式会影响 ES 里的数据。字段格式化完全在 Kibana 渲染层,不写 ES,不影响查询结果的原始值。_search 返回的 bytes 字段值始终是裸数字,格式化只发生在 React 组件把值渲染为 DOM 文本的一步。

运行时字段性能与 Painless 脚本等价,比 indexed 字段慢。运行时字段在查询时对每个命中文档执行脚本,时间复杂度与命中文档数量线性相关。对高命中量的聚合查询,性能代差显著。自 ES 8.1 起支持把运行时字段"升级"为 indexed 字段(synthesize_source 相关),但在 Kibana Data View 层没有一键迁移工具,需要在 ES 侧手动更新 mapping 并 reindex。

删除 Data View 会删除依赖它的 Dashboard 和 Lens。删除 Data View 只删除那个 Saved Object,不会级联删除引用它的 Dashboard、Lens、Saved Search。这些对象的 references 里仍有指向已删除 Data View 的条目,打开时会报"Data View not found",需要手动重新关联或删除。

练习

在 Stack Management → Data Views 里找到一个已有的 Data View,点击字段列表里任意一个 long 类型字段,给它配置 Bytes 格式化器,然后在 Discover 里验证该字段的显示值发生了变化,而 Dev Tools 里直接查 ES 返回的值不变。

在 Dev Tools 里执行 GET .kibana/_doc/index-pattern:<data_view_id>,把 API 返回的 Data View attributes 与 ES 文档里的 _source.index-pattern 对比,确认字段格式化配置和运行时字段定义存储的具体字段名。

为一个 Data View 添加一个运行时字段,在 Discover 里选择该字段做一个 terms aggregation(用 Lens 的 Top values),再用 Dev Tools 把同样的查询附带 runtime_mappings 参数发给 ES,对比两次结果的一致性。

系列导航

主题 核心问题
00 导读:状态存在 ES 里 Kibana 是应用平台,不是 GUI
01 架构:三层与 New Platform 为什么需要 Node.js server
02 Saved Object:统一状态模型 type / attributes / references
03(本篇) Data View:查询前的字段抽象 时间字段、字段格式化、运行时字段

参考资料