上一篇解决了 Lucene 内部的段和索引结构。这一篇进入 ES 如何把 JSON 文档映射到 Lucene 字段。

一个 JSON 文档写入 ES 后,每个字段会变成 Lucene 内部的某种数据结构——倒排索引、BKD-tree、Doc Values 或 Stored Fields。这个从"JSON 字段"到"Lucene 数据结构"的翻译规则,就是 mapping。

本文只抓一个问题:mapping 在 ES 中扮演什么角色,以及 dynamic mapping 和 explicit mapping 的差异会怎样影响搜索行为。

Mapping 是什么

Mapping 定义了一个 index 中文档的字段名、字段类型和索引方式。可以类比为关系型数据库的 DDL(CREATE TABLE),但有几个关键区别:

  • Mapping 允许动态推断。写入一个未知字段时,ES 可以自动猜测类型并添加到 mapping 中。
  • Mapping 一旦创建,字段类型不可修改。只能添加新字段,不能把一个 text 字段改成 keyword。要改变已有字段的类型,必须创建新 index 并 reindex。
  • 同一个字段可以有多种索引方式(multi-fields)。

核心字段类型

ES 的字段类型决定了底层使用哪种 Lucene 数据结构:

字段类型 底层结构 用途
text 倒排索引(经过 analysis) 全文搜索
keyword 倒排索引(不分词,精确存储) 精确匹配、排序、聚合
long, integer, double, float BKD-tree + Doc Values 范围查询、排序、聚合
date BKD-tree + Doc Values 日期范围查询、排序
boolean Doc Values 过滤、聚合
object 扁平化为多个字段 嵌套 JSON 结构
nested 独立的 Lucene 子文档 保持数组元素之间的字段关联
geo_point BKD-tree 地理距离和范围查询
geo_shape tessellated triangles 索引 复杂地理形状查询

下面这张图按"底层数据结构"对字段类型做分组。同一列的字段类型共享相同的 Lucene 存储和查询路径,选型时先确定需要哪种查询能力,再从对应列中选类型。

graph LR
    subgraph 倒排索引
        text["text<br/>全文搜索"]
        keyword["keyword<br/>精确匹配/聚合"]
    end
    subgraph BKD-tree + Doc Values
        numeric["long / integer<br/>double / float"]
        date["date"]
        geo_point["geo_point"]
    end
    subgraph 独立子文档
        nested["nested<br/>保持数组元素关联"]
    end
    subgraph 扁平化字段
        object["object<br/>默认嵌套处理"]
    end

    text -- "经过 analysis" --> 倒排索引
    keyword -- "不分词" --> 倒排索引

textkeyword 的区别是 mapping 中最重要的概念。text 字段在写入时会经过 analysis pipeline——分词、转小写、去停用词——然后把产生的词项写入倒排索引。keyword 字段则把原始值当作一个完整的词项直接存入倒排索引,不做任何分析。

1
2
3
4
5
6
7
"Elasticsearch Engine" 写入 text 字段:
→ analysis → ["elasticsearch", "engine"]
→ 倒排索引中有两个词项

"Elasticsearch Engine" 写入 keyword 字段:
→ 不分析 → ["Elasticsearch Engine"]
→ 倒排索引中只有一个词项(保留大小写和空格)

这个区别直接影响搜索行为:对 text 字段可以用 match 查询做全文搜索,对 keyword 字段应该用 term 查询做精确匹配。

Object 与 Nested

JSON 文档经常有嵌套结构。ES 处理嵌套结构有两种方式。

object 类型是默认方式。ES 把嵌套的 JSON 对象扁平化为多个独立字段:

1
2
3
4
5
6
{
"user": {
"name": "alice",
"age": 30
}
}

在 Lucene 内部变成两个独立字段:user.name = “alice” 和 user.age = 30。

这种扁平化在处理对象数组时会丢失字段之间的关联:

1
2
3
4
5
6
{
"users": [
{"name": "alice", "age": 30},
{"name": "bob", "age": 25}
]
}

扁平化后变成 users.name = [“alice”, “bob”] 和 users.age = [30, 25]。这时搜索"name=alice AND age=25"会命中,因为 alice 和 25 都存在于各自的数组中——但它们不属于同一个对象。

nested 类型解决这个问题。每个数组元素被索引为一个独立的 Lucene 子文档,保持字段之间的关联。代价是搜索时需要 nested query 来查询这些子文档,性能也比普通 object 查询更重。

Dynamic Mapping

不指定 mapping 直接写入文档时,ES 会自动推断字段类型。推断规则:

JSON 值 推断的 ES 类型
"hello" text + keyword 子字段
123 long
1.5 float
true / false boolean
"2026-06-26" date
{"a": 1} object

Dynamic mapping 的主要陷阱:

  • 数字字符串(如 "42")会被推断为 text,不是 long。如果后续期望做范围查询,会不符合预期。
  • 日期字符串的格式必须匹配 ES 的默认日期格式列表。不匹配的日期字符串会被当作 text
  • 第一条文档的字段值决定了字段类型,后续写入如果类型不兼容会报错。

下面这张图把 Dynamic Mapping 的推断决策画出来。每个写入字段按 JSON 值类型走不同分支,最终落到一个 ES 字段类型上。注意字符串分支有三个出口——日期格式命中、数字格式命中、都不命中——这正是 dynamic mapping 容易产生意外类型的根源。

flowchart TD
    A["写入未知字段"] --> B{"JSON 值类型?"}
    B -->|object / array| C["object"]
    B -->|true / false| D["boolean"]
    B -->|整数| E["long"]
    B -->|浮点| F["float"]
    B -->|字符串| G{"匹配日期格式?"}
    G -->|是| H["date"]
    G -->|否| I{"date_detection /<br/>numeric_detection"}
    I -->|数字格式命中| J["long / float"]
    I -->|都不命中| K["text + keyword 子字段"]

可以通过 dynamic: "strict" 禁用 dynamic mapping,强制所有字段必须预先定义。这是生产环境推荐的做法。

Multi-fields

同一个源字段可以用多种方式索引。最常见的场景是一个字符串字段同时需要全文搜索和精确匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT /products
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": {
"raw": {
"type": "keyword"
}
}
}
}
}
}

写入后,name 字段可以用 match 做全文搜索,name.raw 字段可以用 term 做精确匹配或用于排序和聚合。底层实际上为同一份源数据建了两套索引结构。

实验:Explicit Mapping 与搜索行为

创建一个带 explicit mapping 的 index:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT /mapping-demo
{
"mappings": {
"properties": {
"title": { "type": "text" },
"status": { "type": "keyword" },
"price": { "type": "double" },
"created_at": { "type": "date" },
"description": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
}
}
}
}

写入测试文档:

1
2
3
4
5
POST /mapping-demo/_bulk
{"index": {"_id": "1"}}
{"title": "Elasticsearch Guide", "status": "published", "price": 49.99, "created_at": "2026-01-15", "description": "A comprehensive guide to Elasticsearch"}
{"index": {"_id": "2"}}
{"title": "Search Engine Basics", "status": "draft", "price": 29.99, "created_at": "2026-03-20", "description": "Introduction to search engine technology"}

对比 textkeyword 字段的搜索行为:

1
2
3
4
5
6
7
8
9
10
11
12
# text 字段用 match:能匹配到(因为 "elasticsearch" 是分析后的词项)
GET /mapping-demo/_search
{ "query": { "match": { "title": "elasticsearch" } } }

# keyword 字段用 term:能精确匹配
GET /mapping-demo/_search
{ "query": { "term": { "status": "published" } } }

# 对 text 字段用 term 查询 "Elasticsearch Guide":不会命中
# 因为 text 字段存的是分析后的小写词项,而 term 查询不分析输入
GET /mapping-demo/_search
{ "query": { "term": { "title": "Elasticsearch Guide" } } }

查看 mapping 结构:

1
GET /mapping-demo/_mapping

模式提炼:Schema-on-Write vs Schema-on-Read

1
2
Schema-on-Write:写入时确定结构和类型 → ES mapping, RDBMS DDL
Schema-on-Read: 读取时解释数据结构 → MongoDB (弱 schema), 数据湖
方面 Schema-on-Write (ES Mapping) Schema-on-Read
类型确定时机 写入时 查询时
类型错误发现 写入即报错 查询时才发现
查询性能 索引结构优化,查询快 需要运行时类型转换
灵活性 修改字段类型需要 reindex 随时可改
适用场景 搜索、分析(需要索引优化) 探索性分析、半结构化数据

工程迁移表

概念 Elasticsearch MySQL/PostgreSQL MongoDB Apache Solr
模式定义 Mapping(JSON) DDL (CREATE TABLE) Schema Validation(可选) schema.xml / managed-schema
类型修改 不可修改已有字段类型 ALTER TABLE(有锁) 无限制 修改 schema 后重建
动态字段 Dynamic mapping 默认支持 dynamicField 规则
多字段索引 Multi-fields 多个独立索引 多个独立索引 copyField
嵌套数据 object / nested JOIN / JSON 类型 嵌入式文档 nested documents

常见误解

误解一:字符串字段都应该用 text 类型。 如果一个字段的值是 status code、ID、tag 这类不需要分词搜索的值,应该用 keyword 类型。用 text 类型存 status code 会导致分词后无法精确匹配。

误解二:mapping 可以随时修改。 已有字段的类型不可变更。text 不能改成 keywordstring 不能改成 long。只能添加新字段。要修改已有字段的类型,必须创建新 index、定义新 mapping、然后 reindex。

误解三:object 类型能保持数组元素内的字段关联。 object 类型在处理数组时会丢失关联。需要保持关联时必须用 nested 类型,但 nested 有额外的性能成本。

练习

  1. 创建一个不带 mapping 的 index,写入一条文档 {"count": "42", "label": "test"}。用 GET /_mapping 查看 ES 推断出的类型。"42" 被推断成什么类型?

  2. 创建一个带 multi-field 的 index:title 字段同时是 textkeyword。写入数据后,分别用 match 查 title 和 term 查 title.keyword,验证两种查询的行为差异。

  3. 创建一个 nested 类型的字段,写入包含对象数组的文档,用 nested query 验证字段关联性。

系列导航

上一篇 下一篇
Lucene 内部:Segment、倒排索引与 Doc Values Analysis 管道:从原始文本到可搜索词项

参考资料