搜索全站
插件 12 分钟阅读

WordPress 中文搜索不好用的根因:MySQL 全文索引局限与替代方案

中文搜索不准不是插件没选对,而是 MySQL 全文索引按空格分词、中文没有空格。本文从默认搜索的 SQL 讲起,分析 InnoDB 全文索引的局限,给出 ngram、自建分词与外部搜索引擎三条路。

U
UFO_PF 2024-12-23 · 阅读 12 分钟
Wo 插件
阅读提示
本文约 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 万+

常见踩坑

  1. 直接在 SQL 里用 REPLACE() 或 LIKE 改搜索结果,忘了防注入。 拼接 SQL 必须走 $wpdb->prepare(),用户输入要用 $wpdb->esc_like()。
  2. 建了 FULLTEXT 索引但查询仍全表扫描。 EXPLAIN 看一下 key 列有没有用上 ft_cn_title_content;没用上多半是 MATCH 的列与索引列顺序不一致。
  3. 改了 ngram_token_size 没重启,也没重建索引。 这个参数是只读的启动参数,改配置必须重启,且建好的索引要 OPTIMIZE TABLE 重建。
  4. 两字词搜不到。 innodb_ft_min_token_size 默认 3,中文两字词被直接忽略,必须调到 2。
  5. 搜索命中了草稿、修订版本。 自建查询要显式加 post_status = 'publish' 与 post_type 约束。
  6. 页面缓存把搜索结果也缓存了。 ?s= 的请求必须排除在页面缓存之外,否则 A 搜到 B 的结果。
  7. 案例: 某站给 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 性能优化。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。