node-sqlite-vec について
Node 24 以降で、組み込みの `node:sqlite` と `sqlite-vec` を組み合わせてベクトルストレージを構築する実践ガイドです。ベクトル検索や RAG を実装する開発者向けに、拡張機能の読み込みや rowid、テストなどの注意点をまとめています。
主な機能
- 01node:sqlite と sqlite-vec の設定
- 02SQLite ベクトル拡張の読み込み
- 03vec0 の rowid を BigInt でバインド
- 04KNN によるベクトルデータ検索
- 05Node 組み込みテストランナーでのテスト
活用シーン
- 01Node プロジェクトへのベクトル検索ストレージ追加
- 02SQLite を使った RAG データ層の構築
- 03sqlite-vec の挿入や拡張読み込みエラーの調査
- 04直接実行する TypeScript の DB コードのテスト
インストール
コマンドをコピーしてターミナルで実行します。
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のネイティブビルドの手間を避けたい場合--experimental-strip-typesで直接実行する TypeScript ソース
次の場合は、このスキルを使用しないでください。
- 実行環境が Node ≤ 22 の場合(組み込みの
node:sqliteがありません)。better-sqlite3を使用してください - サーバーグレードの並行処理/レプリケーションが必要な場合。SQLite は適切なツールではありません
落とし穴(最初にお読みください)
| 症状 | 原因 | 対処 |
|---|---|---|
vitest で Failed to load url sqlite (resolved id: sqlite) が発生する | vite が node: プレフィックスを削除し、存在しない sqlite を検索するためです | vitest をやめて、組み込みで vite レイヤーのない node --test を使用します |
挿入時に ERR_SQLITE_ERROR: Only integers are allows for primary key values on <table>_vec が発生する | JS の number は REAL としてバインドされます。vec0 仮想テーブルでは厳密な整数の rowid が必要です | rowid のバインド値を BigInt(id) でラップします |
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}]);`,
);
通常のテーブル(items)とベクトルテーブル(item_vec)を、id ↔ rowid で結合した状態にします。
挿入: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 テーブルでは不要です。厳密な整数の rowid を要求するのは vec0 仮想テーブルだけです。
埋め込みは、生のリトルエンディアン浮動小数点バッファ(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
実際の DB を使用し、モックは使いません。node:test はデフォルトで TAP を出力します。読みやすい形式にするには、--test-reporter=spec を渡してください。
CLI のシバン
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning
import { DatabaseSync } from "node:sqlite";
// ...
env -S はシバンの残りの部分を空白で分割するため、フラグを渡せます。--no-warnings=ExperimentalWarning を指定しないと、CLI を呼び出すたびに次のように出力されます。
(node:12345) ExperimentalWarning: SQLite is an experimental feature and might change at any time
標準エラー出力を取得するスクリプトでは、これが煩わしくなります。エントリーポイントでのみ抑制し、必要であれば開発中は表示したままにしてください。
安定性のタイムライン
| 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 current):
- すべてのスクリプトから
--experimental-sqliteを削除してください。指定しても何も起こりません - モジュールが 1.0 未満の間は
--no-warnings=ExperimentalWarningを維持し、安定性インデックスが 2 に達したら削除してください - それ以外(BigInt の rowid、vitest の回避、tsconfig のフラグ)は安定性とは無関係で、引き続き適用されます
参考資料
sqlite-vec— ロード可能なベクター拡張機能- Node.js
node:sqliteのドキュメント - Node.js テストランナー