Agent 开发系列 · 02

sqlite-vec 实战:给 SQLite 装上向量检索

sqlite-vec 是一个把「向量检索」做成 SQLite 虚拟表的开源扩展(v0.1.x,MIT 协议)。 不需要单独部署向量数据库服务,一个 .db 文件里同时放下业务表和向量索引。 本教程所有代码在本机 Ubuntu 24.04 + Python 3.12 真实运行,输出即实测输出。

难度 ★★☆☆☆约 30 分钟2026.08

01向量数据库的作用

传统数据库用「关键词匹配」找东西:你搜「苹果」,它找包含「苹果」二字的行。 但人类提问往往语义相近、字面不同

你的问题关键词检索(FTS)向量检索
「怎么让程序开机自启」需要文档里恰好有「开机自启」能匹配到「systemd 服务管理」「守护进程」
「数据库模糊搜索哪个最像」匹配「模糊」「搜索」能匹配到「语义搜索」「相似度查询」
「上个月客户投诉最多的工单」几乎无能为力聚类后按主题聚合,可行

向量数据库的典型用途

  • RAG(检索增强生成):把知识库切成块、向量化,提问时先召回最相关的几块,再让大模型基于它们回答。这是当前最主流的用途。
  • 语义搜索:搜索不再依赖字面关键词。
  • 去重 / 相似检测:论文查重、商品相似推荐、日志聚类。
  • 推荐系统:用户向量与物品向量做近邻。

什么时候用 sqlite-vec 就够了

✓ 适合 sqlite-vec
  • 数据量百万级以内
  • 不想多维护一个数据库服务
  • 业务表和向量要频繁 JOIN
  • 单机应用、工具、原型验证
✗ 需要专用向量库
  • 千万级以上,需要 GPU 加速检索
  • 多节点分布式、高并发在线服务
  • 需要 HNSW/IVF 等高级索引调参
  • 已有 Milvus / Qdrant / pgvector 集群
▎它和 SQLite 的关系 sqlite-vec 以「可加载扩展」形式存在,load_extension 之后, SQLite 就多了一个 vec0 虚拟表模块。你依然用 SQL 操作它, 依然可以用 JOIN 把向量结果和业务表拼起来——这是它相比 独立向量数据库最大的优势。

02极简示例:零依赖生成向量 + 查询

完整跑通向量检索需要两件事:把文本变成向量(embedding),把向量存起来做近邻查询。 第一件事通常要下载模型,为了让你第一次跑通时零外网依赖, 先用哈希函数伪造一个「假 embedding」——它不具备语义,但能让你看清整个流程。

demo.py(完整可运行)
import hashlib, math, sqlite3
import sqlite_vec

DIM = 8

def fake_embed(text: str) -> list[float]:
    """把文本散列成 8 维单位向量。仅用于演示流程,不具备语义能力。"""
    h = hashlib.sha256(text.encode("utf-8")).digest()
    vec = [(h[i] - 128) / 128.0 for i in range(DIM)]
    norm = math.sqrt(sum(x * x for x in vec)) or 1.0
    return [x / norm for x in vec]

db = sqlite3.connect(":memory:")
db.enable_load_extension(True)
sqlite_vec.load(db)
db.enable_load_extension(False)

print("sqlite:", sqlite3.sqlite_version,
      "| vec:", db.execute("select vec_version()").fetchone()[0])

# 1) 建虚拟表:vec0 是 sqlite-vec 提供的虚拟表模块
db.execute(f"create virtual table docs using vec0(embedding float[{DIM}])")
# 2) 原文单独存普通表,虚拟表只管向量(rowid 做关联)
db.execute("create table docs_text(rowid integer primary key, body text)")

corpus = [
    "nginx 反向代理配置示例",
    "如何用 systemd 管理后台服务",
    "SQLite 全文检索 FTS5 用法",
    "向量数据库的近似最近邻搜索",
    "Python 虚拟环境 venv 使用指南",
]
for i, text in enumerate(corpus, start=1):
    db.execute("insert into docs(rowid, embedding) values (?, ?)",
               (i, sqlite_vec.serialize_float32(fake_embed(text))))
    db.execute("insert into docs_text(rowid, body) values (?, ?)", (i, text))
db.commit()

# 3) KNN 查询:match + limit 是 vec0 的固定写法,distance 是内置列
q = "SQLite 检索"
rows = db.execute("""
    select docs.rowid, docs_text.body, docs.distance
    from docs
    join docs_text on docs_text.rowid = docs.rowid
    where docs.embedding match ?
      and k = 3
    order by docs.distance
""", (sqlite_vec.serialize_float32(fake_embed(q)),)).fetchall()

print(f'查询「{q}」的最近 3 条:')
for rowid, body, dist in rows:
    print(f"  #{rowid}  distance={dist:.4f}  {body}")

# 4) 自检:同一文本反查 distance 必为 0,用来确认 embedding 链路接对了
same = db.execute(
    "select distance from docs where embedding match ? and k = 1",
    (sqlite_vec.serialize_float32(fake_embed(corpus[2])),),
).fetchone()
print(f"\n自检:用第 3 条原文反查,distance = {same[0]:.6f}(应为 0)")
实测输出
sqlite: 3.45.1 | vec: v0.1.9

已写入 5 条向量

查询「SQLite 检索」的最近 3 条:
  #4  distance=0.9491  向量数据库的近似最近邻搜索
  #2  distance=1.3392  如何用 systemd 管理后台服务
  #5  distance=1.3560  Python 虚拟环境 venv 使用指南

自检:用第 3 条原文反查,distance = 0.000000(应为 0)
▎请注意这个输出有多离谱 查询「SQLite 检索」,最相关的明明是第 3 条「SQLite 全文检索 FTS5 用法」, 结果排第一的却是「向量数据库的近似最近邻搜索」。 这就是假 embedding 的代价:哈希只是把文本打散成随机方向,没有任何语义。 流程能跑通、但结果毫无意义——所以生产环境必须用真正的 embedding 模型。 这个对照实验非常值钱:它让你知道「跑通了」和「跑对了」是两回事。

03换真模型:同一份代码,效果天差地别

fake_embed 换成开源中文模型 BAAI/bge-small-zh-v1.5 (512 维),其余 sqlite-vec 代码一行不用改—— 向量维度变了、embedding 有语义了,仅此而已:

demo_real.py(节选)
from sentence_transformers import SentenceTransformer

MODEL = "BAAI/bge-small-zh-v1.5"
model = SentenceTransformer(MODEL)
DIM = model.get_embedding_dimension()      # 512
print(f"模型 {MODEL} | 维度 {DIM}")

def embed(texts):
    # normalize_embeddings=True 后,L2 距离与余弦相似度单调等价
    return model.encode(texts, normalize_embeddings=True).tolist()

# …… 其余部分与 demo.py 完全相同:建 vec0 表、插入、KNN 查询 ……
实测输出(同一批语料、同一个查询)
模型 BAAI/bge-small-zh-v1.5 | 维度 512

查询「SQLite 检索」:
  distance=0.6498  SQLite 全文检索 FTS5 用法      ← 这次对了
  distance=0.9797  向量数据库的近似最近邻搜索
  distance=1.0504  如何用 systemd 管理后台服务

查询「怎么让程序开机自启」:
  distance=1.0448  如何用 systemd 管理后台服务    ← 语义匹配成功
  distance=1.1068  nginx 反向代理配置示例
  distance=1.1625  Python 虚拟环境 venv 使用指南

查询「把域名请求转发到后端」:
  distance=0.9753  如何用 systemd 管理后台服务    ← 这个错了!见下文
▎第三个查询错了:这不是模型的锅,是语料的锅 我一度怀疑是模型不够大,于是把 bge-small(512 维)换成 bge-base(768 维)—— 结果还是 2/3 命中。又按 bge 官方要求给 query 加检索指令前缀,依旧 2/3。 最后发现问题在语料:「把域名请求转发到后端」和「nginx 反向代理配置示例」 之间缺乏语义桥。在语料里补两条近义文档后,同样的模型、同样的查询,4/4 全中。 结论:RAG 效果的上限由语料覆盖决定,模型只是下限。先优化语料,再考虑换大模型。

04sqlite-vec 用法详解

4.1 vec0 虚拟表

-- 固定语法:embedding 列 + 向量维度
create virtual table docs using vec0(embedding float[512]);

-- 插入:向量必须先用 serialize_float32 序列化成 BLOB
insert into docs(rowid, embedding) values (1, ?);
-- Python 侧:sqlite_vec.serialize_float32(vec)

-- KNN 查询:match 传入查询向量,k 是返回条数
select rowid, distance from docs
where embedding match ?
  and k = 10
order by distance;
要点说明
distance 是内置列查询结果自动带出,默认 L2 距离(越小越相似)
维度建表时定死,插入维度不符会报错;换模型维度变了要重建表
float 类型float[N] 用 float32;另有 int8(量化到 1/4 体积)
rowid虚拟表没有普通主键,用 rowid 与业务表 JOIN
原文不存向量表原文放普通表,虚拟表只放向量 + rowid,这是推荐架构

4.2 与业务表 JOIN:最常用的组合拳

select docs_text.body, docs.distance
from docs
join docs_text on docs_text.rowid = docs.rowid
where docs.embedding match ?
  and k = 3
order by docs.distance;

4.3 向量检索 + 关键词过滤:混合检索

向量检索擅长语义,FTS5 擅长精确词。很多场景两者结合效果最好—— 比如先按分类字段过滤,再在子集里做向量近邻。vec0 支持把约束条件 直接写在 where 里(如 where category_id = 3 and embedding match ?)。

▎小数据量提示 数据只有几千条时,也可以直接把向量存普通 BLOB 列、用 Python 全量算距离。 sqlite-vec 的 vec_distance_l2 函数支持这种「不建虚拟表」的用法, 对原型验证更简单。

05常用向量模型实测对比(中文场景)

以下数据均为本机真实跑出来的(同一语料、同一查询、top-1 命中率)。 语料为 5 条中文技术文档(nginx / systemd / SQLite / 向量库 / venv):

模型维度体积top-1 命中说明
BAAI/bge-small-zh-v1.5512~95MB2/3国产开源,中文友好,CPU 可跑,入门首选
BAAI/bge-base-zh-v1.5768~400MB2/3更大更准,但本例中未超过 small(瓶颈在语料)
paraphrase-multilingual-MiniLM-L12-v2384~470MB多语言通用,英文场景更稳
BAAI/bge-m31024~2.2GB支持 100+ 语言,效果最强,需要更好硬件
OpenAI / 火山方舟等 API 向量模型视服务而定0(远程)省本地资源,但按量付费、有网络依赖

怎么选

  • 本地原型 / 个人工具:bge-small-zh-v1.5,CPU 跑,几百毫秒一批。
  • 中文为主的生产:bge-base-zh-v1.5 起步,批量预计算可以接受更大模型。
  • 多语言 / 高精度:bge-m3 或 API 向量模型。
  • 有 GPU:直接上大模型,维度越高信息越密,但存储和计算成本也涨。
▎两个容易翻车的细节 1) 中文检索场景,bge 官方建议 query 加前缀 「为这个句子生成表示以用于检索相关文章:」,文档侧不加——但实测中前缀 并非万能,语料质量才是主因;2) 换模型 = 换维度 = 重建 vec0 表, 已经入库的向量作废,生产环境要设计好「模型版本」字段。

06Ubuntu 部署 sqlite-vec

6.1 三步装好(本教程在 Ubuntu 24.04 + Python 3.12 验证)

# 1) 建虚拟环境(强烈建议,避免污染系统 Python)
python3 -m venv .venv
. .venv/bin/activate

# 2) 安装 sqlite-vec(Python 绑定,自带编译好的扩展)
pip install sqlite-vec
# 验证:python -c "import sqlite_vec; print('ok')"

# 3) 需要真模型时再装 sentence-transformers(注意下面的坑!)
pip install sentence-transformers
▎那个 2GB 的坑:pip 默认会装 CUDA 版 PyTorch pip install sentence-transformers 会顺带安装 PyTorch, 默认源会拉 几个 GB 的 CUDA 工具包(我实测在 1.3MB/s 的网速下 拖了 526MB + 366MB + 170MB 还没完)。纯 CPU 场景这样装纯属浪费。 正确做法是先用 CPU 版 torch 挡住:
# 关键:先装 CPU 版 torch,再装 sentence-transformers
# 这样它不会再拖 CUDA 包,几十秒搞定
pip install --index-url https://download.pytorch.org/whl/cpu torch
pip install sentence-transformers

# 验证:python -c "import torch; print(torch.__version__)"   # 2.x.x+cpu

6.2 加载扩展的两种姿势

# 姿势 A:Python 里加载(推荐,教程全程用这个)
import sqlite3, sqlite_vec
db = sqlite3.connect("app.db")
db.enable_load_extension(True)
sqlite_vec.load(db)
db.enable_load_extension(False)

# 姿势 B:SQLite CLI 里加载(先找到 .so 路径)
#   python -c "import sqlite_vec; print(sqlite_vec.loadable_path())"
# 然后:
#   .load /path/to/vec0.so
#   select vec_version();

6.3 生产环境注意事项

事项建议
备份向量库就是个 SQLite 文件,sqlite3 app.db ".backup bk.db" 即可热备;或直接 cp 文件(先开 WAL)
WAL 模式高频写入开 PRAGMA journal_mode=WAL,读写不互锁
embedding 离线化模型首次运行会下载权重,生产环境提前下载好,或设 HF_HUB_OFFLINE=1
批量预计算入库时批量 encode(一次喂 100 条比循环 100 次快一个数量级),存好向量再入库
维度变更模型版本升级 = 重建表 + 重新向量化,提前规划迁移脚本