1 Introduction
In the first article of this series (see article:Building a “Lightweight Knowledge Index” for a Blog (Part 1): Structure Design and Build Process), we have converged the entire knowledge index system into a clear structural model: from post fetching and content organization to relationship building and result output, forming a complete data processing pipeline.
However, this description itself still remains at the “structural layer”—it solves “what parts the system consists of”, rather than “how these parts actually run”.
Therefore, in this article, what we need to do is to further implement the structural design from the previous article into a script system that can actually run. However, it should be noted that this step is not just “translating the process into code”. More precisely, it is a reorganization from an execution perspective: we no longer understand the system in terms of “functional phases”, but begin to organize the entire implementation in terms of “function call chains” and “data flow processes”.
From this perspective, the phases defined in the previous article, such as data fetching, structural organization, semantic generation, and relationship calculation, will naturally correspond to several core steps in the script. They are no longer just conceptual divisions, but will be completed sequentially in the entire execution chain in the form of specific functions.
It must be emphasized that the goal of this part of the implementation is not to pursue complex engineering design or over-optimization, but to prioritize one thing:The entire process can run stably and clearly. Therefore, in terms of implementation, we will try to adopt a direct, readable, and easy-to-debug structure, without introducing too many complex mechanisms prematurely.
In a sense, what this article actually accomplishes is a transition “from structure to execution”: turning a logically valid system into a data processing workflow that can run continuously.
2 Script Running Environment and Initialization
Before diving into the script logic, it is first necessary to clarify one point: the running environment of the semantic index script does not rely on complex frameworks or special runtimes; its entire execution foundation is essentially just a standard Linux Python environment. Therefore, the goal of this step is not to “build a system”, but to converge the production environment from a “usable operating system state” into a “stable running environment for executable Python scripts”.
Here, Debian Linux (Debian 11) is used as an example for illustration. This is a typical server environment characterized by a stable structure, clear dependencies, and default provision of basic Python running capabilities.
Python Running Environment Confirmation
In most basic installations of Debian 11, the system already comes with a Python 3.x interpreter. First, you need to confirm whether the current environment has usable Python execution capabilities:
python3 --version
If it returns something like Python 3.9.x version information, it indicates that the Python runtime already exists, and you can proceed directly to the next step.
Package Management Tool (pip) Confirmation
The dependency management of Python scripts relies on the pip tool, so you need to confirm whether pip is available:
python3 -m pip --version
If the system prompts that pip does not exist, it needs to be installed. In Debian systems, this can be done through the official package manager:
apt update
apt install python3-pip -y
apt install python3-pip -y
After installation, confirm again whether pip is available.
Running Dependency Installation requests The external dependencies of this script are very limited, currently only relying on
python3 -m pip install requests
python3 -m pip install requests
python3 -c "import requests; print(requests.__version__)"
python3 -c "import requests; print(requests.__version__)"
If no error is reported, it means the dependency environment is ready.
Minimal Verification of the Running Environment
- Before entering the full script execution, two key capabilities need to be confirmed:
- Python can execute scripts normally
HTTP requests can access the WordPress REST APIhttp://127.0.0.1/This can be verified through a minimal call (assuming here that the local access address of WordPress is
python3 -c "import requests; print(requests.get('http://127.0.0.1/wp-json').status_code)"
If an HTTP status code (such as 200 or 301/302) is returned, it indicates that the local WordPress API is accessible:

Convergence of Environment State
After completing the above steps, the production environment has actually converged from a “generic Linux system” into an execution environment with the following capabilities:
- Python 3 runtime is available
- pip dependency management is available
- HTTP request capability is normal
- Local WordPress API is accessible
In other words, the system has met all the preconditions for running the semantic indexing script. After this stage, the environment initialization task is complete, and what follows is no longer “preparation,” but the step-by-step debugging and layer-by-layer construction process of the script itself.
3 Construction of Semantic Index
1 Overall Structure of the Script
Before starting the specific implementation, let's take a look at the overall structure of this script.
Functionally, this script is not complex. What it does is essentially chain together the several stages broken down in Chapter 6 in order, forming a complete data processing pipeline. The difference is that this time, these steps are no longer just conceptual divisions, but are concretely organized into executable logic.
Expressed in a more intuitive way, this script can be roughly understood as having the following structure:
fetch_posts()
→ build_articles()
→ generate_semantic_info()
→ compute_related()
→ write_json()
These five steps correspond to different stages in the overall process:
First is fetch_posts(), which is used to fetch raw post data from WordPress. The output of this step is a set of unprocessed post records, which still retain the structure returned by the REST API.
Next is build_articles(), which organizes the raw data into a unified internal structure. In this stage, the basic fields of the posts are extracted and organized into the format required for subsequent processing.
On this basis,generate_semantic_info() is responsible for generating semantically relevant information, such as summaries and keywords. After this step, each post has semantic fields that can participate in calculations.
Subsequently, compute_related(), which is the most core step of the entire process. Here, the script compares posts with each other based on existing semantic information and generates corresponding relationships.
Finally, write_json(), which aggregates all processing results and outputs them as the final JSON file.
In terms of execution, these steps are executed in a strictly sequential manner. The output of the previous stage directly serves as the input for the next stage until the final result is generated. The entire process does not depend on any external state, nor does it require intermediate persistence; all data is processed during the script's execution.
From a data perspective, this process can be understood as a gradual “structure enrichment” process: starting with only raw content, then gradually adding basic fields, semantic information, and relationships, and finally forming complete data records.
Because of this design, the script itself does not require complex control logic. It is more like a linear processing pipeline, where each step is only responsible for a relatively independent task, while the overall complexity is distributed across the various stages.
In the following sections, we will follow this structure, starting with data retrieval, to gradually complete the specific implementation of each step.
2 Fetching Post Data (fetch_posts)
In the script, the first step is to fetch all post data from WordPress. The implementation corresponding to this process is a simple paginated request: starting from the first page, calling the REST API page by page until all posts are retrieved. Since the API itself returns paginated results, a loop must be used to accumulate the data from each page.
At the code level, this step can be directly implemented as an independent function:
import requests
BASE_URL = "http://127.0.0.1"
def fetch_posts(base_url):
posts = []
page = 1
total_pages = None
while True:
url = f"{base_url}/wp-json/wp/v2/posts?page={page}&per_page=100"
resp = requests.get(url)
if resp.status_code != 200:
raise Exception(f"Request failed at page {page}, status: {resp.status_code}")
data = resp.json()
# 第一次请求时获取总页数
if total_pages is None:
total_pages = int(resp.headers.get("X-WP-TotalPages", 1))
print(f"Total pages: {total_pages}")
posts.extend(data)
print(f"Fetched page {page}, total posts: {len(posts)}")
if page >= total_pages:
break
page += 1
return posts
if __name__ == "__main__":
posts = fetch_posts(BASE_URL)
print(f"\nTotal posts fetched: {len(posts)}")
The logic here is relatively straightforward:
- page controls the current request page number
- Each request returns one page of posts
- Append the results to the posts list
- Get the total number of pages (X-WP-TotalPages) in the first request, and use this as the termination condition for the traversal
After the function execution is complete, posts will contain all the post data of the current blog. Each item is the raw structure returned by the REST API, without any processing yet.
Represented in a simplified structure, the data at this point is roughly as follows:
{
“id”: 123,
“title”: { “rendered”: “文章标题” },
“link”: “http://127.0.0.1/post”,
“content”: { “rendered”: “HTML正文” },
“tags”: [1, 2, 3]
}
As can be seen, the data here still retains the raw structure of the WordPress REST API, with various fields scattered across different levels, without any organization or processing.
Up to this point, the script has completed the first step of “data takeover”: obtaining the complete collection of posts from WordPress. Subsequent processing will no longer depend on the WordPress API, but will instead gradually complete structural transformation and semantic computation on top of this data.
In actual operation, the script will fetch post data page by page based on the returned pagination information, and finally converge into a complete collection of posts. Taking my blog as an example, the final number of posts retrieved is 237:

This is consistent with the actual number of posts currently on the blog:

This indicates that both the pagination logic and the termination condition have taken effect correctly.
3 Building the Basic Structure (build_articles)
After obtaining the raw post data, the next step is to organize this data into a unified internal structure.
The content returned by the REST API is inherently oriented toward page display, with different fields scattered across different levels. If subsequent processing is performed directly on this basis, it will add unnecessary complexity. Therefore, before entering semantic processing, a structural “convergence” must first be completed: extracting the fields that will be used later and organizing them into a unified format.
This process can be completed through a simple conversion function:
def build_articles(posts):
articles = []
for post in posts:
article = {
"id": str(post.get("id", "")),
"title": post.get("title", {}).get("rendered", ""),
"url": post.get("link", ""),
"content": post.get("content", {}).get("rendered", ""),
"tags": post.get("tags", [])
}
articles.append(article)
return articles
articles = []
for post in posts:
article = {
{
"id": "123",
"title": "\u6587\u7ae0\u6807\u9898",
"url": "https:\/\/example.com\/post",
"content": "<p>HTML Body<\/p>",
"tags": [
1,
2,
3
]
}
“id”: str(post.get("id", "")),
"title": post.get("title", {}).get("rendered", ""),
”url”: post.get("link", ""),

"content": post.get("content", {}).get("rendered", ""),
4 Generating Semantic Information (generate_semantic_info)
"tags": post.get("tags", [])
}
import re
import html
def clean_html(raw_html):
# 去标签
text = re.sub('<[^<]+?>', '', raw_html or "")
# 反转 HTML 实体( → 空格)
text = html.unescape(text)
# 压缩空白(多个空格/换行 → 一个空格)
text = re.sub(r'\s+', ' ', text)
return text.strip()
def generate_semantic_info(articles):
for article in articles:
content = article.get("content", "")
# 清洗 HTML
text = clean_html(content)
# 生成摘要(保证是干净文本)
summary = f"{article['title']}\n{text[300:600]}"
# 关键词(暂时复用 tags)
keywords = article.get("tags", [])
article["summary"] = summary
article["keywords"] = keywords
return articles
return articles
The processing here is very straightforward: iterate through each record in posts, extract the fields needed for subsequent use, and reorganize them into a new data structure.
It is worth noting that since the data structure returned by the WordPress REST API is inherently nested, both default values and structural safety must be considered when extracting fields. For example, fields like title and content need to be accessed through multi-level get methods to avoid exceptions caused by incomplete data. summary After completing this conversion, the shape of the data will undergo a noticeable change. If represented by a simplified example, the structure at this point is roughly as follows:
summary = f"{article['title']}\n{text[:2000]}"
Up to this point, what the script has completed is an organization at the structural level: the data source is still WordPress, but the data shape has been converted into an internally unified model. This structure does not change the original content, but will serve as the standard input for subsequent semantic processing and correlation calculations. semantic-index.json After the execution of the current script containing the "fetch_posts and build_articles" parts is complete, the script will additionally output a sample article to quickly verify whether the structural conversion is correct. This output is only for debugging and verification and does not represent the complete dataset; it is merely the first record extracted from the articles list (the latest post of the current blog) to check if the fields have been standardized as expected, such as whether fields like id, title, url, and content have been correctly extracted from WordPress's nested structure: summary The sample article seen in the CLI in the image above is essentially a structured snapshot of the processing results at this stage, used to confirm whether the build_articles stage was executed successfully.
After completing the construction of the basic structure, the next step is to supplement each article with semantic information that can participate in calculations. In the current implementation, this step mainly consists of two parts: generating a summary and organizing keywords. They do not replace the original content but are appended to the existing structure as a more compact representation.
The corresponding implementation can be written as an independent processing function:
import re
import html
def clean_html(raw_html):
# Strip tags
{
"id": "123",
"title": "\u6587\u7ae0\u6807\u9898",
"url": "https:\/\/example.com\/post",
"content": "<p>HTML Body<\/p>",
"tags": [
1,
2,
3
],
"summary": "\u8fd9\u662f\u4ece\u6e05\u6d17\u540e\u7684\u6b63\u6587\u4e2d\u622a\u53d6\u7684\u4e00\u6bb5\u6587\u672c...",
"keywords": [
1,
2,
3
]
}
As can be seen, compared to the previous stage, the change here does not lie in the structure itself, but in the addition of new semantic fields on top of the original structure. These fields do not attempt to “understand” the article content, but rather provide a more compact, computable representation, allowing the subsequent comparison process to be carried out independently of the full text.
Up to this point, each article already meets the basic conditions for participating in relationship calculations. The next step will build the relationships between articles based on this semantic information.
After the current script (which includes three parts: ”fetch_posts” + ”build_articles” + ”generate_semantic_info") finishes execution, the script will also output a sample data record to verify whether the semantic information is generated as expected:

Unlike the previous stage, the focus of the output here is no longer the structure itself, but whether the newly added semantic fields are usable. For example, it can be observed through the sample article in the CLI:
- The summary field no longer contains HTML tags, but is instead cleaned, continuous text;
- Entity characters in the original content (such as ) have been restored;
- Extra whitespace and line breaks are compressed, making the summary a readable piece of natural language;
- The keywords field has been populated for subsequent correlation calculations.
This output can be understood as a snapshot of the results from the semantic processing stage: it does not represent the final output file, but is used to confirm whether the conversion of “content → semantic representation” has been completed.
After confirming that the output of this stage is stable, the article data meets the basic conditions for participating in relationship calculations.
After completing the generation of summaries and keywords, each article already possesses the basic semantic information for comparison. The next step is to calculate the correlation relationships between articles on top of these structured fields and write the results into "keywords": ["...", "..."] fields.
The overall implementation can be broken down into a simple calculation process: using the current article as a benchmark, compare it with other articles one by one; based on multiple fields (such as title、summary、tags) calculate similarity scores; sort based on the scores, and select the top few most relevant articles as the result.
Unlike the previous stage, the similarity calculation here no longer relies solely on a single field, but instead combines information from different sources for evaluation. Each field provides a different type of “semantic signal,” which together determine the closeness between articles.
In terms of implementation, we can first define some basic functions to handle similarity calculations between texts and sets:
import re
def tokenize(text):
return set(re.findall(r'\w+', text.lower()))
def jaccard(set_a, set_b):
if not set_a or not set_b:
return 0.0
return len(set_a & set_b) / len(set_a | set_b)
def tokenize(text):
def compute_score(a, b):
# 标签相似度
tags_a = set(a.get("keywords", []))
tags_b = set(b.get("keywords", []))
tag_score = jaccard(tags_a, tags_b)
# summary 相似度
summary_a = tokenize(a.get("summary", ""))
summary_b = tokenize(b.get("summary", ""))
summary_score = jaccard(summary_a, summary_b)
# title 相似度
title_a = tokenize(a.get("title", ""))
title_b = tokenize(b.get("title", ""))
title_score = jaccard(title_a, title_b)
# 加权融合
score = (
0.5 * summary_score +
0.3 * tag_score +
0.2 * title_score
)
return score
def jaccard(set_a, set_b):summary if not set_a or not set_b:tags return 0.0title return len(set_a & set_b) / len(set_a | set_b)
On this basis, a multi-field similarity calculation function can be defined:
def compute_related(articles, top_n=5):
for article in articles:
related = []
for other in articles:
if article["id"] == other["id"]:
continue
score = compute_score(article, other)
if score > 0:
related.append({
"id": other["id"],
"score": round(score, 3)
})
# 按 score 排序并取前 N 条
related.sort(key=lambda x: x["score"], reverse=True)
article["related"] = related[:top_n]
return articles
The processing logic here still remains within a controllable and intuitive range:
- For each article, traverse all other articles
- Calculate similarity scores based on multiple fields
- Filter out irrelevant items (score is 0)
- Sort by score and keep the top few items
After completing this process, the structure of the data will change once again. Each article will no longer just contain its own information, but will also be accompanied by a set of related content. For example:
{
"id": "123",
"title": "文章标题",
"url": "...",
"summary": "...",
"keywords": [1, 2, 3],
"related": [
{ "id": "456", "score": 0.812 },
{ "id": "789", "score": 0.637 }
]
}
Up to this point, the data has transformed from an “independent collection of content” into a set of structured records with relationships. Each article establishes a connection with other content through the "keywords": ["...", "..."] field.
This step does not pursue complex semantic understanding, but rather generates a stable and sortable set of association results through a combination of multiple signals, while keeping the implementation simple and controllable. For this current layer of structure, such processing is already sufficient to support the subsequent recommendation and display logic.
The final output sample result shows the structural state of a complete article, which not only contains basic information (id, title, summary, keywords) but also includes a list of related articles calculated based on semantics:

As you can see, at this point, each article is no longer an isolated data unit, but is embedded in a collection with a local relational structure. Each "keywords": ["...", "..."] record represents the “neighboring node” of the current article in the semantic space.
Compared to relying solely on tags, the introduction of text-based similarity calculation here ensures that the association between articles no longer depends entirely on the existing structure, but can form a supplement at the content level. This also means that even if the tag system is not perfect, the system can still establish a certain degree of semantic relationship based on the body text information.
Up to this point, a preliminary semantic association structure based on content rather than a tag system has been built in the system.
It should be noted that the implementation in this section has a computational complexity of O(n²), meaning that as the number of articles increases, the computational cost will grow quadratically. At the current blog scale, this overhead is completely acceptable, but if the data scale expands further, methods such as inverted indexes or candidate set filtering can be introduced to narrow down the computational scope.
6 Outputting JSON (write_json)
After completing the construction of the association relationships, the data of each article has further evolved from “independent structured records” into a complete unit containing semantic relationships. At this point, the articles in memory is no longer just a temporary calculation result, but a data collection that can be directly consumed by external systems.
The next step is to solidify this entire set of structures into a single JSON file as the final output of the entire semantic indexing system. This process can be completed through a very straightforward serialization operation:
import json
import os
def write_json(articles, output_path=None):
# 默认输出到当前脚本所在目录
if output_path is None:
base_dir = os.path.dirname(os.path.abspath(__file__))
output_path = os.path.join(base_dir, "semantic-index.json")
with open(output_path, "w", encoding="utf-8") as f:
json.dump(articles, f, ensure_ascii=False, indent=2)
print(f"JSON written to: {output_path}")
From an implementation perspective, this step itself is not complex; it is essentially just persisting the in-memory data structure to disk. However, in the context of the entire workflow, its significance lies not in “writing the file” itself, but inthe boundaries of the semantic calculation results being formally fixed.。
Prior to this, all steps (fetch, build, semantic, compute_related) occurred during the calculation process, but at this step, the system's output is converged into a stable data product.
The structure of the output JSON file is roughly as follows:
[
{
"id": "123",
"title": "文章标题",
"url": "https://example.com/post",
"summary": "...",
"keywords": [1, 2, 3],
"related": [
{ "id": "456", "score": 0.812 },
{ "id": "789", "score": 0.635 }
]
}
]
As you can see, each article already contains a complete information hierarchy:
- Basic information (id / title / url)
- Semantic compression result (summary)
- Structural information (keywords)
- Relational information (related)
The essence of this file is no longer an “article list”, but rather astatic representation of a local semantic graph structure calculated based on content.。
7 Complete Script Example
After breaking down the previous sections, we have mastered:
- Fetching articles: Retrieve all articles from the WordPress API.
- Building structured data: Organize into an article list containing ID, title, URL, content, and tags.
- Generating semantic information: Clean HTML, extract summary and keywords.
- Calculating association relationships: Generate based on title, summary, and tags
"keywords": ["...", "..."]If we directly enter the subsequent index construction stage based on this raw structure, it will bring an implicit problem: although the data is complete, it lacks consistent access semantics. Although the fields of each article exist, they are still "loosely organized" at the structural level. - Outputting JSON files
In this section, I will integrate these steps to ultimately form a complete script that can be run directly. After running it, you will get a structured list of articles with association relationships, written to a JSON file at the specified path, which can be directly used for front-end display or recommendation systems.
The characteristics of this script are as follows:
- Full-process coverage: From fetching to processing to output, it can be completed with a single command.
- Clear structure: Each step is encapsulated in a function, making it easy to modify and extend.
- Good readability: Formatted output of JSON files, with Chinese displaying normally.
- Easy to extend: More complex logic can be added to summary generation, keyword extraction, or similarity calculation without rewriting the entire process.
The complete Python script example is as follows:
import requests
import re
import html
import json
import os
# 根据自己实际环境修改
BASE_URL = "http://127.0.0.1"
# -------------------------
# Step 1: 获取文章
# -------------------------
def fetch_posts(base_url):
posts = []
page = 1
total_pages = None
while True:
url = f"{base_url}/wp-json/wp/v2/posts?page={page}&per_page=100"
resp = requests.get(url)
if resp.status_code != 200:
raise Exception(f"Request failed at page {page}, status: {resp.status_code}")
data = resp.json()
if total_pages is None:
total_pages = int(resp.headers.get("X-WP-TotalPages", 1))
print(f"Total pages: {total_pages}")
posts.extend(data)
print(f"Fetched page {page}, total posts: {len(posts)}")
if page >= total_pages:
break
page += 1
return posts
# -------------------------
# Step 2: 构建基础结构
# -------------------------
def build_articles(posts):
articles = []
for post in posts:
article = {
"id": str(post.get("id", "")),
"title": post.get("title", {}).get("rendered", ""),
"url": post.get("link", ""),
"content": post.get("content", {}).get("rendered", ""),
"tags": post.get("tags", [])
}
articles.append(article)
return articles
# -------------------------
# Step 3: HTML 清洗
# -------------------------
def clean_html(raw_html):
text = re.sub('<[^<]+?>', '', raw_html or "")
text = html.unescape(text)
text = re.sub(r'\s+', ' ', text)
return text.strip()
# -------------------------
# Step 4: 生成语义信息
# -------------------------
def generate_semantic_info(articles):
for article in articles:
content = article.get("content", "")
text = clean_html(content)
# 使用“标题 + 正文中段片段”
summary = f"{article['title']}\n{text[300:600]}"
keywords = article.get("tags", [])
article["summary"] = summary
article["keywords"] = keywords
return articles
# -------------------------
# Step 5: 相似度工具
# -------------------------
def tokenize(text):
return set(re.findall(r'\w+', text.lower()))
def jaccard(set_a, set_b):
if not set_a or not set_b:
return 0.0
return len(set_a & set_b) / len(set_a | set_b)
def compute_score(a, b):
# tags
tags_a = set(a.get("keywords", []))
tags_b = set(b.get("keywords", []))
tag_score = jaccard(tags_a, tags_b)
# summary
summary_a = tokenize(a.get("summary", ""))
summary_b = tokenize(b.get("summary", ""))
summary_score = jaccard(summary_a, summary_b)
# title
title_a = tokenize(a.get("title", ""))
title_b = tokenize(b.get("title", ""))
title_score = jaccard(title_a, title_b)
# 加权融合
return (
0.5 * summary_score +
0.3 * tag_score +
0.2 * title_score
)
# -------------------------
# Step 6: 构建关联关系
# -------------------------
def compute_related(articles, top_n=5):
for article in articles:
related = []
for other in articles:
if article["id"] == other["id"]:
continue
score = compute_score(article, other)
if score > 0:
related.append({
"id": other["id"],
"score": round(score, 3)
})
related.sort(key=lambda x: x["score"], reverse=True)
article["related"] = related[:top_n]
return articles
# -------------------------
# Step 7: 输出 JSON
# -------------------------
def write_json(articles, output_path=None):
# 脚本会将文件写入 `output_path` 参数指定的路径,如果未指定,则默认在当前目录生成 `semantic-index.json`
if output_path is None:
base_dir = os.path.dirname(os.path.abspath(__file__))
output_path = os.path.join(base_dir, "semantic-index.json")
with open(output_path, "w", encoding="utf-8") as f:
json.dump(articles, f, ensure_ascii=False, indent=2)
print(f"JSON written to: {output_path}")
# -------------------------
# 主流程
# -------------------------
if __name__ == "__main__":
# Step 1
posts = fetch_posts(BASE_URL)
print(f"\nTotal posts fetched: {len(posts)}")
# Step 2
articles = build_articles(posts)
print(f"Total articles built: {len(articles)}")
# Step 3
articles = generate_semantic_info(articles)
print("Semantic info generated.")
# Step 4
articles = compute_related(articles)
print("Related articles computed.")
# Step 5(新增):写入 JSON
write_json(articles)
# Sample check
print("\nSample article:")
print({
"id": articles[0]["id"],
"title": articles[0]["title"],
"summary": articles[0]["summary"],
"keywords": articles[0]["keywords"],
"related": articles[0]["related"]
})
To give everyone an intuitive feel, in the ”semantic-index.json” file I actually generated, the article ”Awakening of Voice · Foundation (III): Visualization of Interval Structure and Historical Conventions“ corresponding "keywords": ["...", "..."] content is displayed below, as shown in the following figure:

The figure above shows that the most relevant 5 articles(sorted by similarity score from highest to lowest, with IDs 14026、14017、13767、14048、13751) are respectively:
“Awakening of Voice · Foundation (II): From Pitch Names to Numbered Musical Notation”
“Awakening of the Voice · Basics (Part 1): Pitch Structure and Auditory Stability”
“The Awakening of Voice (III) The Misconception of “High Position” and Its Scientific Interpretation”
“The Awakening of Voice (VI) How to Determine Whether You Are a Low, Middle, or High Voice?”
“The Awakening of Voice (II) Misunderstood Voice: The Illusion of High Pitch and the Hallucination of Low Pitch”
As can be seen, the recommendation results are highly relevant to the article topic, indicating that "keywords": ["...", "..."] the accuracy of the function is relatively high. At the same time, this also verifies that the goal of writing the script this time has been successfully achieved: from article acquisition, structured processing, semantic generation, to relationship construction, the logic of the entire process can produce a reliable semantic index, providing a directly usable data foundation for subsequent recommendations or displays.
8 Location and Update of the JSON File
After completing semantic-index.json generation, a practical problem still needs to be solved: where to place this file and how to keep it updated during subsequent use.
By default, the script outputs the file to the current directory, but in actual deployment, the output location is usually adjusted according to the running environment. Common practices include:
- Uploading to object storage and providing access via CDN
- Placing it in the website's static resource directory and deploying it along with the site
- Serving as a local data file on the server side, read by the backend program on demand
The differences between different methods are mainly reflected in access paths, caching strategies, and update mechanisms, but essentially they are all to ensure that this JSON file can be stably accessed by the system.
It should be noted that although semantic-index.json itself is a static file, its content is not permanently fixed. Since this index is calculated and generated based on current blog posts, as long as a new post is published or the content of an existing post changes, the original index actually becomes invalid and needs to be regenerated.
For personal blogs with low update frequency, the simplest way is usually to manually run the script once after publishing a new post; if you want the entire process to be more automated, you can also combine cron jobs or CI/CD pipelines to automatically regenerate the index at fixed intervals and overwrite the old file. From a maintenance perspective, this type of JSON file is closer to a “reproducibly built generation result”: it does not require manual modification, but is automatically recalculated by the script based on the current content.
In addition, during actual use, attention also needs to be paid to the file size. As the number of articles increases,semantic-index.json the size of the file will also gradually grow. For example, the index file generated based on my current blog content has already reached about 6MB:

If the entire file is loaded directly on the frontend, it will not only increase network transmission overhead but also bring additional browser parsing costs.
From the actual running situation, this script itself does not have high requirements for system resources. The entire process mainly consists of sequential requests, text processing, and simple similarity calculations. Under the current data scale, even on a common VPS with a 2-core CPU and 2-3GB RAM, it can run stably. Taking my Chicago VPS as an example, with 3 cores and 4.5G RAM, the script, from start to finish and outputting thesemantic-index.jsonfile, takes only about 20 seconds.
Therefore, for most personal blog scenarios, this solution can already achieve a relatively ideal balance between deployment cost and running overhead.
4 Functional Extension Based on semantic-index.json
After completing semantic-index.json generation, the semantic indexing system of the entire blog has actually just entered the “usable” stage. One thing that needs to be made clear is: this JSON file itself is not the final implementation of a specific function; it is more like an intermediate structure between “content” and “function”, used to support all subsequent semantic-related expansion capabilities.
In other words, the process of the script generating semantic-index.json is essentially completing a “semantic preprocessing” in advance: the content relationships that originally needed to be dynamically calculated at runtime are solidified into a structured result beforehand. What subsequent functions actually need to do is often just reading, filtering, and reorganizing around this result.
Therefore, from the perspective of system structure, the entire process is actually closer to such a relationship:
WordPress 原始文章
↓
语义索引生成脚本
↓
semantic-index.json
↓
不同功能按需提取与加工
↓
最终功能输出
This also means that the focus of subsequent extended features is no longer on “re-understanding the article,” but rather on “how to utilize the existing semantic results.”
For example, for a related articles recommendation feature, the data that actually needs to be used might only be "keywords": ["...", "..."] the first few results in the field; whereas for a content aggregation feature, the focus might be more on keywords the relationships between articles; if it is a search, navigation, or AI summarization feature, it might rely more on summary、title or even the original content fields. Different features focus on different aspects of the data—but they share one thing in common: they are all built upon the same semantic-index.json dataset.
From this perspective, what is truly important about this file is not the “JSON” itself, but rather that it stores a set of pre-calculated semantic relationship results. Because of this, subsequent extended features of the blog typically do not need to traverse the full text again, recalculate article similarity, or re-analyze content during runtime; they only need to read the existing data.
The core value of this approach is essentially the concept of “offline pre-computation”—concentrating complex calculations in the generation phase to minimize runtime costs during actual use. For a personal blog, this is actually crucial because it means that even without a vector database, embedding models, GPUs, or complex AI infrastructure, you can still build a content system with “semantic capabilities.”
Although this approach cannot match a true vector-based semantic system in terms of theoretical expressiveness, in scenarios like personal blogs, it offers a very clear advantage: it is low-cost, highly controllable, easy to maintain, and already capable of solving a wide range of real-world problems.
In many cases, a truly long-term runnable and sustainably maintainable engineering solution often has higher practical value than a “theoretically most advanced” solution.
And this current set based on the semantic-index.json structure is, in essence, a balance struck between complexity, cost, and effectiveness.
5 Summary and Outlook
Looking back, this entire implementation is, in essence, not limited to the implementation of a specific function, but is introducing a new layer of capability to the blog: allowing content that was originally only intended for human reading to begin to have a foundation for being processed and understood by machines. And the implementation method adopted in this article is precisely an engineering realization built around this layer of capability.
Currently, this semantic index adopts a weighted calculation method based on keywords, titles, and abstracts, which is essentially an engineered semantic approximation. It does not attempt to fully understand language, but rather compresses and expresses content at a controllable cost, and establishes relationships between articles on this basis. This approach can be seen as a compromise or even a transitional solution: when the data scale is small or the tag system is relatively stable, it can already provide sufficiently usable results while keeping the implementation simple, explainable, and easy to debug.
In a sense, this is also where the greatest value of this solution lies: it is not pursuing the “theoretically strongest” semantic capability, but is trying to find a balance point that is more suitable for personal blog scenarios. In many technical discussions, “semantic processing” almost naturally points to embeddings, vector databases, and large model-related systems. But for personal blogs, although such solutions have stronger capabilities, they often mean higher complexity: requiring additional model runtime environments, more complex data storage structures, and ongoing subsequent maintenance costs.
And the current solution actually provides another line of thought: without introducing heavy systems, through reasonable structural design and offline pre-computation mechanisms, a layer of semantic relationship network with practical value can also be established. The core advantage of this approach is not just “simple implementation”, but that it can enter the stage of “semantic content organization” at an extremely low cost.
For many personal blogs, what is truly lacking is often not the most advanced semantic model, but a layer of basic structure that allows content to establish connections with each other. And the current solution precisely solves this problem: it allows originally isolated articles to begin forming relationships, gradually evolving the blog from a “collection of content stacked by time” into a content system with internal connection capabilities.
Of course, from a longer-term perspective, this is not the end point. As the number of articles grows and the requirements for semantic precision increase, this rule-and-field-combination-based approach will eventually approach its limit. The more natural direction for future evolution will still be vector representation (embedding): mapping text to a higher-dimensional semantic space and measuring the relationship between content through distance rather than rules, thereby obtaining finer-grained and more generalizable semantic expressions.
But even so, this current layer of structure will not lose its value because of this. Because whether embedding is introduced in the future or not, the article structure, semantic fields, correlation relationships, and the index system itself remain important foundations of the entire system. In other words, the current implementation is not a “wrong route”, but rather like a sustainably evolving middle layer: it can both play a practical role directly at the current stage and reserve enough space for subsequent upgrades.
From the perspective of the implementation process, this practice itself actually validates one thing: in many scenarios, it is not always necessary to introduce complex models or heavy systems from the very beginning. Through reasonable structural design and appropriate engineering methods, a sufficiently usable set of semantic approximations can also be obtained at a lower cost, and gradually evolve as demand grows.
This path of “starting from usability and then gradually approaching higher semantic capabilities” is often more realistic and sustainable for personal projects than pursuing a complete large model system from the start.
And this is precisely the state that this attempt truly wants to achieve.