3. 索引模板与映射优化

在 Elasticsearch 中,索引模板与映射(Mapping)决定了文档如何被解析、存储和检索。很多性能问题并非来自集群规模不足,而是映射设计从一开始就埋下了隐患:一个被误设为 text 的 ID 字段、一个本不需要打分的日志字段、一个开启了 doc_values 却从不聚合的文本字段,都会在写入和查询路径上持续放大开销。本节围绕动态模板、字段类型选择以及 doc_values、norms、index_options 这几个关键参数,说明它们如何影响磁盘占用、写入吞吐与查询性能。

动态模板:把映射规则前置到模板层

动态映射(Dynamic Mapping)在字段首次出现时自动推断类型,方便但危险。例如 JSON 中 "status": "200" 会被推断为 long,而 "trace_id": "abc123" 会被推断为 text 并附带一个 keyword 子字段,造成双份索引。动态模板(Dynamic Templates)允许我们按字段名或类型匹配,强制指定映射。常见做法是:所有以 *_id、*_no 结尾的字段映射为 keyword;所有以 *_at、*_time 结尾的字段映射为 date;其余字符串默认映射为 keyword,只有明确需要全文检索的字段才手动声明为 text。

PUT _index_template/logs_template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": { "number_of_shards": 3, "number_of_replicas": 1 },
    "mappings": {
      "dynamic_templates": [
        { "ids_as_keyword": {
            "match_mapping_type": "string",
            "match": "*_id",
            "mapping": { "type": "keyword" } } },
        { "times_as_date": {
            "match_mapping_type": "string",
            "match": "*_at",
            "mapping": { "type": "date" } } },
        { "strings_as_keyword": {
            "match_mapping_type": "string",
            "mapping": { "type": "keyword" } } }
      ],
      "properties": {
        "message": { "type": "text", "index_options": "positions" }
      }
    }
  }
}

这段模板把“默认字符串即 keyword”作为兜底规则,只有 message 显式声明为 text。这样既避免了全文索引的滥用,也保证了聚合与精确匹配的效率。

字段类型选择:keyword、text 与数值的取舍

字段类型直接决定倒排索引、正排索引(doc_values)和存储结构。核心判断标准是:该字段是否需要分词检索?是否需要排序或聚合?

  • keyword:不分词,适合精确匹配、排序、聚合、脚本访问。用于 ID、状态码、标签、枚举值。
  • text:分词后建立倒排索引,适合全文检索。默认不开启 doc_values,不能直接用于排序和聚合(需借助 fielddata,代价高)。
  • 数值类型:long、integer、short、byte、double、float、half_float、scaled_float。能用 integer 就不要用 long,能用 scaled_float 就不要用 double,可显著降低 doc_values 的磁盘占用。
  • date:底层是 long,支持毫秒精度;若只需秒级精度,可用 format 控制解析,但存储仍是 long。

一个常见误区是把日志中的 level 字段设为 text,导致每次按级别过滤都要走分词查询,且无法高效聚合。正确做法是 keyword,配合 doc_values 支持聚合。

doc_values、norms、index_options 的作用与取舍

doc_values 是列式存储,用于排序、聚合和脚本访问。除 text 外,绝大多数类型默认开启。对于只用于过滤、从不排序或聚合的字段,可以关闭 doc_values 以节省磁盘和写入开销。例如日志中的 message 字段若只做全文检索,关闭 doc_values 是合理的。

norms 存储字段长度归一化因子,用于评分。对于不需要打分的字段(如过滤条件、日志字段),关闭 norms 可节省大量磁盘空间,并略微提升索引速度。注意:关闭后该字段的评分将不再考虑长度因素。

index_options 控制倒排索引中记录哪些信息,仅对 text 字段有效:

  • docs:只记录文档号,适合仅需“是否包含”判断的场景,如 match 查询但不关心词频和位置。
  • freqs:记录文档号和词频,适合需要评分的查询,但不支持短语查询。
  • positions:记录位置信息,支持短语查询和高亮,是默认值。
  • offsets:记录偏移量,支持高亮,但开销最大。

对于日志中的 message 字段,如果只需要关键词匹配而不需要短语查询和高亮,可以设为 freqs 甚至 docs,显著减少索引体积。

字段类型与参数对存储/查询的影响路径 字段类型选择 keyword 精确匹配/排序/聚合 text 全文检索/分词 数值类型 integer/short/scaled_float date 时间范围/排序 boolean/ip 专用类型 关键参数开关 doc_values 开启:排序/聚合/脚本 关闭:省磁盘/省写入 norms 开启:评分考虑长度 关闭:省空间/不评分 index_options docs:仅文档号 freqs:+词频 positions:+位置(默认) offsets:+偏移量 dynamic_templates 按名称/类型强制映射 对存储与查询的影响 磁盘占用 doc_values 列存 + 倒排索引 + norms 关闭无用参数可减少 30%~60% 索引体积 写入吞吐 index_options 越简单,索引构建越快 关闭 norms/doc_values 减少段合并压力 查询性能 keyword 过滤走倒排索引,快且可缓存 text 聚合需 fielddata,内存开销大 评分与排序 norms 影响评分;doc_values 支撑排序 关闭 norms 后评分不再考虑字段长度 核心原则:按查询需求反向设计映射——只开启真正需要的索引结构,避免“默认全开”带来的存储与性能浪费

参数组合的实践判断

把上述参数组合起来,可以形成一套可操作的判断逻辑。以下表格给出典型字段的推荐配置,供实际建模时参考:

字段场景 推荐类型 doc_values norms index_options 说明
订单号 / 用户ID keyword true false — 精确匹配、排序、聚合
日志级别 / 状态码 keyword true false — 过滤与聚合,无需评分
日志正文 message text false false freqs 或 docs 仅关键词匹配,不做短语/高亮
商品标题 text + keyword 子字段 text:false / keyword:true true positions 全文检索 + 聚合排序
价格 scaled_float (scaling_factor:100) true false — 范围查询与排序,节省空间
时间戳 date true false — 范围过滤与时间聚合

需要强调的是,关闭 norms 和 doc_values 并非“越省越好”。如果某个字段后续需要排序或聚合,关闭 doc_values 会导致查询直接报错或退化为 fielddata,反而更慢。因此,映射设计必须基于明确的查询模式,而不是盲目压缩。

验证与迭代

映射调整后,可以通过以下方式验证效果:使用 _cat/indices?v 观察索引体积变化;用 _search 配合 profile: true 查看查询各阶段耗时;用 _nodes/stats/indices 观察 segments.memory 和 doc_values.memory 的变化。对于已有索引,字段类型无法直接修改,通常需要重建索引(Reindex)并配合别名切换。因此,在模板阶段就把动态模板和参数策略定好,是成本最低的优化手段。

实践建议:新索引上线前,先用少量样本数据写入,执行一次 GET /index_name/_mapping 检查实际生成的映射,确认没有意外的 text 字段或多余的 keyword 子字段,再批量导入。