Mapping精讲:动态映射与显式映射、字段类型详解、Mapping参数最佳实践

Mapping 这东西,说白了就是告诉 ES 你的数据长什么样。很多新手上来就一把梭,让 ES 自己猜字段类型——嗯,我当年也这么干过,结果被坑得不轻。今天咱们就把 Mapping 掰开揉碎了讲清楚。

一、动态映射 vs 显式映射

ES 有两种方式定义字段类型:动态映射和显式映射。我个人习惯是:开发环境用动态映射省事,生产环境必须显式定义。

1. 动态映射

你往索引里塞一条数据,ES 自动推断字段类型。比如你传了个 "age": 25,ES 就认为 age 是 long 类型。

动态映射的推断规则

JSON 数据ES 推断类型
"hello"text + keyword
123long
true / falseboolean
"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-ddyyyy-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 这块就讲这么多。下一章咱们聊聊索引生命周期管理,那个也是实战中的重头戏。