SQLite向量搜索实战:Java 开发者用 JDBC + sqlite-vec 完成轻量向量检索的完整路径
【免费下载链接】sqlite-vecA vector search SQLite extension that runs anywhere!项目地址: https://gitcode.com/GitHub_Trending/sq/sqlite-vec
手上有几千到几百万条 embedding 要落地,但只为这点数据部署一套专门的向量数据库集群,运维成本和资源开销都不太划算。sqlite-vec 是一个直接运行在 SQLite 之上的向量搜索扩展:Java 项目通过 JDBC 驱动加载扩展文件后,就能完成建向量表、写入向量、按距离做 KNN 检索的完整闭环——一句话概括,这就是 SQLite向量搜索。
痛点:向量数据要落地,但不想多养一套数据库
很多 Java 业务系统对向量检索的需求其实很朴素:文档 RAG、内容去重、推荐召回,规模在万级到百万级,QPS 也不高。这种场景下,Milvus、Qdrant 这类分布式方案带来的部署、备份、扩缩容成本,远超收益。而引入一个「文件即数据库」的方案,向量数据和业务数据放在同一个存储介质里,备份就是复制一个文件,回滚就是换个文件。sqlite-vec 瞄准的正是这个缝隙:任何能跑 SQLite 的地方,都能跑向量检索,Linux、macOS、Windows、树莓派甚至浏览器里的 WASM 环境都支持。
🧩 sqlite-vec 是什么,不是什么
先说清楚定位,避免过度期待。
sqlite-vec 用纯 C 编写、零依赖,核心是一个名为vec0的 SQLite 虚拟表:向量、标量元数据、分区键都存在这一张表里,KNN 检索直接写在WHERE子句中。它的距离计算是暴力扫描(brute force),官方给出的定位是"足够快"——对百万级以下的向量,配合分区键,单核毫秒到几十毫秒量级的查询体验是可以预期的。
它不是的东西同样重要:
- 不是分布式数据库,没有副本、没有分片集群,就是一个本地文件;
- 没有 HNSW、IVF 这类近似索引,向量规模上千万后暴力扫描会明显变慢;
- 项目目前仍是 pre-1.0,升级版本时要有 breaking change 的心理准备。
边界画清楚之后,它的优点就很明确:零部署、单文件、和 SQL 生态完全同构,业务侧的标量过滤和向量检索可以在一条语句里完成。
最短跑通路径:从 JDBC 连接到第一次 KNN 检索
下面按最小可运行路径走一遍,每一步只讲关键动作。
第一步:拿到扩展文件
sqlite-vec 的发布产物里带有预编译扩展;也可以自己从源码构建,仓库根目录的 Makefile 里就有现成目标:
git clone https://gitcode.com/GitHub_Trending/sq/sqlite-vec cd sqlite-vec make loadable # 产物:dist/vec0.so(Windows 下为 .dll)把vec0.so放到 Java 进程有读权限的路径下即可,比如/opt/lib/vec0.so。关于预编译产物的获取方式,可参考仓库内的安装文档。
第二步:JDBC 连接并加载扩展
Java 侧只需一个 JDBC 驱动依赖(以 xerial 为例):
<dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> <version>3.45.1.0</version> </dependency>连接建立后,先开启扩展加载开关,再执行load_extension,用vec_version()验证是否生效:
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:search.db")) { try (Statement st = conn.createStatement()) { st.execute("PRAGMA enable_load_extension = ON"); st.execute("SELECT load_extension('/opt/lib/vec0.so')"); try (ResultSet rs = st.executeQuery("SELECT vec_version()")) { rs.next(); System.out.println("sqlite-vec " + rs.getString(1)); } } }有一个容易踩的点:扩展是按连接加载的。如果应用里使用连接池,每个新连接都要执行一次load_extension,可以在连接初始化回调里统一处理。
第三步:建表、写入、KNN 检索
建一张 384 维的向量表,默认 L2 距离;如果模型输出的是归一化向量,通常改成正余弦距离更符合直觉:
CREATE VIRTUAL TABLE note_vec USING vec0( note_id INTEGER, note_embedding FLOAT[384] distance_metric=cosine );写入时向量既可以直接给 JSON 字符串,也可以给二进制 BLOB,前者调试方便,本文示例用字符串:
String insertSql = "INSERT INTO note_vec(rowid, note_id, note_embedding) VALUES (?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(insertSql)) { ps.setInt(1, 1); ps.setInt(2, 1001); ps.setString(3, "[0.12, -0.34, 0.56, /* ... 共384维 */ 0.21]"); ps.executeUpdate(); }KNN 检索用MATCH指定查询向量,k = N限定返回条数:
String knnSql = """ SELECT note_id, distance FROM note_vec WHERE note_embedding MATCH ? AND k = 5 ORDER BY distance """;PreparedStatement绑定查询向量后执行,结果按distance升序排列,第一行就是最近的邻居。这里有个版本相关的坑:k = N写法在所有 SQLite 版本都可用;而用LIMIT N替代k =只在 SQLite 3.41+ 生效。如果你的 JDBC 驱动捆绑的 SQLite 版本较老,统一用k =最保险。更多 KNN 写法见KNN 查询文档。
工程细节:批量写入、分区键与连接释放
跑通之后,真正进生产要处理的是这几件事。
批量写入向量怎么写
逐条executeUpdate在万级以上会很慢。批量绑参加显式事务,按批提交:
conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement(insertSql)) { for (EmbeddingRow row : rows) { ps.setInt(1, row.id()); ps.setInt(2, row.noteId()); ps.setString(3, row.vectorJson()); ps.addBatch(); if (row.id() % 500 == 0) { ps.executeBatch(); } } ps.executeBatch(); conn.commit(); } conn.setAutoCommit(true);百万级一次性灌库时,事务批次再放大一些(比如每批几千条),速度差异是数量级的。
用分区键把检索范围切小
如果业务里「每次只查某个用户/租户的数据」,partition key能让 KNN 直接跳过其他分区,这是最便宜的提速手段:
CREATE VIRTUAL TABLE tenant_vec USING vec0( tenant_id INTEGER partition key, chunk_embedding FLOAT[768] ); SELECT chunk_id, distance FROM tenant_vec WHERE chunk_embedding MATCH ? AND k = 10 AND tenant_id = 42;sqlite-vec会识别tenant_id = 42这个等值约束,在检索前就把扫描范围收窄到该租户的向量。两条经验:每个分区键取值对应的向量数量保持在百级以上,否则会出现过度分片反而拖慢查询;分区键列最多声明 4 个,但实际项目里超过 1 个就要警惕了。详细规则见vec0 虚拟表文档。
JSON 字符串还是二进制 BLOB
JSON 字符串适合开发和日志排查,但同样一个 384 维向量,文本体积明显大于 1536 字节的float32二进制。数据量上来后,推荐直接绑 BLOB:把float[]写入ByteBuffer(FloatBuffer)后setBytes绑定,磁盘占用和解析开销都会更优。
连接与资源怎么释放
Java 侧用 try-with-resources 统一收口,ResultSet、Statement、Connection逐层关闭即可。sqlite-vec 的内存占用和 SQLite 本身一致,没有额外的后台进程需要清理;但注意load_extension是连接级状态,连接池扩容时新连接别漏掉加载步骤,否则表现为「随机报 no such function: vec_f32」这类迷惑性错误。标量函数(vec_f32、vec_distance_cosine等)的完整清单见API 参考。
取舍:什么时候用、什么时候不用
适合用 sqlite-vec 的场景:
- 向量是业务的「伴生数据」,规模万级到百万级,单机承载足够;
- 本地 RAG、桌面应用、边缘设备、嵌入式端,没有条件常驻一个服务端;
- 向量检索要和既有 SQL 业务数据(权限、时间过滤、标量条件)混合查询;
- 备份运维要求极简,「文件即数据」。
不适合的场景:
- 单表向量上千万且查询延迟要求苛刻——暴力扫描撑不住,需要 HNSW/IVF 索引的专用引擎;
- 多节点水平扩展、高并发读写、跨机房复制;
- 需要向量库的周边能力:多租户权限体系、增量同步、模型推理流水线。
一句话判断法:如果数据能装进一台机器的一个文件里,且查询 QPS 在单机 SQLite 的舒适区,选它几乎不会错;反过来任何一条不满足,直接上专用向量数据库,不要在 sqlite-vec 上硬扛。
收尾提醒
两个收尾信息值得记住。第一,sqlite-vec 还是 pre-1.0 状态,锁版本升级、观察 breaking change 是基本操作。第二,检索效果不达预期时,优先检查三件事:距离度量是否匹配模型特性(归一化向量配 cosine)、查询是否命中了分区键、k与下游重排的配合。把这三点调顺,这套「JDBC + 单文件」的向量检索方案在中小规模业务里是相当省心的选择。
【免费下载链接】sqlite-vecA vector search SQLite extension that runs anywhere!项目地址: https://gitcode.com/GitHub_Trending/sq/sqlite-vec
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考