Phorge 现代化改造实战(八):迁移搜索服务,写进索引不等于搜得到

💡 原文中文,约13900字,阅读约需33分钟。
📝

内容提要

本文介绍Phorge搜索服务迁移至Gorge的实战。迁移重点在于替换已有实现,需确保字段一致、索引兼容、查询正确。文章强调搜索链路中写入与查询的闭环,权限检查保留在PHP侧,并处理了中文搜索的CJK支持、配置切换、错误处理及测试验证,避免静默失效。

🔎

延伸解读

迁移的隐藏风险:静默失效

搜索服务迁移中,最危险的问题不是服务完全不可用,而是“写进去了但查不到”或“配置了但没生效”等静默失效。例如,字段写入索引但查询未使用,或配置指向Gorge但请求仍由MySQL回答,这些情况服务都返回200,难以察觉。因此,迁移后必须验证写入、查询、权限回查、配置切换和索引重建的完整闭环,而不能仅检查服务是否健康。

权限检查为何保留在PHP侧

Gorge搜索服务只返回PHID列表,不返回文档内容,这是为了保留Phorge的权限检查。如果搜索服务直接返回正文,就会绕过Phorge的policy层,使全文索引成为缺少访问控制的数据接口。因此,Phorge收到PHID后仍通过自己的Query类加载对象,并执行可见性、项目策略和用户权限判断,确保搜索结果符合用户权限。

中文搜索的CJK支持与索引重建

旧版Elasticsearch mapping的英文分析器对中文无效,直接迁移会导致中文难以检索。r2使用cjk_width和cjk_bigram分析器,并增加cjk子字段,但查询侧也必须使用cjk_text分析器,否则新增字段只是空操作。由于mapping变化,旧索引必须重建(bin/search init和index --all --force),大库可能耗时数小时,期间搜索结果不完整。

配置切换的陷阱:服务顺序与默认值

Phorge读取搜索服务时按列表顺序选择第一个可读且成功的服务。若将Gorge追加在MySQL之后,MySQL一直可用时Gorge永远不会参与读取,导致配置看似生效但搜索无变化。因此脚本将Gorge条目插在列表首位,并默认只保留Gorge,除非设置GORGE_SEARCH_KEEP_MYSQL=1才显式加入MySQL作为回滚路径。

Q&A

Phorge 搜索服务迁移到 Gorge 时,为什么不能直接返回文档内容而只返回 PHID 列表?

因为权限检查必须保留在 Phorge 的 PHP 侧。如果搜索服务直接返回正文,就会绕过 Phorge 的 policy 层,使全文索引变成缺少访问控制的数据接口。因此 Gorge 只返回命中的 PHID 列表,由 Phorge 加载对象并执行权限判断。

在 Phorge 搜索迁移中,如何配置 cluster.search 以确保 Gorge 优先于 MySQL 被使用?

需要将 Gorge 条目插入到 cluster.search 列表的首位,而不是追加。因为 Phorge 按列表顺序选择第一个可读且成功的服务,如果 MySQL 在前且一直可用,Gorge 永远不会参与读取。使用 array_unshift 将 Gorge 条目放在首位,并默认只保留 Gorge,除非设置 GORGE_SEARCH_KEEP_MYSQL=1 才显式加入 MySQL 条目。

Phorge 搜索迁移中,为什么 Elasticsearch 的 mapping 需要增加 cjk 子字段?

因为旧 mapping 只有英文分析器,对中文(CJK)文本无效。为了支持中文搜索,需要增加 cjk 子字段,使用 Elasticsearch 自带的 cjk_width 和 cjk_bigram 分析器,并设置 output_unigrams: true 以保留单字。同时查询侧需要增加对应的 should 子句,否则索引虽写入但查询不会使用,导致中文搜索失效。

为什么 Gorge 搜索服务的 readiness 探针只检查是否配置了带 read 角色的后端,而不实际连接 Elasticsearch?

因为 readiness 探针的目的是判断服务是否有能力发起检索,而不是依赖外部服务的实时状态。如果 readiness 跟随外部服务抖动,会导致编排反复摘除容器,削弱应用层的容错能力。因此只检查是否配置了至少一个带 read 角色的后端,确保服务本身就绪即可。

在 Phorge 搜索迁移中,为什么 /init 和 /sane 接口在 docTypes 为空时要返回 400 错误?

因为如果 docTypes 为空,期望的 mapping 也几乎为空,任何线上索引都可能满足 sanity check,导致返回 sane:true 的假阳性。这会让管理员误以为索引结构正确,而实际上可能缺少必要的字段。因此 r2 让 /init 和 /sane 在空 docTypes 时返回 400 ERR_BAD_REQUEST,以阻止这种永远正确的健康检查。

Phorge 搜索迁移中,为什么新增的 cjk 字段必须同时修改索引和查询构建器?

因为如果只修改索引侧(mapping)而不修改查询构建器,新增的 cjk 字段永远不会被查询使用,导致索引增长但搜索无变化,且所有请求仍返回 200,形成静默错误。因此必须同时确保查询构建器使用 cjk_text 分析器查询 titl.cjk、body.cjk、cmnt.cjk 等字段,才能实现真正的中文搜索。

Phorge 搜索迁移中,为什么 Gorge 的默认编排不附带 Elasticsearch 容器?

因为索引存储属于 Elasticsearch 或 Meilisearch,有自己的数据卷、内存配额和备份策略。如果默认编排附带 Elasticsearch 容器,一次 docker compose down -v 就可能删除生产索引,造成数据丢失。因此 Gorge 刻意不提供开箱即用的 Elasticsearch 容器,避免将存储埋进服务编排。

Phorge 搜索迁移中,为什么 GORGE_SEARCH_BACKENDS 解析失败时只记录错误而不退出?

因为当前设计允许空配置启动,以便开发或分阶段部署。但解析失败会导致零后端,/readyz 返回 503,虽然不会伪装成就绪,但无法区分“尚未配置”和“配置写错”。后续计划引入显式严格模式(如 GORGE_STRICT_CONFIG=1),在生产环境解析失败直接退出,以提供更明确的错误信号。

🏷️

标签

➡️

继续阅读