Mapping 设计:字段类型选错有多惨

22 May 2026 – wusfe · 4 min read

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.00000000018998.9999999999,聚合时误差累积。

原则:金额用 scaled_float,缩放因子设为 100(精确到分),或者用 double

坑 4:date 格式猜错

ES 支持的日期格式很多,但自动检测可能识不出你的自定义格式(比如 yyyyMMddMM/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_abovedynamic 等少数参数)。你需要的是 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 数据层的骨架。自动映射省事但危险——它能让你快速启动,也能在深夜给你惊喜。记住三条铁律:

  1. 不信任自动映射——结构化数据一律显式声明类型
  2. 默认为 keyword,按需开 text——宁可缺分词,不要误匹配
  3. 改 Mapping = 重建索引——设计时多想一步,上线后少一次 reindex