Elasticsearch 索引模板与映射:Composable 模板、mapping 与动态模板原创
在 Elasticsearch 中,字段类型一旦写入就锁定无法修改。这意味着第一条文档的字段类型决定了整个索引的 mapping——如果 ES 自动推断错了类型,后续查询和聚合都会受影响。索引模板与动态模板存在的理由,就是在文档写入之前就确定正确的字段类型,而不是靠 ES 瞎猜。
版本说明
本文原写于 2022-03,2026-09 重写。_template API(Legacy Index Template)已在 7.8 起废弃,8.x 移除;本文基于 Composable Index Template(_index_template)与 Dynamic Templates。
# 1. 概念关系与生效时机
| 概念 | 作用 | 生效时机 |
|---|---|---|
| Composable Index Template | 定义新建索引的默认 settings、mappings、aliases | 匹配到 index_patterns 时自动应用 |
| Component Template | 把公共配置抽取为可复用单元 | 被 Index Template 通过 composed_of 引用 |
| Mapping | 定义字段类型(text/keyword/date 等)和分析器 | 字段首次写入时生效,或显式 PUT mapping |
| Dynamic Template | 字段首次出现时按规则自动映射类型 | 新字段首次写入且未被显式 mapping 定义时 |
四层关系:Index Template(顶层)→ 引用 Component Template → 包含 Mapping 定义 → Dynamic Template 处理未定义的新字段。
# 2. Composable Index Template
# 2.1 创建模板
PUT _index_template/log_template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"message": { "type": "text" },
"level": { "type": "keyword" }
}
}
},
"priority": 500,
"composed_of": ["base_mappings"],
"version": 1,
"_meta": {
"description": "日志索引模板"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
关键字段说明:
priority:数值越大优先级越高,覆盖其他匹配同模式的模板composed_of:引用组件模板(见下文),实现配置复用template:内联的 settings/mappings/aliases(7.8+ 新结构)
# 2.2 组件模板(Component Template)
把公共配置抽取为可复用组件:
PUT _component_template/base_mappings
{
"template": {
"mappings": {
"properties": {
"@timestamp": { "type": "date" }
}
}
}
}
2
3
4
5
6
7
8
9
10
索引模板通过 composed_of: ["base_mappings"] 引用。
# 2.3 模板优先级与冲突解决
当多个模板匹配同一索引名时:
- 优先比较
priority:数值大的胜出 - 优先级相同:创建时间晚的胜出(不推荐依赖,应显式设置 priority)
# 检查某索引将应用哪个模板
GET _index_template/logs-2024-01
2
# 2.4 Legacy Index Template(已废弃,了解即可)
7.8 之前的 _template API:
# ⚠️ 废弃命令,仅作对比
PUT _template/legacy_template
{
"index_patterns": ["logs-*"],
"settings": { ... },
"mappings": { ... }
}
2
3
4
5
6
7
与 Composable 的核心区别:
- Legacy 模板合并所有匹配模板的设置(隐式叠加,易混乱)
- Composable 只应用 priority 最高的那个(显式覆盖,更清晰)
升级建议:
- 存量 Legacy 模板建议逐步迁移到 Composable
- 混合使用时,Composable 默认优先于 Legacy(可通过
priority控制)
# 3. Mapping 定义
Mapping 定义字段类型,字段一旦写入即锁定类型,不可修改。
为什么字段类型改不了:倒排索引在写入时就已经按该字段的类型和分析器完成了分词与编码,类型是「已经写进磁盘结构里的事实」,不是一层可以重新解释的元数据。所以改类型只能通过 reindex 重建索引。这也解释了为什么动态模板只对首次出现的字段生效——一旦字段的类型被确定并写入,后续的动态模板规则不再适用。
# 3.1 显式创建索引时定义 mapping
PUT my_index
{
"mappings": {
"properties": {
"user": { "type": "keyword" },
"content": {
"type": "text",
"analyzer": "standard"
},
"created_at": { "type": "date" }
}
},
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.2 在已有索引上添加字段
PUT my_index/_mapping
{
"properties": {
"new_field": { "type": "integer" }
}
}
2
3
4
5
6
限制:
- ✅ 可以新增字段
- ❌ 不能修改已有字段的类型(需 reindex)
- ❌ 不能删除字段(可用
ignore_malformed或 reindex)
# 3.3 text 与 keyword 的取舍
text 类型会经过分析器分词,支持全文检索(如 match 查询),但默认不能用于聚合排序;keyword 类型不分词,保存原始整值,支持精确匹配(term 查询)和聚合排序。当一个字段需要两种能力时,可以使用 multi-fields 配置让字段同时具备 text 和 keyword 两种类型:
{
"properties": {
"title": {
"type": "text",
"analyzer": "standard",
"fields": {
"raw": { "type": "keyword" }
}
}
}
}
2
3
4
5
6
7
8
9
10
11
查询时用 title 做全文搜索,聚合时用 title.raw 做精确聚合。代价是该字段会存储两份倒排索引,占用更多磁盘空间。
# 4. 动态模板(Dynamic Templates)
动态模板在字段首次出现时按规则自动映射类型,只对新字段生效。
# 4.1 创建带动态模板的索引
PUT /my_index
{
"mappings": {
"dynamic_templates": [
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
},
{
"custom_text_fields": {
"match": "text_*",
"unmatch": "*_raw",
"mapping": {
"type": "text",
"analyzer": "standard"
}
}
},
{
"path_based_geo": {
"path_match": "location.*",
"mapping": {
"type": "geo_point"
}
}
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 4.2 匹配规则表
| 规则 | 说明 | 示例 |
|---|---|---|
match | 字段名通配符匹配 | "match": "custom_*" 匹配 custom_name |
unmatch | 排除特定字段名 | "unmatch": "*_raw" 排除 raw 结尾字段 |
match_mapping_type | 按 JSON 类型匹配 | "match_mapping_type": "string" |
path_match | 按字段路径匹配 | "path_match": "location.*" 匹配 location.lat |
path_unmatch | 按路径排除 | "path_unmatch": "*.raw" |
# 4.3 优先级顺序
执行顺序:按 dynamic_templates 数组中的定义顺序,第一个匹配的模板生效。
{
"dynamic_templates": [
{ "specific_rule": { "match": "name", "mapping": { "type": "keyword" } } },
{ "generic_rule": { "match_mapping_type": "string", "mapping": { "type": "text" } } }
]
}
2
3
4
5
6
上例中,字段 name 会匹配 specific_rule 而非 generic_rule。
重要限制:动态模板只影响首次遇到的字段。若 name 已在第一个文档中被动态映射为 keyword,后续即使修改动态模板也无法改变该字段类型,除非重建索引。
# 5. 坑与边界
# 5.1 模板与动态模板的生效范围
- 模板只对新索引生效:修改模板后,存量索引不会自动变更;需 reindex 或滚动新建索引
- 动态模板只对新字段生效:已有字段的类型一旦确定,动态模板无法重刷
- 两者的"只对新 X 生效"特性是设计约束,不是 bug
# 5.2 字段类型锁定
- 字段类型一旦写入即锁定在索引生命周期内不可变
- 如需修改类型,唯一办法是 reindex 到新建索引
# 5.3 动态映射的默认值风险
若字段未被模板定义,ES 会根据字段值自动推断类型:
- 字符串 → text + keyword 子字段
- 数字 → long 或 float
- 日期格式字符串 → date
生产环境建议:关闭动态映射或显式控制,避免字段爆炸:
PUT my_index
{
"mappings": {
"dynamic": "strict",
"properties": {
"known_field": { "type": "keyword" }
}
}
}
2
3
4
5
6
7
8
9
dynamic: strict 表示遇到未定义字段直接拒绝写入,而非自动推断。
# 5.4 字段爆炸与 dynamic 的三种取值
dynamic 参数控制新字段的处理策略,有三种取值:
dynamic: true(默认):任意新字段自动建 mapping,这是字段爆炸的根源。日志类数据里一个不受控的 JSON 字段就能把 mapping 撑到index.mapping.total_fields.limit(默认 1000)上限,之后整个索引写入报错dynamic: false:新字段仍会被存下(出现在_source中),但不建索引,无法被搜索dynamic: strict:遇到未定义字段直接拒绝写入并报错
生产环境的日志索引建议设置为 strict 或 runtime,配合显式动态模板控制字段数量,防止不受控的数据结构撑爆 mapping。
# 5.5 Priority 未设置时默认 0
与系统模板冲突时可能意外被覆盖,建议始终显式设置 priority。
# 5.6 通配符冲突
多个动态模板可能匹配同一字段,顺序决定结果,建议精确规则在前、宽泛规则在后。
# 5.7 _mapping API 无法更新动态模板规则
PUT /index/_mapping 添加的是字段映射,不是更新动态模板规则。动态模板只能在创建索引时定义,或在关闭索引后重建。
# 6. 可复用要点
- 7.8+ Always use
_index_template(Composable),弃用_template(Legacy) - 用
priority显式控制模板覆盖关系,不要依赖创建时间 - 公共配置抽取为
_component_template,通过composed_of复用 - Mapping 变更受限,生产索引提前规划字段类型;动态模板规则需在索引创建时确定
- 生产环境建议
dynamic: strict或配合显式动态模板,防止字段爆炸
# 7. 验证
# 1. 查看现有 Composable 模板列表
GET _index_template
# 2. 查看特定模板详情
GET _index_template/log_template
# 3. 查看某索引实际应用的 settings/mappings
GET my_index/_settings
GET my_index/_mapping
# 4. 模拟模板匹配(预览某索引名将应用哪个模板)
GET _index_template/logs-test-index
# 5. 预演模板命中结果(检查设置是否如预期)
GET _index_template/_simulate_index/my-test-index-2024-01
# 6. 查看组件模板
GET _component_template
# 7. 创建带动态模板的索引并验证
PUT /test_dynamic_index
{
"mappings": {
"dynamic_templates": [
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": { "type": "keyword" }
}
}
]
}
}
POST /test_dynamic_index/_doc
{
"name": "John",
"age": 30
}
GET /test_dynamic_index/_mapping
# 预期:name 为 keyword 类型(由动态模板设置),age 为 long(动态映射默认推断)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
索引模板与映射快速参考
{
"_meta": {
"doc_version": "1.0",
"article_id": "es-index-template-mapping",
"profile_context": "elasticsearch-schema",
"last_updated": "2026-09"
},
"quick_start": {
"create_index_template": "PUT _index_template/<name> {\"index_patterns\": [\"logs-*\"], \"template\": {...}, \"priority\": 500}",
"create_component_template": "PUT _component_template/<name> {\"template\": {...}}",
"create_mapping_explicit": "PUT /<index> {\"mappings\": {\"properties\": {...}}}",
"add_field_to_existing": "PUT /<index>/_mapping {\"properties\": {\"new_field\": {...}}}",
"create_dynamic_template": "PUT /<index> {\"mappings\": {\"dynamic_templates\": [{\"rule_name\": {\"match\": \"*\", \"mapping\": {...}}}]}}"
},
"safety_rules": [
{"risk": "模板只对新索引生效", "action": "修改模板后存量索引不变,需 reindex 或滚动新建", "scope": "index_template"},
{"risk": "动态模板只对新字段生效", "action": "已有字段类型锁定,无法通过修改动态模板重刷", "scope": "dynamic_template"},
{"risk": "字段类型一旦写入即锁定", "action": "如需修改类型必须 reindex 到新建索引", "scope": "mapping"},
{"risk": "动态映射默认推断可能不符合预期", "action": "生产环境用 dynamic: strict 或显式动态模板", "scope": "mapping"},
{"risk": "Priority 默认 0 可能被系统模板覆盖", "action": "始终显式设置 priority", "scope": "index_template"},
{"risk": "动态模板顺序决定匹配结果", "action": "精确规则在前、宽泛规则在后", "scope": "dynamic_template"}
],
"verification": {
"list_templates": "GET _index_template",
"get_template": "GET _index_template/<name>",
"simulate_match": "GET _index_template/_simulate_index/<index_name>",
"check_index_settings": "GET <index>/_settings",
"check_index_mapping": "GET <index>/_mapping",
"list_component_templates": "GET _component_template"
},
"version_notes": {
"legacy_template": "_template API 已废弃,7.8+ 使用 _index_template",
"composable_priority": "Composable 模板通过 priority 显性控制覆盖"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35