大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Mapping 深入:字段类型决定能怎么搜
这一课你将能做到
- 说清楚 text 与 keyword 的本质区别,并为题库每个字段选对类型
- 用显式 mapping 创建索引,理解动态 mapping(dynamic mapping)的便利与 mapping 爆炸风险
- 区分 object、nested、flattened 三种复杂类型,说出选项数组在 object 下为什么会「串味」
为什么要学
很多人第一次用 ES 是「直接写文档,类型自动猜」——这在演示里很爽,在生产里是事故来源。ES 会为每个新字段自动创建倒排索引:你把 "difficulty": "2" 写进去,它可能猜成文本类型,之后范围查询、按难度排序全部失败;你在 JSON 里随手塞一个带时间戳的嵌套对象,字段数无限膨胀,最终撑爆集群元数据。
人话:mapping 就是 ES 世界的「建表语句(DDL)」。MySQL 里你得先定义 difficulty INT,ES 里同样要定义 difficulty: integer。区别在于 ES 允许你不定义、它替你猜——猜对了省事,猜错了要重建索引才能改(字段类型不能原地修改,只能 reindex)。所以 mapping 是 ES 学习中「偷懒代价最大」的一块。
先建立图像
text 与 keyword 对同一个值「Spring 事务管理」的处理:
text 类型(lessonTitle):
写入时经过分词器 → ["spring", "事务", "管理"]
用途:match 全文搜索,搜「事务」能中
不能:精确等值、排序、聚合(默认)
keyword 类型(lessonTitle.keyword):
写入时整串不分词 → "Spring 事务管理" 一个词项
用途:term 精确过滤、排序、terms 聚合(如按课时分组统计)
不能:搜「事务」两个字命中整句
常见类型速查:
| 类型 | 用途 | 题库字段 |
|---|---|---|
| text | 全文检索内容 | stem、analysis、lessonTitle |
| keyword | 精确值/枚举/标签 | tags、courseId、answer |
| integer/byte | 整数范围与排序 | difficulty(1–5) |
| date | 日期 | updatedAt |
| boolean | 布尔 | published |
| dense_vector | 向量(第 9 课) | embedding |
跟着做
步骤 1:显式 mapping 创建题库索引
curl -u elastic:abcd1234 -X PUT "http://localhost:9200/questions" -H "Content-Type: application/json" -d'
{
"mappings": {
"properties": {
"stem": { "type": "text" },
"analysis": { "type": "text" },
"options": { "type": "text" },
"answer": { "type": "keyword" },
"tags": { "type": "keyword" },
"courseId": { "type": "keyword" },
"lessonId": { "type": "keyword" },
"lessonTitle":{
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
},
"difficulty": { "type": "byte" },
"published": { "type": "boolean" },
"updatedAt": { "type": "date", "format": "yyyy-MM-dd||epoch_millis" }
}
}
}'
注意 lessonTitle 用了多字段(fields):一个字段同时建两份索引,lessonTitle 走分词用于全文搜,lessonTitle.keyword 不分词用于排序聚合。这是 ES 的惯用手法,动态 mapping 给字符串自动加 .keyword 子字段也是这个原因。
步骤 2:查看 mapping 与字段能力
curl -u elastic:abcd1234 "http://localhost:9200/questions/_mapping?pretty"
# 查看某个字段
curl -u elastic:abcd1234 "http://localhost:9200/questions/_mapping/field/tags?pretty"
步骤 3:实验 dynamic 的三种策略
先看默认行为:写入一个 mapping 里没有的新字段,ES 自动推断并加入 mapping。然后试试严格模式,建新索引时设置:
curl -u elastic:abcd1234 -X PUT "http://localhost:9200/questions_strict" -H "Content-Type: application/json" -d'
{
"mappings": {
"dynamic": "strict",
"properties": {
"stem": { "type": "text" }
}
}
}'
此时写入带未知字段的文档会直接报错(400),强制你先改 mapping 再写。三种策略对比:
true(默认) 未知字段自动推断加入 mapping —— 方便,但可能猜不准、字段失控
false 未知字段被存入 _source 但不建索引 —— 存得下、搜不到,适合临时宽容
strict 未知字段直接拒绝写入 —— 最可控,推荐生产核心索引用
步骤 4:object 与 nested 的「串味」实验
题目有多条评论(数组对象),用默认 object 类型写入:
curl -u elastic:abcd1234 -X PUT "http://localhost:9200/questions/_doc/99" -H "Content-Type: application/json" -d'
{
"stem": "测试题",
"comments": [
{ "user": "张三", "content": "讲得好" },
{ "user": "李四", "content": "没看懂" }
]
}'
ES 对 object 数组会拍平成:
comments.user: ["张三", "李四"]
comments.content: ["讲得好", "没看懂"]
两个数组之间的归属关系丢失了!于是搜「张三 且 content=没看懂」竟然会命中——张三从没说过没看懂。需要保留对象内部边界时,字段必须声明为 nested:
"comments": {
"type": "nested",
"properties": {
"user": { "type": "keyword" },
"content": { "type": "text" }
}
}
nested 把数组中每个对象索引成独立的隐藏文档,查询时也要用专门的 nested 查询(第 5 课演示)。
步骤 5:类型写错后如何「改」
字段类型不能原地修改。正确路径是:新建一个 mapping 正确的索引 → reindex 搬数据 → 切换别名:
curl -u elastic:abcd1234 -X POST "http://localhost:9200/_reindex" -H "Content-Type: application/json" -d'
{
"source": { "index": "questions_v1" },
"dest": { "index": "questions_v2" }
}'
所以生产索引从第一天就该用别名(alias)访问,应用只认别名,重建时切换别名零停机。
讲清楚要点
- 选型口诀:要被「按词搜索」的内容用 text;要被「精确匹配、分组、排序」的值用 keyword;拿不准的文本就多字段(text + keyword 子字段)。标签、ID、枚举一律 keyword,哪怕它长得像数字——不参与算术的数字没必要用数值类型,keyword 聚合更快。
- dynamic mapping 的推断规则:字符串→text+keyword 子字段;整数→long;小数→float;布尔→boolean;日期格式字符串→date。注意它只看第一次出现的值,先到
"id": 123后到"id": "abc"就会写入冲突报错。 - mapping 爆炸:字段数没有硬上限,但每个字段都占集群状态(cluster state)内存。典型事故:把请求参数、用户 ID 直接作为 key 写进对象(如
{"props":{"u_10086":...}}),字段数涨到几十万,master 集群状态更新变慢甚至卡死。对策:dynamic: strict/false,或用 flattened 类型——整个子对象作为单个字段索引,key/value 可查但不产生海量字段。 - flattened vs nested 是两种极端:nested 保留完整结构、可精确查询对象内部,但成本高(隐藏文档);flattened 把对象拍平成一个 keyword 字段、防爆炸,但丢失内部归属。外层结构简单、只需要「对象里出现过某值」时选 flattened;选项/评论这类需要「同一对象内多条件」时选 nested。
- date 与数值类型开启 doc_values:keyword、数值、日期默认写一份列式结构(doc values),支撑排序、聚合、脚本;text 默认没有 doc_values(fielddata 要慎用,下一课讲)。这也是为什么聚合只能打在 keyword 上。
你可能会懵的地方
-
字段类型为什么不能改? 倒排索引是按写入时的类型建的:long 的索引结构装不下文本词项。改类型等于重新解析全部
_source重建整份索引,这就是 reindex 干的事。加新字段、给已有字段加子字段可以,改已有字段类型不行。 -
_source里明明有这个字段,为什么搜不到?_source是写入时原始 JSON 的存档,dynamic: false 或 index: false 的字段不建倒排索引——存得下、搜不到。能搜到取决于有没有索引,不取决于在不在_source里。 -
tags 为什么不用 text? 标签是「整体」,标签「spring boot」是一个值而不是 spring 和 boot 两个词。term 过滤
tags = "spring boot"要求词项完全相等;text 会把标签拆开,聚合分组时会按词拆出两组,统计全乱。 -
nested 这么好,为什么不所有数组都用 nested? 每个 nested 元素是独立 Lucene 文档,100 个元素的文档膨胀成 101 个文档,索引体积、查询开销都涨;且 nested 查询结果默认不返回匹配的内部对象,要用 inner_hits。只有确实需要对象内多条件联合时才用。
-
ignore_above 是什么? keyword 字段长度超过阈值(默认 256)就跳过索引(仍在
_source),防止超长字符串变成巨大词项。题干这种长文本不该走 keyword;长枚举值注意调阈值。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 所有字段都用 text | 无法精确过滤、聚合、排序 | 标签/ID/枚选用 keyword |
| 所有字符串都 keyword | 题干无法分词全文搜 | 长文本用 text 或多字段 |
| 放任 dynamic: true 接外部数据 | 类型猜歪、mapping 爆炸 | 核心索引 dynamic: strict |
| 选项数组用 object 后做联合查询 | 拍平丢失归属,假命中 | 联合条件必须 nested |
| 应用直连物理索引名 | 改 mapping 要停机 | 别名 + reindex 切换 |
| 把数值 ID 建成 long | 精确查、聚合没问题但无必要 | ID 类用 keyword |
卡点急救
- 写入报
mapper_parsing_exception:多半是类型冲突——同名字段先按 A 类型建过索引,现在传 B 类型值。先_mapping查实际类型;必须改就走 reindex 流程,别尝试硬写。 - terms 聚合报 fielddata 警告或结果怪异:你对 text 字段聚合了。改用
字段.keyword;若 mapping 里没有 keyword 子字段,加子字段后对新写入生效,历史数据需 reindex。 - 集群字段数失控、master 变慢:立刻排查产生动态 key 的字段,对该字段改用 flattened 或 dynamic: false 止损;已产生的垃圾字段只能通过 reindex 到干净索引清除。
面试常问
-
text 和 keyword 有什么区别? text 写入时分词,建立分词后的倒排索引,用于全文检索,默认不支持排序聚合;keyword 整体作为一个词项不分词,用于精确匹配、排序、terms 聚合。常见做法是多字段:text 主字段加 keyword 子字段,一份数据两种用法。
-
什么是 mapping 爆炸?怎么防? ES 为每个字段维护集群状态元数据,当字段数无界增长(如把用户 ID、参数名当 JSON key)会耗尽 master 堆内存、拖慢集群状态更新。防御:dynamic 设为 strict 或 false、使用 flattened 类型收敛字段、配合别名和 reindex 定期清理。
-
nested 和 object 的区别? object 数组会被拍平成多值平行字段,元素内部字段间的对应关系丢失,跨对象条件会产生假命中;nested 把每个数组元素索引为独立隐藏文档,保留内部边界,配合 nested 查询实现对象内联合过滤,代价是文档和索引开销增大。
自测三问
- 题库字段
answer、tags、analysis分别该是什么类型?为什么? - 一道题有多份「纠错记录」(每条含 user 和 reason),要查「张三提的且包含‘答案错误’的记录」,用 object 会发生什么?该怎么建?
- 为什么推荐应用只通过别名访问索引?结合 reindex 流程解释。
小结一句话
mapping 是 ES 的建表语句,字段类型决定索引结构和搜索方式:text 管分词全文搜、keyword 管精确与聚合,生产索引用显式 mapping + strict + 别名,把「重建」的退路提前留好。
延伸
- Mapping
- Field data types
- Using nested fields
- 下一课:索引与文档操作——单条 CRUD、批量写入怎么调优,以及 reindex 的实战场景。