关于 node-sqlite-vec
为 Node 24 及以上项目提供使用内置 node:sqlite 与 sqlite-vec 构建向量存储的实践指南,面向实现向量搜索或 RAG 的开发者。内容聚焦扩展加载、vec0 行 ID、测试及 TypeScript 配置等常见问题。
主要功能
- 01配置 node:sqlite 与 sqlite-vec
- 02加载 SQLite 向量扩展
- 03处理 vec0 行 ID 的 BigInt 绑定
- 04使用 KNN 查询向量数据
- 05以 Node 内置测试运行器测试
适用场景
- 01为 Node 项目添加向量搜索存储
- 02构建基于 SQLite 的 RAG 数据层
- 03排查 sqlite-vec 插入或扩展加载错误
- 04测试直接运行的 TypeScript 数据库代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mizchi/ skills --skill node-sqlite-vec安装后在 Claude Code 中输入/node-sqlite-vec确认已加载。
附带文件README.md
详细介绍
node-sqlite-vec
将 Node 24 内置的 node:sqlite 与 sqlite-vec 扩展结合使用的配方。下面的每个部分都列出一个常见陷阱及其可行的解决方法——如果你只有一分钟,请先阅读 常见陷阱 部分。
适用场景
- 为 Node 24+ 项目添加向量搜索 / RAG 存储
- 想避免
better-sqlite3的原生构建流程 - TypeScript 源代码可直接通过
--experimental-strip-types运行
以下情况请跳过此技能:
- 运行时为 Node ≤ 22(没有内置的
node:sqlite)——请使用better-sqlite3 - 需要服务器级别的并发 / 复制能力——SQLite 并不适合
常见陷阱(请先阅读)
| 症状 | 原因 | 解决方法 |
|---|---|---|
vitest 报错 Failed to load url sqlite (resolved id: sqlite) | vite 去掉了 node: 前缀,并尝试查找不存在的普通 sqlite | 不要用 vitest。使用 node --test(内置,不经过 vite 层) |
插入时出现 ERR_SQLITE_ERROR: Only integers are allows for primary key values on <table>_vec | JS number 会绑定为 REAL;vec0 虚拟表要求 rowid 必须是严格整数 | 使用 BigInt(id) 包装 rowid 绑定值 |
loadExtension is not a function 或 "Not authorized to use load_extension" | 必须开启两个扩展加载开关 | new DatabaseSync(path, { allowExtension: true }) 以及 db.enableLoadExtension(true) |
每次运行 CLI 都会打印 ExperimentalWarning: SQLite is an experimental feature | Node 24 会为任何 node:sqlite 导入触发此警告 | 在 bin 的 shebang 中添加 #!/usr/bin/env -S node --no-warnings=ExperimentalWarning |
An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled | tsc 默认会拒绝 import "./x.ts";--experimental-strip-types 要求使用 .ts 扩展名 | 在 tsconfig.json 中设置 allowImportingTsExtensions: true 和 rewriteRelativeImportExtensions: true |
设置
依赖项
pnpm add sqlite-vec
pnpm add -D @types/node typescript
无需 better-sqlite3、bindings 或 node-gyp。
tsconfig.json
{
"compilerOptions": {
"target": "ES2023",
"module": "ES2022",
"moduleResolution": "bundler",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"allowImportingTsExtensions": true, // import "./x.ts" allowed in source
"rewriteRelativeImportExtensions": true, // tsc emits "./x.js" in output
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
}
}
rewriteRelativeImportExtensions 让你可以在源代码中保留 .ts 导入(这样 node --experimental-strip-types tests/x.test.ts 就能运行),同时仍可通过 tsc 生成有效的 .js 输出。
package.json 脚本
{
"scripts": {
"build": "tsc",
"test": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
"typecheck": "tsc --noEmit"
}
}
不要传入 --experimental-sqlite。 在 Node
v22.13.0 / v23.4.0 中,此标志已不再必需——在本技能目标涵盖的所有版本中,
node:sqlite 开箱即用即可导入,而且此标志现在不起作用。由于该模块的稳定性级别为 1.2(截至 Node 25.7 为候选发布版),Node 仍会打印
ExperimentalWarning;请参阅下文“屏蔽 ExperimentalWarning”。
--experimental-strip-types 是另一个独立标志,直接在 Node 22 上运行
.ts 文件时 仍然需要;Node 23+ 默认会剥离类型。
打开并加载扩展
// src/db.ts
import { DatabaseSync } from "node:sqlite";
import * as sqliteVec from "sqlite-vec";
export function openDB(path: string) {
const db = new DatabaseSync(path, { allowExtension: true });
db.enableLoadExtension(true);
sqliteVec.load(db);
db.enableLoadExtension(false); // re-disable after load (defense in depth)
return db;
}
allowExtension: true(构造函数)和 enableLoadExtension(true)(实例)都必不可少。缺少其中任一项,加载都会失败。
验证:
const row = db.prepare("SELECT vec_version() AS v").get();
console.log(row.v); // → "v0.1.9"
架构:vec0 虚拟表
const EMBEDDING_DIM = 1024;
db.exec(`
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
body TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
db.exec(
`CREATE VIRTUAL TABLE IF NOT EXISTS item_vec USING vec0(embedding FLOAT[${EMBEDDING_DIM}]);`,
);
通过 id ↔ rowid 关联常规表(items)和向量表(item_vec)。
插入:BigInt rowid 陷阱
const insertItem = db.prepare(
`INSERT INTO items (body) VALUES (?) RETURNING id`,
);
const insertVec = db.prepare(
`INSERT INTO item_vec (rowid, embedding) VALUES (?, ?)`,
);
function add(body: string, embedding: Float32Array): number {
const { id } = insertItem.get(body) as { id: number };
// ❌ insertVec.run(id, blob)
// → ERR_SQLITE_ERROR: Only integers are allows for primary key values on item_vec
//
// ✅ Wrap with BigInt to force INTEGER affinity:
insertVec.run(
BigInt(id),
new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength),
);
return id;
}
对每一个 vec0 绑定值都使用相同的 BigInt(id) 包装方式:例如 DELETE FROM item_vec WHERE rowid = ?。常规的 items 表不需要这样处理——只有 vec0 虚拟表会严格要求 rowid 为整数。
嵌入向量以原始小端序浮点缓冲区的形式写入(即 Float32Array 上的 Uint8Array 视图)。
KNN 查询
const sql = `
SELECT i.id, i.body, v.distance
FROM item_vec AS v
JOIN items AS i ON i.id = v.rowid
WHERE v.embedding MATCH ? AND v.k = ?
ORDER BY v.distance ASC
LIMIT ?
`;
const blob = new Uint8Array(query.buffer, query.byteOffset, query.byteLength);
const rows = db.prepare(sql).all(blob, k, k);
v.k = ? 会在 vec0 内部预先筛选出前 k 个结果;外层 LIMIT 使用相同数量,以便清晰表意。如果还需要非向量筛选条件(例如 WHERE kind = 'X'),请通过设置更大的 v.k 来获取更多候选项,避免次级筛选导致候选项不足。
测试:不要使用 vitest
vitest 依赖 vite,而 vite 的解析器会在解析导入时去掉 node: 前缀。node:sqlite 没有不带前缀的名称作为回退,因此 vitest 会在任何测试运行前失败:
Error: Failed to load url sqlite (resolved id: sqlite). Does the file exist?
改用 Node 内置的测试运行器:
// tests/db.test.ts
import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { openDB } from "../src/db.ts";
let dir: string;
let db: ReturnType<typeof openDB>;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "spike-"));
db = openDB(join(dir, "test.db"));
});
afterEach(() => {
db.close();
rmSync(dir, { recursive: true, force: true });
});
describe("vec0", () => {
it("loads sqlite-vec", () => {
const row = db.prepare("SELECT vec_version() AS v").get() as { v: string };
assert.match(row.v, /^v\d+\.\d+\.\d+/);
});
});
node --experimental-strip-types --test tests/*.test.ts
使用真实数据库,不要使用模拟对象。node:test 默认输出 TAP;传入 --test-reporter=spec 可使用易读格式。
CLI shebang
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning
import { DatabaseSync } from "node:sqlite";
// ...
env -S 会按空白字符拆分 shebang 的其余部分,因此可以传入标志。不加 --no-warnings=ExperimentalWarning 时,每次调用 CLI 都会打印:
(node:12345) ExperimentalWarning: SQLite is an experimental feature and might change at any time
这会让捕获 stderr 的脚本变得很烦人。只在入口点屏蔽此警告——如果需要,开发期间仍让它显示。
稳定性时间线
| Node | node:sqlite 状态 |
|---|---|
| v22.5.0 | 已添加,但需启用 --experimental-sqlite |
| v22.13.0 / v23.4.0 | 不再需要标志;仍处于实验阶段(警告仍会显示) |
| v25.7.0+ | 稳定性 1.2——候选发布版;仍会发出警告 |
当前的实际影响(Node 24 LTS「Krypton」、当前版本 Node 26):
- 从所有脚本中移除
--experimental-sqlite——它不起作用 - 在该模块达到 1.0 版之前保留
--no-warnings=ExperimentalWarning;稳定性指数达到 2 后将其移除 - 其他所有内容(BigInt 行 ID、避免使用 vitest、tsconfig 标志)都与稳定性无关,仍然适用
参考资料
sqlite-vec— 可加载的向量扩展- Node.js
node:sqlite文档 - Node.js 测试运行器