真实测试integer改long这种看似安全的类型扩容依然被拒绝,再验证新增字段、追加多字段、调整ignore_above等参数真实都是被允许的增量修改,划清Mapping不可变性真正锁死的边界。
代码PUT /immut_test {"mappings":{"properties":{ "a_int": {"type":"integer"}, "b_keyword":{"type":"keyword"}, "c_text":{"type":"text"} }}} PUT .../_mapping {"properties":{"a_int":{"type":"long"}}} # integer扩成long,安全吗 PUT .../_mapping {"properties":{"b_keyword":{"type":"text"}}} # keyword换成text PUT .../_mapping {"properties":{"c_text":{"type":"text","fields":{"raw":{"type":"keyword"}}}}} # 加子字段 PUT .../_mapping {"properties":{"b_keyword":{"type":"keyword","ignore_above":50}}} # 改参数
PUT .../_mapping {"properties":{"a_int":{"type":"long"}}}
→ 400: "mapper [a_int] cannot be changed from type [integer] to [long]"
PUT .../_mapping {"properties":{"b_keyword":{"type":"text"}}}
→ 400: "mapper [b_keyword] cannot be changed from type [keyword] to [text]"直觉上integer(4字节)改成long(8字节)应该是一次绝对安全的"扩容"——long能表示的范围完整覆盖integer,不会丢失任何已有数据的精度,但ES真实依然拒绝了这次变更。原因不在于数据会不会丢,而在于Lucene底层给不同数值类型建的索引结构(真实涉及不同的编码方式、不同的BKD树参数)本身就不一样——哪怕语义上是"安全"的扩容,物理存储结构的变更也真实需要整个重建,ES选择用同一条"类型不可变"的规则一刀切,不去对每种类型变更单独判断是否语义安全,避免引入一堆特例和潜在的边界bug。
PUT .../_mapping {"properties":{"d_new_field":{"type":"keyword"}}}
→ {"acknowledged": true} ← 真实允许,新增一个之前不存在的字段
PUT .../_mapping {"properties":{"c_text":{"type":"text","fields":{"raw":{"type":"keyword"}}}}}
→ {"acknowledged": true} ← 真实也允许,给已有的c_text字段追加一个新的keyword子字段
GET .../_mapping 真实确认:
c_text.fields.raw.type = "keyword" ← 子字段真实加上了,c_text本身的type一点没变PUT .../_mapping {"properties":{"b_keyword":{"type":"keyword","ignore_above":50}}}
→ {"acknowledged": true}
GET .../_mapping 真实确认:
b_keyword.ignore_above = 50 ← 真实改成功了,type依然还是keyword这是本文认为最容易被忽略的一类"可变"情形:像ignore_above这种只影响"索引阶段怎么处理值"、不改变底层存储结构本身的参数,真实是可以原地更新的——它改变的只是"以后新写入的文档该怎么被索引",不涉及对已有倒排索引做任何结构性的重写(已经写入、超过旧ignore_above而被排除索引的老文档,不会因为改了这个参数就重新被索引,这点本系列前面验证过ignore_above只影响新写入时就已经暗示了这一层)。真实的结论是:Mapping的"不可变"不是一刀切地锁死整个字段定义,而是精确锁死"会改变Lucene底层存储编码方式"的那部分(也就是type本身),任何不触碰编码方式、纯粹是"调整未来写入行为"或者"追加全新结构"的修改,真实都留了口子。
每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。
_mapping API 多种更新场景对比