CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
索引与 Bulk — CodeRoadMap
索引与 Bulk CRUD、bulk 与 reindex。
索引管理与文档操作:CRUD、Bulk 与 Reindex
这一课你将能做到
熟练使用文档的增(PUT/POST)、查(GET)、改(整体覆盖与局部更新)、删四类操作,并看懂乐观锁版本号
用 Bulk API 批量导入题库 JSON,知道批次大小、并发数怎么调;会用 update_by_query / delete_by_query
说清楚 reindex 的三个典型场景,并能把题库全量数据在不接数据库同步的前提下灌入 ES
为什么要学
前几课解决了「建什么结构」,这一课解决「数据怎么进去、怎么维护」。真实项目里 99% 的文档不是一条一条 PUT 进去的——题库初始化导入 5 万道题,逐条请求光网络往返就要几小时;运营批量下架某课程的题目,也不可能查出来一条条删。Bulk、按查询更新/删除、reindex 就是为此准备的。
人话:单条 API 是「寄一封信」,Bulk 是「装一个集装箱一次发走」。但集装箱也不是越大越好——太大撑爆内存,太小浪费运费。这一课的操作全部基于本站真实场景:题库 JSON 从 MySQL 导出,不接 CDC,靠全量初始化 + 应用层增量写入 + 每日按 updatedAt 增量更新维护 ES。
先建立图像
文档操作全景:
单条:
PUT /questions/_doc/{id} 指定 ID 写入(不存在则建,存在则整体覆盖)
POST /questions/_doc 不指定 ID,ES 自动生成
GET /questions/_doc/{id} 按 ID 取
DELETE /questions/_doc/{id} 按 ID 删(标记删除,段合并时物理清除)
POST /questions/_update/{id} 局部更新(doc 合并/脚本)
批量:
/_bulk 一次请求多行操作
/_update_by_query 按查询条件批量更新
/_delete_by_query 按查询条件批量删除
/_reindex 索引间搬运
Bulk 请求体是「两行一组」的特殊格式,注意每行结尾都要有换行符:
{ "index": { "_id": "1" } } ← 动作行
{ "stem": "...", ... } ← 数据行
{ "delete": { "_id": "2" } } ← delete 没有数据行
← 文件末尾也要有一个换行
跟着做
步骤 1:单条 CRUD 与版本控制
写入并观察版本:
# 写入(指定 ID,与题库 question.id 一致)
curl -u elastic:abcd1234 -X PUT "http://localhost:9200/questions/_doc/100" -H "Content-Type: application/json" -d'
{
"stem": "Redis 如何实现分布式锁?",
"tags": ["redis", "锁"],
"difficulty": 3,
"published": true,
"updatedAt": "2026-10-05"
}'
# 按 ID 读取
curl -u elastic:abcd1234 "http://localhost:9200/questions/_doc/100?pretty"
# 局部更新:只改难度,其他字段保留
curl -u elastic:abcd1234 -X POST "http://localhost:9200/questions/_update/100" -H "Content-Type: application/json" -d'
{
"doc": { "difficulty": 4 }
}'
# 删除
curl -u elastic:abcd1234 -X DELETE "http://localhost:9200/questions/_doc/100?pretty"
乐观并发:第一次写入返回 _version: 1,之后每次修改递增。更新时带上条件可以防止「我读到的旧版本被别人先改了还被我覆盖」:
curl -u elastic:abcd1234 -X PUT "http://localhost:9200/questions/_doc/100?if_seq_no=1&if_primary_term=1" -H "Content-Type: application/json" -d'
{ "stem": "更新后的题干" }'
版本不匹配会返回 409 Conflict,应用层重试即可。ES 的更新本质是「GET 旧文档 → 合并 → 删除旧文档 → 写入新文档」,所以局部更新依然是整文档重建。
步骤 2:用 Bulk 初始化题库 把导出的题目存成 questions-bulk.json(动作行+数据行交替),文件开头示例:
{ "index": { "_id": "1" } }
{ "stem": "Spring 事务在什么情况下会失效?", "tags": ["spring","事务"], "difficulty": 2, "lessonTitle": "Spring 事务管理", "published": true, "updatedAt": "2026-10-01" }
{ "index": { "_id": "2" } }
{ "stem": "MySQL 索引为什么用 B+ 树?", "tags": ["mysql","索引"], "difficulty": 3, "lessonTitle": "索引原理", "published": true, "updatedAt": "2026-10-02" }
curl -u elastic:abcd1234 -X POST "http://localhost:9200/questions/_bulk" \
-H "Content-Type: application/x-ndjson" \
--data-binary "@questions-bulk.json"
注意三个细节:Content-Type 是 application/x-ndjson(换行分隔的 JSON);用 --data-binary 而不是 -d,后者会破坏换行;Bulk 不是事务——某几行失败不影响其他行,响应里要检查 errors: true 再逐条看错误原因。
批次大小经验:从每批 5MB 起步(约 1000–5000 道题,视文档大小),压测找拐点;单批别超过 15MB。导入提速还可以临时设 refresh_interval: -1、副本数 0,导完恢复:
curl -u elastic:abcd1234 -X PUT "http://localhost:9200/questions/_settings" -H "Content-Type: application/json" -d'
{
"index": {
"refresh_interval": "-1",
"number_of_replicas": 0
}
}'
# 导入完成后恢复为 "1s" 和 1,并 force merge 可选
步骤 3:update_by_query 增量同步本站数据 每日增量任务:把 MySQL 中昨天以来更新的题目查出来,直接用 update_by_query 不合适(它只能基于 ES 现有数据改)。本站的做法是应用层查出增量列表后仍走 Bulk(用 index 动作做覆盖写)。而 update_by_query 适合「对 ES 内已有文档做规则化批量修改」,例如给所有含某标签的题目追加一个新标签:
curl -u elastic:abcd1234 -X POST "http://localhost:9200/questions/_update_by_query" -H "Content-Type: application/json" -d'
{
"query": { "term": { "tags": "spring" } },
"script": {
"source": "if (ctx._source.tags != null && !ctx._source.tags.contains(\"javaee\")) { ctx._source.tags.add(\"javaee\") }",
"lang": "painless"
}
}'
步骤 4:delete_by_query 批量下架 curl -u elastic:abcd1234 -X POST "http://localhost:9200/questions/_delete_by_query?conflicts=proceed" -H "Content-Type: application/json" -d'
{
"query": {
"bool": {
"must": [
{ "term": { "lessonId": "L_OLD_88" } },
{ "term": { "published": false } }
]
}
}
}'
conflicts=proceed 表示遇到版本冲突继续执行,最后报告冲突数量。大量删除会产生很多标记删除的旧数据,必要时段合并回收空间。
步骤 5:reindex 的三个实战场景 # 场景1:mapping 改了(如 difficulty 从 text 改成 byte),搬到新索引
# 场景2:只搬运满足条件的数据(导出现网某课程题目做测试库)
curl -u elastic:abcd1234 -X POST "http://localhost:9200/_reindex" -H "Content-Type: application/json" -d'
{
"source": {
"index": "questions",
"query": { "term": { "courseId": "C_JAVA" } }
},
"dest": { "index": "questions_java_backup" }
}'
# 场景3:大批量 reindex 限速,避免打满集群
curl -u elastic:abcd1234 -X POST "http://localhost:9200/_reindex?wait_for_completion=false" -H "Content-Type: application/json" -d'
{
"source": { "index": "questions", "size": 500 },
"dest": { "index": "questions_v2" }
}'
wait_for_completion=false 会立即返回一个 task ID,用 GET _tasks/{id} 查进度,适合大数据量搬运。
讲清楚要点
PUT 覆盖写是最省事的更新 :相同 ID 再 PUT 就是整体覆盖。本站增量同步直接用 MySQL 查出的完整文档覆盖写 ES,不需要判断 ES 里存不存在,幂等且简单。
删除不是立即释放空间 :删除只是在段里打标记,查询自动跳过;物理空间要等段合并(merge)。大量删除后磁盘使用率不降是正常的,必要时 POST /questions/_forcemerge?only_expunge_deletes=true。
Bulk 的失败是「行级」的 :HTTP 状态码可能是 200,但响应体 errors: true 表示有失败行。常见失败原因:版本冲突、字段超长、JSON 格式错。批量程序必须检查这个字段并收集重试失败行。
导入三板斧 :临时关 refresh(-1)、临时 0 副本、分批 + 多线程并行;导完恢复设置、等副本分配完成。5 万题的题库通常几分钟内可完成。
refresh 与 translog 的安全角色 :数据写入内存缓冲的同时写 translog(事务日志并持久化),即使 refresh 前宕机,重启也能靠 translog 恢复——所以 ES 不是「裸写内存」,默认每 5 秒或请求显式 fsync 一次。
你可能会懵的地方
POST 和 PUT 写文档有什么区别?
带 ID 时两者都可写,语义上 PUT 是「幂等放置」、POST 是「新增」;不带 ID 必须用 POST,ES 自动生成随机 ID。本站固定用 PUT /_doc/{题库ID},文档 ID 与 MySQL 主键一致,方便对账和重写。
为什么更新后版本号一次跳了好几?
每次内部重试、主副本同步操作都可能让版本递增,不必依赖连续版本号,只要知道它单调递增、用于乐观锁即可。8.x 乐观锁实际用 _seq_no 和 _primary_term 组合判断。
Bulk 文件最后一行为什么要单独留换行?
NDJSON 按换行切分动作行和数据行,缺少结尾换行会导致最后一组解析失败。用 --data-binary 也是为了保留原始换行与字符。
reindex 会不会把旧索引删掉?
不会,reindex 只读取旧索引、写入目标索引,旧索引原样保留。确认新索引无误后,切换别名,再手动删除旧索引。
导入期间能正常搜索吗?
能,但 refresh 关闭后导入期间新数据搜不到,且大量写入会占 IO。初始化最好选低峰;本站是离线题库,初始化窗口没有业务流量,无需顾虑。
错法对照 错法 问题 正解 逐条 PUT 导入 5 万题 网络往返极慢 Bulk 分批导入 Bulk 一批塞 500MB 内存压力、失败重试成本高 5–15MB/批,压测定值 只看 HTTP 200 不看 errors 失败行被忽略 检查 errors 并重试失败行 导入时不关 refresh 还带 1 副本 写入放大、速度慢数倍 导前关闭,导后恢复 用 update 改单个大字段当高频操作 整文档重建,吞吐低 高频变更字段不该放 ES 删完数据奇怪磁盘没小 标记删除待合并 forcemerge 回收
卡点急救
Bulk 报 413 Request Entity Too Large :Nginx/网关对请求体有限制。调大 client_max_body_size,更推荐直接把批次调小。
reindex 跑得极慢卡住集群 :任务默认单线程且受分片影响。降低 size 之外可在 source 加 requests_per_second 限速;用 task API 看进度;必要时取消任务(POST _tasks/{id}/_cancel)重新规划。
update_by_query 中途版本冲突中断 :冲突说明查询期间有文档在被改。加 conflicts=proceed 让它跑完,再对冲突文档补跑一轮,直到 deleted/updated 数稳定。
面试常问
Bulk 一次请求里某条失败了,其他的会回滚吗?
不会,Bulk 不是事务,各操作独立执行,失败只体现在响应对应行的错误信息中。客户端要先看顶层 errors 字段,再逐条收集失败项做重试,重试时注意幂等(index 动作天然幂等)。
大批量导入怎么优化?
临时把 refresh_interval 设为 -1、副本数设 0,用 Bulk 分批配合多客户端并行,单批 5–15MB;导入完成后恢复 refresh 和副本,可再做一次 force merge。整个过程中 translog 仍保证数据不丢。
ES 的更新是怎么做的?
ES 的 Lucene 段不可变,更新 = 查询旧文档、应用变更得到新文档、旧文档标记删除、新文档写入索引。因此 ES 更新是整文档级操作,局部更新 API 也不例外,频繁单字段更新成本很高。
自测三问
本站为什么用 PUT /_doc/{id} 做增量同步,而不是先查存在性再决定 POST/PUT?
Bulk 响应 HTTP 200 但 errors: true,你的导入程序该怎么处理?
说两个 reindex 的真实场景,并说明为什么操作完旧索引不能急着删。
小结一句话 数据进 ES 的正确姿势是 Bulk 批量、覆盖写更新、按查询批处理,配合「关 refresh/副本」的导入三板斧,而 ES 的更新与删除都建立在「段不可变、整文档重建、标记删除」之上。
延伸