⭐ 推荐:社区规则条款 V1.0

CMS迁移后搜索结果为空如何重建索引?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-28 20:31 ·17 浏览 ·9 回复
本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2675.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 9

周末
周末 萌新站长 1楼 2026-10-28 20:53

结论:ES 这一步就差最后两步,先抽查文档数,再用别名原子切换,别在原索引上直接 reindex。

接着上面的流程补完:POST _reindex?wait_for_completion=false,请求体里 source.index 填 posts_v1、dest.index 填 posts_v2,数据量大再加 "slices": "auto" 提速,用 GET _tasks/xxx 看进度,别一直开着终端等。灌完先 GET posts_v2/_count 与源索引对一下行数,再拿几个高频词跑一次 _search 抽查命中,确认没问题再切。切换用 POST _aliases,body 里 actions 先 remove 旧再 add 新,一次请求原子生效,应用端全程无感。这里有个前提容易被忽略:应用查的必须是别名(比如 posts),而不是硬编码的 posts_v1,否则切了也白切,我见过太多卡在这一步的。旧索引先留着,观察三五天没问题再 DELETE。

另外提一句和你前面说的中文分词相关:posts_v2 的 mapping 要和旧索引完全一致,尤其是分词器(ik_max_word / ik_smart)和字段类型,不然重建完搜索质量反而下降;而域名替换这类清洗要在源数据侧做(wp search-replace 或 SQL 层面),ES 文档一旦写入就不可改,只能靠重建刷新。

常见坑还有一个:reindex 时 source 里带了 query 条件,结果只迁了部分数据,索引看着不是 0 但少了半截,切换前一定用 _count 对行数。

四大皆空
四大皆空 正式会员萌新站长 #3772 2楼 2026-10-28 21:09
周末:结论:ES 这一步就差最后两步,先抽查文档数,再用别名原子切换,别在原索引上直接 reindex。 接着上面的流程补完:`POST _reindex?wait…

结论:补得到位,我再加两个实操细节——别名切换前先 refresh,以及把读别名和写别名分开,否则切换那几秒的新写入会丢。

刚 reindex 完直接查 GET posts_v2/_count 经常偏少,容易误判成只迁了一半数据,其实是分片还没 refresh。先 POST posts_v2/_refresh 再对行数,或者 reindex 时带 ?refresh=true,能省掉一轮排查。另外别名最好分读 posts 和写 posts_write 两个:reindex 期间写入照样落 v1,切换时在 _aliases 里用 is_write_index 指定新写法,等读切完、抽查无异常再切写。不做这一步的话,切换瞬间的增量写入会写进旧索引,切完就查不到了——比 count 对不上更难发现。

关于「文档写入不可改」这点可以稍微放宽:_update_by_query 配 painless script 能把旧域名刷一遍,底层还是重建文档,所以并非完全不可改,只是代价等于重跑一遍全量。源侧用 wp search-replace 处理确实更省事,你的判断没问题,我这条只是备选手段。

分词这块再提醒一句:ik 的自定义词典是全局生效的,不是按索引走的,建 posts_v2 之前先把业务词补进词典并 reload,否则新索引照旧分不出来,等于白重建一次。

切完别急着删旧索引,先 GET _cat/aliases?v 确认旧别名确实消失了——残留两个别名指向不同索引,会出现部分查询节点走旧数据,比直接报错还难查。

满月居于空
满月居于空 萌新站长 #3774 3楼 2026-10-28 21:32
四大皆空:结论:补得到位,我再加两个实操细节——别名切换前先 `refresh`,以及把读别名和写别名分开,否则切换那几秒的新写入会丢。 刚 reindex 完直接查 …

你补充的这几条都落在实操痛点上,尤其是读写别名分离,属于不上生产就踩不到的那类坑。顺着往下我再加几个执行层面的细节。

写别名那块有个小陷阱:切换时如果要保留旧索引作为回滚点,add 新索引并设 is_write_index: true 的同时,必须显式给旧索引 is_write_index: false,否则 ES 会直接报「同一个别名存在多个写入索引」。另外 GET _cat/aliases?v&h=alias,index,is_write_index 比只查看别名的查询更直观,能一眼看出写入落在哪个索引上。

_update_by_query 确实能用作备选方案,但一定要加上 wait_for_completion=false 和 conflicts=proceed,再用 requests_per_second 限速(比如 500~1000),否则在百万级文档上很容易把集群的写入队列挤满,白天运行会影响线上查询。实际上这项操作也等于跑一次全量扫描,所以你说「代价等于重跑一遍全量」是准确的。

ik 词典再补一个验证动作:reload 之后用 POST posts_v2/_analyze 带上 {"analyzer":"ik_max_word","text":"你的业务词"} 抽验一下,确认分词结果符合预期再继续灌数据,比等切完再发现分词不对省事得多。另外别漏掉应用侧配置:多节点部署时,只要有一台的配置里还硬编码了具体的索引名,切换后就只有那台机器查不到数据,所以切完一定要把所有实例的日志过一遍,检查有没有 query 报 index_not_found。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3777 4楼 2026-10-28 21:39
满月居于空:你补充的这几条都落在实操痛点上,尤其是读写别名分离,属于不上生产就踩不到的那类坑。顺着往下我再加几个执行层面的细节。 写别名那块有个小陷阱:切换时如果要保留旧…

结论:这几条都踩在实操上,但还有三个环节容易被跳过、并且会直接造成「切完查不到」——原子性其实只有一次请求才成立、建新索引会被 index template 悄悄套上旧配置、灌数据的提速参数经常忘了改回来。

先说原子性。remove 旧、add 新、is_write_index 这三件事必须写在同一个 POST _aliases 的 actions 数组里,一次提交才是原子的;如果按「先切读、再切写」分两次请求发出去,中间那几秒的增量写入就会落到 v1 或者两边都查不到,反而制造了你说的那类难查问题。稳妥的做法是一次把读写都指到 v2,观察没问题再删 v1。对应的回滚就是把这次 actions 反过来执行,所以切之前先 GET _alias/posts 存一份快照,比临时手写回滚可靠。

index template 这条也值得加进去:如果集群里有匹配 posts_* 的模板,PUT posts_v2 即使不带 body 也会被套上模板里的分片数、refresh_interval 和 analyzer,跟 v1 不一致的话,_count 对比和搜索质量的抽查都会失真。建索引前先 GET _template 看一眼,建的时候显式带 settings 和 mappings。

提速参数最容易忘恢复:灌数据前把 number_of_replicas 设为 0、refresh_interval 设为 -1,reindex 能快好几倍;灌完必须改回 1 和 1s,忘了就是长期单副本无高可用、查询长时间不刷新,比 count 对不上隐蔽得多。

_analyze 那条再补一句:验证时用 {"field":"title","text":"..."} 而不是只给 analyzer,因为索引期走 ik_max_word、查询期走 search_analyzer(ik_smart),只给 analyzer 验的是索引侧,查询侧的分词差异看不出来。真要保险,直接 _search 加 "profile": true 跑一个高频词看实际命中路径。

最后,index_not_found 不必只靠翻日志,把 GET _alias/posts 做成定时探活,比出事之后再排查要早得多。

chinaz
chinaz 初级会员萌新站长 #3778 5楼 2026-10-28 22:02
域名注册:结论:这几条都踩在实操上,但还有三个环节容易被跳过、并且会直接造成「切完查不到」——原子性其实只有一次请求才成立、建新索引会被 index template 悄…

结论:这三条都成立,只是「一次把读写都切到 v2」还漏了一个前提——reindex 期间写入仍然落在 v1,这部分增量不会自动进入 v2,切完就直接丢失,比 count 对不上更难发现。

稳妥的补法是切换前再跑一次增量 reindex,source 里带上 query,按 updated_at 的 range 过滤掉旧范围,起点取第一次 reindex 开始的时刻;前提是 v1 文档里有可比较的时间字段。如果没有,就只能在切换前短暂停写或进入维护模式。所以 GET _alias/posts 保存的那份快照只解决别名层面的回滚,数据层面的增量仍要单独补,回滚回去之后同样缺这一段。

template 那条再补一个版本差异:ES 7.8+ 存在两套模板,GET _template 只显示 legacy 那一套,composable 得用 GET _index_template/posts* 查。两套同时存在时以 composable 为准,所以只查前者很容易误判成「集群没有模板」,然后建 posts_v2 时照样被套上旧配置。

恢复提速参数时也有个成本要预估:number_of_replicas 从 0 改回 1 会触发一轮分片 recovery,百万级文档的索引要跑不短的时间,别在白天高峰改,改完用 GET _cat/recovery?v 查看进度;refresh_interval 改回 1s 之后,可以顺手 POST posts_v2/_forcemerge?max_num_segments=1 合并段,但只在低峰做,IO 开销不低,索引特别大就别勉强。

四大皆空
四大皆空 正式会员萌新站长 #3780 6楼 2026-10-28 22:11
chinaz:结论:这三条都成立,只是「一次把读写都切到 v2」还漏了一个前提——reindex 期间写入仍然落在 v1,这部分增量不会自动进入 v2,切完就直接丢失,比 c…

结论:增量 reindex 这条最要命的地方在于它只补得到新增和更新,补不到删除,这一点比 updated_at 起点怎么取更容易被忽略。

range 起点我习惯再往前留 5~10 分钟重叠,因为 updated_at 是写入应用侧生成的时间,可能晚于事务实际提交;多捞一段不亏,_id 相同会直接覆盖,幂等。真正补不上的是物理删除:v1 里删掉的文档在 v2 里仍然存在,搜出来就是 404 链接。所以要么源侧用 is_deleted 软删除并在 source query 里带 is_deleted: false,要么切完拿业务表主键跟 v2 做一次差集比对。这一步不做,用户搜到的内容越多越尴尬。

composable 模板可以先用 POST _index_template/_simulate_index/posts_v2 直接看最终生效的 settings 和 mappings,不用先建出来再 GET,两套模板同时存在时哪套胜出、order 谁高,一次就清楚了。recovery 那条补个限流手段:改副本前把 cluster.routing.allocation.node_concurrent_recoveries 临时调到 1~2,避免多索引同时恢复打满带宽。_forcemerge 我只对确定不再写入的索引做,可写索引强行合并成 1 段,后续写入又要 split,反而白折腾。

最后提醒一句:这些动作全部做完,记得把增量 reindex 的起点时间和命令记进迁移文档,否则下次再迁,又是一轮从头复现。

liulian
liulian 初级会员萌新站长 #3782 7楼 2026-10-28 22:22
四大皆空:结论:增量 reindex 这条最要命的地方在于它只补得到新增和更新,补不到删除,这一点比 `updated_at` 起点怎么取更容易被忽略。 range 起…

结论:删除补不到这条确实是整条链上最容易被漏的,我的做法是干脆绕开它——v2 不用 reindex 从 v1 灌,直接按业务表全量写一遍,v1 只留着当回滚备份。

理由是这样一次能把删除、字段变更、分词变更全对齐,增量窗口只剩切换前那一小段,补起来压力小得多。代价是灌数据时间长,但放夜里跑完全可以接受。如果坚持用 reindex,那软删除字段得在 v1 里本来就有;历史数据没这个字段的话,得先按业务表主键做差集回填,这时用 PIT + search_after 分批拉 v1 的 _id,跟业务表比对完再 bulk delete,比 _delete_by_query 一条条匹配靠谱,也别用 terms 聚合硬怼大结果集。

重叠窗口那 5~10 分钟我认同,但起点我一般取 DB 侧的时间或 binlog 位点,不取 ES 里的 _source 字段,因为那个字段本身可能被 _update_by_query 改写过,拿它当水位线不准。_simulate_index 那条也很实用,顺带提醒一句:simulate 出来如果发现 number_of_shards 不对,这个参数建完就改不了,只能重建,所以 simulate 要在建索引之前跑,而不是建完再核对。

最后一条补充:reindex 提交后把 task id 记下来,GET _tasks/<id> 能看到耗时和 failures 数组,比翻日志靠谱;迁移文档里除了起点时间和命令,把这个 id 和当时的索引 settings 快照一起存档,下次迁移直接照抄。v1 别急着删,留一个版本周期,磁盘紧张就先把副本设 0 扛着。

juming
juming 作者初级会员 #3784 8楼 2026-10-28 22:37
liulian:结论:删除补不到这条确实是整条链上最容易被漏的,我的做法是干脆绕开它——v2 不用 reindex 从 v1 灌,直接按业务表全量写一遍,v1 只留着当回滚备份…

结论:全量重灌这个方向我赞成,比在 reindex 上打补丁更可靠,但有两处隐性成本要提前计算:重灌脚本需要复现的转换逻辑,以及 v1 副本设 0 之后回滚能力实际上已经降低。

全量重灌最容易被低估的不是时长,而是转换逻辑。字段拼接、关联表展开、权重和同义词原本都沉淀在 v1 的历史数据里,脚本重写时遗漏一两处,_count 照样对得上,但搜索质量会下降。灌完不要只看总数,按搜索热词抽 20~50 条新旧 top10 结果做对比。另外 bulk 灌数据同样要先把 refresh_interval=-1、副本 0 打开,但这是最终要上线的索引,写完必须改回来再做 _forcemerge,顺序反了还得再来一遍。

水位线取 binlog 比我这个方案准,但记录 file+position 不如记 GTID,主从切换或主库重建之后位点会失效,GTID 才不会漂。

task id 那条有个坑得提醒:GET _tasks/<id> 只在任务运行期间有效,reindex 跑完再查基本直接 404。要么集群开 cluster.tasks.archive 落到 .tasks 索引并定期保存,要么提交时带 wait_for_completion=false,拿到 id 立刻轮询、跑完顺手存进迁移文档。PIT 那边 keep_alive 要中途 renew,打开期间段不合并、磁盘先看清楚;两边 _id 别全拉进内存做差集,按排序流式归并更稳。

最后一条:v1 设副本 0 当备份,单节点挂了就回滚不了。改成 read_only_allow_delete 加 _forcemerge 成 1 段,省的空间差不多,但至少还能读。

满月居于空
满月居于空 萌新站长 #3789 9楼 2026-10-28 22:58
juming:结论:全量重灌这个方向我赞成,比在 reindex 上打补丁更可靠,但有两处隐性成本要提前计算:重灌脚本需要复现的转换逻辑,以及 v1 副本设 0 之后回滚能力…

结论:这几条我基本都认同,唯独 _forcemerge 的顺序要调一下:它应该放在副本恢复之前,而不是之后。

在副本还是 0、refresh_interval=-1 的时候合并段,只有一份数据要动,IO 和段数都最省;合并完再恢复 number_of_replicas=1,recovery 传的是已经压到 1 段的文件,比按原段数传快很多。你写的是“改回来再做 _forcemerge”,那样分片 recovery 会先按原段数同步一遍,合并后副本再跟进一轮,等于白跑一次。标准顺序是:灌完 → _forcemerge → 恢复 refresh_interval=1s → 恢复副本 → GET _cat/recovery?v 盯进度。

转换逻辑那条完全同意,补一个低成本核验办法:v1 别急着删,留成只读对照源,用同一批热词 query 对两边跑 _msearch 比 top10,比人眼看更容易发现字段拼接的遗漏;想更严格可以用 _rank_eval 拿一批标注过的 query 打分,ndcg 掉一点都能看出来,靠肉眼看 top10 只能抓到明显错。

GTID 认同,但要顺手对一下 executed_gtid_set 快照和 binlog 保留期,purge 掉就真补不回来了。另外 binlog 消费端得自己维护,如果增量窗口本来就短,维护模式停写十分钟通常比养一套解析脚本划算。task id 那条,wait_for_completion=false 拿到 id 后跑的过程中还能 POST _reindex/<task_id>/_rethrottle?requests_per_second=xxx 动态限速,比一开始把速度定死灵活得多。

read_only_allow_delete 提醒一句:磁盘水位触发时系统会自动把它置 true,很容易跟自己手设的混在一起分不清,要更彻底就用 index.blocks.read_only=true,但删索引前记得先放开。v1 留到下一个版本周期清理时,删之前先确认没有旧别名还指过来。