Elasticsearch ES|QL “读取时模式”:你的未映射字段一直都在那里
作者:来自 Elastic Tyler Perkins
ES|QL 的新未映射字段功能使任何从未映射的字段都可以针对历史数据进行查询(无需重新索引)。
亲手体验 Elasticsearch:深入探索 Elasticsearch Labs 仓库中的示例 notebooks,开始免费的云试用,或者现在就在你的本地机器上试用 Elastic。
Elasticsearch Query Language( ES|QL )现在支持读取时模式。只需添加一行:SET unmapped_fields="load",_source 中的每个未映射字段都会变得可查询。无需重新索引。无需修改 pipeline。还可以追溯性地作用于你几个月前摄取的数据。
添加 JSON_EXTRACT 用于从原始 JSON 字符串中进行精确提取:flattened 字段、嵌入式 payloads、OTel resource attributes( 60 多个标准映射不会单独索引的语义约定)。它们共同将模式从一道门变成一个光谱:索引字段用于速度,_source 回退用于其他所有内容。另请参阅我们关于ES|QL views 和 ES|QL subqueries 的配套文章。
映射悬崖边缘
Elasticsearch mappings 定义了字段如何被索引。当 mapping 完整时,查询速度很快;它们会命中倒排索引、doc values,以及 Elasticsearch 在索引时构建的所有性能结构。但当一个字段从未被映射时(在 onboarding 期间遗漏、由新的 integration 添加,或者根本没有被预料到),它就是不可见的。引用它的查询会失败。传统修复方式是:更新 mapping,然后重新索引数据。对于一个多 TB 的索引,这意味着数小时的重新处理,以及在 reindex 窗口期间双倍的存储占用。
ES|QL 的读取时模式(schema-on-read)改变了这一契约。mapping 不再是一个悬崖边缘 —— “已映射” 意味着可查询,而 “未映射” 意味着不可见。相反,它变成了一个光谱:
- 已映射字段→ 快速路径。查询命中已索引结构。这仍然是生产工作负载的首选模式。
- 使用 SET unmapped_fields="load" 的未映射字段→ 在查询时从 _source 中可查询。比已索引路径更慢(没有倒排索引来加速过滤),但数据可以在几秒内访问,而不是完全无法访问:立即生效、可追溯生效,并且适用于历史索引。
你可以先发现并查询,然后再决定一个字段是否值得为了性能而进行 mapping。
未映射字段访问:战略性抽象
SET 指令是查询级别的设置,出现在 ES|QL 查询顶部、FROM 子句之前。它们用于配置查询引擎在该特定查询中的行为,而不会影响其他任何内容。unmapped_fields 指令控制当查询引用一个不存在于 mapping 中的字段时会发生什么。
考虑一个 OTel logs 索引,其中 resource.cost_center 从未被映射。如果没有 SET unmapped_fields,引用它会产生错误:
-- This fails: "Unknown column [resource.cost_center]" FROM otel-logs-* | STATS errors = COUNT(*) BY service.name, resource.cost_center添加一行后,查询即可工作:
SET unmapped_fields="load"; FROM otel-logs-* | WHERE log.level IN ("error", "warn") | STATS errors = COUNT(*), latest = MAX(@timestamp) BY service.name, resource.cost_center | SORT errors DESC该字段会在查询时从 _source 中加载。无需修改 mapping。无需重新索引。可作用于数周或数月前摄取的数据。
| 模式 | 行为 | 使用场景 |
|---|---|---|
default | 未映射字段不可见;引用它们的查询会报错( 9.4 之前的行为)。 | 你希望严格的 schema 强制执行。 |
nullify | 未映射字段会作为值为null的列出现。 | 你希望保留列结构而不加载数据,或者你需要与 subqueries 和 views 兼容。 |
load | 未映射字段会在查询时从_source中加载。 | 你需要过滤、聚合或检查实际值。 |
当你希望即使 wildcard pattern 中的某些索引没有映射特定字段,查询仍然能够成功时,nullify 会很有用。而当你真正需要这些数据时,load 才是你应该使用的模式。
部分映射字段
unmapped_fields 的强大之处在于 wildcard index patterns(通配索引模式),其中某个字段在部分索引中存在映射,但在其他索引中不存在。假设 resource.team 在较新的索引中已被映射,但在较旧的索引中没有:
SET unmapped_fields="load"; FROM otel-logs-* | WHERE log.level IN ("error", "warn") | STATS errors = COUNT(*), latest = MAX(@timestamp) BY service.name, resource.team | SORT errors DESC对于已映射的索引,值来自快速的索引路径。对于未映射的索引,值从 _source 中加载。查询会在整个时间范围内返回统一的结果;无需对历史数据进行重新索引。
JSON_EXTRACT:更底层的工具
SET unmapped_fields 是针对那些在 _source 中以可预测名称存在字段的正确方案。但有些数据需要更精细的提取:例如从存储在 text 字段中的 JSON 字符串中读取内容,或者在 flattened 字段类型中导航嵌套对象(此时点号语法无法访问某些子键)。
JSON_EXTRACT 是更底层的 “逃生舱口(escape hatch)”。它接收一个 source 字段和一个 JSON path 表达式,并返回该路径对应的值:
从字符串字段中提取
支付服务通常会将结构化错误详情以 JSON 字符串的形式存储在 response body 字段中。JSON_EXTRACT 可以在查询时直接从该字符串中提取内容:
FROM svc-payments-* | WHERE transaction.status IN ("failed", "timeout") | EVAL error_code = JSON_EXTRACT(response_body, "$.error_code"), reason = JSON_EXTRACT(response_body, "$.reason"), retryable = JSON_EXTRACT(response_body, "$.retry") | STATS failure_count = COUNT(*) BY error_code, reason, retryable | SORT failure_count DESC无需 ingest pipeline 即可从 response body 中提取 error_code。schema 可以演进,而无需重新索引。
从 _source 中提取:flattened 字段与 OTel 数据
对于被映射为 flattened 的字段,例如许多 OTel 索引中的 resource.attributes,它可能包含每个文档 20–30 个嵌套键,整个 JSON 对象会作为一个不透明的 token 存储。Elasticsearch 会将叶子值索引为扁平的 keyword 用于过滤,但不会将其拆解为单独的已映射字段,因此点号语法无法解析到对应的子结构。JSON_EXTRACT 在 _source 上运行时,会按照存储文档中的真实嵌套结构进行解析:
FROM svc-auth-* METADATA _source | EVAL svc_version = JSON_EXTRACT(_source, "$.resource.attributes['service.version']"), env = JSON_EXTRACT(_source, "$.resource.attributes['deployment.environment']"), host = JSON_EXTRACT(_source, "$.resource.attributes['host.name']") | STATS login_failures = COUNT(*) BY svc_version, env, host | SORT login_failures DESC对嵌套结构使用点号表示法,而对包含点号的叶子键使用方括号表示法(例如将 service.version 当作一个整体键名)。这是 OpenTelemetry 数据中的常见模式。
什么时候用什么
| 场景 | 工具 | 原因 |
|---|---|---|
| 字段从未被映射 | SET unmapped_fields="load" | 面向 schema 演进的战略方案。一行指令,可追溯历史数据。 |
| 需要从原始 JSON 字符串中提取某个 key | JSON_EXTRACT | 对字符串字段进行精确提取。path 表达式提供细粒度控制。 |
| 字段位于 flattened 类型内部 | 在_source上使用JSON_EXTRACT | 桥接方案。ES|QL 中对 flattened 字段的原生支持已在规划中。 |
| 查询需要兼容 subqueries 或 views | SET unmapped_fields="nullify" | 当前版本中load模式与 subqueries 和 views 不兼容,nullify可以正常工作。 |
SET unmapped_fields是大多数用户应首先采用的抽象。JSON_EXTRACT则用于需要直接进行 JSON 操作的场景,或像 flattened 这类当前尚未被原生完整处理的数据模式。
对比一下
“schema on read(读取时模式)” 是 Splunk 的创始叙事:将所有数据以原始文本索引,并在查询时决定 schema。ES|QL 采取了一个根本不同的立场:两者都给你。下面是它在实践中的表现。
Splunk SPL使用 spath、rex、search-time field extractions 以及 calculated fields 等 search commands 在查询时提取字段。其优势是灵活:你永远不需要预先声明 schema。代价是:字段值查询会扫描原始事件数据。Splunk 的 TSIDX 文件只索引元数据(host、source、sourcetype)和 index-time fields,但用户定义的 search-time fields 会在每次查询时命中 raw events。一个涉及十亿 events 的查询就会扫描十亿 events。Splunk 通过 summary indexing 和 data models 进行补偿,但这些是手动、预配置的加速机制,需要在灵活性和性能之间再次做权衡;本质上是 Elasticsearch mappings 所做权衡的另一种形式,只是步骤更多。ES|QL 的 SET unmapped_fields 提供了同样 “查询你从未声明过的字段” 的灵活性,但已映射字段仍然可以命中 Elasticsearch 的索引结构并以全速运行。只有未映射字段才会承担 _source 扫描成本,而不是查询中的每一个字段。
ElasticsearchQuery DSL 有 runtime fields,OpenSearch有 derived fields —— 两者都可以在查询时从 _source 提取数据而无需重新索引。但它们都需要按字段配置:runtime fields 需要为每个字段编写 Painless script 并声明类型,可以在 index mapping 层或每个查询中定义;derived fields 需要 index-level 或 cluster-level 配置。你必须在查询之前知道字段名并定义提取逻辑。SET unmapped_fields="load" 是一个 per-query 指令,绕过了这一切:一行即可让索引中所有未映射字段变得可查询。无需 per-field 定义,无需修改 index settings,无需脚本。
ClickHouse需要严格 schema 来定义标准 columns;新增字段意味着 ALTER TABLE。然而 ClickHouse 的 JSON 类型(在 25.3 GA)会在写入时自动为遇到的每个路径创建 typed dynamic subcolumns,无需逐字段声明。限制在于历史数据访问:已经写入 String column 的数据必须使用 JSONExtract* 函数访问字段,类似 ES|QL 的 JSON_EXTRACT,并且无法在不改变数据 pipeline 的情况下 retroactively 迁移到 JSON column。也没有类似 SET unmapped_fields 的机制,可以在不触碰 schema 或不重新 ingest 的情况下让任意历史字段变得可查询。
| Capability | Splunk SPL | ES|QL | OpenSearch / Query DSL | ClickHouse |
|---|---|---|---|---|
| Schema model | 读取时模式(在搜索时提取字段) | 两者兼具(索引快速路径 +_source回退) | 写入时模式 + 按字段查询时运行时提取 | 写入时模式;JSON 类型自动创建子列 |
| Unmapped field access | 始终可用(所有字段在搜索时都会被提取) | SET unmapped_fields="load";按查询配置,零配置 | runtime fields / derived fields;需要逐字段配置 | JSON 类型:自动;String 列仅支持JSONExtract* |
| JSON extraction | spath | JSON_EXTRACT(JSONPath 子集) | Painless 脚本 | JSONExtract*函数 |
| Performance on mapped fields | 全量扫描(没有索引结构) | 倒排索引 + doc values + 列式结构 | 倒排索引 + doc values | 列式存储 + 主索引 |
| Retroactive access to old data | 可以(始终基于原始事件数据) | 可以(通过_source) | 可以(runtime / derived fields,需要逐字段配置) | 不可以;String 列无法回溯迁移为 JSON |
| Cost of flexibility | 每个查询都承担扫描成本 | 仅未映射字段承担_source扫描成本 | 每个字段需要 Painless 脚本或 index-level 配置 | 必须在建表时选择 JSON 类型 |
关键区别:Splunk 在平台层面让你在灵活性和性能之间做选择;ES|QL 则允许你在字段和查询粒度上做选择。已映射字段是高速路径,未映射字段是可访问路径 —— 你不必为整个数据集选择单一模型。
当前限制
在当前版本中,SET unmapped_fields="load"与 subqueries 和 views 不兼容;在构建查询时应使用nullify模式替代。详见 SET 文档。
下一步发展
schema-on-read 不只是两个功能,而是一种策略。未来的方向是:在不要求 ingest 阶段具备完美 schema 的情况下,让更多数据在查询时变得可查询。
原生 flattened 字段支持(下一步):
允许直接使用点号语法访问 flattened 字段,而无需通过_source上的JSON_EXTRACT作为绕行方案。
这将消除当前用户最常使用JSON_EXTRACT的主要原因之一,并且已在规划中。解除 load 模式对 subqueries 和 views 的限制:
这将允许 schema-on-read 与 views 和 subqueries 等查询组合能力一起使用(相关内容见 views 与 subqueries 的文章)。
长期目标:SET unmapped_fields将成为用户处理 schema 演进的主要方式,而JSON_EXTRACT仅用于真正精细的 JSON 操作场景。
试用方式
未映射字段访问与JSON_EXTRACT当前均作为Tech Preview 功能提供。你可以在 Kibana Dev Tools 或 Discover 中试用。
欢迎反馈体验,也可以在 GitHub 提交带有ES|QL 标签的问题。
ES|QL 未映射字段访问与JSON_EXTRACT目前属于 Tech Preview 功能。Tech Preview 功能可能会发生变化,并且不受 GA 功能的支持 SLA 覆盖。本文章中所描述的任何功能或特性的发布与时间安排均由 Elastic 全权决定。任何当前尚未可用的功能或特性,可能无法按时交付,甚至可能不会交付。
原文:https://www.elastic.co/search-labs/blog/esql-unmapped-fields
