Mapping 设计:字段类型选错有多惨
Mapping 设计:字段类型选错有多惨
线上订单搜索功能上线第三周,客服反馈:用户按"已支付"状态筛选订单,结果里混入了"已取消"的。你打开 Kibana 一查,好家伙——status 字段存的是 "paid",但搜索结果里 "canceled" 也回来了。
根因:status 被自动映射成了 text 类型,分词器把英文单词拆得稀碎,模糊匹配到了不该匹配的文档。
这就是 Mapping 设计出错的代价。本篇讲清楚 ES 字段类型怎么选、自动映射的坑在哪里、以及生产环境怎么设计才不出事。
自动映射:方便但危险
往 ES 写第一条数据时不指定 Mapping,ES 会根据 JSON 值自动推断类型:
// 你写入的文档
{
"sku_id": 10086,
"title": "iPhone 15 Pro Max",
"price": 8999.00,
"created_at": "2025-06-01T10:00:00Z"
}
ES 自动创建的 Mapping 长这样:
{
"sku_id": { "type": "long" },
"title": { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
"price": { "type": "float" },
"created_at": { "type": "date" }
}
看起来还行?那是因为这个例子太简单。实际生产环境的雷都在这些地方:
坑 1:数字 ID 被映射成 long 而不是 keyword
sku_id 被映射成 long。这意味着你不能用 term 精确查询,只能用 term 按数值匹配——大多数场景下完全没问题,直到某天你需要对 ID 做前缀匹配或者 ID 超过了 long 的 2^63 上限。
原则:不会参与数值计算的 ID,一律用 keyword。
坑 2:字符串优先级:text > keyword
ES 遇到字符串,默认生成 text 字段,外加一个 .keyword 子字段。结构化数据(订单状态、国家代码、标签)被当成全文文本索引,白占磁盘和内存不说,还可能导致开头那种误匹配。
原则:枚举值、状态码、标签、ID 类字符串,一律手动指定为 keyword。
坑 3:float 精度丢失
price 被映射成 float(32 位浮点)。电商金额用 float 是大忌——8999.00 存储后可能变成 8999.0000000001 或 8998.9999999999,聚合时误差累积。
原则:金额用 scaled_float,缩放因子设为 100(精确到分),或者用 double。
坑 4:date 格式猜错
ES 支持的日期格式很多,但自动检测可能识不出你的自定义格式(比如 yyyyMMdd 或 MM/dd/yyyy)。一旦识别失败,字段就退化为 text,范围查询和排序全部报废。
正确的 Mapping 设计思路
重新设计上面那条文档的 Mapping:
PUT /orders
{
"mappings": {
"properties": {
"sku_id": { "type": "keyword" },
"title": { "type": "text", "analyzer": "ik_max_word" },
"price": { "type": "scaled_float", "scaling_factor": 100 },
"status": { "type": "keyword" },
"category": { "type": "keyword" },
"tags": { "type": "keyword" },
"quantity": { "type": "integer" },
"discount": { "type": "half_float" },
"description": { "type": "text", "analyzer": "ik_smart" },
"created_at": { "type": "date", "format": "yyyy-MM-dd'T'HH:mm:ss||yyyy-MM-dd||epoch_millis" },
"is_deleted": { "type": "boolean" }
}
}
}
几个关键决策的说明:
| 字段 | 类型 | 为什么 |
|---|---|---|
sku_id |
keyword |
标识符,不做数值计算,需要精确匹配和聚合 |
title |
text + ik_max_word |
商品名需要全文搜索,用 IK 分词做细粒度匹配 |
price |
scaled_float |
金额精度敏感,乘以 100 存为 long,无浮点误差 |
status |
keyword |
枚举值,精确匹配 |
discount |
half_float |
折扣率对精度要求不高,16 位浮点省一半空间 |
description |
text + ik_smart |
商品描述搜索,用粗粒度 IK 分词 |
created_at |
date + 多格式 |
兼容 ISO 和 Unix 时间戳,加 ` |
is_deleted |
boolean |
真就是真,假就是假 |
Dynamic Template:给自动映射上保险
你不可能枚举所有字段——业务迭代太快,字段会不断新增。这时候用 dynamic template 给自动映射加规则:
PUT /orders
{
"mappings": {
"dynamic_templates": [
{
"strings_as_keyword": {
"match_mapping_type": "string",
"mapping": { "type": "keyword" }
}
},
{
"longs_as_long": {
"match_mapping_type": "long",
"mapping": { "type": "long" }
}
},
{
"amounts_as_scaled_float": {
"match": "*_amount",
"mapping": { "type": "scaled_float", "scaling_factor": 100 }
}
}
]
}
}
三条规则的含义:
- 所有新来的字符串字段默认映射为
keyword,想全文搜索的再手动改为text - 长整型保持
long - 字段名以
_amount结尾的自动用scaled_float
核心思路:默认安全,按需放开。 keyword 不会丢数据也不会误匹配,是最安全的默认值。
线上改 Mapping 的正确姿势
Mapping 一旦创建,已有字段的类型不能修改(只能改 ignore_above、dynamic 等少数参数)。你需要的是 reindex:
# 用正确的 Mapping 创建新索引
PUT /orders_v2
{ "mappings": { ... } }
# 把老数据迁到新索引
POST /_reindex
{
"source": { "index": "orders_v1" },
"dest": { "index": "orders_v2" }
}
# 用 Alias 切换(第 08 篇会详细讲)
POST /_aliases
{
"actions": [
{ "remove": { "index": "orders_v1", "alias": "orders" } },
{ "add": { "index": "orders_v2", "alias": "orders" } }
]
}
生产环境 Checklist
- ID 类字段用
keyword,不走 text 分词 - 金额用
scaled_float,不用float - 枚举/状态字段显式指定
keyword - date 字段显式声明
format,不依赖自动检测 - 配置
dynamic_templates,字符串默认keyword - 关闭不需要索引的字段:
"index": false - 不需要源文档的场景关闭
_source -
dynamic设为"strict"或"runtime"防止意外字段写入(视业务需要)
总结
Mapping 是 ES 数据层的骨架。自动映射省事但危险——它能让你快速启动,也能在深夜给你惊喜。记住三条铁律:
- 不信任自动映射——结构化数据一律显式声明类型
- 默认为 keyword,按需开 text——宁可缺分词,不要误匹配
- 改 Mapping = 重建索引——设计时多想一步,上线后少一次 reindex