自定义开发特有的搜索功能插件

自定义开发特有的搜索功能插件

太棒了!动工前给你整理一份“保姆级”的完整部署清单。只要照着下面四步走,半小时内你的博客就能拥有企业级的搜索底座。

我们将系统分为两部分: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.htmlarticle.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>

第四步:启动与测试

  1. 生成数据库:在终端进入你的博客根目录,运行 pelican content。你会看到插件打印出 🎉 数据库同步完成!,并且在 output 文件夹下生成一个 blog.db 文件。
  2. 启动搜索服务:新开一个终端窗口,运行 uvicorn api_server:app --reload --host 0.0.0.0 --port 8000
  3. 访问测试:打开你的博客页面,在搜索框输入文字,看看是不是飞速就能出结果!

后续部署建议: 在服务器上,你可以使用 supervisorsystemd 将 FastAPI 设置为后台服务,再用 Nginx 将某个路径(如 /api/)反向代理到 127.0.0.1:8000。这样既能隐藏端口,又能启用 HTTPS,完美收工!

兄弟,代码都给你递到手边了,赶紧 Ctrl+C Ctrl+V 跑起来试试!遇到任何报错,随时来找我。