为博客构建“轻量级知识索引”(三):基于预计算语义索引的 WordPress 轻量推荐系统设计与实现
文章摘要
针对博客推荐系统轻量化需求,本文提出基于预计算语义索引的实现方案。通过Python脚本解析JSON数据生成结构化索引文件,WordPress运行时直接读取该索引完成推荐列表渲染。方案跳过实时计算环节,将文章关联关系在离线阶段固化为ID映射表,运行时仅需执行简单数据查找与结构映射。最终实现无需复杂算法的推荐功能,使推荐列表自然嵌入文章阅读流,既保持系统轻量特性,又有效提升内容关联性与用户阅读连贯性。
Qwen3-14B · 2026-06-18

1 博客智能推荐的起点

在这个系列文章的第二篇(参见:为博客加一层“语义索引”(二):JSON 结构与生成脚本的实现)最后,我们已经通过 Python 脚本生成了关键的 semantic-index.json 文件。而下一步,最自然的问题就是:我们该如何利用这份已经整理好的内容关系,让博客中的文章真正开始形成连接?

经过思考,我认为目前博客最需要的功能,就是 “推荐列表”:它能让读者在浏览文章时,顺着推荐列表继续探索更多相关主题,从而提升阅读体验,同时也让博客中的内容能够被更充分地发现与利用。

而在当前这个场景下,我真正关注的其实并不是“推荐算法”本身,而是:如何利用现有的数据结构,把已经存在的内容关联真正转化为可运行的推荐能力。

这里最核心的前提,就是上一篇文章中生成的 semantic-index.json 文件。因此,这一篇文章将重点围绕:如何从现有 JSON 数据出发,将其中已经整理好的文章关系,进一步转化为实际可用的推荐系统。

整个目标并不复杂:尽量避免引入额外运行时计算,在保持结构轻量的前提下,实现一个真正能够体现文章内容关联的推荐系统。

接下来,本文会重点围绕以下几个部分展开:

  1. 如何从现有的 semantic-index.json 中提取并整理推荐系统所需的数据结构,并最终生成用于运行时查询的文章索引文件 article-index.json
  2. 如何在 WordPress 运行时基于 article-index.jsonrelated 关系,实现文章 ID 到展示数据(title / url)的轻量映射,并构建最终推荐结果
  3. 如何将推荐列表功能真正接入 WordPress 页面,并完成最终的前端展示

整个过程的重点,并不是重新计算文章之间的关系,而是:如何将已经整理好的内容关联,转化为一个真正可运行、可展示、可维护的轻量推荐系统。

2 数据提取

2.1 JSON 文件概览

在上一章中,我们明确了第1步要实现的功能目标:”从现有的 semantic-index.json 中提取并整理推荐系统真正需要的数据,并生成适合 WordPress 调用的文章索引结构”。在开始处理之前,我们先快速回顾一下 semantic-index.json 文件的基本结构。

每篇文章在 semantic-index.json 中大致包含以下字段:

  • id:文章唯一标识,用于快速查找和映射
  • title:文章标题,用于展示和生成推荐列表
  • url:文章链接,供推荐列表跳转使用
  • summary:文章摘要,可用于推荐列表的简短展示
  • keywords:文章关键词,可辅助生成语义相关性
  • related:推荐的相关文章列表(可为空或部分填充),是生成推荐列表的重要依据

在了解了 semantic-index.json 的基本结构之后,下一步就是把文件中的数据读入程序中,以便我们能够在后续步骤中处理和生成推荐列表。

根据使用场景不同,读取 JSON 的方法也略有差异。这里我们分别给出 Python 和 WordPress/PHP 两种常见方式。

2.2 数据读取与处理

2.2.1 Python 示例

如果你在本地或者服务器上使用 Python 来处理 JSON 文件,可以这样做:

import json

# 读取 semantic-index.json 结构化数据文件
with open('semantic-index.json', 'r', encoding='utf-8') as f:
    articles = json.load(f)

# 输出前两条记录,用于验证数据结构是否正确
for article in articles[:2]:
    print(article['id'], article['title'], article['url'])

json.load() 会将 JSON 文件解析为 Python 中的列表与字典结构,每一篇文章对应一个字典对象。这里的读取操作本身非常直接,主要作用是:

  • 验证数据结构是否正确
  • 确认字段(id / title / url / related)是否完整
  • 为后续的索引生成阶段提供基础数据来源

2.2.2 WordPress / PHP 示例

如果你希望在 WordPress 中直接读取 JSON 文件,可以使用以下方法:

<?php
// 读取本地 JSON 文件
json_file = get_template_directory() . '/semantic-index.json';json_data = file_get_contents(json_file);

// 将 JSON 转为 PHP 数组articles = json_decode(json_data, true);

// 查看前两篇文章的数据
for (i = 0; i<2;i++) {
    echo articles[i]['id'] . ' - ' . articles[i]['title'] . ' - ' . articles[i]['url'] . '<br>';
}
?>
  • file_get_contents() 用于读取本地文件内容
  • json_decode($json_data, true) 会把 JSON 转成 PHP 关联数组
  • 之后同样可以根据 $articles 数组的字段进行整理和处理

2.2.3 最终方式选择

虽然 WordPress / PHP 可以直接读取 semantic-index.json 并完成基础的数据解析,但从整个推荐系统的设计目标来看,最终的数据处理阶段并不适合放在运行时环境中完成,因此在实现上,我选择将核心数据处理流程交给 Python 来完成。

原因本质上并不复杂:对于推荐系统而言,真正有价值的工作集中在数据预处理阶段,包括字段提取、结构重组以及文章关系构建。这一类操作本质上属于“离线数据加工”,而 Python 在处理 JSON 数据、字典映射以及批量结构转换方面,具有更清晰、更高效的表达能力。

相比之下,PHP 在 WordPress 体系中的定位更偏向运行时环境,其核心职责是页面渲染与内容输出,而不是承担复杂的数据结构构建任务。将数据处理逻辑放在 PHP 运行时中,不仅会增加页面生成时的负担,也会让数据层与展示层的边界变得模糊。

因此,在本文的整体实现方案中,数据处理阶段统一由 Python 承担,而 WordPress / PHP 仅作为最终的消费端,负责读取已经构建完成的索引数据并进行页面渲染。


需要特别强调的是:无论使用 Python 还是 PHP,当前 2.2 阶段所处理的内容,本质上都只是原始 JSON 数据输入层(Input Layer)。这一阶段的目标并不是构建最终的推荐系统结构,而是完成对基础数据的读取与验证,确保后续处理的数据来源是可靠的。

真正用于 WordPress 运行时的结构化索引(article-index.json),将在后续的构建阶段(2.4)中统一生成。这一层将承担“运行时数据入口”的职责,是整个推荐系统的数据核心,而不再属于原始数据处理范畴。


2.3 数据整理与结构化

在成功读取 semantic-index.json 之后,我们已经获得了一份完整的原始文章数据集合。但从结构上看,这份数据仍然停留在“原始列表形态”,更偏向内容存储,而不是面向索引系统的结构化表达。

如果直接基于这种原始结构进入后续索引构建阶段,会带来一个隐含问题:数据虽然完整,但缺乏一致的访问语义。每一篇文章的字段虽然存在,但在结构层面仍然是“松散组织”的。

因此,在进入 article-index.json 构建之前,需要先完成一次轻量的结构整理。这个过程的目的并不是改变数据本身,而是让每一条数据在结构上趋于统一,从而为后续索引构建提供一个稳定、可预测的输入源。

在这个阶段,处理逻辑其实非常克制:不引入任何新的数据关系,也不做任何索引化操作,只是将原始数据重新组织为一个标准化的结构集合。

在这个过程中,每一篇文章被重新映射为一个统一的数据结构,其核心字段保持不变,仅做最小必要的提取与规范化处理,例如 idtitleurl 以及用于后续关系表达的 related 字段。

对应的处理逻辑可以用一段非常直接的 Python 表达:

cleaned_articles = []

for article in json_data:
    cleaned_articles.append({
        'id': article['id'],
        'title': article['title'],
        'url': article['url'],
        'related': article.get('related', [])
    })

这段逻辑本身并不复杂,它所做的事情只是确保所有文章在进入下一阶段之前,都具备统一的结构形式。

需要强调的是,这一阶段的数据形态仍然保持为“列表结构”,并没有引入任何索引结构或查找优化。换句话说,它仍然是一个线性数据集合,只是在结构层面变得更加干净和一致:

[
    {
        'id': 'article_id_1',
        'title': '文章标题 1',
        'url': '/article-1/',
        'related': ['article_id_5', 'article_id_9']
    },
    {
        'id': 'article_id_2',
        'title': '文章标题 2',
        'url': '/article-2/',
        'related': []
    }
]

这一结构的意义不在于性能或查询效率,而在于为后续的索引构建阶段提供一个“语义一致的数据起点”。只有当所有文章具备统一的数据表达形式之后,才可能进一步稳定地生成运行时所需的 article-index.json

因此,这一阶段更准确的定位并不是“处理数据”,而是“约束数据形态”——它所做的事情,是把原始数据从一种松散的内容集合,收敛为一个结构统一的中间数据层,为后续索引化处理提供条件。

2.4 构建运行时文章索引

在完成前一阶段的数据整理之后,接下来要做的事情其实非常明确:将已经结构化的文章数据进一步固化为一个可以在运行时直接使用的索引文件。

这一阶段的关键点不在于“再处理数据”,而在于“确定数据的最终形态”。也就是说,数据在这一刻开始从“中间结构”转向“运行时资产”,其用途也从处理对象转变为直接被消费的索引资源。

在当前设计中,这个最终形态被统一定义为 article-index.json。它不再承载任何数据加工逻辑,也不参与任何结构演化过程,而只是作为一个静态、稳定、可跨环境读取的数据载体存在。

从系统角色来看,这个索引文件的意义非常明确:它是 WordPress 在运行时唯一依赖的数据入口。因此,它必须彻底脱离运行时逻辑的约束,确保其结构在生成之后不再发生任何变化。

在这种设计下,WordPress 的职责也随之被严格收敛,它只负责读取索引、根据文章 ID 定位数据,并在此基础上完成最终的展示,而不再参与任何形式的数据构建或关系计算。

最终生成的 article-index.json 在结构上表现为一个以文章 ID 为核心键的映射表。每一个条目都完整包含文章的基础信息以及其预先定义好的关联关系,例如标题、链接以及 related 列表:

{
    "article_id_1": {
        "title": "WordPress 缓存优化实践",
        "url": "/wordpress-cache/",
        "related": [
            "article_5",
            "article_9"
        ]
    },

    "article_id_2": {
        "title": "Cloudflare Tunnel 使用记录",
        "url": "/cloudflare-tunnel/",
        "related": [
            "article_1",
            "article_7"
        ]
    }
}

从结构本质上看,这个文件不再是“数据集合”,而是一个已经完成扁平化处理的运行时索引图。所有文章之间的关系不再以计算的形式存在,而是以静态映射的方式被固化下来。

整个索引的生成过程可以理解为一种单向的数据收敛过程:原始的语义数据经过整理之后,被压缩并转化为一个稳定的 ID 映射结构,最终输出为运行时可以直接消费的 JSON 文件:

semantic-index.json(原始语义数据)
        ↓
结构整理(2.3)
        ↓
索引化转换
        ↓
article-index.json(运行时数据资产)
        ↓
WordPress 只读消费

在这个过程中,最关键的变化并不是“生成了一个文件”,而是系统边界的重新定义:数据的构建阶段在这里彻底结束,之后所有运行时行为都只能在既定结构之上进行展开。

因此可以说,从这一刻开始,整个推荐系统的结构已经完全固定。后续所有逻辑都不再改变数据本身,而只是基于这个索引结构进行读取与映射。

3 推荐列表的生成逻辑

3.1 推荐逻辑的核心思路

在上一章中,我们已经完成了运行时索引文件 article-index.json 的构建。接下来要解决的问题就变得非常直接:如何在运行时,基于当前文章快速生成对应的推荐列表。

在这个阶段,推荐逻辑本身其实已经不再复杂,因为文章之间的关系并不是在运行时动态推导出来的,而是在前面的索引构建过程中就已经被“固化”进了数据结构中。换句话说,推荐系统不再需要思考“文章之间是否相关”,它只需要接受一个既定事实——每篇文章的 related 字段,本身就已经定义好了它的推荐对象。

因此,从当前文章 ID 出发,推荐逻辑的核心动作其实非常单一:先定位到 article-index.json 中对应的文章节点,然后读取其中的 related 列表,再将这些 ID 逐个映射回文章的完整信息(如 title 和 url),最终拼装成前端可以直接使用的推荐结果。

如果用一个更直观的方式理解,这个过程本质上并不是“计算”,而是一次标准的数据展开:从一个已知节点出发,沿着预先定义好的关系边,做一次有限的展开操作:

current_article_id
        ↓
article-index.json lookup
        ↓
获取 related ID 列表
        ↓
ID → article 数据映射
        ↓
生成推荐列表

也正因为这种结构的存在,推荐系统在这一层已经不再具备任何“算法意义上的复杂性”:它不涉及相似度计算,也不涉及排序策略,更不依赖语义模型的实时判断。所有这些可能影响推荐质量的因素,都已经被前置到了数据生成阶段,而运行时只保留了一个最稳定、最可控的执行路径。

从工程角度来看,这种设计对 WordPress 这类内容系统是非常友好的。它避免了数据库层面的复杂 JOIN,也完全绕开了运行时的计算开销,整个过程退化为纯内存级别的数据查找与结构映射。换句话说,推荐系统在运行时的成本,几乎等同于一次数组读取加上一次简单遍历。

更重要的是,在这个模型中,推荐质量的上限不再由运行时决定,而是完全取决于前置阶段对 related 字段的构建方式。一旦 related 被生成并固化下来,运行时所能做的事情只是忠实地“还原”这一关系结构,而不是对其进行再加工或优化。

也因此可以说,这一阶段的推荐系统已经发生了本质变化:它不再是一个“计算系统”,而是一个典型的“数据展开系统”,所有逻辑的核心都收敛在一个动作上——从已定义的关系中,把结果完整地展开出来。

3.2 推荐列表的生成

在明确了推荐逻辑之后,运行时的推荐列表生成就变得非常直接了:由于在前面的阶段已经构建好了 article-index.json,因此在 WordPress 的执行环境中,已经不再需要处理任何原始数据,也不需要进行结构转换或关系计算。

从整体流程来看,运行时真正做的事情只有一件:基于当前文章 ID,从预先构建好的索引中找到对应节点,然后读取其中的 related 列表,并将这些 ID 映射回具体的文章信息。

换句话说,推荐列表的生成不再是一个“构建过程”,而是一个“数据还原过程”——系统只是把在离线阶段已经确定好的关系结构,重新展开成用户可见的内容。

在 WordPress 中,这种逻辑通常可以被封装为一个非常轻量的函数,其核心目的不是实现算法,而是完成一次标准的数据映射:

function get_related_posts(current_article_id,limit = 5)
{
    json_path = get_template_directory() . '/cache/article-index.json';articles_index = json_decode(
        file_get_contents(json_path),
        true
    );

    if (!isset(articles_index[current_article_id])) {
        return [];
    }related_ids = articles_index[current_article_id]['related'] ?? [];

    recommendations = [];

    foreach (related_ids as related_id) {

        if (!isset(articles_index[related_id])) {
            continue;
        }article = articles_index[related_id];

        recommendations[] = [
            'title' =>article['title'],
            'url'   => article['url']
        ];

        if (count(recommendations) >= limit) {
            break;
        }
    }

    returnrecommendations;
}

最终输出的数据结构也非常简单,本质上就是一个标准的文章列表:

[
    [
        'title' => 'Cloudflare Tunnel 使用记录',
        'url'   => '/cloudflare-tunnel/'
    ],
    [
        'title' => 'WordPress 缓存优化实践',
        'url'   => '/wordpress-cache/'
    ]
]

到这里,推荐系统在运行时层面的职责已经完全收敛:它不再构建关系,不再计算相关性,而只是把已经确定好的结果,稳定地呈现出来。

4 推荐列表系统的基础构建与呈现设计

4.1 运行环境准备

在进入 WordPress 侧的推荐列表接入之前,需要先确认当前系统已经具备基本的运行条件。这里的“环境准备”并不涉及任何推荐逻辑或数据结构设计,而只是确保离线构建阶段与运行时执行环境能够正常工作,从而为后续步骤提供基础保障。

从整体架构来看,这一阶段关注的核心并不是“功能是否实现”,而是“执行条件是否成立”。

1、需要确认的是 Python 运行环境是否可用。

由于前置的数据构建流程依赖 Python 完成,因此该环境是整个系统离线阶段的基础执行载体。

在实际使用中,只需要确保服务器或本地环境能够正常执行 Python 即可,例如通过简单的版本检查来验证:

python3 --version

如果能够正确返回版本号,则说明 Python 环境已经具备基本可用性。

2、需要确认前置数据文件是否已经成功生成。

在当前系统中,离线阶段的核心输出是 semantic-index.json 文件,它承载了文章之间的语义关系来源,并用于后续索引构建阶段的数据输入。

因此,在进入下一阶段之前,需要确保该文件已经存在,并满足以下基本条件:

  • 文件已成功生成
  • JSON 结构完整可解析
  • 编码格式为 UTF-8
  • related 字段数据存在且结构正确

这一文件的状态,直接决定了后续整个推荐系统是否具备可计算的基础输入。

3、需要确认 WordPress 主题具备基本的修改能力。

由于后续阶段需要在主题层进行推荐列表的展示接入,因此必须能够访问并修改主题中的相关文件,例如:

  • functions.php
  • single.php 或对应文章模板文件

如果使用的是子主题结构,则推荐在子主题中完成所有修改,以避免主主题更新带来的覆盖问题。

当上述三个条件同时满足时,可以认为整个系统的基础运行环境已经准备完成。这一阶段的意义在于:确认离线数据生成能力与运行时执行能力已经打通,但尚未涉及任何索引结构或推荐逻辑本身。

4.2 生成运行时文章索引文件

在完成前置的语义数据准备与结构设计之后,接下来就进入整个推荐系统中最关键的一步:生成运行时可直接被 WordPress 使用的文章索引文件。

这一阶段的本质,并不是“再加工数据”,而是将前面已经确定的结构化结果,固化为一个稳定的运行时数据资产,使其从“处理对象”转变为“直接可消费的数据入口”。

换句话说,这一步标志着整个系统从“数据构建阶段”正式过渡到“运行时消费阶段”。在实现层面,这一过程通常通过一个独立的 Python 脚本完成,例如:

build_article_index.py

该脚本的作用非常单一:将 semantic-index.json 中已经存在的结构化关系,重新组织为以文章 ID 为核心的索引结构,并输出为运行时文件。

核心处理逻辑如下:

import json

json_file = 'semantic-index.json'
output_file = 'article-index.json'

with open(json_file, 'r', encoding='utf-8') as f:
    json_data = json.load(f)

articles_index = {}

for article in json_data:
    article_id = article['id']

    articles_index[article_id] = {
        'title': article['title'],
        'url': article['url'],
        'related': [
            item['id'] if isinstance(item, dict) else item
            for item in article.get('related', [])
        ]
    }

with open(output_file, 'w', encoding='utf-8') as f:
    json.dump(articles_index, f, ensure_ascii=False, indent=2)

print("article-index.json 已生成")

从逻辑上看,这个过程本身并不复杂,它只是完成了一次结构转换:

  • 输入:语义层数据(semantic-index.json)
  • 输出:运行时索引(article-index.json)
  • 中间过程:字段提取 + ID 映射 + 结构固化

到这里,整个推荐系统最核心的数据准备阶段实际上已经完成。从系统视角来看,这一步的完成意味着:
– 语义关系数据已经被固化
– 运行时索引已经生成
– WordPress 后续只需要进行数据读取与映射

4.3 推荐列表在文章页面中的接入位置

在真正开始实现推荐列表之前,还有一个很容易被忽略,但实际上非常重要的问题:推荐列表,应该放在文章页面的什么位置?

从技术实现角度来说,这似乎只是一个简单的页面布局问题,但如果从阅读体验与内容组织结构的角度来看,这一步其实会直接影响整个推荐系统的最终效果。

对于很多传统博客而言,相关文章通常会被放在:侧边栏、页面底部、评论区之后或者随机插入到正文区域中。但这些方式大多数都属于“流量分发”思路,其核心目标往往是增加点击率,而不是帮助读者建立内容之间的结构关系。

而本文实现的推荐系统,其设计目标并不是传统意义上的“热门推荐”,而是:基于当前文章的语义关联,为读者提供真正具有延伸价值的阅读路径。这意味着,推荐列表必须依赖当前文章的上下文,因此它最合理的位置,并不是侧边栏,而是:正文结束之后

不过,在我当前博客中,文章正文结束后其实已经存在一个固定模块——“内容结构提示”:

image.png

该模块的作用,是从整体知识体系的角度说明当前文章的位置,以及它可能关联的主题方向。从阅读逻辑上看,它属于“结构认知层”。

而推荐列表的作用则不同,它并不负责解释结构,而是将结构转化为具体的下一步阅读路径。因此,两者在阅读链路中的关系可以抽象为:

正文内容
↓
内容结构提示(结构认知层)
↓
相关文章推荐列表(路径延伸层)
↓
评论区

在这个顺序中,“内容结构提示”提供的是抽象层面的知识定位,而推荐列表则提供具体的文章跳转入口。这种设计的关键意义在于:它让推荐系统不再作为一个孤立模块存在,而是嵌入到整个阅读逻辑链条中,形成连续的认知与行为引导。

同时,这种顺序也具有一个非常现实的优势:推荐列表不会干扰正文阅读过程,只有在用户完成当前内容之后才出现,从而避免了传统侧边栏推荐对注意力的持续分散。

从系统设计角度来看,这种布局方式实际上是在定义推荐系统的“触发时机”——它不是在阅读过程中持续存在的干扰元素,而是在阅读结束后才被激活的延伸模块。

当这一位置关系确定之后,后续的问题就变得非常明确:如何在 WordPress 运行时,将 article-index.json 中的关联数据映射为推荐列表,并嵌入到文章内容流中。

4.4 推荐列表生成的理论执行模型

如果把前面的构建过程抽象掉,其实整个推荐系统在运行时阶段所发生的事情非常简单:它并不是在“计算推荐”,而是在“执行一次受约束的数据展开”。

这里的关键点在于,推荐逻辑并没有发生在 WordPress 运行时,而是被提前固化在 semantic-index.jsonarticle-index.json 这一离线阶段完成了。因此运行时所面对的,并不是一个需要推理的问题,而是一个已经被结构化定义好的数据问题。

从模型上看,WordPress 在文章页面加载时,只做三件非常确定的事情:识别当前文章、定位索引节点、展开关联集合。但这三步并不是一个算法流程,而更接近一个“映射链路”。

当前文章 ID 在这里扮演的角色,并不是“输入变量”,而更像是一个“定位锚点”。它唯一的作用,是在一个已经构建好的索引空间中,找到对应的节点。这个节点一旦被定位成功,后续的行为其实就不再具备任何不确定性。

因为在 article-index.json 中,文章之间的关系已经被预先定义为一个静态结构,例如:

article_id → { title, url, related[] }

因此运行时所谓的“推荐生成”,本质上只是把 related 这一字段所指向的 ID 集合重新展开,并映射回可展示的信息结构。这个过程更像是一次“结构还原”,而不是“内容计算”。

这里真正重要的变化在于:系统的复杂性被完全前置了。运行时不再参与任何语义判断、相似度计算或排序决策,它只是忠实执行一个已经被定义好的结构映射关系。

也正因为如此,这套模型的核心不在于“推荐算法设计”,而在于“数据是否已经被结构化为可展开关系”。一旦这一点成立,运行时逻辑就会自然退化为一个非常轻量的读取过程,例如在 PHP 中只是简单的索引访问与数组遍历:

$related_ids = $article_index[$current_post_id]['related'] ?? [];

这一行代码所表达的,其实已经不是“推荐逻辑”,而是“关系展开入口”。

从理论上来说,这种设计的本质是一种典型的预计算驱动模型(precomputed relation model)。它把原本属于运行时的问题,整体迁移到了构建阶段,从而换取运行时的极简性与确定性。

因此,当我们回头看整个推荐系统时,会发现它真正成立的前提并不是 WordPress 或 PHP 的实现能力,而是一个更基础的假设:文章之间的关系可以在离线阶段被完整确定,并以结构化形式稳定保存。

一旦这个假设成立,推荐系统本身就不再是一个“系统”,而更像一个“展开器”——它只负责把已经存在的结构,重新呈现出来。

5 WordPress 运行时推荐系统实现

5.1 本方案的实现本质:仅两段代码

如果把前面几章的内容全部收敛掉,其实这个推荐系统在实现层面并没有想象中复杂。甚至可以说,它刻意被压缩成了一个非常“克制”的形态——整个系统真正参与工作的,只有两段代码。

第一段是离线构建脚本 build_article_index.py,负责把 semantic-index.json 转换为运行时可用的 article-index.json;第二段是 WordPress 侧的 PHP 逻辑,负责在文章页面加载时读取这个索引,并生成最终的推荐列表。

除此之外,没有数据库 JOIN,没有实时相似度计算,也没有任何运行时的语义推理过程。

从实现角度看,这种结构本质上是在做一件很简单的事情:推荐系统不再参与计算,只负责展示结果。

5.2 build_article_index.py

完整代码如下:

import json

# semantic-index.json 文件路径
# 该文件由前置 Python 脚本生成,包含文章语义关系数据
json_file = 'semantic-index.json'

# 输出运行时索引文件(JSON版本)
# WordPress 在运行时将直接读取该文件(路径需要根据实际环境进行修改)
output_file = '/docker/wordpress/html/wp-content/themes/argon-theme-master/cache/article-index.json'

# 读取原始语义数据
# 输入结构为 list,每一项代表一篇文章的语义信息
with open(json_file, 'r', encoding='utf-8') as f:
    json_data = json.load(f)

# 初始化运行时索引结构
# 最终结构为 dict:{ article_id: article_data }
articles_index = {}

# 构建运行时索引结构(以 article_id 为 key)
for article in json_data:

    # 当前文章唯一标识
    article_id = article['id']

    # 构建标准化文章索引结构
    # 注意:related 字段统一转换为 ID 列表
    articles_index[article_id] = {
        'title': article['title'],
        'url': article['url'],

        # related 兼容两种结构:
        # 1. dict(包含 id 字段)
        # 2. 直接为 string/int id
        # 最终统一转换为 string ID 列表
        'related': [
            item['id'] if isinstance(item, dict) else item
            for item in article.get('related', [])
        ]
    }

# 写入运行时索引文件(JSON)
# ensure_ascii=False:保留中文
# indent=2:提高可读性(便于调试)
with open(output_file, 'w', encoding='utf-8') as f:
    json.dump(
        articles_index,
        f,
        ensure_ascii=False,
        indent=2
    )

# 构建完成提示
print("article-index.json 已生成")

脚本保存完成后,可以直接执行生成过程:

python3 build_article_index.py

执行完成后,如果一切正常,终端会输出类似的执行结果:

image.png

此时,就可以在目标目录中看到最终生成的运行时索引文件:
image.png

索引文件生成完成之后,通常会进行两个层面的快速验证,以确保数据在“内容形态”和“结构合法性”两个维度上均符合预期。

首先,可以通过直接查看文件内容的方式,确认索引结构是否已经按预期生成,以我博客argon主题为例:

head -20 /docker/wordpress/html/wp-content/themes/argon-theme-master/cache/article-index.json

如果输出内容能够清晰看到以文章 ID 为 key 的结构,并包含 titleurlrelated 等字段,那么说明索引数据已经正确生成并具备运行时可用性:

image.png

在确认数据结构看起来正常之后,还可以通过 JSON 解析工具进行一次更严格的结构校验,例如:

python3 -m json.tool /docker/wordpress/html/wp-content/themes/argon-theme-master/cache/article-index.json > /dev/null

该步骤不会输出内容,其作用是对整个 JSON 文件进行完整解析。如果没有任何错误信息输出,则说明文件在语法层面完全合法,可以被 WordPress 正常读取:

image.png

5.3 WordPress PHP代码(读取 + 渲染)

下面是我当前在 WordPress 中实际使用的推荐列表实现代码,可以直接放入”Code Snippets”插件或者当前主题的”functions.php”中使用:

function add_recommend_list_after_content(content) {

    if (!is_single() || !in_the_loop() || !is_main_query()) {
        returncontent;
    }

    post_id = get_the_ID();

    // =========================
    // 地图页 / 结构页过滤
    // =========================map_slugs = [
        '/map/',
        '/cloudflaremap/',
        '/aimap/',
        '/roadmap'
    ];

    current_url =_SERVER['REQUEST_URI'] ?? '';

    foreach (map_slugs asslug) {
        if (strpos(current_url,slug) !== false) {
            return content;
        }
    }

    // =========================
    // 读取 article-index.json
    // =========================article_index_file = get_template_directory() . '/cache/article-index.json';

    if (!file_exists(article_index_file)) {
        returncontent;
    }

    article_index = json_decode(
        file_get_contents(article_index_file),
        true
    );

    if (!isset(article_index[post_id])) {
        return content;
    }current = article_index[post_id];

    // =========================
    // 相关文章
    // =========================
    related_ids =current['related'] ?? [];
    related_list = [];

    foreach (related_ids as related_id) {

        if (!isset(article_index[related_id])) {
            continue;
        }related_list[] = [
            'title' => article_index[related_id]['title'],
            'url'   => article_index[related_id]['url']
        ];
    }

    related_list = array_slice(related_list, 0, 5);

    if (empty(related_list)) {
        returncontent;
    }

    // =========================
    // UI
    // =========================
    html = '<div class="post-recommend-wrapper" style="
    width:60%;
    min-width:360px;
    max-width:600px;

    margin:28px 0;
    padding:16px 18px;

    background:#f0f7ff;
    border-left:4px solid #3b82f6;
    border-radius:6px;

    font-size:14px;
    line-height:1.7;
    color:#444;
">';html .= '<div style="
        font-size:13px;
        color:#666;
        margin-bottom:10px;
        font-weight:600;
    ">
        📎 相关文章
    </div>';

    foreach (related_list asitem) {

        html .= '<div style="margin:6px 0;">
            <a href="' . esc_url(item['url']) . '" target="_blank" style="
                color:#1d4ed8;
                text-decoration:none;
                font-weight:500;
            ">
            ' . esc_html(item['title']) . '
            </a>
        </div>';
    }html .= '</div>';

    return content .html;
}

add_filter('the_content', 'add_recommend_list_after_content', 20);

5.4 推荐系统最终效果展示

到目前为止,整个基于语义索引的 WordPress 推荐系统已经完整实现。从数据构建、运行时索引生成,到 WordPress 端的读取与渲染,整条链路已经可以在真实环境中稳定运行。

在实际文章页面中,推荐列表会出现在“内容结构提示”之后、评论区之前,作为正文阅读结束后的自然延伸模块,以我这个系列第一篇文章:”为博客加一层“语义索引”(一):结构设计与构建流程“最后的推荐列表为例:

image.png

可以看到,推荐列表以独立块状结构展示,包含当前文章语义关联的相关阅读内容,并提供直接跳转链接。它不干扰正文阅读过程,而是在阅读结束后提供一个自然的延续路径。

从最终效果来看,这个推荐系统已经不再是一个“功能模块”,而是阅读链路中的一个固定组成部分。

6 总结:一个基于预计算索引的推荐系统

到这里,这套基于语义索引的 WordPress 轻量推荐系统,就算真正完整跑通了。

整个系统其实并不复杂:前置阶段通过 semantic-index.json 整理文章之间的关系,再进一步构建运行时使用的 article-index.json,最后由 WordPress 在页面加载时读取索引并生成推荐列表。

也正因为如此,这套方案的运行时结构非常轻。它不依赖复杂数据库查询,不需要实时计算文章相似度,也不涉及动态语义分析。对于 WordPress 而言,它更像是在“消费”一份提前准备好的结构化关系数据,而不是实时参与推荐计算。但回头再看,这个系统真正重要的地方,其实并不只是“页面底部多了一个相关文章模块”。真正发生变化的是:博客中的文章,第一次开始拥有了一层可以被程序消费的“结构关系”。

在传统博客中,文章更多是彼此独立存在的。它们之间的联系,大多数时候只能依赖分类、标签、搜索或者人工内链来维持一种相对松散的连接。而当 semantic-index.json 出现之后,文章之间的关系开始第一次以结构化数据的形式被真正保存下来。从这一刻开始,文章不再只是单独的页面,而开始逐渐变成一种“内容节点”;而博客本身,也开始具备了一层可以被程序读取、组织和展开的语义结构。

于是,“相关文章推荐”其实只是这层结构最先落地的一种表现形式而已。后续无论是文章级局部知识图谱、主题聚合、站内语义导航,甚至更进一步的博客 RAG,本质上都建立在同一个前提之上:文章之间的关系,已经不再只是作者脑中的理解,而变成了一种真正存在的数据结构。

所以到最后,这个系列真正想解决的问题,其实并不是:“如何给 WordPress 做一个推荐插件”,而是:“博客中的内容关系,能不能被提前整理、保存,并重新参与到博客本身的阅读体验中。”

至少对我来说,答案是可以的。而推荐系统,只是这件事的第一步。


需要额外说明的是:虽然本文整个实现过程是围绕 WordPress 展开的,但从架构角度来看,这套基于 semantic-index.jsonarticle-index.json 的“预计算推荐模型”,其实天然更适合静态站点。因为它的核心思想,本质上就是:把所有语义关系提前离线构建完成,然后在运行时直接读取静态数据。

而静态站点本身就不存在传统意义上的数据库查询、运行时内容计算以及动态渲染压力,因此这种“预生成索引 + 前端直接消费 JSON”的结构,与静态博客的工作方式其实是高度一致的。换句话说,在 WordPress 中,这套系统更像是在“绕开 runtime”;而在静态站中,它反而会变成一种非常自然的默认架构。

例如,在 Hugo、Hexo、Jekyll、Astro、Next.js SSG 等静态博客体系中,完全可以在构建阶段直接生成 article-index.json,然后由前端 JavaScript 在文章页面中读取 JSON 并渲染推荐列表。整个过程甚至不再需要 PHP,运行时也不会产生任何服务端计算行为。

某种意义上,这类方案真正适合的场景,其实并不是传统 CMS,而是:“内容在构建阶段完成组织,运行时只负责展示”的博客体系。

而这也是为什么,后来我会越来越觉得:这种基于 JSON 的“轻语义结构”,虽然看起来并不复杂,但它其实已经开始带有一种“静态知识系统”的味道了。


7 后话:为什么我没有选择现成的”相关文章”插件

其实最开始准备给博客增加“相关文章推荐”功能时,我第一反应并不是自己实现,而是先去研究现成的 WordPress 插件方案。毕竟,“相关文章推荐”本身已经是一个非常成熟的需求,像 YARPP、Contextual Related Posts 这类插件,也已经发展很多年了。但后来真正研究之后,我发现:大多数插件虽然都叫“相关文章推荐”,本质上仍然是在运行时基于标签、分类、关键词等信息做相似度匹配。

这种方案对于普通博客其实已经足够,但对于我现在这种越来越偏“知识脉络型”的内容结构来说,会逐渐出现一个问题:很多真正重要的文章关联,并不一定能够通过关键词准确表达。例如某个主题的延伸、同一问题的不同阶段、某种底层思路在不同领域中的重复出现……这些关系,本质上更接近“内容结构”,而不是简单的关键词重合。

这也是为什么,后来我最终选择直接基于 semantic-index.json 来构建推荐系统。因为在这种模型里,“related” 不再是运行时动态猜测出来的结果,而是在前面的语义整理阶段,就已经提前确定好的内容关系。

换句话说:传统插件:运行时推断“什么可能相关”;semantic-index:构建阶段提前确定“什么真正相关”。这两种思路,在结果稳定性和可控性上,其实差别非常大。

与此同时,我现在也越来越谨慎给 WordPress 增加重型插件。因为很多插件背后往往还伴随着额外数据库查询、运行时动态计算以及不可预测的性能消耗。而相比之下,我现在这套方案在运行阶段几乎没有复杂计算,本质上只是一次 JSON 读取与数组映射。对于个人博客而言,这种“预计算 + 静态索引”的方式,反而更轻,也更稳定。

当然,它的代价也并不是没有:当文章新增、URL 修改或者语义关系调整之后,需要重新执行一次索引构建流程。不过对于低频更新的个人博客来说,这种维护成本其实完全可以接受。

某种意义上,这套系统真正吸引我的地方,并不只是“实现了推荐功能”,而是:整个推荐逻辑终于开始变得可解释、可控制、可预测。

而这背后,其实也是我后来越来越重视的一件事——博客中的内容关系,不应该只存在于人的理解里,也应该能够被结构化地保存下来。

📚 系列文章:为博客构建“轻量级知识索引”(3 / 7)

📌 内容结构提示:
这篇内容属于「博客知识地图」的一部分,你可以从这里查看完整内容路径: 博客知识地图
查看相关分类·3个匹配
📎 相关文章
分享这篇文章
博客内容均系原创,转载请注明出处!博客的RSS地址为:https://blog.tangwudi.com/feed,欢迎订阅;如有需要,可以加入Telegram群一起讨论问题。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇
       

👋 欢迎来到“无敌的个人博客”

这里主要围绕以下方向展开长期探索:

🧱 个人数字基础设施与博客系统构建
☁️ Cloudflare 与网络架构实践
🧠 AI 与知识系统探索
🛡️ 网络安全与访问优化
🎵 音乐与声音认知
👁️ 认知视角与世界观