mcpField
N
node-sqlite-vec

node-sqlite-vec

github.com/mizchi数据与数据库41665 次安装1 周前更新
代码仓库

关于 node-sqlite-vec

为 Node 24 及以上项目提供使用内置 node:sqlite 与 sqlite-vec 构建向量存储的实践指南,面向实现向量搜索或 RAG 的开发者。内容聚焦扩展加载、vec0 行 ID、测试及 TypeScript 配置等常见问题。

主要功能

  1. 01配置 node:sqlite 与 sqlite-vec
  2. 02加载 SQLite 向量扩展
  3. 03处理 vec0 行 ID 的 BigInt 绑定
  4. 04使用 KNN 查询向量数据
  5. 05以 Node 内置测试运行器测试

适用场景

  1. 01为 Node 项目添加向量搜索存储
  2. 02构建基于 SQLite 的 RAG 数据层
  3. 03排查 sqlite-vec 插入或扩展加载错误
  4. 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>_vecJS 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 featureNode 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 enabledtsc 默认会拒绝 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 的脚本变得很烦人。只在入口点屏蔽此警告——如果需要,开发期间仍让它显示。

稳定性时间线

Nodenode: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 标志)都与稳定性无关,仍然适用

参考资料

评论

支持 Markdown