Appearance
Mapping 应该怎么设计?
Mapping 是 ES 里非常关键的一层。它决定一个字段是什么类型、是否分词、怎么分词、能不能排序聚合、对象怎么展开。很多 ES 查询问题,最后追根溯源都是 Mapping 设计不合适。
Mapping 是什么?
Mapping 可以类比 MySQL 的表结构,但它不只是字段类型定义,还包含倒排索引相关配置。
Mapping 主要决定:
- 字段名称。
- 字段类型,比如 keyword、text、long、date。
- 字符串字段是否分词。
- 使用什么 analyzer。
- 是否建立索引。
- 对象字段如何展开。
- 是否保存 doc_values,用于排序、聚合和脚本访问。
创建一个索引时,可以显式指定 Mapping:
http
PUT /articles
{
"mappings": {
"properties": {
"articleId": { "type": "keyword" },
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
},
"publishTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis"
},
"tags": { "type": "keyword" }
}
}
}text 和 keyword 怎么选?
这是最常见的问题。
text 会分词,适合全文检索:
json
{ "title": { "type": "text", "analyzer": "ik_max_word" } }keyword 不分词,适合精确匹配、排序、聚合:
json
{ "status": { "type": "keyword" } }可以按这个规则判断:
- 用户会输入关键词搜它,用 text。
- 业务代码要精确等值匹配它,用 keyword。
- 要 terms 聚合、排序、去重,用 keyword。
- 标题既要搜又要精确匹配,用 text + keyword 多字段。
不要对 text 字段直接做 term 查询,除非你非常确定它的分词结果。也不要对 text 字段做排序或聚合,通常应该用它的 .keyword 子字段。
term 和 match 为什么经常查不出数据?
先看两个查询:
http
GET /articles/_search
{
"query": {
"term": {
"title": "Elasticsearch"
}
}
}http
GET /articles/_search
{
"query": {
"match": {
"title": "Elasticsearch"
}
}
}term 查询不会对查询词分词,它拿原始 value 去倒排索引里找 term。
match 查询会先使用字段的搜索分词器处理查询内容,再拿分词结果去匹配。
所以:
- keyword 字段常用 term、terms。
- text 字段常用 match、match_phrase。
如果不确定字段到底被分成了什么词,可以用 _analyze:
http
GET /articles/_analyze
{
"field": "title",
"text": "金十期货"
}数值和日期字段
数值字段常见类型有 integer、long、float、double、scaled_float 等。
选择时不要一味用 long 或 double:
- 计数、ID、时间戳可以用 long。
- 金额如果需要精确计算,不建议用 float/double,可以用 long 保存分,或用 scaled_float。
- 枚举状态不要用 text,通常用 keyword 或 integer。
日期字段底层会转成 UTC 时间戳。建议写入时使用带时区的标准时间,或者明确指定 format:
json
{
"publishTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis"
}
}如果业务传的是 2025-05-16 10:00:00 这种无时区字符串,要确认应用、ES、Kibana 的时区展示逻辑,避免“查出来差 8 小时”的误会。
数组不需要单独声明
ES 没有专门的 array 类型。一个字段既可以写单值,也可以写数组,但数组里的元素类型要一致。
比如 Mapping 是:
json
{ "tags": { "type": "keyword" } }写入时可以这样:
http
POST /articles/_doc/1
{
"tags": ["MySQL", "Elasticsearch", "Redis"]
}查询包含某个标签:
http
GET /articles/_search
{
"query": {
"term": {
"tags": "Elasticsearch"
}
}
}object:默认会被拍平
对象字段默认是 object。比如:
json
{
"authors": [
{ "name": "小林", "age": 18 },
{ "name": "小王", "age": 30 }
]
}ES 默认会把它拍平成类似:
json
{
"authors.name": ["小林", "小王"],
"authors.age": [18, 30]
}这会带来一个问题:对象内部关系丢失了。
如果查询:
text
authors.name = 小林 AND authors.age = 30object 可能会误命中,因为 name 和 age 来自数组里的不同对象。
nested:保留数组对象关系
如果需要保留数组对象里每个对象的独立关系,要使用 nested:
http
PUT /articles
{
"mappings": {
"properties": {
"authors": {
"type": "nested",
"properties": {
"name": { "type": "keyword" },
"age": { "type": "integer" }
}
}
}
}
}查询时也要使用 nested query:
http
GET /articles/_search
{
"query": {
"nested": {
"path": "authors",
"query": {
"bool": {
"must": [
{ "term": { "authors.name": "小林" } },
{ "term": { "authors.age": 18 } }
]
}
}
}
}
}nested 的查询和更新成本比普通 object 更高,不要滥用。只有当你真的需要保持数组对象内部关系时才用它。
flattened:避免 Mapping 爆炸
如果某个字段是动态 JSON,比如设备上报 payload、扩展属性 ext,你不知道里面会出现多少 key,就要警惕 Mapping 爆炸。
默认 object 会给每个子字段建立 Mapping,字段数可能迅速膨胀。
这时可以使用 flattened:
http
PUT /iot_devices
{
"mappings": {
"properties": {
"deviceId": { "type": "keyword" },
"payload": { "type": "flattened" }
}
}
}flattened 会把整个对象作为一个扁平字段处理,可以搜索 key-value,但查询能力不如完整 object/nested 灵活。它适合“字段很多、结构不稳定、只需要有限搜索”的场景。
enabled false:只存不查
如果某个对象只需要保存在 _source,完全不需要搜索,可以设置:
json
{
"rawPayload": {
"type": "object",
"enabled": false
}
}这样 ES 不会解析里面的字段,也不会为它建立索引,可以减少 Mapping 膨胀和写入成本。
dynamic mapping 要谨慎
ES 可以自动根据写入数据推断字段类型,这叫 dynamic mapping。它方便,但也容易埋坑。
比如第一次写入:
json
{ "price": "10" }ES 可能把 price 推断成 keyword。后面你想做 range 查询,就会很别扭。
生产建议:核心索引尽量显式定义 Mapping,至少要把关键字段提前定义好。
如果确实需要动态字段,可以配合 dynamic_templates 控制规则:
http
PUT /logs
{
"mappings": {
"dynamic_templates": [
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword",
"ignore_above": 256
}
}
}
]
}
}Mapping 能不能修改?
已经存在的字段类型通常不能直接修改。比如一个字段已经是 keyword,不能原地改成 date。
常见做法是:
- 创建新索引,定义正确 Mapping。
- 使用
_reindex把旧索引数据迁移到新索引。 - 用别名切换读写流量。
- 验证后删除旧索引。
这也是为什么一开始设计 Mapping 要慎重。
设计 Mapping 的几个经验
- 搜索字段用 text,过滤、聚合、排序字段用 keyword。
- 标题、名称这类字段常用 text + keyword 多字段。
- 日期字段明确 format,时间尽量统一时区。
- 金额、比例等字段提前想清楚精度。
- 数组对象要不要保持对象关系,决定用 object 还是 nested。
- 动态 JSON 字段优先考虑 flattened 或 enabled false。
- 避免让用户自定义 key 无限制进入 Mapping。
- 核心业务字段显式建 Mapping,不依赖自动推断。
小结
Mapping 是 ES 查询质量和性能的地基。字段类型错了,后面 DSL 写得再漂亮也很难补救。
尤其要记住:
- text 会分词,keyword 不分词。
- object 会拍平,nested 才能保留数组对象关系。
- flattened 可以缓解动态字段导致的 Mapping 爆炸。
- 已有字段类型基本不能原地改,通常要重建索引。
把 Mapping 设计好,ES 后面的查询、聚合、排序、同步都会轻松很多。