本文约 4,602 字,预计阅读 12 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:换了一圈搜索插件,中文还是搜不准
很多站长的搜索优化之路是这样的:默认搜索不准 → 装一个搜索增强插件 → 好一点但还是不准 → 换另一个 → 还是不准 → 怀疑是主题问题。
其实问题不在插件层,而在更底下两层:WordPress 默认搜索本身就没用全文索引,而 MySQL 的全文索引又天生不认识中文。这两件事叠加,才是中文站点搜索体验差的根源。
接下来把这两层拆开讲清楚,再给出三条可行的改造路径。看完你会明白:为什么”换插件”往往无效,以及在什么数据量下该上什么方案。
原理分析:从默认搜索的 SQL 说起
第一层:WordPress 默认搜索用的是 LIKE,不是 FULLTEXT。
WP_Query 传入 s=关键词 时,WP_Query::parse_search() 会拼出这样的条件(WP_Query::parse_search 内部对关键词按空格拆分,每个词生成三个 LIKE):
AND (
(wp_posts.post_title LIKE '%关键词%')
OR (wp_posts.post_excerpt LIKE '%关键词%')
OR (wp_posts.post_content LIKE '%关键词%')
)
LIKE '%...%' 前导通配符,无法使用任何索引,只能全表扫描。文章上万后,一次搜索就是一次几万行的字符串比对,慢是必然。而且它没有任何相关度概念——谁先查出来谁在前面,顺序基本等于 ID 或日期。
第二层:就算改用 MySQL FULLTEXT,中文照样不行。
MySQL 的全文索引(MyISAM 从很早就有,InnoDB 从 5.6 开始支持)默认按空白字符与标点分词。英文天然有空格,所以好用;中文一整句没有空格,”如何配置Nginx缓存”会被当成一个完整的 token。
后果有两个:
- 用户搜”缓存”,索引里根本没有”缓存”这个 token,查不到;
- 即使索引里存了整句,只有完整输入整句才能命中。
再加上两个长度限制:
| 参数 | 默认值 | 影响 |
|---|---|---|
ft_min_word_len(MyISAM) |
4 | 短于 4 的词不进索引 |
innodb_ft_min_token_size(InnoDB) |
3 | 短于 3 的词不进索引 |
ngram_token_size |
2 | ngram 解析器的切分长度,启动参数,不可动态修改 |
中文常用词大量是两字词(”缓存””备案””主机”),默认阈值直接把它们排除在索引之外。
第三层:停用词与编码。 MySQL 自带英文停用词表,对中文没有意义;而 utf8mb4 下每个汉字占 3–4 字节,分词长度阈值按字符还是字节计算,也容易造成困惑。
保姆级实操
方案 A:MySQL ngram 全文索引(改动最小)
MySQL 5.7.6+ 的 InnoDB 支持 ngram 解析器,它按固定长度滑动切分,不依赖空格,天然适配中文。
先确认版本与参数:
SELECT VERSION();
SHOW VARIABLES LIKE 'ngram_token_size';
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';
建索引(ngram 解析器要在建索引时指定):
-- 需要索引的列上建 FULLTEXT,指定 ngram 解析器
ALTER TABLE wp_posts
ADD FULLTEXT INDEX ft_cn_title_content (post_title, post_content) WITH PARSER ngram;
-- 查询时用 IN BOOLEAN MODE,中文效果更可控
SELECT ID, post_title,
MATCH(post_title, post_content) AGAINST ('缓存 配置' IN BOOLEAN MODE) AS score
FROM wp_posts
WHERE post_status = 'publish'
AND MATCH(post_title, post_content) AGAINST ('缓存 配置' IN BOOLEAN MODE)
ORDER BY score DESC
LIMIT 10;
ngram_token_size 默认是 2(二元切分),如需改成 1,必须在配置文件里设置并重启 MySQL:
[mysqld]
ngram_token_size = 2
innodb_ft_min_token_size = 2
innodb_ft_enable_stopword = OFF
改完后要重建索引才生效:
SET GLOBAL innodb_optimize_fulltext_only = 1;
OPTIMIZE TABLE wp_posts;
SET GLOBAL innodb_optimize_fulltext_only = 0;
方案 B:让 WordPress 用上 FULLTEXT(自建查询)
用 posts_search 过滤器替换掉默认的 LIKE 条件,是最直接的接入方式:
add_filter( 'posts_search', function ( $search, $query ) {
if ( ! $query->is_main_query() || ! $query->is_search() || empty( $query->get( 's' ) ) ) {
return $search;
}
global $wpdb;
$term = $wpdb->esc_like( $query->get( 's' ) );
// 依赖上面建的全文索引(WITH PARSER ngram)
return $wpdb->prepare(
" AND MATCH( {$wpdb->posts}.post_title, {$wpdb->posts}.post_content )
AGAINST ( %s IN BOOLEAN MODE ) ",
$term
);
}, 10, 2 );
想让标题命中的排在前面,用 posts_search_orderby 补一个排序权重:
add_filter( 'posts_search_orderby', function ( $orderby, $query ) {
global $wpdb;
$term = $wpdb->esc_like( $query->get( 's' ) );
return $wpdb->prepare(
" ( CASE WHEN {$wpdb->posts}.post_title LIKE %s THEN 2 ELSE 0 END ) DESC,
{$wpdb->posts}.post_date DESC ",
'%' . $term . '%'
);
}, 10, 2 );
方案 C:数据量上规模后上外部搜索引擎
当文章超过十万、或需要搜索商品属性、或要支持拼音搜索、错别字容错时,本地 MySQL 就不够了。主流做法是 Elasticsearch(配 IK 分词器,ik_smart / ik_max_word)或 Meilisearch:WordPress 只负责把数据同步过去,搜索请求直接打到搜索引擎,再由它返回 ID 列表。这条路成本最高,但也是唯一能同时做到”准、快、可容错”的方案。
方案 D:最小改动——交给搜索插件
如果不想碰数据库,装 Relevanssi 或 SearchWP 自建索引表也能绕开 MySQL FULLTEXT(见 SearchWP 与 Relevanssi 搜索增强),但中文分词问题依然存在,需要按文中的二元切分思路处理。四者的取舍:
| 方案 | 改动 | 中文效果 | 适用数据量 |
|---|---|---|---|
| 默认 LIKE | 无 | 差 | < 1000 篇 |
| MySQL ngram | 建索引 + 过滤器 | 中等 | 1 万–10 万 |
| 搜索插件自建索引 | 装插件 | 中等(需分词处理) | 1 万–10 万 |
| Elasticsearch + IK | 独立服务 | 好 | 10 万+ |
常见踩坑
- 直接在 SQL 里用
REPLACE()或LIKE改搜索结果,忘了防注入。 拼接 SQL 必须走$wpdb->prepare(),用户输入要用$wpdb->esc_like()。 - 建了 FULLTEXT 索引但查询仍全表扫描。
EXPLAIN看一下key列有没有用上ft_cn_title_content;没用上多半是MATCH的列与索引列顺序不一致。 - 改了
ngram_token_size没重启,也没重建索引。 这个参数是只读的启动参数,改配置必须重启,且建好的索引要OPTIMIZE TABLE重建。 - 两字词搜不到。
innodb_ft_min_token_size默认 3,中文两字词被直接忽略,必须调到 2。 - 搜索命中了草稿、修订版本。 自建查询要显式加
post_status = 'publish'与post_type约束。 - 页面缓存把搜索结果也缓存了。
?s=的请求必须排除在页面缓存之外,否则 A 搜到 B 的结果。 - 案例: 某站给
wp_posts建了 FULLTEXT 索引并改了查询,但搜索依然慢。用EXPLAIN一看,type=ALL、rows=38000,索引根本没被使用——原因是主题在functions.php里用pre_get_posts把主查询换掉了,posts_search的改动被覆盖,实际执行的还是LIKE '%...%'。用pre_get_posts的改动保留、改用插件索引后才解决。教训:改动搜索行为前,先用EXPLAIN确认当前执行计划;不量化就优化,等于闭眼开车。
总结
判断路径很清晰:文章不到一千篇,默认搜索凑合能用,加个标题权重过滤器即可;一千到十万篇,用 MySQL ngram 全文索引或搜索插件自建索引,并针对中文做二元切分;十万篇以上或有电商/容错需求,上 Elasticsearch + IK。
行动清单:先 EXPLAIN 确认当前是不是全表扫描 → 决定走 ngram 还是插件 → 调 innodb_ft_min_token_size 到 2 并重建索引 → 用 posts_search 接入并加标题权重 → 验证两个中文双字词的搜索结果。搜索体验之外,站内推荐也可以参考 相关文章推荐方案,而索引与查询开销最终还是会落到字段读取上,可顺带看看 ACF get_field 性能优化。
