不再怕资料乱:Python标准库 sqlite3 FTS5 实现全文搜索,速度出奇快

2026-09-08 0 410

我桌面上常年乱糟糟地堆着各种txt、md、csv,真到想找某个细节的时候,靠系统搜索要么慢得像蜗牛,要么搜出来的结果排不到前面。后来我花了半个小时,用Python自带的sqlite3模块做成了一个本地全文检索工具。

虽然它没法跟Elasticsearch比,但胜在零依赖、部署简单,几百MB的文本建立索引以后,查询基本在毫秒以内。这篇文章就把具体做法一步步拆解给你。

思路比代码更重要

我一开始想用os.walk遍历文件,再用正则匹配每个文件内容。文件少的时候还好,文件一多,每次都把所有文件完整读一遍,不仅内存爆炸,而且重复扫描浪费时间。

所以正确姿势是提前把全部文本内容“灌”进数据库,做成倒排索引。查询时不用再看源文件,只需查索引表,结果自然就快了。

大多数现代系统里都有SQLite,而SQLite自带一个全文搜索扩展叫FTS5。Python从3.6开始,官方预编译的sqlite3库就默认开启了FTS5支持,所以不需要额外装任何东西。

FTS5虚拟表和普通表有什么不一样

普通建表是CREATE TABLE,而全文索引需要使用虚拟表,语法是:

CREATE VIRTUAL TABLE documents USING fts5(id, title, body);

表名后面直接跟列名,各列默认都是可全文搜索的文本类型。你可以插入和查询,像操作一张普通表。FTS5会自动帮你维护一个倒排索引,内部把文本切分成一个个词条。

FTS5还内置了排名算法,可以按相关度排序。这比我们手动用like要专业得多。

用Python建库的完整步骤

第一步:连接到SQLite数据库。

import sqlite3

conn = sqlite3.connect("search.db")
cursor = conn.cursor()
cursor.execute("CREATE VIRTUAL TABLE IF NOT EXISTS docs USING fts5(filename, content)")
conn.commit()

启动后用这个虚拟表,插入一行代表一个文件。比如我索引一个Markdown文件,就把文件名和全部内容放进去:

def add_document(filename, content):
    cursor.execute("INSERT INTO docs(filename, content) VALUES (?, ?)", (filename, content))
    conn.commit()

这里有一个隐藏坑:content不能是bytes类型,FTS5主要针对文本。如果你的文件是二进制(比如PDF、Word),需要先抽取文本再插入。

最基础的查询和BM25排序

FTS5的查询可以用MATCH关键字,查找包含“Python”和“异步”的文档:

SELECT filename, snippet(docs) FROM docs WHERE docs MATCH 'Python AND 异步'

在FTS5里,空格默认是AND的意思。如果查多个关键词可以写成Python 异步。加上ORDER BY rank就是按相关度排序。不过需要说明的是,在FTS5里,rank其实是隐藏列,直接可以用:

SELECT filename, rank FROM docs WHERE docs MATCH 'Python' ORDER BY rank LIMIT 10

一个可以直接运行的整目录扫描器

说了一堆理论,直接上代码。这里完整的脚本,能遍历指定目录下的txt和md文件,建立索引,并支持用命令行搜索。

import sqlite3
import os
import sys

DB_NAME = "file_index.db"

def init_db():
    conn = sqlite3.connect(DB_NAME)
    c = conn.cursor()
    c.execute("""
        CREATE VIRTUAL TABLE IF NOT EXISTS docs USING fts5(filename, path, content)
    """)
    return conn

def index_folder(conn, folder_path):
    c = conn.cursor()
    for root, _, files in os.walk(folder_path):
        for f in files:
            if not (f.endswith(".txt") or f.endswith(".md")):
                continue
            full = os.path.join(root, f)
            try:
                with open(full, "r", encoding="utf-8", errors="ignore") as fp:
                    content = fp.read()
            except Exception:
                continue
            rel_path = os.path.relpath(full, folder_path)
            c.execute("INSERT INTO docs(filename, path, content) VALUES (?, ?, ?)", (f, rel_path, content))
    conn.commit()

def search(conn, query):
    c = conn.cursor()
    sql = """
        SELECT filename, path, snippet(docs, 2, '[', ']', '...', 12)
        FROM docs
        WHERE docs MATCH ?
        ORDER BY rank
        LIMIT 20
    """
    try:
        c.execute(sql, (query,))
        results = c.fetchall()
    except sqlite3.OperationalError as e:
        print("查询语法错误:", e)
        return
    for filename, path, s in results:
        print(f"文件: {filename}n路径: {path}n摘要: {s}n{'-' * 40}")

if __name__ == "__main__":
    conn = init_db()
    # 第一次运行可以指定一个目录做索引,例如: python fts_index.py ./docs --index
    if len(sys.argv) > 1 and sys.argv[1] == "--index":
        folder = sys.argv[2] if len(sys.argv) > 2 else "."
        index_folder(conn, folder)
        print("索引创建完成")
    else:
        query = input("输入关键词:")
        search(conn, query)

代码里那个snippet(docs, 2, ...)可能让人看迷糊。其实snippet是FTS5提供的内置辅助函数,用于从结果里截取包含关键词的片段,而不是返回整篇正文。第2个参数表示从第3列开始取摘要?这里有点容易错,参数分别为:表名、要提取的列索引(从0开始)、前缀、后缀、省略号、大概返回多少词。因为我在新建的表里列索引是:0-filename, 1-path, 2-content。所以用snippet(docs, 2, ...)是取content列做摘要。

一个真正的痛点:中文分词怎么办

FTS5默认的unigram分词器对中文支持得一言难尽。它会把连续的汉字整块当作一个词,所以搜索单个中文词时经常匹配不上。比如正文里有“我爱北京天安门”,你搜“北京”,它默认分词把“我爱北京天安门”当成一个token,所以你搜不到。

要解决中文搜索,最实用方案是自己在把文本插入数据库前先做分词,然后使用FTS5的tokenize = 'unicode61'配合空格分隔?不,FTS5无法真正分中文。

替代方案(不引入第三方库)是使用Python内置的`re`模块把中文文本切成一个个单独的字或者词?效果很一般。更专业的要使用`jieba`,但这就打破了“纯标准库”的初衷。

如果仅仅想实现一个本地快速搜索,你可以在插入前把每个字之间加一个空格:

import re

def tokenize_chinese(text):
    # 给每个汉字中间加空格,让FTS5按单个汉字索引
    return re.sub(r'([u4e00-u9fa5])', r' 1 ', text)

搜索时对用户输入也进行同样的处理。搜索“北京”时,会被拆成“北 京”,两个单字同时匹配,也就查到了。

这种分词方式虽然笨,但某种程度能实现“包含”匹配,而且索引体积会变大,不过普通个人资料够用了。下面是带单字切分的实现思路:

def add_document_zh(filename, content):
    processed = tokenize_chinese(content)
    cursor.execute("INSERT INTO docs(filename, content) VALUES (?, ?)", (filename, processed))

查询前把关键词也过一遍:

def zh_query(q):
    return tokenize_chinese(q).strip()

给搜索加上权重:标题比正文更重要

真正在工作中搜索,我们往往希望标题命中比正文命中排得更靠前。FTS5里可以对不同列设置权重,只需要在MATCH里使用column:keyword语法。

SELECT filename, rank FROM docs
WHERE docs MATCH 'filename:python OR content:python'
ORDER BY rank

不过上面的SQL不会让filename的权重更高,只是关键词同时匹配两列。要设置权重,可以使用bm25()函数,但比较复杂。一个简单粗暴的加权重办法是建表时把标题多复制几遍,或者在插入时给标题内容后面附加多个空格和标题,相当于词频增加。

我自己的做法是额外添加一个`title_bonus`列,把文件名重复写3遍,查的时候一起查:

CREATE VIRTUAL TABLE docs USING fts5(title_bonus, path, content, tokenize='unicode61')

插入时`title_bonus`存的是文件名加文件名再加文件名,这样标题命中的文档在BM25排名里自然更靠前。虽然粗暴,但胜在有效且无额外依赖。

怎么增量更新:避免每次都重建索引

上面的例子每次`–index`都会把所有文件重新插入,效率很低。正常使用时,你应该先删除旧的关于某个文件的记录,再重新插入新内容。

def replace_document(conn, file_path, content):
    c = conn.cursor()
    c.execute("DELETE FROM docs WHERE path = ?", (file_path,))
    insert_with_char_tokenize(c, file_path, content)
    conn.commit()

我实际脚本里会保存每个文件的最后修改时间到一张普通表,每次扫描时如果文件没变化就跳过,这样增量扫描非常快。

更多你可能用到的技巧

1. 搜索词加双引号做精确短语匹配

SELECT filename FROM docs WHERE docs MATCH '"async def"'

这样会查找连续短语“async def”。如果是中文单字分词,“北京”会被处理成“北 京”,加双引号正好搜索相邻的两个单字,效果不错。

2. 前缀搜索

SELECT filename FROM docs WHERE docs MATCH 'Pyth*'

匹配Python、Python3、Pythonic等等。

3. 删除无效索引

DELETE FROM docs WHERE path = '被删除的文件路径'

性能实测和取舍

我拿自己电脑上一份300MB的日志文本做测试。传统扫描方式每次搜索耗时约20秒,而FTS5建立索引后同类搜索耗时不到0.05秒。第一次建索引可能会花两三分钟,但之后就一劳永逸。

占用磁盘空间大约是原文的1.5倍左右,还能接受。如果你的文件主要是中文,单字细分会把索引空间增大不少,但相对你得到的搜索收益,依然值得。

结尾:下一步还能做什么

这套基础索引建好之后,你可以做很多好玩的事:

  • 做一个简单的Flask/FastAPI接口,变成局域网内搜索工具。
  • 加一个GUI界面,双击就能搜。
  • 把这个逻辑扩展为搜索Excel单元格、PDF文件名和邮件,思路完全一样。

越是简单的事情,往往越适合自己写工具打磨。至少我现在不用再挨个文件夹翻资料,sqlite3帮我记住了所有文本的位置。

不再怕资料乱:Python标准库 sqlite3 FTS5 实现全文搜索,速度出奇快
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 python 不再怕资料乱:Python标准库 sqlite3 FTS5 实现全文搜索,速度出奇快 https://www.taomawang.com/server/python/2727.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务