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,显著减少索引体积。
参数组合的实践判断
把上述参数组合起来,可以形成一套可操作的判断逻辑。以下表格给出典型字段的推荐配置,供实际建模时参考:
| 字段场景 | 推荐类型 | 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 子字段,再批量导入。