做语义搜索、RAG问答系统或者推荐功能时,向量检索几乎绕不开。社区里最常见的两个选择是SQLite(配合sqlite-vec等扩展)和PostgreSQL的pgvector扩展。前者轻量到只需要一个文件,后者则依靠成熟的数据库服务端能力站稳脚跟。这两个方案在设计哲学上差异很大,硬凑在一起比较容易得出片面的结论。本文结合一个实际项目,从架构、索引、性能、语法、部署五个层面做一次完整的对比,帮你根据自己的数据规模和团队能力做出选型。

一、架构差异:嵌入式与服务端的根本分野
SQLite是嵌入式数据库,整个数据库就是一个文件,程序通过函数调用直接读写,不存在独立的服务进程。这种架构决定了它的并发模型是写独占、读共享,多个进程同时读没问题,但写的时候需要加锁。基于SQLite做向量检索,通常借助sqlite-vec这样的扩展,把向量以BLOB形式存储在表中,检索时由扩展在C层面完成距离计算。
pgvector走的完全是另一条路。PostgreSQL本身是客户端/服务器架构,pgvector作为扩展运行在数据库服务端进程内,提供vector类型、距离运算符和索引支持。所有向量操作都发生在服务端,客户端只发送SQL语句。这意味着pgvector天然继承了PostgreSQL的事务、复制、权限体系,也继承了它需要单独部署运维一个数据库服务的成本。
从架构上看,SQLite适合“跟着应用走”的场景:桌面软件、移动端、边缘设备、单机小工具。pgvector适合“数据集中、多端访问”的场景:Web服务、多人协作、需要水平扩展查询能力的后端。选型的第一步不是看性能,而是确认你的应用形态属于哪一类。
二、索引与检索性能:暴力扫描与ANN的较量
sqlite-vec目前主要依靠暴力扫描(brute-force search),也就是逐条计算查询向量与所有存储向量的距离,然后排序取Top-K。这种方式在小数据量下其实非常快——一万条向量以内的检索通常在毫秒级完成,而且结果100%准确,不存在近似误差。但数据量上去之后,延迟会线性增长,十万条向量的暴力扫描可能就要几十毫秒甚至更久,百万级基本不可接受。
pgvector提供了HNSW(分层可导航小世界图)和IVFFlat两种ANN索引。以HNSW为例,建索引的语句很简单:
-- 启用扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 建表,向量维度为768
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding VECTOR(768)
);
-- 建立HNSW索引,使用余弦距离
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);
建好索引后,百万级向量的检索通常也能稳定在几毫秒到几十毫秒。代价是ANN是近似检索,召回率不等于100%,需要通过调整ef_search参数在速度和召回之间权衡。此外HNSW索引会占用不小的磁盘空间,维度越高膨胀越明显,768维向量建索引后体积可能是原始数据的两三倍。
实践建议很简单:数据量在十万条以内、对召回率要求苛刻的场景,sqlite-vec的暴力扫描反而更合适;数据量超过百万或者查询QPS较高时,pgvector加HNSW索引是更稳妥的选择。值得一提的是,sqlite-vec的开发路线图里也包含量化(quantization)等优化手段,未来暴力扫描的性能上限还会提升,但目前生产环境中大规模检索仍以pgvector为主流。
三、SQL语法与开发体验对比
两个方案在写法上差异不小。sqlite-vec使用虚拟表和自定义函数,查询大致长这样:
-- 加载sqlite-vec扩展后创建虚拟表
CREATE VIRTUAL TABLE vec_documents USING vec0(
embedding FLOAT[768]
);
-- 检索:传入序列化的查询向量,匹配距离限制
SELECT rowid, distance
FROM vec_documents
WHERE embedding MATCH :query_vector
AND k = 10
ORDER BY distance;
pgvector的语法则更贴近SQL直觉,距离计算直接用运算符表达:
-- 余弦距离检索Top 10
SELECT id, content,
1 - (embedding <=> :query_vector) AS similarity
FROM documents
ORDER BY embedding <=> :query_vector
LIMIT 10;
从开发体验看,pgvector的向量是一等公民类型,插入、更新、计算距离都和普通列一样自然,还能和PostgreSQL丰富的生态结合,比如用JSONB存元数据、配合全文检索做混合搜索。sqlite-vec的向量是BLOB,写入前需要自己序列化成字节流,Python里通常这样处理:
import sqlite3
import struct
conn = sqlite3.connect("app.db")
conn.enable_load_extension(True)
# 注意路径中的反斜杠在Windows下必须原样保留
# conn.load_extension(r"C:\tools\vec0.dll")
def serialize(v):
return struct.pack("%sf" % len(v), *v)
query_vec = serialize(embedding_text("用户的问题"))
rows = conn.execute(
"SELECT rowid, distance FROM vec_documents "
"WHERE embedding MATCH ? AND k = 5",
(query_vec,)
).fetchall()
这套序列化逻辑不难写,但确实多了一层心智负担,调试时也得注意字节序和维度对齐的问题。
四、实战项目:语义搜索的两种实现
我们用一个真实需求来收尾:给一个内部文档站加语义搜索,文档大约八千篇,每篇生成一个768维向量。这个规模下两个方案都完全能胜任,最终选择更多取决于部署环境。下面是两种实现的核心代码。
SQLite版本,整个数据库文件不到30MB,直接随应用分发:
import sqlite3
def search_sqlite(query, top_k=5):
conn = sqlite3.connect("docs.db")
conn.enable_load_extension(True)
qvec = serialize(embed(query))
rows = conn.execute(
"SELECT rowid, distance FROM vec_docs "
"WHERE embedding MATCH ? AND k = ?",
(qvec, top_k)
).fetchall()
ids = [r[0] for r in rows]
# 回表取原文
docs = conn.execute(
"SELECT title, body FROM docs WHERE id IN (%s)"
% ",".join(map(str, ids))
).fetchall()
conn.close()
return docs
pgvector版本依赖一个PostgreSQL实例,但换来了元数据过滤和并发查询能力:
import psycopg2
def search_pg(query, category=None, top_k=5):
conn = psycopg2.connect(
host="127.0.0.1", dbname="docs", user="app"
)
cur = conn.cursor()
qvec = "[" + ",".join(map(str, embed(query))) + "]"
sql = """
SELECT title, body, 1 - (embedding <=> %s::vector) AS score
FROM documents
"""
params = [qvec]
if category:
sql += " WHERE category = %s"
params.append(category)
sql += " ORDER BY embedding <=> %s::vector LIMIT %s"
params += [qvec, top_k]
cur.execute(sql, params)
results = cur.fetchall()
conn.close()
return results
注意pgvector版本里带了分类过滤,这类“向量检索+结构化条件”的混合查询是它的强项,PostgreSQL的优化器会合理处理索引的使用。sqlite-vec目前对预过滤的支持较弱,通常是先召回再在应用层过滤,某些数据分布下会导致有效结果不足。
五、选型结论与建议
总结成一张简单的决策表:
| 维度 | SQLite + sqlite-vec | PostgreSQL + pgvector |
|---|---|---|
| 部署成本 | 零依赖,单文件 | 需独立数据库服务 |
| 适用数据量 | 十万条以内 | 百万至亿级 |
| 检索方式 | 精确暴力扫描 | HNSW/IVFFlat近似检索 |
| 元数据过滤 | 能力有限 | 完善,与向量检索深度整合 |
| 并发能力 | 读多写少场景 | 完整的事务与并发控制 |
| 典型场景 | 桌面应用、边缘设备、个人知识库 | Web服务、企业级RAG系统 |
如果你在做本地优先的应用,比如一个跑在用户电脑上的知识库工具,SQLite加sqlite-vec是几乎唯一合理的选择,零安装、零配置、数据就是一个可备份的文件。如果你在搭建面向多用户的在线服务,向量数据会持续增长到百万级,那就别犹豫,直接上pgvector,HNSW索引、混合查询、备份复制这些能力都是实打实的生产力。
还有一个折中思路:原型阶段用SQLite快速验证,上线后再把向量数据迁移到pgvector。因为两者的写入端都只依赖“文本加向量”这一份数据,迁移脚本写起来并不复杂,把SQLite里的BLOB反序列化成数组再批量插入PostgreSQL即可。这种渐进式路线在实际项目中被广泛采用,能有效降低前期选型失误的成本。