1. Flarum 中文搜索为什么搜不到:先把病根找准
Flarum 中文搜索这个问题,几乎每个中文站长在装完论坛的第三天就会撞上:标题明明写着“服务器选型讨论”,用户在搜索框敲“选型”,返回结果零条。我最早遇到这个现象时也怀疑过是不是权限、缓存、插件冲突,折腾了一圈才发现,根子在数据库的全文索引上——Flarum 默认那套MATCH ... AGAINST的检索逻辑,是为英文单词设计的,中文字符串在它眼里根本没有“词”的概念。
先把结论摆在前面:Flarum 的搜索依赖 MySQL 的 FULLTEXT 全文索引,而 MySQL 自带的默认解析器(parser)按“空格和标点”切分 token。英文句子天然有空格,server configuration guide会被切成三个 token,搜configuration就能命中。中文“服务器配置指南”在它看来是一个连续的、没有分隔符的长字节串,整段被当成一个巨长的 token 塞进索引。于是你搜“配置”,索引里压根不存在这个 token,自然是零结果。
这就解释了为什么很多站长的体感是“英文标题能搜到,中文标题搜不到”“搜完整标题能命中,搜其中两个字就不行”。不是 Flarum 的 bug,是字符切分规则和中文不匹配。要解决它,本质上只有一条路:让入库的文本被切成中文语义单元。至于在哪个环节切——数据库层切、应用层切、还是交给专门的搜索引擎切——就是后面要展开的方案选型。
1.1 默认全文索引对中文到底做了什么
想搞清楚这件事,最直接的办法是把索引里的 token 捞出来看。MySQL 提供了一个特别好用的调试入口innodb_ft_aux_table,把某张表设为观察对象后,就能直接查information_schema里的索引缓存表,看看分词器到底把你的中文切成了什么样。
-- 把目标库表挂到观察位上,注意格式是 库名/表名 SET GLOBAL innodb_ft_aux_table = 'your_db/discussions'; -- 看看分词结果,这一步能直接暴露问题 SELECT word, doc_count, doc_id FROM information_schema.INNODB_FT_INDEX_CACHE LIMIT 30;如果跑出来的word列里出现了整段中文标题(比如服务器选型讨论帖),那就实锤了:整句一个 token。如果连一条中文都看不到,说明中文压根没进索引——这种情况通常发生在文章太短、或者被停用词表吞掉的时候。
还有一点容易被忽略:innodb_ft_min_token_size这个参数默认是 3,意思是长度小于 3 的 token 不建索引。英文里a、of、to这类被过滤掉没影响,但如果你站上有个讨论叫“IPO”,三个字母勉强过线,两个字母的“AI”“UI”就直接进不了索引,搜出来也是空。所以英文短词搜索异常,和中文搜索异常,很多时候是同一个病根的两种表现。
1.2 三种典型“搜不到”的现场还原
我在排查时整理过三类最常见的情况,症状看着像但处理方式完全不同,先对号入座能省掉一半时间。
第一类:全中文标题,搜任意子串都无结果。这是最典型的默认解析器问题,SHOW CREATE TABLE discussions里看到FULLTEXT KEY ... (title, content_plain),但索引里存的是整句中文,属于纯配置层面的事,改解析器就能治。
第二类:标题能搜到,帖子正文里的回复搜不到。这种情况说明索引建在了discussions表上,而discussions.content_plain存的只是首帖的纯文本,后续回复的正文在posts表里。如果你的版本里posts表没有可用的全文索引,或者索引有但没重建,搜正文自然就落空。这一条特别坑,因为它会让人误以为“方案已经生效了,只是效果不好”。
第三类:搜两个字不行,搜四个字可以。这个最迷惑。出现这种现象,往往是因为数据库层已经改过解析器(用上了二元切分),但 Flarum 传过去的查询词被构造成了“短语匹配”,四个字刚好是一个完整短语能对上,两个字反而因为上下文不同而失配。这是查询构造层面的问题,不是索引层面的,改法完全不一样,后面第 4 章会详细说。
对着这三类症状挨个验证,基本能在十分钟内锁定问题在哪一层。别一上来就装扩展、上搜索引擎,很多时候只是一个WITH PARSER没加。
1.3 先做一次体检:版本、表结构、分词结果
动手之前必须先把家底摸清楚,尤其是数据库的种类和版本,它直接决定了哪条路走得通、哪条路是死胡同。
-- 1. 确认数据库种类和版本,这一步最关键 SELECT VERSION(); -- 2. 看现有的全文索引建在哪些字段上 SHOW INDEX FROM discussions WHERE Index_type = 'FULLTEXT'; SHOW INDEX FROM posts WHERE Index_type = 'FULLTEXT'; -- 3. 看完整的建表语句,确认字段名到底是 content 还是 content_plain SHOW CREATE TABLE discussions\G SHOW CREATE TABLE posts\G -- 4. 用 LIKE 兜底,确认数据本身在库里,排除“搜索逻辑没错、是数据丢了”的误判 SELECT id, title FROM discussions WHERE title LIKE '%选型%' LIMIT 5;最后那条LIKE查询是个很实用的对照实验。如果LIKE能查到、MATCH AGAINST查不到,问题百分之百出在索引分词上;如果LIKE也查不到,那你该去查的是数据写入和缓存,别在索引上耗时间。
注意:
SELECT VERSION()如果返回的字符串里带MariaDB,请直接跳到第 2.3 节。MariaDB 不支持WITH PARSER ngram这个语法,照着 MySQL 的教程抄命令会一直报语法错误,很多人卡在这里以为是权限问题。
另外 Flarum 的字段命名在不同小版本之间有过调整,posts表里正文可能叫content(存的是 s9e 的 XML 标记),也可能旁边还有个content_plain(纯文本)。索引纯文本字段永远比索引 XML 更划算,因为 XML 里的<r>、<p>、<e>这些标签会被一起切进索引,白白撑大体积还引入噪声。所以建表语句一定要看仔细,别照抄别人博客里的字段名。
2. 方案选型:四条路的成本与效果对比
摸清病根之后,摆在面前的路其实不止一条,而且各有各的适用边界。我在不同规模的站上分别试过下面几种,没有绝对的好坏,只有“你这个体量配不配”。
2.1 只改 MySQL 的 ngram 方案
这是成本最低的一条路,不动代码、不装扩展、不加服务,只改一个 MySQL 配置项加几条ALTER TABLE。原理是 MySQL 5.7.6 之后内置了一个ngram解析器,它把中日韩文本按固定长度切成 token,默认ngram_token_size = 2,也就是两字一组。
举个具体的例子,“服务器选型”这五个字,在ngram_token_size = 2下会被切成服务、务器、器选、选型四个 token。用户搜“选型”的时候,这个词本身就是一个完整的 token,直接命中。这就是它为什么管用——它把中文强行转换成了“两个字一个词”的伪英文结构。
优点很明确:零外部依赖,不用装 PHP 扩展,不用维护词典,不用多跑一个服务进程。对几千到几十万帖的论坛,效果足够让人满意。缺点也同样明确:
- 单字搜索失效。
ngram_token_size = 2时,搜“帖”“图”“锁”这种单字一定是空结果,因为索引里最细的粒度是两个字。 - 短词误报。这是二元切分的天然缺陷,“中华人民共和国”会切出
华人、民共这样的组合,用户搜“华人”可能命中一堆完全不相关的内容。 - 短语语义的坑。ngram 在 BOOLEAN MODE 下会把连续字符串当作短语处理,要求 token 相邻,这个特性会导致一部分本该命中的查询落空(第 4 章细讲)。
- MariaDB 完全不支持。这是硬门槛,没有绕过的办法。
如果你的论坛是中文为主、帖子量在几十万以内、对搜索精度要求是“能搜到就行”,我建议先上这个方案跑两周看看,投入产出比最高。
2.2 装分词扩展写独立索引表
第二类方案是把分词工作从数据库挪到应用层。社区里流传的中文搜索扩展基本都是这个思路:装一个 PHP 分词扩展(常见的是基于词库的切词组件),在帖子保存时把标题和正文切好词,写进一张独立的索引表,搜索时对关键词做同样的切分再去匹配。
它的优势是精度高。因为有真正的词库,“服务器选型”会被切成“服务器”+“选型”,而不是“服务”+“务器”+“器选”+“选型”这种带噪声的二元组。搜“选型”能精准命中,搜“务器”不会给你返回一堆垃圾结果。对技术论坛、行业论坛这种术语密集的场景,体验提升是肉眼可见的。
代价也很实在:
- 要装 PHP 扩展,虚拟主机或者受限的容器环境里编译扩展会很痛苦,尤其是找不到预编译包的时候。
- 要维护词库,冷门术语、新造词、产品型号都需要手动加词,不加就切错。
- 要维护同步逻辑,帖子编辑、删除、批量导入都得触发索引更新,漏一个环节就会出现“帖子改了但搜索结果还是旧的”。
- 索引表会额外占空间,而且和主表的一致性需要你自己保证。
2.3 外接专业搜索引擎
第三条路是把检索整个外包出去,Flarum 侧只负责把数据同步过去,查询走外部引擎。这类引擎对 CJK 文本的处理通常比 MySQL 好得多,很多还自带前缀索引、拼写纠错、相关度调优。
效果当然是最好的,但运维成本也是最高的:多一个服务要部署、要监控、要备份、要处理同步延迟和失败重试;服务器内存起步就要多留 1G 到 2G;Flarum 升级时还要确认扩展跟着升级。对于一个人维护的小站,这些隐性成本经常被低估。
我的判断标准是:帖子量突破五十万、或者搜索是站内核心功能(比如资料库型论坛),才值得上这条路。否则为了一个日均几十次的搜索请求,多养一个常驻服务,实在不划算。
2.4 一张表看懂怎么选
把上面几条路的取舍整理成表,对着自己的情况打勾就行。
| 方案 | 实施成本 | 中文效果 | 单字搜索 | 运维负担 | 适合规模 |
|---|---|---|---|---|---|
| MySQL ngram 解析器 | 低,改配置加快索引重建 | 中上,满足“能搜到” | 不支持(除非改成 1) | 几乎为零 | 几千到几十万帖 |
| 分词扩展 + 独立索引表 | 中,要装扩展维护词库 | 高,术语精准 | 支持 | 中,需保证同步 | 几万到百万帖 |
| 外接搜索引擎 | 高,多一套服务 | 很高,可调优 | 支持 | 高,需监控备份 | 五十万帖以上 |
| LIKE 模糊查询兜底 | 极低,写个查询过滤器 | 中,字面匹配 | 支持 | 无 | 一万帖以内 |
最后一行是我加的私货,很多教程里不提,但特别实用。帖子总量在一万条以内的时候,直接WHERE title LIKE '%关键词%'全表扫,执行时间也就几十毫秒,用户完全无感。这种情况下搞什么索引、装什么扩展都是过度设计。判定方法很简单:跑一次SELECT COUNT(*) FROM posts;,五位数以下,放心用 LIKE。
3. 落地实操:用 ngram 把中文搜索救回来
下面这套流程是我在几台机器上验证过的,按顺序执行基本不会出岔子。整个过程的核心是两件事:把解析器换掉,把索引重建。
3.1 改配置与重建索引的完整命令
第一步,改 MySQL 配置文件。ngram_token_size是只读变量,SET GLOBAL改不了,必须写进配置文件然后重启服务。
# my.cnf 或 mysql.conf.d 下的配置文件 [mysqld] ngram_token_size = 2 innodb_ft_enable_stopword = OFF innodb_ft_cache_size = 80000000 innodb_ft_total_cache_size = 640000000逐条说下为什么这么配。ngram_token_size = 2是主流选择,切成两字组在中文里的语义完整度最好,索引体积也可控。改成 1 能让单字搜索生效,但索引体积会膨胀两到三倍,而且“的”“了”“是”这种高频字会命中海量结果,实际体验反而更差,除非你的站有大量单字检索需求,否则别改。innodb_ft_enable_stopword = OFF是为了防止中文 token 被英文停用词表误伤,虽然两字组一般撞不上,但关掉更保险。后面两个 cache 参数是给重建索引提速用的,innodb_ft_cache_size默认只有 8M,大表重建时会频繁刷盘,调到 80M 能让整个过程快不少。
如果论坛跑在容器里,配置文件不是直接改的,得在编排文件里追加启动参数:
services: db: image: mysql:8.0 command: > --ngram_token_size=2 --innodb_ft_enable_stopword=OFF --innodb_ft_cache_size=80000000 environment: MYSQL_ROOT_PASSWORD: change_me第二步,重启 MySQL,然后确认参数真的生效了。这一步必须验证,我见过配置文件写错位置、被!includedir后面的文件覆盖、容器没真正重启等各种情况。
SHOW VARIABLES LIKE 'ngram_token_size'; -- 期望输出 2,如果还是空的或者 0,说明配置没加载第三步,先备份再重建索引。全文索引重建会锁表或者大量写盘,大表上耗时可观,出事没备份就只能哭了。
# 备份结构加数据,论坛库一般不大,全量备一份最省心 mysqldump -u root -p --single-transaction your_db > backup_before_ngram.sql第四步,删旧索引、建新索引。索引名别照抄,用前面SHOW INDEX查出来的真实名字。
-- 删掉旧的默认解析器索引 ALTER TABLE discussions DROP INDEX discussions_title_content_plain_fulltext; ALTER TABLE posts DROP INDEX posts_content_fulltext; -- 用 ngram 解析器重建,字段按你自己的表结构调整 ALTER TABLE discussions ADD FULLTEXT INDEX discussions_title_content_plain_fulltext (title, content_plain) WITH PARSER ngram; ALTER TABLE posts ADD FULLTEXT INDEX posts_content_fulltext (content) WITH PARSER ngram;第五步,清 Flarum 缓存。改完索引不清缓存,前端可能还在用旧的搜索结果缓存(主要影响讨论列表里的相关度展示)。
php flarum cache:clear3.2 分词结果验证与索引体积估算
重建完先别急着开香槟,把 token 捞出来看一眼,确认解析器真的换过去了。
SET GLOBAL innodb_ft_aux_table = 'your_db/discussions'; SELECT word, doc_count FROM information_schema.INNODB_FT_INDEX_CACHE LIMIT 30;这次应该看到大量两字的中文 token,比如服务、选型、配置。如果还是整句中文,说明WITH PARSER ngram没生效,回头检查是不是建索引的语句被截断了,或者字段类型不是TEXT/VARCHAR(ngram对字段类型有要求)。
顺便做个体积估算,心里有个数。全文索引的空间占用大致是原文字节数的 0.8 到 1.5 倍,ngram_token_size = 2时取上限。假设帖子表正文平均 800 字(UTF-8 下约 2400 字节),十万帖就是 240MB 原文,索引大概要吃掉 200MB 到 360MB。如果改成ngram_token_size = 1,这个数字会翻到 800MB 上下。磁盘余量不足的机器,重建前务必df -h看一眼。
注意:
ALTER TABLE重建全文索引在 InnoDB 上是 rebuild 操作,大表期间会占用大量 IO。帖子表超过二十万行的话,建议在凌晨执行,或者用在线改表工具分批做。别在业务高峰期操作,否则整个站会卡到打不开。
3.3 前端与接口层的实测验证
数据库层验证完,一定要从前端走一遍真实链路。因为 Flarum 的搜索请求会经过一层查询构造,最终拼出来的 SQL 可能和你想的不一样。
最省事的办法是直接打 API,绕过前端干扰:
curl -s "https://your-forum.example/api/discussions?filter%5Bq%5D=%E9%80%89%E5%9E%8B&page%5Blimit%5D=5" \ -H "Accept: application/json" | head -c 2000filter[q]就是搜索关键词,%E9%80%89%E5%9E%8B是“选型”的 URL 编码。返回里能看到data数组有没有内容、total是多少。这一步能快速区分“数据库层没问题、前端没生效”和“数据库层就没修好”。
如果接口返回正常,但页面上还是搜不到,那多半是浏览器缓存或者 CDN 缓存的问题,硬刷新一次再看。如果接口返回空,但直接跑 SQL 有结果,那就是 Flarum 的查询构造把搜索词改写了,进入第 3.4 节的范畴。
再补一个实测技巧:搜单个词和搜整句,结果差异极大,一定要分开测。
# 测整句 ?...filter[q]=服务器选型讨论 # 测双字 ?...filter[q]=选型 # 测单字,ngram=2 下基本必然是空,这不算 bug ?...filter[q]=型三个请求的结果分布,能直接反映你的配置处在哪个状态。
3.4 搜索词构造的补丁:让长词也能搜到
这是整个流程里最容易被忽略、但影响最大的一环。ngram解析器在BOOLEAN MODE下有个特性:连续的字符串会被当成短语处理,要求 token 在原文中相邻。
举个例子。用户搜“论坛搜索功能”,Flarum 把查询词拼成+论坛搜索功能之类的形式传给 MySQL,ngram 解析后变成短语论坛 坛搜 搜索 索功 功能,要求这五个 token 在原文里连续出现。但实际帖子里写的可能是“论坛的搜索功能”,中间多个“的”字,短语就断了,直接零结果。
这个坑的解法是在构造查询词时做一次拆解。目标是把长串拆成若干个独立的二元组,用 OR 语义连接,而不是整体做短语匹配。最省事的做法是写个小的查询过滤器,在拼 SQL 之前处理搜索词:
<?php namespace App\Search; class ChineseSearchTerm { /** * 把中文长串拆成二元组,用空格分隔交给 BOOLEAN MODE * 返回形如 "论坛 坛搜 搜索 索功 功能",MySQL 会按 OR 语义处理 */ public static function split(string $keyword): string { $keyword = trim($keyword); if ($keyword === '') { return ''; } // 只有含中日韩字符才走拆解,英文原样返回 if (!preg_match('/[\x{4e00}-\x{9fa5}]/u', $keyword)) { return $keyword; } $length = mb_strlen($keyword, 'UTF-8'); if ($length <= 2) { return $keyword; } $tokens = []; for ($i = 0; $i + 1 < $length; $i++) { $tokens[] = mb_substr($keyword, $i, 2, 'UTF-8'); } // 去掉重复项,避免同一个 token 重复出现拖慢查询 return implode(' ', array_unique($tokens)); } }关键在于别给每个 token 都加+。加了+就变成 AND 语义,要求所有二元组同时存在,反而更搜不到(“坛搜”这种组合几乎不可能出现)。不加+是 OR 语义,只要命中任意一个二元组就能返回结果,召回率一下就上来了。
代价是相关度排序会变差,因为命中的 token 越多的帖子应该排越前,而 OR 语义下这个排序不如短语精确。我的做法是两级查询:先用带+的精确短语查一次,有结果就用;没结果再退化成 OR 拆解查一次。两次查询加起来也就多几十毫秒,换来的是“几乎不会搜不到”的体验。
// 思路示意,具体挂在哪个位置取决于你的版本和扩展方式 public function search(string $keyword) { $strict = $this->queryWithPhrase($keyword); // 先精确 if ($strict->count() > 0) { return $strict; } return $this->queryWithOrTokens($keyword); // 再放宽 }如果你不想动代码,还有个更简单的办法:在搜索框的提示文案里引导用户用空格分词。把 placeholder 改成“多个关键词请用空格分开”,用户搜“论坛 搜索 功能”时,Flarum 会把它当成三个独立词处理,绕开了短语相邻的限制。这个土办法不优雅,但零成本,我先用了小半年才动代码。
注意:ngram 解析器下,BOOLEAN MODE 的
*前缀通配符基本失效。别指望+选型*能匹配“选型指南”,通配符会被忽略。想实现前缀匹配,只能靠应用层的额外索引,或者在搜索词拆解时把可能的后续组合也加进去。
4. 搜索结果不理想的排查清单
改完之后效果不达预期,是最常见的情况。这一章把我在不同站上踩过的坑整理成速查表,照着排能省很多时间。
4.1 十大高频问题速查表
| 现象 | 最可能的原因 | 处理办法 |
|---|---|---|
| 建索引时报语法错误 | 数据库是 MariaDB,不支持WITH PARSER | 改用分词扩展或外接引擎 |
| 参数改了但没生效 | 配置文件位置不对,或被后续 include 覆盖 | SHOW VARIABLES确认,检查!includedir |
| 索引建好了,搜中文还是空 | 索引没真正重建,只是删了没加 | SHOW INDEX确认索引存在且Index_type为 FULLTEXT |
| 标题能搜到,回复搜不到 | 只重建了discussions,漏了posts | 补建posts表的全文索引 |
索引里全是r、p之类的碎片 | 索引了 XML 原文字段 | 改用纯文本字段,或加应用层清洗 |
| 搜两个字偶尔命中偶尔不中 | ngram 短语相邻限制 | 参照 3.4 节做查询词拆解 |
| 单字搜索永远为空 | ngram_token_size = 2 | 属于预期行为,不建议为此改成 1 |
| 重建索引跑到一半超时 | innodb_ft_cache_size太小,或磁盘 IO 打满 | 调大缓存,避开业务高峰,分批处理 |
| 搜索结果数量对不上 | 相关度排序导致分页 offset 偏移 | 检查排序参数,必要时固定按时间排序 |
| 前台搜不到但 SQL 有结果 | Flarum 缓存未清或查询构造改写了搜索词 | php flarum cache:clear,抓 SQL 日志确认 |
这张表里最值得单独拎出来说的是“索引里全是碎片”那一条。很多人图省事,直接把posts.content拿去建索引,结果content里存的是<r><p>正文内容</p></r>这种格式化标记。这些标签会被 ngram 切成r、p、p正之类的垃圾 token,索引体积平白多出两三成,而且某些查询会莫名命中。正确做法是找纯文本字段,没有的话就在应用层先用格式化组件把 XML 转成纯文本再索引。
4.2 大表重建索引的停机与性能处理
帖子表上了三十万行之后,重建全文索引就不是敲一行命令的事了。我第一次在三十万行的表上直接跑ALTER TABLE,跑了将近四十分钟,期间整站搜索不可用,CPU 一直是满的,用户体验极差。
后来摸出几条经验,按这个顺序准备能稳很多。
第一,先估时间。找个从库或者本地环境,拷一万行数据跑一次,测出单行耗时,再乘以总行数。虽然不准,但能判断是“几分钟”还是“几小时”,心理预期完全不同。
第二,调参数再跑。前面提到的innodb_ft_cache_size调到 80M 以上,innodb_ft_total_cache_size给到 600M 以上,重建速度能快两三倍。这两个参数改完要重启才生效,所以要跟ngram_token_size一起改,别分两次重启。
第三,考虑在线改表。数据量实在太大,用在线改表工具做,它会在后台建影子表、分批拷数据、最后短暂切换,全程只在切换瞬间锁表。缺点是配置复杂,而且对全文索引的支持要看工具版本,跑之前必须在测试环境验证一遍。
第四,准备好回滚。重建前把索引定义记下来,出问题直接删掉新索引、按原样建回去。最坏情况下搜索不可用几小时,总比表结构坏了强。
4.3 我踩过的几个坑与规避建议
有几个坑不属于技术难点,纯粹是流程上的疏忽,但坑起来一样疼。
坑一:以为改配置会自动重建索引。实际上ngram_token_size改了之后,已有的索引还是按旧的切分方式存的,必须手工ALTER TABLE重建。我第一次改完配置就直接测,看到还是搜不到,差点以为方案走不通。
坑二:只测了标题,没测正文。上线后用户反馈“搜正文里的词搜不到”,才发现posts表压根没建全文索引。后来我养成习惯,每次改动都按“标题 / 首帖正文 / 楼中回复”三处各测一遍,一个都不能少。
坑三:批量导入历史数据后忘了重建索引。从老论坛迁移过来十万帖,导入完搜索一片空白。原因是导入的数据虽然进了表,但全文索引没跟着更新(某些导入方式会绕过索引维护)。解决办法是导入完再跑一次ALTER TABLE,或者用OPTIMIZE TABLE触发重建。
坑四:在容器里改了my.cnf但重启后丢失。容器的配置文件系统是临时的,重启容器配置就回到镜像默认值。只能通过编排文件的command参数传,或者把配置文件挂载进去,别想着进容器里vi一下就行。
提示:改完任何配置,都跑一次完整的回归测试:搜标题、搜首帖正文、搜回复、搜英文、搜数字、搜单字、搜长句。七个用例跑完没问题,再放开给用户用。我吃过太多“改 A 坏 B”的亏。
5. 长期维护:升级、备份与后续扩展
搜索这块跟别的不一样,它没有明显的报错,坏了就是悄悄地搜不到,用户也不会主动告诉你,只会默默关掉页面。所以维护策略要提前想好。
5.1 Flarum 升级后索引会不会丢
这个问题的答案是:表结构的索引一般不会丢,但你自己打的补丁会丢。
Flarum 升级时,核心的数据库迁移会检查索引是否存在,通常不会主动删掉你手工加的全文索引。但如果你改动过核心文件(比如直接改了搜索过滤器里的查询构造代码),升级会把文件覆盖掉,补丁失效,表现就是“升级前好好的,升级后长词又搜不到了”。
我的做法是把所有改动收敛到一个自己的小扩展里,通过扩展机制挂载,不碰核心文件。这样升级时只需要确认扩展兼容性,不用重新 patch 一遍。哪怕只是几行查询词处理逻辑,也值得单独做成扩展——打过一次补丁丢过一次的人,都懂这个道理。
升级完的验证清单我固定三条:跑一次 API 搜索、跑一次前端搜索、SHOW INDEX确认索引还在。三分钟能做完,能挡住绝大多数升级事故。
5.2 备份与恢复索引的正确姿势
全文索引不需要单独备份,它跟着表数据走。mysqldump导出的 SQL 里会带上ALTER TABLE ... ADD FULLTEXT的语句,恢复时会自动重建。
但有个细节要注意:导出时不带WITH PARSER ngram。mysqldump生成的索引定义通常是标准语法,恢复出来的索引会退化成默认解析器,中文搜索又回到原点。所以恢复完必须补一条:
-- 恢复后检查索引定义,看有没有 WITH PARSER ngram SHOW CREATE TABLE discussions\G -- 如果发现丢了,重建一次 ALTER TABLE discussions DROP INDEX discussions_title_content_plain_fulltext; ALTER TABLE discussions ADD FULLTEXT INDEX discussions_title_content_plain_fulltext (title, content_plain) WITH PARSER ngram;这个坑我在一次灾备演练里踩过,恢复完一切正常,就是搜中文搜不到,排查了半天才想起来是解析器没跟过来。现在我把这条重建语句直接写进恢复手册里,恢复流程的最后一步固定执行。
5.3 什么时候该换成外部引擎
ngram 方案不是万能的,用久了会碰到天花板。我的判断信号有三个,出现任意一个就该考虑换路了。
信号一:用户开始抱怨“搜出来的东西不对”。不是搜不到,是搜出来的不相关。这说明二元切分的噪声已经影响到体验了,靠调参数救不回来,需要真正的分词能力。
信号二:帖子量过五十万,搜索变慢。ngram 索引体积大,数据量上来之后MATCH AGAINST的响应时间会明显上升,加上 Flarum 还要做权限过滤和分页,整体响应超过一秒就该动手了。
信号三:需要高级搜索能力。比如拼写纠错、同义词扩展、按相关度精细调权、搜索结果高亮片段。这些在 MySQL 全文索引上做起来都很别扭,换个专业引擎反而简单。
换的时候不用一次性切干净。先在外部引擎上把新帖同步过去,搜索走新引擎、老帖回落到 MySQL,跑一段时间确认没问题再全量迁移。这个灰度过程能避免一次性切换带来的风险,虽然麻烦一点,但搜索是用户高频接触的功能,值得稳一点。
最后分享一个我自己一直在用的判断标准:如果你每周要花超过半小时折腾搜索相关的问题,那说明当前方案已经到极限了,该升级了。半小时乘以一年,够你把新方案部署三遍。
5.4 一个被低估的小优化:搜索结果的展示
技术层面解决完,还有个体验层面的细节值得花十分钟改一下。ngram 方案下,用户搜到的结果里,关键词往往藏在正文深处,列表页只显示标题,用户得点进去才知道为什么这条被搜出来。
Flarum 的搜索结果列表默认不展示匹配片段。如果你改的是自定义过滤器,完全可以顺手把命中的上下文截出来,作为摘要跟着结果一起返回。做法是在查询时多取一个content_plain字段,在应用层用mb_strpos定位关键词位置,前后各截 30 个字,把关键词包上高亮标记。
这十几行代码带来的感知提升,说实话比把搜索精度从 80% 提到 85% 更明显。用户看到“哦,是因为这句话里有我要的词”,信任感立刻就不一样了。我现在的习惯是,任何搜索相关的改造,都把“结果可解释”作为收尾动作,不做完不算完工。