本文约 5,377 字,预计阅读 14 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:搜什么都搜不到
站点文章超过 200 篇后,站内搜索就会开始”不好用”:
- 搜”缓存插件”,只返回标题里有”缓存”的文章,正文里提到的全搜不到;
- 搜”Redis”,返回一堆不相关的(因为 MySQL 的匹配很粗糙);
- 中文搜”优化”和”优化方案”结果完全不一样,没有分词;
- 搜错一个字就零结果,没有容错。
这不是 bug,是 WordPress 默认搜索的设计就如此简陋。想做好,得先知道它到底怎么查的。
原理分析:默认搜索到底查了什么
WordPress 的默认搜索生成这样的 SQL 条件:
AND (
(wp_posts.post_title LIKE '%关键词%')
OR (wp_posts.post_excerpt LIKE '%关键词%')
OR (wp_posts.post_content LIKE '%关键词%')
)
AND wp_posts.post_status = 'publish'
AND wp_posts.post_type IN ('post','page')
四个硬伤一目了然:
- 只搜三个字段:标题、摘要、正文。不搜自定义字段、分类名、标签名、评论。
- 用
LIKE '%...%':无法走索引,数据量大时全表扫描,慢。 - AND 逻辑:多个词之间是”与”关系,且必须出现在同一个字段里,词一多就零结果。
- 无分词、无相关性排序:结果按日期倒序,不是按匹配度。中文没有空格分词,
LIKE匹配的是整串子串,所以”wordpress缓存”这种连写搜不到。
理解这四点,改进方向就很清楚了。
保姆级实操
方案一:扩展搜索范围(改代码,零插件)
/**
* 让搜索支持分类名、标签名、自定义字段
*/
function mmf_search_join( $join, $query ) {
global $wpdb;
if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
return $join;
}
$join .= " LEFT JOIN {$wpdb->term_relationships} tr ON {$wpdb->posts}.ID = tr.object_id ";
$join .= " LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id ";
$join .= " LEFT JOIN {$wpdb->terms} t ON tt.term_id = t.term_id ";
$join .= " LEFT JOIN {$wpdb->postmeta} pm ON {$wpdb->posts}.ID = pm.post_id ";
return $join;
}
add_filter( 'posts_join', 'mmf_search_join', 10, 2 );
function mmf_search_where( $where, $query ) {
global $wpdb;
if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
return $where;
}
$term = '%' . $wpdb->esc_like( $query->get( 's' ) ) . '%';
$extra = $wpdb->prepare(
" OR (t.name LIKE %s) OR (t.slug LIKE %s)
OR (pm.meta_value LIKE %s AND pm.meta_key IN ('_seo_desc','subtitle','keywords')) ",
$term, $term, $term
);
return $where . ' ' . $extra;
}
add_filter( 'posts_where', 'mmf_search_where', 10, 2 );
// JOIN 会产生重复行,必须去重
function mmf_search_distinct( $distinct, $query ) {
if ( ! is_admin() && $query->is_main_query() && $query->is_search() ) {
return 'DISTINCT';
}
return $distinct;
}
add_filter( 'posts_distinct', 'mmf_search_distinct', 10, 2 );
注意两个必须做的保护:is_admin() 判断(否则后台列表搜索也被改),和 DISTINCT(否则结果重复)。
方案二:按相关性排序(标题命中优先)
add_filter( 'posts_orderby', function( $orderby, $query ) {
global $wpdb;
if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
return $orderby;
}
$s = $query->get( 's' );
if ( empty( $s ) ) {
return $orderby;
}
$like = '%' . $wpdb->esc_like( $s ) . '%';
return $wpdb->prepare(
" CASE
WHEN {$wpdb->posts}.post_title LIKE %s THEN 1
WHEN {$wpdb->posts}.post_excerpt LIKE %s THEN 2
WHEN {$wpdb->posts}.post_content LIKE %s THEN 3
ELSE 4
END ASC, {$wpdb->posts}.post_date DESC ",
$like, $like, $like
);
}, 10, 2 );
方案三:限定搜索范围(减少噪音)
add_action( 'pre_get_posts', function( $q ) {
if ( ! is_admin() && $q->is_main_query() && $q->is_search() ) {
$q->set( 'post_type', array( 'post' ) );
$q->set( 'posts_per_page', 15 );
}
});
方案四:换用专业搜索插件
代码方案解决不了中文分词和模糊容错,这两块必须靠插件:
| 插件 | 原理 | 中文分词 | 适合场景 |
|---|---|---|---|
| Relevanssi | 自建索引表,模糊匹配、权重、词干 | 一般(靠模糊匹配补救) | 内容站、文档站,免费版够用 |
| SearchWP | 索引正文 + PDF + 自定义字段 + 短代码展开内容 | 一般 | 商业站,付费 |
| Ivory Search | 增强默认搜索,支持 WooCommerce | 一般 | 轻量需求 |
| Ajax Search Lite | 实时下拉联想 | 一般 | 提升前端体验 |
| ElasticPress + Elasticsearch | 外部搜索引擎 | 好(配 IK 分词器) | 万篇以上大站 |
中文站的务实选择:文章量 2000 篇以内,用 Relevanssi 免费版 + 开启模糊匹配,再配一个 Ajax 实时搜索插件做体验,成本最低效果最好。
Relevanssi 关键设置:
- 索引里勾上”自定义字段””分类””标签””摘要”;
- 搜索设置里把模糊匹配设为 “Partial matches”;
- 排序选 relevance(相关性)而不是 date;
- 装完必须点一次”建立索引”,之后增量更新。
方案五:搜索结果页的 SEO 与体验
搜索结果页(/?s=关键词)绝不能让搜索引擎索引,否则会产生大量低质量页面:
add_action( 'wp_head', function() {
if ( is_search() ) {
echo '<meta name="robots" content="noindex,follow">' . "\n";
}
});
主流 SEO 插件默认已处理,自定义主题要自己加。
零结果页要做引导,别只显示”找不到”:
if ( ! have_posts() ) {
echo '<p>抱歉,没有找到与「' . esc_html( get_search_query() ) . '」相关的内容。</p>';
echo '<p>你可以试试:</p><ul>';
echo '<li>检查拼写,或使用更短的关键词</li>';
echo '<li><a href="' . esc_url( home_url( '/sitemap/' ) ) . '">浏览网站地图</a></li>';
echo '</ul>';
}
常见踩坑与失败案例
- 案例一:加了
posts_join扩展搜索后,文章列表页出现重复项——JOIN 产生了笛卡尔积。必须配posts_distinct返回DISTINCT。 - 案例二:改了搜索代码后后台搜索也变了(后台列表搜出奇怪结果)。根因是没加
is_admin()判断。 - Relevanssi 索引不更新:定时任务没跑(wp-cron 被禁用)。手动重建索引,或参见 wp-cron 定时任务原理与系统 cron 替代。
- 案例三:某站上了 Elasticsearch,但中文搜”数据库优化”零结果——没装 IK 分词器,默认 standard 分词把中文切成单字。中文站上 ES 必须配 IK。
- 搜索被整页缓存:搜索结果页必须排除缓存,否则所有用户看到同一份结果。宝塔静态缓存要手动加排除规则。
- 搜索 URL 美化:把
/?s=xxx改成/search/xxx,利于统计和体验:
php
add_action( 'template_redirect', function() {
if ( is_search() && ! empty( $_GET['s'] ) && false === strpos( $_SERVER['REQUEST_URI'], '/search/' ) ) {
wp_redirect( home_url( '/search/' ) . urlencode( get_query_var( 's' ) ) . '/' );
exit;
}
});
总结
WordPress 默认搜索的四个硬伤是只搜三个字段、LIKE 全表扫描、AND 逻辑、无分词无相关性排序。改进路线分三档:轻量需求用 posts_join / posts_where / posts_orderby 三个钩子扩展范围和排序(记得加 is_admin() 和 DISTINCT);中文站文章量中等用 Relevanssi 开模糊匹配;万篇以上大站上 Elasticsearch + IK 分词。无论哪种,搜索结果页必须 noindex。零结果页做好引导,能把一部分搜索流失转成浏览转化。
