4. Mapping 与字段类型优化

Mapping 是 Elasticsearch 索引的“表结构契约”,它决定了每个字段如何被解析、索引、存储和检索。Mapping 一旦建立,字段类型基本不可修改,因此它既是性能优化的第一道关口,也是最难回退的决策点。本节围绕四个核心问题展开:keyword 与 text 如何选、doc_values 与 fielddata 的适用边界、norms 与 index_options 如何调优、动态映射风险如何控制。

keyword 与 text:先问“怎么查”,再定类型

keyword 与 text 的本质差异在于是否分词。keyword 将整个字段值作为一个不可分割的词项写入倒排索引,适合精确匹配、排序、聚合、脚本访问;text 会经过分析器切分成多个词项,适合全文检索,但默认不能直接用于排序和聚合。

选择时先明确查询方式:如果字段用于 term、terms、prefix 精确过滤,或用于 terms 聚合、sort 排序,应使用 keyword;如果字段用于 match 全文检索,应使用 text。对于“既要全文检索又要聚合排序”的字段,标准做法是使用 text 配合 fields.keyword 多字段:

PUT /products
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "raw": { "type": "keyword", "ignore_above": 256 }
        }
      },
      "brand": { "type": "keyword" },
      "price": { "type": "scaled_float", "scaling_factor": 100 }
    }
  }
}

这里 title 承担全文检索,title.raw 承担聚合与排序。ignore_above: 256 表示超过 256 字符的值不写入 keyword 子字段,避免超长字符串撑大索引。品牌、状态、类目等枚举型字段直接用 keyword;价格用 scaled_float 以整数存储,比 double 更省空间且精度可控。

doc_values 与 fielddata:列式存储与堆内存的取舍

doc_values 是 Elasticsearch 默认开启的列式存储结构,位于磁盘,用于排序、聚合和脚本访问。它按列存储,读取时顺序扫描效率高,且不占用 JVM 堆内存。除 text 外的字段默认开启 doc_values,这是聚合性能的基础。

fielddata 则是 text 字段为了支持排序和聚合而使用的堆内存结构。它把倒排索引反向加载到堆中,构建成本高、内存占用大,且容易触发 GC 甚至 OOM。因此 fielddata 应视为“最后手段”:

  • 优先用 keyword 多字段替代 text 聚合,而不是开启 fielddata;
  • 确实需要 text 聚合时,设置 fielddata: true 并配合 fielddata_frequency_filter 限制加载词项;
  • 对只写不查的字段,可关闭 doc_values 以节省磁盘和写入开销。

例如日志索引中 message 只做全文检索、不做聚合,可关闭其 doc_values;而 service_name 需要按服务聚合,必须保留 keyword 的 doc_values。

norms 与 index_options:按“是否算分”和“是否高亮”裁剪

norms 存储字段长度归一化因子,用于 BM25 相关性评分。如果字段只用于过滤或聚合、不参与算分,关闭 norms 可显著减少索引体积。典型场景是枚举字段、ID 字段、日志标签字段。

index_options 控制倒排索引中记录哪些信息,取值包括 docs、freqs、positions、offsets。级别越高,索引越大,但支持的能力越多:

index_options记录内容支持能力适用场景
docs仅文档号term 过滤只做精确过滤的字段
freqs文档号 + 词频评分需要算分但不需要短语
positions+ 位置短语查询默认,全文检索常用
offsets+ 偏移量高亮需要 highlighting

实践原则:不需要高亮的字段不要用 offsets;不需要短语匹配的字段可降到 freqs;纯过滤字段可降到 docs。例如日志的 trace_id 只做精确过滤,可设 index_options: docs 并关闭 norms。

动态映射风险控制

动态映射在写入未知字段时自动推断类型,方便但危险:字符串可能被推断为 text + keyword 双字段,数字可能被推断为 long,日期格式不一致会导致字段冲突,甚至因字段爆炸(mapping explosion)撑爆集群状态。控制手段有三层:

  1. 生产索引显式定义 mapping,关闭动态映射:"dynamic": false 或 "strict";
  2. 使用 dynamic template 约束推断规则,例如所有字符串默认映射为 keyword;
  3. 对日志类索引使用 index template + ILM,统一字段类型并限制字段数量。
PUT /logs-2024
{
  "mappings": {
    "dynamic": "strict",
    "dynamic_templates": [
      {
        "strings_as_keyword": {
          "match_mapping_type": "string",
          "mapping": { "type": "keyword", "ignore_above": 256 }
        }
      }
    ],
    "properties": {
      "@timestamp": { "type": "date" },
      "message": { "type": "text", "index_options": "freqs" }
    }
  }
}

下面这张图把本节四个决策点串成一条“字段类型决策链”,从查询用途出发,依次判断分词、列存、算分与索引选项,最终落到具体 mapping 配置。

字段类型决策链:从查询用途到 mapping 配置 字段用途 精确 / 全文 / 聚合 keyword term / 聚合 / 排序 doc_values 默认开启 text match 全文检索 需聚合时加 .keyword 数值 / 日期 scaled_float / date 按精度选类型 doc_values 排序/聚合/脚本 只写不查可关闭 fielddata text 聚合才开启 堆内存,慎用 norms 算分才需要 过滤字段可关闭 index_options docs / freqs positions / offsets 动态映射 dynamic: strict dynamic_templates 落地原则 精确过滤/聚合 → keyword;全文检索 → text + .keyword 子字段 聚合靠 doc_values,fielddata 仅作兜底;不算分关 norms,不高亮降 index_options

调优检查清单

把上述决策固化为可执行的检查项,可在建索引前逐条核对:

检查项判断依据推荐配置
枚举/ID 字段精确匹配、聚合keyword,关闭 norms
全文检索字段match 查询text + .keyword 子字段
只写不查字段无排序聚合doc_values: false
text 聚合需求无法改 keywordfielddata + frequency_filter
纯过滤字段不参与算分index_options: docs
未知字段写入字段爆炸风险dynamic: strict + template
Mapping 优化的收益通常体现在三处:索引体积下降、聚合延迟降低、集群状态稳定。建议在预发环境用真实数据做一次 mapping 评审,重点检查 text 字段是否被误用于聚合、是否存在未关闭 norms 的过滤字段、动态映射是否仍为默认开启。
fielddata 一旦开启,text 字段的聚合会持续占用堆内存,且无法通过重启释放。生产环境应优先通过重建索引把聚合字段改为 keyword,而不是长期依赖 fielddata。