相关文章的选文逻辑:标签、权重与死链检查

前几天一个刚开始写博客的朋友问我:他站上每篇文章的底部都有个「相关文章」,是程序自动生成的,一共五条,但几乎每次都跟他写的内容没什么关系,有时候还会链到一篇两年前的、已经被他删掉的产品介绍。他想知道这东西到底有没有用,值不值得手工去做。

这个问题比看起来重要。相关文章在小站上往往是一个「装了插件就忘了」的模块,但它同时是两件事:对读者,它是「读完这篇还能看什么」的指路牌;对搜索引擎,它是站内链接结构里唯一能做到「每篇文章都有几根指向同主题的线」的位置。自动生成的、无关的、失效的三类相关文章,恰好把这两个作用都抵消了——读者点了发现不对,下次就不点了;搜索引擎看到的是一堆无意义链接,甚至是一堆 404。

这篇讲的是我在这几百篇博客上把相关文章重做一遍的过程:怎么让程序选得准、怎么处理「手写的比自动的好」这个矛盾、怎么验证改完真的有效、以及几个最常见的失败形态。

一、先明白相关文章在两个地方起作用

做任何优化之前,先分清它服务谁。相关文章这个模块,价值来自两个完全不同的机制,混在一起想就会做错。

对读者,它解决的是「读完之后呢」。一个人通过搜索某篇长尾词文章进来,他关心的其实是那个更大的问题——比如搜「Nginx 504 超时」进来的人,真正想解决的是「我的 PHP 站点为什么偶尔 504」。如果文末正确链到了「PHP-FPM 进程池怎么配」「慢查询导致后端超时」这几篇,他的问题可能就真的解决了,会记住这个站。如果链到的是「如何给博客换主题」,他就走了,而且大概率不会回来。

对搜索引擎,它解决的是「这篇文章属于哪个主题群」。搜索引擎判断一个页面的主题,除了看它自己的内容,还看它链向谁、谁链向它。当你给每篇文章都配了 3~5 条同主题的链出,这些链接就构成了一张语义网:一个主题下有十几篇文章互相连着,搜索引擎很容易识别出「这是一个完整的主题簇」,于是这个簇里的每一篇都获得一定程度的主题权重加成。反过来,如果相关文章链的是随机文章,这张网就是随机的,等于没有网。

这两个机制的方向一致但要求不同。对读者,要的是「高度相关 + 标题有吸引力」;对搜索引擎,要的是「同主题 + 锚文本带关键词 + 链接不断」。同时满足这两条,才是好的相关文章。

二、别急着装插件:先看你现在的推荐是怎么来的

大多数博客系统的「相关文章」只有三种实现,知道自己在哪一种,才知道该改什么。

第一种是纯随机的。有些老主题就是 ORDER BY RAND() 抽五条。这种完全没用,而且是负资产——读者点进去发现不对,搜索引擎看到一堆无关链接。看到这种,第一件事就是换掉,不用犹豫。

第二种是按分类取最新的。就是「同分类下的最新五篇」。这个比随机好,但问题在于:分类通常很粗(比如「技术教程」下面几百篇),最新五篇可能全是这个月写的、跟当前这篇完全不相干的内容。而且它的偏好是「最新的」而不是「最相关的」,导致老文章永远得不到内链,越老的越沉底——这是小站最常见的内链贫富分化问题。

第三种是按标签匹配。取「和本文共享标签最多的文章」。这个思路是对的,但效果完全取决于标签质量。如果标签是写文章时随手加的、或者从来没维护过,那匹配出来的还是一片噪音。

判断自己属于哪一种,最快的办法是读一下主题或插件的相关文章代码(通常在 functions.php 或主题的 post.php 里,搜 related 或 ORDER BY RAND),或者更简单——在自己站上打开同一篇文章的同一个位置,刷新三次,看那五条变不变。变了就是随机,不变就是定了规则。

我自己那站原来是第二种。改的第一件事不是重写查询,而是先确认标签数据能不能用。

三、相关性的基础是标签质量,不是算法

这里要说一个很多人不愿意承认的事实:相关文章推荐效果差,90% 的原因是标签太烂,不是算法不好。你换十种推荐算法,标签里全是「教程」「分享」「经验」这种泛词,结果一样。

先做一次标签审计。把所有标签列出来,按使用次数排序,看两头:

SELECT t.name, COUNT(c.id) AS n
FROM typecho_metas t
JOIN typecho_relationships r ON r.mid = t.mid
JOIN typecho_contents c ON c.cid = r.cid AND c.type='post' AND c.status='publish'
WHERE t.type = 'tag'
GROUP BY t.mid
ORDER BY n DESC;

如果某个标签下面挂了一百多篇,那这个标签基本没有区分度,相当于「全部文章」——它产生的相关性是假的。如果某个标签只挂了一篇,那它永远匹配不到任何东西,纯属占位。健康的分布是:大部分标签挂 3~20 篇,中间有一层挂 20~50 篇的核心标签。

审计完会发现问题通常分三类:一是泛词(教程、分享、经验、笔记),这类应该删掉或者降级,它们不承载语义;二是同义重复(MySQL 和 mysql 和 Mysql、Nginx 和 nginx 和 反向代理),要合并;三是缺失——很多文章只挂了一两个宽泛标签,导致它压根没有可匹配的对象。

我的做法是先定一份「主题词表」:把站里真正值得成系列的方向列出来,大概二三十个,比如「Nginx 配置」「MySQL 性能」「备份与恢复」「SEO 收录」「PHP 安全」「服务器监控」。然后逐篇回填,每篇文章保证挂上 2~4 个词表里的标签,加上一两个具体的细标签。这一步是纯体力活,一百多篇文章我花了两三个晚上,但它是后面所有推荐效果的地基,绕不过去。

回填完再去看「随机刷新三次都不变」的那个推荐,你会发现只改标签、不改算法,推荐质量就已经好了一大截。因为「同分类最新」里面如果也按标签过滤了一层,出来的自然就对了。

四、判定相关性的四层权重,从便宜到精确

标签整理好之后,如果要自己写查询(而不是依赖插件),有一个四层的权重设计,从便宜到精确排列,可以在准确率和性能之间取平衡。

第一层:共享标签数。最基本的一层,两个文章共享的标签越多越相关。SQL 大概是这样:

SELECT c.cid, c.title, COUNT(r2.mid) AS shared
FROM typecho_relationships r1
JOIN typecho_relationships r2 ON r2.mid = r1.mid AND r2.cid != r1.cid
JOIN typecho_contents c ON c.cid = r2.cid AND c.type='post' AND c.status='publish'
WHERE r1.cid = ?
GROUP BY c.cid
ORDER BY shared DESC, c.created DESC
LIMIT 10;

这一层就够用了,尤其标签整理过之后。注意 LIMIT 10 然后在外层再筛,因为后面还要加过滤。

第二层:标签的稀缺度加权。共享一个「MySQL」比共享一个「教程」有意义得多。改进方式是用 IDF 的思路——标签下文章越少,权重越高。在 SQL 里实现会稍复杂,简单做法是给标签表加一个 weight 字段,人工给核心具体标签设 3、宽泛标签设 1,然后 SUM(w) 排序。这个改动很小,效果提升明显。

第三层:时间衰减。完全不考虑时间的推荐会永远推那几篇「标签最多的经典文章」,导致少数几篇抢走所有内链,其他文章得不到链接。加一个衰减:半年内的加满分,一年内的打七折,两年以上的打对折。实现上可以用 ORDER BY shared * (1.0 / (1 + DATEDIFF(NOW(), c.created)/365)) 之类的平滑衰减,也可以直接分段。

第四层:正文相似度。这一层最精确也最贵——把两篇文章的正文分词后算余弦相似度。个人站一般不需要做到这一步,除非文章量很大且标签实在整理不动。如果要做,中文分词用 jieba、英文用 TF-IDF,提前算好存成一张「相似文章」表,查询时直接读。不要在每次页面请求时现算,那会把页面响应时间拖到几秒。

实操建议:先做第一层 + 第三层(共享标签数 + 时间衰减),这两层不用任何外部依赖,一个 SQL 搞定,效果已经能覆盖八成需求。第二层和第四层作为后续优化。

五、一篇文章推荐几条?三到五条,别贪多

数量这个参数经常被忽略,但它直接影响效果。推荐十条,读者的点击率不会提高,反而稀释了每一条的注意力;对搜索引擎,十条链接里如果有三条不相关,整块的可信度都会下降。

经验值是 3~5 条,并且要做「宁缺勿滥」的处理:如果按规则只匹配到两条,就只显示两条,不要用「最新的文章」补足。补足的那些一定不相关,而且会稀释掉真正相关的那两条的价值。

还有一个细节是去重:相关文章里不该出现读者刚刚可能已经读过的文章。最简单的做法是排除本系列的前一篇和后一篇(那两篇已经在「上一篇/下一篇」里了),以及排除「置顶推荐」里已经出现过的。这个去重看着琐碎,但它决定了这个模块看起来是「精心整理」还是「机器堆砌」。

展示上也有讲究。每条只给标题和一句很短的理由(比如「同属 MySQL 性能排查」),比给一段摘要好——读者扫一眼标题就决定点不点,理由的作用是让他相信这个推荐不是随机的。这一段话通常直接从共享标签里生成,不用手写。

六、自动生成做不到的地方:手工接管头部文章

纯自动的相关文章有一个天花板:它能找到「同主题」,但找不到「最好的那几篇」。比如你写过十几篇 Nginx 的文章,自动推荐可能会推一篇「Nginx 安装」给所有 Nginx 相关文章,因为那篇标签最多——但实际上读者更该读的是「Nginx 配置变更的安全流程」那篇更深入的。

解决办法是分层管理,不用全站手工,只接管两类:

第一类是流量入口文章,也就是那些排名靠前、每天有稳定搜索进来的文章。这几篇的相关文章值得手工指定,因为它们带来的读者最多,转化效率最高。去搜索后台把「点击量前 20」的页面列出来,这 20 篇逐一手工设定推荐的 3~4 篇。

第二类是系列文章。如果你有一个写了七八篇的系列,那这个系列内部的相关性应该是「按顺序的前后文 + 同系列其他篇」,纯靠标签匹配不出来「顺序」。做法是给系列文章加一个统一的标签或者一个自定义字段 series_id,然后在模板里判断:如果有 series_id,优先按系列内序号相邻的推荐,剩下的位置用标签匹配填充。

实现上用一个自定义字段就够,不一定要改数据库结构:Typecho 的 typecho_fields 表、WordPress 的 postmeta 都能存。模板里读这个字段,有值就走手工逻辑,没值就走默认算法。这样一个改动覆盖了「头部手工、长尾自动」的分层,维护成本也低——新文章不用管,老的头部文章手工配一次长期有效。

七、一定要做失效检查:相关文章最容易变成 404

这是最容易被忽略、后果最直接的一条。相关文章里链到已删除、已改 URL 的文章,是所有站内链接里最常见的 404 来源,因为它是自动生成的,没人会逐条点。

我朋友遇到的就是这个:他删掉了一篇早期的产品介绍,但那篇文章的标签还在,于是所有共享那个标签的文章,相关文章里都出现了那条死链。他自己不知道,搜索引擎已经爬到了几十次 404。

检查方式很简单,把全站渲染出的相关文章链接抓下来批量请求:

curl -s "https://你的域名/" | grep -oE 'href="https://你的域名/[0-9]+\.html"' | sort -u

对文章页同理,抓 related 区块里的 href,去重后逐个 curl -o /dev/null -w "%{http_code}"。几百个链接几分钟就跑完了。发现 404 分两种情况处理:

如果是文章被删了,从关系表里清掉对应的标签关联(Typecho 的 typecho_relationships、WordPress 的 term_relationships),否则它会持续被推荐。如果是文章改了 URL,加一条 301 重定向指到新地址——这条重定向本身还有价值,老链接的权重能传过去。

这件事应该做成每月一次的例行检查,而不是发现了才做。写成一个脚本扔进 cron,每周跑一次,输出非空就发消息,成本极低。

八、怎么验证改完之后真的有用

改完不能只看「看着像相关了」,要有数字。三个观察点,按见效快慢排列。

第一,站内点击。如果你有前端统计(比如自己埋的一个点击事件),直接看相关文章模块的点击率变化。没有统计的话,至少看整体跳出率——相关文章做得好,跳出率通常会下降几个百分点。观察周期两周。

第二,抓取分布。搜索引擎后台里看「抓取统计」,改完后几个月内,老文章的抓取频次应该上升——因为更多文章通过相关文章被链接到了,链接深度变小了。这条的观察周期要一两个月,因为搜索引擎重新计算链接结构需要时间。

第三,长尾词的排名。长远看这才是核心收益:主题簇形成之后,簇里的每一篇都会从「孤立的一篇」变成「某主题下的组成部分」,排名上限会提高。用搜索后台的「查询」页面,筛出那些之前排在 15~30 位、有展现无点击的页面,看它们改完后有没有向上移动。

一个反面的验证也很重要:改完之后,去搜索后台看「404」和「软 404」的数量。如果这个数字没下降,说明你的相关文章里还有失效链接没清干净。这比看排名更快、更直接。

九、小结:一次做完,长期不用管

相关文章这件事,投入产出比在站内优化里算很高的,因为它只做一次(标签整理 + 查询规则 + 头部手工配置 + 死链检查),之后新文章自动进体系,不需要持续维护。

四条可以带走的判断:标签质量决定一切,算法再精巧也救不了烂标签,先做标签审计和回填;共享标签数 + 时间衰减是性价比最高的查询方案,两层就能覆盖大部分需求;宁缺勿滥,只匹配到两条就显示两条,绝不用最新文章凑数;死链检查必须例行化,因为自动生成的东西没人会去点,坏了也没人知道。

至于我朋友那个问题——值不值得手工做?答案是:不用全站手工,但头部二十篇值得,剩下的交给整理好的标签。这个投入大概两个晚上,换来的是一张真正连起来的站内网,而不是一堆看起来像链接的装饰。

Last modification:November 3rd, 2026 at 09:33 pm

Leave a Comment