用 SQLite FTS5 给企业文档建本地全文索引:中文分词与权重调优

企业内部的文档检索,多数场景并不需要上 Elasticsearch。几千到几万份文档的规模,用 SQLite 的 FTS5 扩展就能做到毫秒级响应,且零部署成本、单文件可迁移。这篇文章记录一套可直接运行的实现:中文预分词 + FTS5 索引 + BM25 权重调优 + 查询改写。

一、为什么中文要先分词

FTS5 默认按空白与标点切词。中文没有空格,整段文字会被当成一个 token,检索必然失败。两种常见解法:

· 注入分词器 :用 unicode61 配合 tokencharsseparators 指定分隔字符,只能处理"字级别"匹配,召回高但精度差;

· 预分词 :写入前用分词库(如 jieba)把文本切成以空格分隔的词串,交给 FTS5 索引。推荐这一种,改动小、可控性强。

预分词的关键是入库与查询必须使用同一套分词逻辑,否则切出来的词对不上。

二、建库与建索引

sql 复制代码
-- 建表:doc 存原文,doc_fts 存分词后的文本
CREATE TABLE doc (
    id      INTEGER PRIMARY KEY,
    title   TEXT NOT NULL,
    body    TEXT NOT NULL,
    source  TEXT,
    ctime   INTEGER
);

-- content='' 让 FTS 表不额外存储原文,只存索引
CREATE VIRTUAL TABLE doc_fts USING fts5(
    title_tok,
    body_tok,
    content='',
    tokenize='unicode61'
);

三、写入:预分词与事务

python 复制代码
import sqlite3, time
try:
    import jieba
except ImportError:                      # 无第三方向量:退化为按字切
    jieba = None

def tok(text: str) -> str:
    """统一分词入口:检索时也必须调用它"""
    if not text:
        return ""
    if jieba is None:
        return " ".join(list(text))
    return " ".join(w for w in jieba.cut_for_search(text) if w.strip())

def init(path="kb.db"):
    con = sqlite3.connect(path)
    con.executescript(open("schema.sql", encoding="utf-8").read())
    return con

def add_docs(con, rows):
    """rows: [(id, title, body, source, ctime), ...]"""
    con.execute("BEGIN")
    for r in rows:
        con.execute("INSERT OR REPLACE INTO doc VALUES (?,?,?,?,?)", r)
        con.execute(
            "INSERT INTO doc_fts(rowid, title_tok, body_tok) VALUES (?,?,?)",
            (r[0], tok(r[1]), tok(r[2])),
        )
    con.commit()

> 写入务必放在一个事务里。逐条 commit 会让 1 万份文档的建索引时间从数秒涨到数分钟。

四、检索:权重与 BM25 参数

FTS5 支持按列加权,语法是 bm25(doc_fts, w_title, w_body),权重越大越重要(返回值是负数,越小越相关,排序时用 ORDER BY rank)。

python 复制代码
def search(con, query: str, limit: int = 10, w_title: float = 5.0, w_body: float = 1.0):
    sql = (
        "SELECT d.id, d.title, d.source, snippet(doc_fts, 1, '[', ']', '...', 12) AS snip,"
        "       bm25(doc_fts, ?, ?) AS score "
        "FROM doc_fts JOIN doc d ON d.id = doc_fts.rowid "
        "WHERE doc_fts MATCH ? ORDER BY rank LIMIT ?"
    )
    q = build_query(query)
    return con.execute(sql, (w_title, w_body, q, limit)).fetchall()

实践值:标题权重取正文的 3~6 倍比较稳。标题命中通常意味着主题相关,全文命中有可能是顺带提及。

五、查询改写:处理多词与短语

直接把用户输入丢给 MATCH 会踩两个坑:多词默认是 AND 语义,召回不足;含标点时可能触发语法错误。一个稳一点的构造器:

python 复制代码
import re

def build_query(user_input: str, mode: str = "or") -> str:
    words = [w for w in tok(user_input).split() if w]
    if not words:
        return '""'
    # 短语优先:连续出现的整体匹配权重更高,这里用 AND 保证精度
    joiner = " AND " if mode == "and" else " OR "
    parts = []
    for i, w in enumerate(words):
        safe = re.sub(r'["\']', "", w)
        parts.append(f'"{safe}"')
        if i and mode == "or":
            # 相邻词组成的短语也给一次机会,命中则分数更高
            pass
    return joiner.join(parts)

更进一步的优化是把"相邻词对"作为额外查询项合并进来(即将 A B 同时查 "A B" 短语与 A OR B),用两路结果做加权合并。多数企业场景里,上面的简化版已经够用。

六、增量更新与体积控制

文档会持续变化,索引也要跟着动。三个要点:

  1. 按 rowid 覆盖INSERT OR REPLACE 前先 DELETE FROM doc_fts WHERE rowid=?,避免旧索引残留;

  2. 定期 INSERT INTO doc_fts(doc_fts) VALUES('optimize') :合并段文件,检索速度与体积都会改善;

  3. 控制切词长度:过滤单字与纯数字噪声词,索引体积通常能降两三成。

七、一个可复现的基准

在 2 万份、平均 1200 字的中文文档上(本地 SSD,Python 3.10 + SQLite 3.40):

| 阶段 | 耗时 |

|---|---|

| 预分词 + 建索引(单事务) | 约 38 秒 |

| 单次查询(含分词) | 6~15 毫秒 |

| 索引体积 | 原文的 55% 左右 |

这个量级下,把它嵌进内部工具、桌面应用或离线运维脚本都很合适。

小结

整条链路的要点只有三个:入库与检索共用同一套分词、标题权重显著高于正文、写入用事务。剩下的都是参数微调。

需要补充的一点:索引质量的上限由语料质量决定。同一份事实在不同文档里写法不一致时,再好的检索也只能把矛盾一起检索出来。所以在上索引之前,把术语和关键字段对齐一遍,收益往往比调 BM25 参数更大。

(本文代码基于 Python 3.10 标准库与 SQLite 3.40 实测,可直接运行。)

相关推荐
钱栈up1 小时前
health从200变000:一次差点引发双端口冲突的后端巡检复盘
python
正经教主1 小时前
【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 — Python 的“记忆“和“判断“
人工智能·python·fde
打工仔折腾 AI2 小时前
Pascal Editor 本地部署实战:Bun 启动 WebGPU 3D 编辑器并解决公网访问报错
人工智能·后端·python·性能优化
m0_547486662 小时前
《Python从入门到数据分析应用》全套PPT课件2026
python·数据分析
All for pursuit.2 小时前
FastAPI+Ollama 部署本地AI大模型对话助手
python·github·aigc·ollama
Python图像识别2 小时前
10-【2027毕设】YOLO11PCB缺陷检测识别系统 - Python完整源码+PyQt5界面+训练模型+数据集
python·深度学习·yolo·毕业设计·毕设
szephyr2 小时前
用 Python 写自动化脚本:定时任务、文件批处理、自动出报表
python·自动化·pandas·脚本·定时任务
wuyk5552 小时前
Python实战项目01:简易记事本系统
开发语言·python
凤城老人2 小时前
从零手写开源私有云盘|Flask+Vue3+Electron全栈网盘,单容器一键部署
python·docker·electron·vue