自定义开发特有的搜索功能插件
太棒了!动工前给你整理一份“保姆级”的完整部署清单。只要照着下面四步走,半小时内你的博客就能拥有企业级的搜索底座。
我们将系统分为两部分:Pelican 插件(负责导出数据) 和 FastAPI 服务(提供搜索接口)。
第一步:创建并配置 Pelican 插件
Pelican 加载插件需要一个特定的目录结构。请在你的 Pelican 博客根目录下(与 pelicanconf.py 同级)创建以下文件夹和文件:
your_blog_root/
├── content/
├── output/
├── plugins/ <-- 1. 新建文件夹
│ └── save_to_sqlite/ <-- 2. 新建文件夹
│ ├── __init__.py <-- 3. 必须的文件 (可以为空)
│ └── save_to_sqlite.py <-- 4. 我们的插件代码放这里
├── pelicanconf.py
└── publishconf.py
将以下完善后的插件代码复制到 plugins/save_to_sqlite/save_to_sqlite.py 中:
import sqlite3
import os
from pelican import signals
def save_articles_to_sqlite(sender, writer):
"""将 Pelican 文章同步到 SQLite 数据库,并处理增删改查逻辑"""
output_path = sender.output_path
db_path = os.path.join(output_path, 'blog.db')
os.makedirs(os.path.dirname(db_path), exist_ok=True)
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 1. 确保表结构存在
cursor.execute('''
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT UNIQUE NOT NULL,
slug TEXT UNIQUE NOT NULL,
content TEXT,
tags TEXT,
category TEXT,
date TEXT
)
''')
articles = sender.context.get('articles', [])
current_slugs = set()
print(f"🔍 开始扫描 {len(articles)} 篇文章...")
for article in articles:
current_slug = getattr(article, 'slug', None)
current_url = getattr(article, 'url', None)
if not current_slug or not current_url:
continue
current_slugs.add(current_slug)
# 只截取前500字符作为摘要存入数据库,兼顾搜索体验和性能
clean_content = article.content[:500]
tags_str = ','.join([tag.name for tag in getattr(article, 'tags', [])])
try:
# 核心:UPSERT 逻辑 (存在则更新,不存在则插入)
cursor.execute('''
INSERT INTO articles (title, url, slug, content, tags, category, date)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(slug) DO UPDATE SET
title=excluded.title,
url=excluded.url,
content=excluded.content,
tags=excluded.tags,
category=excluded.category,
date=excluded.date
''', (
article.title, current_url, current_slug,
clean_content, tags_str,
getattr(article, 'category', ''),
str(getattr(article, 'date', ''))
))
except Exception as e:
print(f"❌ 处理文章失败 {article.source_path}: {e}")
# 2. 垃圾回收 (GC):清理数据库中已物理删除的文章
cursor.execute('SELECT slug FROM articles')
old_slugs = {row[0] for row in cursor.fetchall()}
slugs_to_delete = old_slugs - current_slugs
if slugs_to_delete:
print(f"🧹 发现 {len(slugs_to_delete)} 篇已删除文章,正在清理...")
placeholders = ', '.join(['?'] * len(slugs_to_delete))
cursor.execute(f'DELETE FROM articles WHERE slug IN ({placeholders})', list(slugs_to_delete))
print(f"✅ 成功清理了 {cursor.rowcount} 条无效记录。")
conn.commit()
conn.close()
print("🎉 数据库同步完成!")
# 注册 Pelican 构建完成的信号
signals.finalized.connect(save_articles_to_sqlite)
🔴 关键:激活插件
打开你的 pelicanconf.py,添加或修改以下配置,告诉 Pelican 去哪里找插件以及加载它:
# pelicanconf.py
# 指向你存放自定义插件的文件夹
PLUGIN_PATHS = ["plugins"]
# 加载我们的插件 (名称必须与文件夹名称一致)
PLUGINS = [
'save_to_sqlite',
# 这里可以放其他你需要的插件
]
第二步:编写 FastAPI 搜索接口
在你的博客根目录(或者任何你觉得方便的位置),新建一个文件叫 api_server.py。这个服务将独立于 Pelican 运行。
from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
import sqlite3
import os
app = FastAPI()
# 允许跨域,方便本地开发。生产环境建议限制为你的域名
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注意:这里的 DB_PATH 需要指向 Pelican 生成 output 的绝对路径
# 示例:假设 api_server.py 在博客根目录,output 在根目录下的 output 文件夹
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, 'output', 'blog.db')
@app.get("/api/search")
def search_articles(q: str = Query(..., min_length=1)):
"""高性能搜索接口"""
if not os.path.exists(DB_PATH):
return {"error": "数据库未找到,请先运行 Pelican 构建"}
conn = sqlite3.connect(DB_PATH)
# 让返回结果直接转化为字典列表,方便 JSON 序列化
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 使用参数化查询防止 SQL 注入
cursor.execute("""
SELECT title, url, content, tags
FROM articles
WHERE title LIKE ? OR content LIKE ?
ORDER BY date DESC
LIMIT 20
""", (f'%{q}%', f'%{q}%'))
results = cursor.fetchall()
conn.close()
return [dict(row) for row in results]
if __name__ == "__main__":
import uvicorn
# 监听在所有网卡上,端口 8000
uvicorn.run(app, host="0.0.0.0", port=8000)
第三步:前端调用示例 (原生 JS)
你需要在你的 Pelican 主题目录下(通常是 theme/your_theme/templates/base.html 或 article.html),加入一个搜索框和这段 JS 逻辑:
<!-- 放在你想要显示搜索框的地方 -->
<input type="text" id="search-input" placeholder="输入关键词搜索..." style="padding: 5px; width: 200px;">
<div id="search-results" style="margin-top: 10px; border: 1px solid #eee; padding: 10px; display: none;"></div>
<script>
document.getElementById('search-input').addEventListener('input', async (e) => {
const query = e.target.value;
const resultsDiv = document.getElementById('search-results');
if (query.length < 2) {
resultsDiv.style.display = 'none';
return;
}
try {
// 如果你的 API 和博客在同一个域名下,可以直接用相对路径 '/api/search'
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
resultsDiv.innerHTML = '';
resultsDiv.style.display = 'block';
if (!data || data.length === 0) {
resultsDiv.innerHTML = '<p>暂无结果</p>';
return;
}
data.forEach(item => {
const div = document.createElement('div');
div.style.marginBottom = '10px';
div.innerHTML = `
<h4 style="margin: 0;"><a href="/${item.url}">${item.title}</a></h4>
<p style="font-size: 12px; color: #666; margin: 5px 0 0 0;">${item.content}...</p>
`;
resultsDiv.appendChild(div);
});
} catch (error) {
console.error('搜索请求失败:', error);
}
});
</script>
第四步:启动与测试
- 生成数据库:在终端进入你的博客根目录,运行
pelican content。你会看到插件打印出🎉 数据库同步完成!,并且在output文件夹下生成一个blog.db文件。 - 启动搜索服务:新开一个终端窗口,运行
uvicorn api_server:app --reload --host 0.0.0.0 --port 8000。 - 访问测试:打开你的博客页面,在搜索框输入文字,看看是不是飞速就能出结果!
后续部署建议:
在服务器上,你可以使用 supervisor 或 systemd 将 FastAPI 设置为后台服务,再用 Nginx 将某个路径(如 /api/)反向代理到 127.0.0.1:8000。这样既能隐藏端口,又能启用 HTTPS,完美收工!
兄弟,代码都给你递到手边了,赶紧 Ctrl+C Ctrl+V 跑起来试试!遇到任何报错,随时来找我。