深入 Elasticsearch(03):Mapping 与字段类型
上一篇解决了 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 -- "不分词" --> 倒排索引
text 和 keyword 的区别是 mapping 中最重要的概念。text 字段在写入时会经过 analysis pipeline——分词、转小写、去停用词——然后把产生的词项写入倒排索引。keyword 字段则把原始值当作一个完整的词项直接存入倒排索引,不做任何分析。
1 | |
这个区别直接影响搜索行为:对 text 字段可以用 match 查询做全文搜索,对 keyword 字段应该用 term 查询做精确匹配。
Object 与 Nested
JSON 文档经常有嵌套结构。ES 处理嵌套结构有两种方式。
object 类型是默认方式。ES 把嵌套的 JSON 对象扁平化为多个独立字段:
1 | |
在 Lucene 内部变成两个独立字段:user.name = “alice” 和 user.age = 30。
这种扁平化在处理对象数组时会丢失字段之间的关联:
1 | |
扁平化后变成 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 | |
写入后,name 字段可以用 match 做全文搜索,name.raw 字段可以用 term 做精确匹配或用于排序和聚合。底层实际上为同一份源数据建了两套索引结构。
实验:Explicit Mapping 与搜索行为
创建一个带 explicit mapping 的 index:
1 | |
写入测试文档:
1 | |
对比 text 和 keyword 字段的搜索行为:
1 | |
查看 mapping 结构:
1 | |
模式提炼:Schema-on-Write vs Schema-on-Read
1 | |
| 方面 | 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 不能改成 keyword,string 不能改成 long。只能添加新字段。要修改已有字段的类型,必须创建新 index、定义新 mapping、然后 reindex。
误解三:object 类型能保持数组元素内的字段关联。 object 类型在处理数组时会丢失关联。需要保持关联时必须用 nested 类型,但 nested 有额外的性能成本。
练习
-
创建一个不带 mapping 的 index,写入一条文档
{"count": "42", "label": "test"}。用GET /_mapping查看 ES 推断出的类型。"42"被推断成什么类型? -
创建一个带 multi-field 的 index:title 字段同时是
text和keyword。写入数据后,分别用match查 title 和term查 title.keyword,验证两种查询的行为差异。 -
创建一个
nested类型的字段,写入包含对象数组的文档,用nestedquery 验证字段关联性。
系列导航
| 上一篇 | 下一篇 |
|---|---|
| Lucene 内部:Segment、倒排索引与 Doc Values | Analysis 管道:从原始文本到可搜索词项 |
