Skip to content

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 = 30

object 可能会误命中,因为 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。

常见做法是:

  1. 创建新索引,定义正确 Mapping。
  2. 使用 _reindex 把旧索引数据迁移到新索引。
  3. 用别名切换读写流量。
  4. 验证后删除旧索引。

这也是为什么一开始设计 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 后面的查询、聚合、排序、同步都会轻松很多。