Mapping精讲:动态映射与显式映射、字段类型详解、Mapping参数最佳实践
Mapping 这东西,说白了就是告诉 ES 你的数据长什么样。很多新手上来就一把梭,让 ES 自己猜字段类型——嗯,我当年也这么干过,结果被坑得不轻。今天咱们就把 Mapping 掰开揉碎了讲清楚。
一、动态映射 vs 显式映射
ES 有两种方式定义字段类型:动态映射和显式映射。我个人习惯是:开发环境用动态映射省事,生产环境必须显式定义。
1. 动态映射
你往索引里塞一条数据,ES 自动推断字段类型。比如你传了个 "age": 25,ES 就认为 age 是 long 类型。
动态映射的推断规则
| JSON 数据 | ES 推断类型 |
|---|---|
| "hello" | text + keyword |
| 123 | long |
| true / false | boolean |
| "2024-01-01" | date |
| [1, 2, 3] | long(数组) |
看起来挺智能对吧?但我在项目中遇到过一个问题:某个字段本来存的是数字 ID,结果某天上游传了个空字符串进来,ES 直接把它当成 text 类型了。后面再传数字,类型冲突,整批数据写不进去。
避坑指南:动态映射下,同一个字段不能有两种类型。一旦类型定下来,后面再传不同类型的数据就会报错。
2. 显式映射
说白了就是你自己定义每个字段的类型。我建议所有生产环境都用显式映射,原因很简单——可控。
PUT /my_index
{
"mappings": {
"properties": {
"title": { "type": "text" },
"price": { "type": "float" },
"created_at": { "type": "date" }
}
}
}
你看,清清楚楚。title 是 text 用来全文搜索,price 是 float 做范围查询,created_at 是 date 按时间排序。每个字段的用途一目了然。
二、核心字段类型详解
ES 的字段类型很多,但日常用得最多的就这几个。我挑重点讲。
1. text vs keyword
这两个是 ES 里最容易被搞混的类型。我简单说:
- text:会被分词器拆成一个个词,用于全文搜索。比如搜索"苹果手机",能匹配到"苹果"和"手机"。
- keyword:不分词,当成一个整体。用于精确匹配、排序、聚合。比如订单状态"已支付",你搜"已支付"才能匹配到。
我的经验:如果一个字段既要全文搜索又要精确匹配,可以用 multi-fields。比如 title 字段同时定义 text 和 keyword:
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
这样你既可以用 title 做搜索,又可以用 title.keyword 做聚合排序。一举两得。
2. date 类型
日期类型看着简单,坑其实不少。ES 支持的日期格式很多,但默认只认 yyyy-MM-dd 和 yyyy-MM-dd'T'HH:mm:ssZ。
我曾经遇到过一个线上问题:上游传了个 "2024/01/01" 过来,ES 直接报错。因为斜杠不是默认格式。解决方案是自定义 format:
PUT /my_index
{
"mappings": {
"properties": {
"publish_date": {
"type": "date",
"format": "yyyy-MM-dd||yyyy/MM/dd||epoch_millis"
}
}
}
}
这样三种格式都能兼容。记住一点:日期字段尽量用标准格式,别自己发明格式。
3. nested 类型
ES 的数组有个大坑:内部对象的关联关系会丢失。举个例子:
{
"users": [
{ "name": "张三", "age": 25 },
{ "name": "李四", "age": 30 }
]
}
如果你搜索 name=张三 AND age=30,ES 居然能匹配到!因为 ES 把数组拍平了,张三和 30 虽然不在同一个对象里,但被当成同一份文档处理了。
解决办法就是用 nested 类型:
PUT /my_index
{
"mappings": {
"properties": {
"users": {
"type": "nested",
"properties": {
"name": { "type": "text" },
"age": { "type": "integer" }
}
}
}
}
}
nested 会把每个内部对象独立存储,查询时就能保证字段之间的关联关系正确。
注意:nested 查询性能比普通字段差,因为每个嵌套对象都是独立的 Lucene 文档。能用普通对象就别用 nested。
4. join 类型
join 类型用来建立父子关系。比如一个订单有多个商品,你可以把订单当父文档,商品当子文档。
PUT /my_index
{
"mappings": {
"properties": {
"my_join_field": {
"type": "join",
"relations": {
"order": "item"
}
}
}
}
}
写入父文档时指定 name: "order",子文档指定 name: "item" 和 parent: 父文档ID。
说实话,join 类型我用的不多。因为 ES 的父子查询性能开销大,而且同一个索引的文档要放在同一个分片上,容易造成热点。能用 nested 解决的问题,尽量别用 join。
三、Mapping 参数最佳实践
Mapping 参数很多,但真正关键的没几个。我挑几个实战中最重要的说。
1. index 参数
控制字段是否被索引。默认是 true。如果一个字段你只存不查,比如日志里的原始报文,可以关掉索引:
"raw_message": {
"type": "text",
"index": false
}
这样能节省存储空间,写入速度也更快。我在日志场景里经常这么干。
2. doc_values 参数
控制字段是否用于排序和聚合。默认是 true。如果你确定某个字段不需要排序或聚合,可以关掉:
"description": {
"type": "text",
"doc_values": false
}
text 类型默认没有 doc_values,但 keyword 有。关掉 doc_values 能省不少磁盘空间,尤其是字段值很长的时候。
3. ignore_above 参数
只对 keyword 类型有效。控制字段的最大长度,超过这个长度的值会被忽略:
"tags": {
"type": "keyword",
"ignore_above": 256
}
为什么要设这个?因为 keyword 字段默认会加载到内存里做聚合。如果某个字段值特别长(比如 10KB 的文本),内存直接爆炸。我见过一个案例,就是因为没设 ignore_above,导致集群 OOM。
4. coerce 参数
控制是否自动转换类型。比如字符串 "123" 自动转成数字 123。默认是 true:
"price": {
"type": "float",
"coerce": false
}
关掉 coerce 的好处是:如果上游传了脏数据,ES 会直接报错,而不是悄悄帮你转成 0。这样你能及时发现数据问题。
5. copy_to 参数
把多个字段的值合并到一个字段里,方便全文搜索:
PUT /my_index
{
"mappings": {
"properties": {
"first_name": {
"type": "text",
"copy_to": "full_name"
},
"last_name": {
"type": "text",
"copy_to": "full_name"
},
"full_name": {
"type": "text"
}
}
}
}
这样你搜 full_name 就能同时匹配到 first_name 和 last_name 的内容。省得写复杂的 multi_match 查询。
四、总结一下
Mapping 设计是 ES 使用的基础,设计得好,后面省心很多。我最后给几个建议:
- 生产环境用显式映射,别偷懒
- text 和 keyword 搞清楚用途,别混用
- 能用 nested 就别用 join
- 不需要的索引和 doc_values 关掉,省空间
- ignore_above 一定要设,防止内存爆炸
嗯,Mapping 这块就讲这么多。下一章咱们聊聊索引生命周期管理,那个也是实战中的重头戏。