2540 字
约 8 分钟
15
S13-X06 · 索引设计与映射:mapping 定错的代价

mapping 错一次,代价不是改一行配置,而是新建索引 + 全量 reindex + 切别名。字段类型不可变更,所有设计决策都得在第一次写入之前做完。

下面按"错了会怎样 → 怎么定对 → 怎么安全地改"走:不可变性 → 动态映射 → 显式定义 → 省空间开关 → 嵌套与关联 → 模板与切换 → 分片。


1. 为什么定错就难改

操作 能否在线做 后果
新增字段 能,PUT _mapping 老文档该字段为空,查询按缺失处理
改字段类型 不能 报 mapper [x] cannot be changed from type [text] to [keyword]
改 analyzer 名义能,实际不能 只对新文档生效,老 token 仍是旧切法
改 doc_values / null_value 不能 已写 segment 不重写,按你的 8.x 小版本核对
改 number_of_shards 不能 只能 _split/_shrink/_rollover 或 reindex

2. 动态映射的隐患

默认 dynamic: true,第一次见到字段就按 JSON 类型猜:

JSON 值 推断类型 隐患
123 long 超过 2^63 直接拒文档
1.23 float 精度约 7 位,金额会失真
true boolean 字符串 "true" 不转换,成 text
"2026-11-09" date(date_detection 默认开) 非标准格式误判或漏判
其他字符串 text + keyword 子字段 体积双份;数字串永远无法范围查询

典型翻车:第一版把 price 写成 "199.00" 定成 text,第二版写数字,报 mapper_parsing_exception,整条文档被拒,只能 reindex。

dynamic 四档:true(自动加字段)、false(忽略新字段,只进 _source 不可搜)、strict(未知字段报错)、runtime(变 runtime field,查询时算)。日志用 false,业务核心用 strict——让错误在写入时暴露。


3. 显式 mapping 该定义什么

PUT /article
{
  "settings": { "number_of_shards": 3, "number_of_replicas": 1 },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "title":   { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" },
      "author":  { "type": "keyword", "ignore_above": 128 },
      "price":   { "type": "scaled_float", "scaling_factor": 100 },
      "created": { "type": "date", "format": "strict_date_optional_time||epoch_millis" },
      "status":  { "type": "keyword", "null_value": "unknown" },
      "raw":     { "type": "object", "enabled": false }
    }
  }
}
参数 默认 什么时候显式写
type 动态推断 永远
analyzer / search_analyzer standard 中文、代码、邮箱等非自然语言
index true 只取回不检索的字段设 false
doc_values true 不排序不聚合的大字段设 false
null_value 无 需要空值参与过滤/聚合时
ignore_above keyword 子字段动态映射时为 256 防止聚合桶爆炸
format strict_date_optional_time 自定义日期格式必须写死

4. 三个省空间的开关

开关 省掉什么 代价
index: false 倒排(posting list + 词典条目) 不可检索;仍可 doc_values 排序聚合、仍可 _source 取回
doc_values: false 列存 不能排序、聚合、脚本取值、terms 查询
enabled: false(仅 object) 整个子树的解析与索引 子字段全不可查不可聚合,只能读 _source
_source: false(索引级) 原文 不能 reindex、高亮、update,只剩倒排

收益线性叠加:日志里"_source: false + enabled: false"的原始报文常省掉一半以上存储(按字段构成核对)。但 _source: false 等于放弃 reindex——改 mapping 的唯一后路就是 reindex,别把后路一起关。


5. 多字段:一份值,多套索引

用法 收益 开销
text + keyword 全文检索与精确聚合并存 正文倒排 + 列存双份
加 ngram 子字段 前缀、子串搜索 term 数成倍膨胀
加 icu 子字段 多语言归一化 又一份倒排

子字段不是免费的第二类型:每个都有独立倒排与 doc_values,4KB 正文挂三个子字段,存储可能接近翻倍(典型量级)。按需求挂,别"先挂上以后可能用"。


6. nested 与 object:跨对象匹配的陷阱

{ "comments": [ { "user": "a", "score": 5 }, { "user": "b", "score": 1 } ] }

object 被扁平化成 comments.user=[a,b]、comments.score=[5,1],于是 user=a AND score=1 假命中——两个条件命中不同子对象。nested 给每个子对象一个隐藏文档,匹配限制在单个子对象内:

维度 object nested
存储 与父文档同一 Lucene 文档 每子对象一个隐藏文档(block join)
查询 字段级,会跨对象假命中 必须包 nested 查询
聚合 普通聚合 nested + reverse_nested 两跳
成本 低 文档数放大,排序/更新更贵
GET /article/_search
{ "query": { "nested": { "path": "comments",
  "query": { "bool": { "must": [ { "term": { "comments.user": "a" } },
                                { "range": { "comments.score": { "gte": 4 } } } ] } },
  "inner_hits": {} } } }

漏写 nested 包裹是"配了没生效"的头号原因,查询返回空。nested_objects.limit 默认 10000(单文档内)、nested_fields.limit 默认 50;子对象上千的评论树该反范式成独立索引。


7. join 字段:父子关系的适用面

限制 说明
每索引只能一个 join 字段 多关系要在同一字段里声明
父子必须同分片 写入按父 id routing,否则 has_child 查不到
不能跨索引 跨索引关联靠应用层拼
删父文档留孤儿 需 delete_by_query 定期清理
查询成本高 has_child/has_parent 走 block join,比反范式慢一档

join 适合"父子数量悬殊、更新频繁、需整体一致"的场景(订单-明细);数据量大又只读时反范式更划算。


8. 模板:多索引共用一套 mapping

PUT _component_template/article_mappings
{ "template": { "mappings": { "dynamic": "strict", "properties": {
  "title": { "type": "text", "analyzer": "ik_max_word" } } } } }

PUT _index_template/article_template
{ "index_patterns": ["article-*"], "priority": 200,
  "composed_of": ["article_mappings", "article_settings"],
  "template": { "settings": { "number_of_replicas": 1 } } }
概念 作用 优先级
component template 可复用的 settings/mappings 片段 被 composed_of 引用
index template 匹配 index_patterns 的完整模板 priority 高者覆盖同 pattern
模板自带 template 段 索引级覆盖 胜过同名 component

上线前用 POST _index_template/_simulate_index/article-000001 预览最终 mapping,比建完索引才发现分词器是 standard 便宜得多。


9. 别名与 reindex:改 mapping 的唯一安全路径

步骤 动作 校验点
1 双写 应用同时写 v1、v2 两边 _count 增速一致
2 建新索引 用新 mapping 建 article-v2 _simulate_index 通过
3 reindex 拉历史数据 failures 为空、created 对得上
4 抽样比对 关键 query 两边跑 命中数与 top10 一致
5 切别名 原子 _aliases 读写别名都指向 v2
6 观察后删旧 停双写、留一周快照 无 mapper_parsing_exception,保留回滚窗口
POST _reindex?wait_for_completion=false&slices=auto
{ "source": { "index": "article-v1" },
  "dest": { "index": "article-v2", "op_type": "create" } }

op_type: create 让重跑幂等:重复文档报 409 计入 version_conflicts,加 conflicts=proceed 跳过。_reindex 不复制 settings/mappings,目标索引必须先建好。别名切换要一次请求两个动作,否则留下读写分叉:

POST _aliases
{ "actions": [
  { "add":    { "index": "article-v2", "alias": "article" } },
  { "remove": { "index": "article-v1", "alias": "article" } } ] }

10. 分片数怎么定

数据量 分片数 说明
< 10GB 1 ES 8 默认就是 1,别按"节点数"配
10–50GB 2–3 每分片控制在 10–50GB(典型量级)
时间序列 按天/周滚动 _rollover + is_write_index

三条硬约束:分片数创建后不可改;堆与分片数正相关(经验值每 GB 堆不超过 20 分片量级);cluster.max_shards_per_node 默认 1000。分片过多的症状是 cluster state 变大、主节点压力高、恢复慢。


11. 排错

现象 原因 处理
total_fields.limit 超限 动态映射把对象键当字段 调 limit 只止血,根治靠 dynamic: strict + 扁平化
mapper_parsing_exception 类型冲突:历史 text,新值数字 写入端转字符串,或 reindex
nested 查询返回空 没包 nested,或 path 写错 补包裹,核对全路径
聚合报 fielddata 错误 在 text 上聚合 用 field.keyword 或加子字段
reindex 后数据变少 源索引 _source: false 必须开 _source,或从上游重放
双写漏数据 双写失败无补偿、起点时间取错 用 _count 与 _seq_no 增量校验

12. 边界

场景 该选什么 理由
字段结构频繁变化 dynamic: false + 应用层校验 避免字段爆炸,写入不被拒
强类型业务核心 dynamic: strict 错误前移到写入
高基数字段聚合 keyword + eager_global_ordinals 提前建全局序数,省首次聚合开销
需要事务一致性 不用 ES 当主库 无跨文档事务,reindex 有窗口
关联查询复杂 反范式冗余或外部存储 join/nested 都有放大成本

第一原则不是"类型选对",而是先想清楚哪些字段要检索、哪些要聚合、哪些只取回——这决定 type、index、doc_values,其余都是细节。


13. 自测

  1. 为什么 PUT _mapping 改 analyzer 返回成功,却等于没改?
  2. 动态映射把 "199.00" 定成 text 后,第二版写数字为什么整条文档被拒?
  3. index: false 与 enabled: false 分别省掉什么?后者为什么只能用在 object 上?
  4. object 类型下 user=a AND score=1 为什么假命中?nested 怎么避免?
  5. 双写 + reindex 流程里哪一步最容易漏数据?怎么校验?
  6. 为什么 ES 8 的默认 number_of_shards 是 1,而"按节点数配分片"是过时做法?
S13-X06 · 索引设计与映射:mapping 定错的代价
https://www.clxhxhhr.top/posts/5822/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。