搜索全站
插件 15 分钟阅读

WordPress 相关文章推荐:插件方案与自建方案对比

相关文章靠插件还是自己写?插件省事但有云端依赖和查询开销,自建可控但需要缓存设计。本文对比四类方案的实现原理、性能代价与适用场景,给出可直接用的自建代码。

U
UFO_PF 2026-08-23 · 阅读 15 分钟
Wo 插件
阅读提示
本文约 5,808 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:文末那几篇”相关文章”,到底该怎么来

文章底部放一组相关文章,是提升停留时长和内部链接密度的标准做法。看似简单,实现方案却有四五种,而且差别不小:

  • 主题自带的”相关文章”模块,多半是按同分类随机取几条,质量一般;
  • Jetpack 的 Related Posts 模块,把计算放到云端;
  • YARPP、Contextual Related Posts 这类专用插件,本地算相关度并缓存;
  • 自己写一段 WP_Query,按标签/分类重合度取。

选哪个,取决于三个问题:数据量多大(几百篇还是几万篇)、要不要中文语义相关(见 中文搜索不好用的根因)、以及能不能接受外部依赖(Jetpack 需要连 WordPress.com)。

原理分析:相关度是怎么算出来的

主流做法有四种,代价完全不同:

方案 相关度依据 计算位置 主要代价
同分类/同标签随机 分类法重合 本地 MySQL 查询简单,相关性一般
标题+正文全文匹配 词频(TF-IDF) 本地,需要索引 需要额外索引表,中文分词是问题
云端语义(Jetpack) Elasticsearch 云端 需要连接 WordPress.com,国内可能不稳
插件本地缓存(YARPP/CRP) 词频 + 分类,结果入缓存表 本地 首次计算慢,缓存表会膨胀

几个关键事实:

  • 按分类/标签取是最廉价的方案,命中 wp_term_relationships 的索引,几十万篇文章也能毫秒级返回;
  • 全文相关度(YARPP 那一类)对中文效果有限,因为中文没有空格,插件内部的 tokenizer 通常按标点切分,长句会变成一个词;
  • Jetpack 的 Related Posts 在云端用 Elasticsearch 计算,中文处理比本地插件好,但依赖外网连接,国内站点常出现”相关文章一直加载不出来”;
  • 任何方案都必须缓存,否则每次访问文章页都要做一次多表 JOIN。

保姆级实操

第一步:小站点先试 Related Posts By Taxonomy

如果文章量在几千以内、分类体系清晰,最省心的是 Related Posts By Taxonomy:纯本地、按分类法算相关度、有缓存。它提供短代码和模板函数:

// 取相关文章(该函数由插件提供)
$related = km_rpbt_related_posts_by_taxonomy( get_the_ID(), array(
    'taxonomies' => array( 'category', 'post_tag' ),
    'posts_per_page' => 6,
    'post_types'     => array( 'post' ),
) );

if ( $related ) {
    echo '<ul class="related-list">';
    foreach ( $related as $p ) {
        printf(
            '<li><a href="%s">%s</a></li>',
            esc_url( get_permalink( $p->ID ) ),
            esc_html( get_the_title( $p->ID ) )
        );
    }
    echo '</ul>';
}

也可以直接用短代码 [related_posts_by_tax] 放进文章模板。

第二步:中等站点考虑 YARPP 或 Contextual Related Posts

两者都按”标题+正文词频 + 分类”算相关度,并把结果写进自己的缓存表,前台直接读缓存。配置要点:

  • YARPP:后台「Settings → YARPP」里调 “Threshold”(相关度阈值)与 “Display options”,勾选 “Cache results”;提供 related_posts() 模板函数与 [yarpp] 短代码;
  • Contextual Related Posts:后台「Contextual Related Posts」里设置展示数量、是否显示缩略图、是否排除某些分类;提供小工具与短代码,可直接在内容后追加。

两者共同的配置纪律:开启缓存,并定期清理缓存表。缓存表会随文章更新不断膨胀,一年不清理可能涨到几百 MB。

第三步:自建方案(可控性最高)

自建的核心是”按标签/分类重合度排序 + 结果缓存到 transient”:

function mimangfei_get_related_posts( $post_id, $limit = 6 ) {
    $key      = 'mimangfei_rel_' . $post_id;
    $cached   = get_transient( $key );
    if ( false !== $cached ) {
        return $cached;
    }

    // 取当前文章的标签与分类 ID
    $tags = wp_get_post_tags( $post_id, array( 'fields' => 'ids' ) );
    $cats = wp_get_post_categories( $post_id, array( 'fields' => 'ids' ) );

    $args = array(
        'post_type'           => 'post',
        'post_status'         => 'publish',
        'posts_per_page'      => $limit,
        'post__not_in'        => array( $post_id ),
        'ignore_sticky_posts' => true,
        'no_found_rows'       => true,
        'orderby'             => 'date',
        'tax_query'           => array( 'relation' => 'OR' ),
    );

    if ( $tags ) {
        $args['tax_query'][] = array(
            'taxonomy' => 'post_tag',
            'field'    => 'term_id',
            'terms'    => $tags,
        );
    }
    if ( $cats ) {
        $args['tax_query'][] = array(
            'taxonomy' => 'category',
            'field'    => 'term_id',
            'terms'    => $cats,
        );
    }

    // 标签/分类都没有时,退化为最新文章
    if ( ! $tags && ! $cats ) {
        unset( $args['tax_query'] );
    }

    $query   = new WP_Query( $args );
    $related = wp_list_pluck( $query->posts, 'ID' );

    set_transient( $key, $related, 12 * HOUR_IN_SECONDS );
    return $related;
}

// 文章更新时清掉自己的相关缓存
add_action( 'save_post', function ( $post_id, $post ) {
    if ( 'post' === $post->post_type ) {
        delete_transient( 'mimangfei_rel_' . $post_id );
    }
}, 10, 2 );

前台渲染:

$ids = mimangfei_get_related_posts( get_the_ID() );
if ( $ids ) {
    echo '<div class="related-box"><h3>相关文章</h3><ul>';
    foreach ( $ids as $id ) {
        printf(
            '<li><a href="%s">%s</a></li>',
            esc_url( get_permalink( $id ) ),
            esc_html( get_the_title( $id ) )
        );
    }
    echo '</ul></div>';
}

第四步:让结果更”相关”的两个改进

  1. 同标签优先于同分类:拆成两次查询,先取同标签的,不够再补同分类的;
  2. 主分类加权:如果站点用 Yoast 或自定义的主分类字段,让主分类相同的文章排在前面。
// 先按标签取,不足再用分类补齐
$by_tag = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => 6,
    'post__not_in'   => array( $post_id ),
    'tag__in'        => $tags,
    'no_found_rows'  => true,
) );
$ids = wp_list_pluck( $by_tag->posts, 'ID' );

if ( count( $ids ) < 6 ) {
    $need = 6 - count( $ids );
    $by_cat = new WP_Query( array(
        'post_type'      => 'post',
        'posts_per_page' => $need,
        'post__not_in'   => array_merge( array( $post_id ), $ids ),
        'category__in'   => $cats,
        'no_found_rows'  => true,
    ) );
    $ids = array_merge( $ids, wp_list_pluck( $by_cat->posts, 'ID' ) );
}

常见踩坑

  1. orderby => rand 取相关。 随机数排序会让 MySQL 全表排序并写临时表,且完全绕过查询缓存,几千篇文章时明显变慢。
  2. 没缓存,每篇文章页多一次多表 JOIN。 用 transient 或对象缓存存 ID 列表即可,别存整段 HTML(改样式时要批量清缓存)。
  3. Jetpack 相关文章在国内加载不出来。 它是云端计算的,接口不通时前台就是空白。国内站点优先本地方案。
  4. 缓存表无限膨胀。 YARPP/CRP 的缓存表要定期清理,或限制缓存有效期。
  5. 相关文章里出现已下线/私密内容。 自建查询必须显式写 post_status => publish,并排除 post__not_in 当前文章。
  6. 相关文章取不到,因为文章没有标签。 退化策略要写好,否则文末一片空白。
  7. 案例: 某站用一款全文相关度插件,文章到 8000 篇之后,文章页 TTFB 从 400ms 涨到 1.6s。慢查询显示插件每次都要对 wp_posts 做一次 LIKE '%词%' 的匹配并遍历候选集。最终改成”标签优先 + 分类补齐 + transient 缓存 ID”的自建方案,查询从每次 3 条降到 0 条(命中缓存),TTFB 回到 350ms,相关度在人工抽查 50 篇后反而更准(因为站点的标签体系维护得不错)。结论:相关度算法不是越复杂越好,能利用好已有分类法索引的方案性价比最高。

总结

选型建议一句话:站点分类体系清晰 → 用 Related Posts By Taxonomy 或自建按分类法取;需要语义相关且能接受外部依赖 → Jetpack;数据量中等且懒得维护 → YARPP / Contextual Related Posts,但必须开缓存并定期清表。

自建方案的必备三件套:no_found_rows => true 省掉计数、tax_query 走索引、结果 ID 存 transient 并在 save_post 时失效。中文站点尤其要避开”全文词频”这类依赖分词的方案,除非你已经做好了分词(参考 中文搜索不好用的根因)。如果还想让相关模块出现在聚合页或侧边栏,字段读取的开销可以顺手按 ACF get_field 性能优化 的方法压一压。

U
UFO_PF作者

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

留下你的想法

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