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)撑爆集群状态。控制手段有三层:
- 生产索引显式定义 mapping,关闭动态映射:
"dynamic": false或"strict"; - 使用 dynamic template 约束推断规则,例如所有字符串默认映射为 keyword;
- 对日志类索引使用 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 配置。
调优检查清单
把上述决策固化为可执行的检查项,可在建索引前逐条核对:
| 检查项 | 判断依据 | 推荐配置 |
|---|---|---|
| 枚举/ID 字段 | 精确匹配、聚合 | keyword,关闭 norms |
| 全文检索字段 | match 查询 | text + .keyword 子字段 |
| 只写不查字段 | 无排序聚合 | doc_values: false |
| text 聚合需求 | 无法改 keyword | fielddata + frequency_filter |
| 纯过滤字段 | 不参与算分 | index_options: docs |
| 未知字段写入 | 字段爆炸风险 | dynamic: strict + template |