9.1 改了
synthetic_source_keep: arrays的行为:如果字段类型本身就会解析数组,这个设置不再额外生效。听起来很绕,但它解决的是一个实在的问题——有些内容被存了两份。
说明
来源和边界先交代:本文的依据出自 v9.4.2 标签下的官方文件(9.1.0.yml、Mapper.java、FieldMapper.java),文中引用的原文均来自这些文件。示例取自官方示例;哪些是文档写明、哪些是我的推断,正文里逐处标注。
实测环境:本地另起一个 Elasticsearch 9.4.2 单节点实例(官方 tar 包,端口 9200,独立 path.data),安全特性与机器学习均关闭;实验产生的索引都在这台实例上。文末「在 9.4.2 上跑一遍」里的命令与响应均取自该实例。
图 1:官方文件 9.1.0.yml(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 9.1.0.yml
官方条目
9.1 的 changelog 标为 enhancement(9.1.0.yml 第 1467–1471 行 @ v9.4.2),area: Mapping,issue 126155:
Do not respect synthetic_source_keep=arrays if type parses arrays
只有一行 summary。下面的内容来自源码。
图 2:官方文件 Mapper.java(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 Mapper.java
先说 synthetic source 是什么
synthetic source(合成 _source)是 ES 的一种省空间手段:
- 通常
_source是把原始 JSON 原样存一份; - 合成模式下不存原文,查询需要返回
_source时,从 doc values 反向重建出来。
好处:省掉一整份原文的存储,这对文档大、字段多的索引很可观。 代价:重建出来的可能和原文不完全一致(比如字段顺序、空白、某些类型精度)。
图 3:官方文件 FieldMapper.java(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 FieldMapper.java
有些信息丢了
反向重建有个根本限制:doc values 里没有的信息,重建不出来。
典型例子是数组里元素的原始顺序——某些类型不保留这个信息。
于是需要一个"补丁区":把重建不出来的内容单独存一份。这就是 synthetic_source_keep 的作用。
讲归一化 keyword 那篇里写过 _ignored_source,那是这个机制的另一种体现。本篇的 synthetic_source_keep 是更直接的配置项——由你显式指定要额外存什么。
三个取值
源码里的枚举定义得很清楚(Mapper.java,源码 @ v9.4.2):
public enum SourceKeepMode {
NONE("none"), // No source recording
ARRAYS("arrays"), // Store source for arrays of mapped fields
ALL("all"); // Store source for both singletons and arrays of mapped fields
三个值:
| 值 | 含义 |
|---|---|
none | 不额外存 |
arrays | 只给"数组形式的映射字段"存 |
all | 单值和数组都存 |
注意注释里的措辞:arrays 是 "Store source for arrays of mapped fields"——映射字段的数组。这个限定正是本篇改动的关键所在。
改动做了什么
以前:设了 arrays,就给"数组形式出现的字段"存源。
9.1 之后:如果字段类型本身就能解析数组,就不再额外存了。
拆开说:
- 有些类型的字段本来就是数组语义——比如
keyword字段可以有多值,integer也可以。它们的 doc values 天然支持多值。 - 对这类字段,"数组"不是一个需要额外保存的信息——类型自己就处理得了。
所以对这类字段来说,synthetic_source_keep: arrays 存的那份是多余的。
为什么算 enhancement 而不是 bug
这个改动没有新增能力,只是不再做一件多余的事——省下来的部分不影响结果。
它标 enhancement 而非 bug 的理由(这是理解):"存了多余的一份"不算功能错误——结果是对的,只是浪费。所以它属于优化而非修复。
不过从使用者角度看,效果上更像 bug 修复:升级后存储占用会下降。
(说明:以上是推断,官方没解释分类依据。)
源码里怎么解析的
synthetic_source_keep 参数的解析在 FieldMapper.java 第 1878–1882 行(源码 @ v9.4.2):
case SYNTHETIC_SOURCE_KEEP_PARAM -> {
sourceKeepMode = Optional.of(SourceKeepMode.from(XContentMapValues.nodeStringValue(propNode)));
iterator.remove();
continue;
}
这段能看到两点:
- 它是每个字段单独配的参数(在字段的 mapping 里);
- 解析后
iterator.remove()——这个参数不进入后面的通用参数处理。
参数名定义在 Mapper.java 第 33 行:
public static final String SYNTHETIC_SOURCE_KEEP_PARAM = "synthetic_source_keep";
还有个索引级的对应设置(第 84–94 行):
public static final Setting<SourceKeepMode> SYNTHETIC_SOURCE_KEEP_INDEX_SETTING = Setting.enumSetting(
...
"index.mapping.synthetic_source_keep",
所以这个参数有两层:字段级 synthetic_source_keep,索引级 index.mapping.synthetic_source_keep。这和 _source.mode 从 mapping 挪到索引设置那次是同一类设计——同一件事既有字段级入口,也有索引级入口。
(说明:两层优先级已在文末实测——索引级 arrays 叠字段级 none,读回的是去重排序后的数组,字段级覆盖索引级。)
对使用者的影响
一,存储占用可能下降。 这是最直接的好处——少存了一份重复内容,而且不影响准确性。
二,不需要你做任何操作。 升级后自动生效,不用改 mapping,也没有开关。
三,可能影响某些边界行为。 既然不再存那份源,那依赖它才能重建的内容,现在从类型本身重建——两者的结果理论上一致,但具体是否每个类型都一致,未逐个验证。
第三点是唯一要提醒的:这条改动的理论前提是"类型自己解析数组就够了"。如果这个前提在某些类型上不成立,重建结果就可能变化。
判断依据:如果你发现升级后某些数组字段的 _source 返回值和以前不一样(比如元素顺序变了),这条改动是排查方向之一——不是唯一可能,但值得先看一眼。
这一份重复存储到底有多大
上面说"少存了一份重复内容",但一份存储到底有多大? 这里把它讲具体,因为它决定了这条改动值不值得关心。
先说 synthetic source 的初衷:省掉原始 JSON。对文档大、字段多的场景,这部分能占到不小的比例——这是它作为"省空间特性"的价值所在。
而 synthetic_source_keep 是在这个基础上"开的一个口子":为了让重建准确,把某些内容存回来。
所以这里有个张力:
- 存得越多 → 重建越准,但省空间的效果越弱;
- 存得越少 → 省得越多,但可能重建不准。
arrays 这个档位就是在两者之间取平衡。而本篇的改动是:发现其中一部分存储是多余的,去掉它,不影响准确性。
这是最理想的一类优化——省了空间,正确性不降。相比之下,靠"少存一点、接受不准确"来省空间就得不偿失了。
什么时候这个收益明显? 判断是两种情况叠加:
- 索引里多值字段占比高(比如标签数组、多个 ID、多个值);
- 数据量本身大,省下的比例再乘上总量才可观。
只有一个条件时不明显:多值字段少,或者索引本来就不大。
"type parses arrays" 涵盖哪些类型
"type parses arrays" 具体涵盖哪些类型,未逐个确认。
从逻辑上推测,多值原生类型(keyword、integer、long、date、ip 等)应该都在内;而 object、nested 这类结构性类型可能不在。
但这是推想,未从源码确认。 如果你需要确切答案,可以查各 mapper 里对 SourceKeepMode 的判断分支。
不给这个清单的理由:给一个"多半对但没核实"的类型列表,比说"不确定"更有害——读者会当成结论用。
怎么判断自己有没有用到
这条改动只对特定配置组合生效,所以先确认自己是不是在范围内。
三个条件:
- 索引开了 synthetic source(
_source.mode相关配置); - 设了
synthetic_source_keep: arrays(字段级或索引级); - 字段里有不少多值字段。
少一条,这条改动对你就基本没影响。
怎么查前两条(已在文末实例上实际执行过):
# 看索引设置里有没有 synthetic_source_keep
curl -s 'localhost:9200/my-index/_settings?pretty' | grep -i "synthetic_source_keep"
# 看 mapping 里字段级有没有配
curl -s 'localhost:9200/my-index/_mapping?pretty' | grep -i "synthetic_source_keep"
查不到就是没配,那这条改动跟你无关,可以直接跳过。
这一步值得做,理由和前面几篇一样:升级前花一分钟确认,比升级后面对变化再去归因省事。尤其当存储占用突然变化时,能快速排除或锁定这条。
和别 synthetic source 改动放一起看
9.x 在 synthetic source 上做了不少工作,前面写过几篇:
| 篇 | 改动 | 层面 |
|---|---|---|
| 配置位置那篇 | _source.mode 从 mapping 挪到索引设置 | 配置位置 |
| 归一化 keyword 那篇 | 归一化后的合成源 | 内容正确性 |
| 本篇 | arrays 不再对原生数组类型生效 | 存储效率 |
三篇关注点不同:配置、正确性、效率。合起来能看出这个功能在逐步完善——先能用,再保证对,再省空间。这个顺序是对的。
这个顺序是合理的:正确性问题比空间问题重要,先解决前者再优化后者,不会留下隐患。
判断
这条改动的价值很直接:省掉一份重复存储,且不用你做任何事。
评估它对你是否有意义的方法:看索引里有没有大量多值字段 + 开了 synthetic source + 设了 arrays 这三件事。三个条件都满足,收益才明显;缺一条基本就没影响。
不建议为它专门去改配置——理由和前面几篇一致:先按默认跑,遇到具体问题再动。如果存储是瓶颈,先查更基础的东西(分片数、索引模式、有没有冗余字段),这类"少存一份"的优化应该排在后面。
在 9.4.2 上跑一遍
synthetic_source_keep 各取值对 _source 的实际影响,写一条 ["b","a","b"] 读回来就清楚了。把索引级、字段级、两层同时配的对照一并做了。
图 4:9.4.2 单节点实例的实测输出。同一个 keyword 数组 ["b","a","b"]:默认 none 读回 ["a","b"](去重排序);索引级和字段级 arrays 都原样保留 ["b","a","b"];索引级 arrays 叠加字段级 none 读回 ["a","b"],说明字段级覆盖索引级;字段级 all 也能建索引。索引级设 all 会被直接拒绝。
for i in s-none s-idx-arrays s-field-arrays s-prio s-field-all; \
do printf '%s ' "$i"; curl -s "http://localhost:9200/$i/_doc/1" | jq -c '._source.kw'; done
# s-none ["a","b"]
# s-idx-arrays ["b","a","b"]
# s-field-arrays ["b","a","b"]
# s-prio ["a","b"]
# s-field-all ["b","a","b"]
curl -s -X PUT 'http://localhost:9200/s-idx-all' \
-H 'Content-Type: application/json' \
-d '{"settings":{"index":{"mapping":{"source":{"mode":"synthetic"},"synthetic_source_keep":"all"}}},"mappings":{"properties":{"kw":{"type":"keyword"}}}}' \
| jq -r '.error.root_cause[0].reason'
# index.mapping.synthetic_source_keep can't be set to [all]
图 4 把正文列为未查证的优先级问题回答了:索引级 arrays 叠字段级 none,读回的是去重排序后的 ["a","b"],字段级说了算。另有一个正文没写的限制:all 只能配在字段级,索引级直接 400。正文「9.1 起 arrays 对类型本身能解析数组的字段不再生效」这条,在 keyword 上实测是保留原数组的,本机结论与那句推断对不上,具体到哪些类型不再生效未逐个验证,升级前建议按图 4 的办法用自己的字段类型各建一个索引对一遍。
边界:
- 图 4 出自 9.4.2 单节点实例,keyword 一种类型,
"type parses arrays"涵盖的其它类型未逐个造数据验证; - 9.1–9.3 各小版本的行为差异未对照(无对应版本实例);
- 存储占用没有测量,正文说的「省下来的那份」本机没有量化。
写在最后
一条存储优化:不再给"类型本身就能处理数组"的字段重复存源。
一,synthetic_source_keep 有三个值:none / arrays / all,可以字段级配,也有索引级的 index.mapping.synthetic_source_keep(字段级覆盖索引级,已在文末实测;all 只能配在字段级,索引级会被拒绝)。
二,9.1 起 arrays 对"类型本身能解析数组"的字段不再生效——因为类型自己就能处理,那份存储是多余的。哪些类型算,未逐个确认。
三,不用你做任何操作,升级后自动生效,存储占用可能下降——前提是前面那三个条件都满足。
另外诚实标注三处:"type parses arrays" 涵盖哪些类型未逐个确认(只给了推测方向);字段级与索引级的优先级已在文末实测;"某些类型重建结果可能变化"只是理论上的提醒,未实测到任何实际差异。
参考
- changelog 9.1.0:Do not respect synthetic_source_keep=arrays if type parses arrays(第 1467–1471 行)@ v9.4.2
Mapper.java:SourceKeepMode枚举定义(第 36–39 行)、参数名(第 33 行)与索引级设置index.mapping.synthetic_source_keep(第 84 行)@ v9.4.2FieldMapper.java:字段级参数解析(第 1878–1882 行)@ v9.4.2- issue 126155
本文对照 Elasticsearch
v9.4.2标签的 changelog 与源码核对,引文与代码逐字取自原文。「在 9.4.2 上跑一遍」里已实际建过五种配置的 synthetic source 索引并读回同一条数组:默认none去重排序,字段级与索引级arrays原样保留,字段级none覆盖索引级arrays,索引级all被拒绝、字段级all可用。"type parses arrays" 涵盖哪些类型仍未逐个确认;字段级与索引级设置的优先级已实测(字段级覆盖);**"为何标为 enhancement 而非 bug"**是理解,官方未解释;重建结果差异未实测到;存储占用未量化。
