tech-book-labs
データ基盤(クライアント完結) · 最終検証 2026-05-10 · @grafeo-db/wasm 0.5.x · 初公開 2026-05-10

GrafeoDB の WASM ビルドでブラウザに組込みグラフ DB を載せる

@grafeo-db/wasm を使って Cypher / GQL / SPARQL / SQL を 1 つの埋め込み DB で扱うブラウザ完結のグラフ DB パターン — importLpg でのデータ投入、executeCypher、textSearch / vectorSearch、exportSnapshot による zero-backend 配布、IndexedDB 永続化までを触れる demo 付きで整理する実装メモ。

grafeodb graph-database wasm cypher rag vector-search

検証日: 2026-05-10

使用バージョン: @grafeo-db/wasm@0.5.x

対象: 「ブラウザだけで完結する RAG / 知識グラフ」を組みたい人。バックエンドが立てられない静的サイトでも、Cypher で関係性を辿りたいケース

@grafeo-db/wasmブラウザに組込みグラフ DB を載せる パターン。Cypher / GQL / SPARQL / SQL を 1 DB で扱える + 全文検索(BM25)とベクトル検索(HNSW)も内蔵という性質を活かして、サーバーなしの RAG / 知識グラフを動く demo で確認します。

触って試す

ブラウザで WASM を読み込み、importLpg で 9 ノード / 10 エッジを投入したあと Cypher を実行している。MATCH (a:Person)-[:WROTE]->(b:Book) のような 関係を辿るクエリ が SQL の JOIN なしで書けるのが要点。

1. なぜ「ブラウザに DB」を再考するか

これまでブラウザ側 DB の選択肢は概ね:

  • IndexedDB:Key-Value / オブジェクトストア。クエリ言語は弱い
  • sql.js / @sqlite.org/sqlite-wasm:SQL は書けるが、再帰クエリは重い
  • @duckdb/duckdb-wasm:列指向で集計は速いが、辺の往来には不向き
  • PGlite:Postgres 完全互換だが size が大きい(数 MB+)

N ホップ先のノードを辿る」「サブグラフを抽出する」用途にはどれも合わなかった。

GrafeoDB は Rust 製の組込みグラフ DB を WASM ビルドした もの。Cypher / GQL / SPARQL / SQL / GraphQL / Gremlin をすべて 1 DB で扱え、BM25 全文検索 + HNSW ベクトル検索 まで内蔵している。これにより「ブラウザだけで完結する RAG / 知識グラフ」が現実的になる。

2. 最小セットアップ(Vite / Astro)

WASM パッケージは wasm-bindgen 出力なので、Vite 系では追加プラグインが必要。

pnpm add @grafeo-db/wasm
pnpm add -D vite-plugin-wasm vite-plugin-top-level-await
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";

export default {
  plugins: [wasm(), topLevelAwait()],
  optimizeDeps: {
    exclude: ["@grafeo-db/wasm"],   // dep optimizer に流すと壊れる
  },
};

ポイント:

  • optimizeDeps.exclude が肝心。wasm-bindgen が出す import * as wasm from "./...wasm" を Vite が事前バンドルすると壊れる
  • Next.js の場合は next.config.jsexperiments.asyncWebAssembly = true(turbopack はデフォルト対応)
  • WASM ファイルは .wasm のまま fetch されるので、production では HTTP の Content-Type: application/wasm + gzip/br 配信を確認

3. データを入れる(importLpg)

最小単位の「ノード + エッジ」一括投入は importLpg:

import { Database } from "@grafeo-db/wasm";

const db = new Database();

db.importLpg({
  nodes: [
    { labels: ["Person"], properties: { name: "Alix", role: "author" } },
    { labels: ["Person"], properties: { name: "Gus", role: "author" } },
    { labels: ["Book"], properties: { title: "Graph Internals", year: 2024 } },
    { labels: ["Topic"], properties: { name: "graph" } },
  ],
  edges: [
    { source: 0, target: 1, type: "KNOWS" },
    { source: 0, target: 2, type: "WROTE" },          // Alix -> Graph Internals
    { source: 2, target: 3, type: "USES_TOPIC" },     // Book -> Topic
  ],
});

ポイント:

  • source / target は配列内のインデックス(0-origin)。db 内部 ID とは別物だが、初回 import では一致するケースが多い
  • labels / type は配列指定。複数ラベルや 1 つの labels に複数値を入れるパターンも可
  • 大量データは importRows:CSV / JSON 由来の行配列を {mode: "nodes" | "edges"} で流し込める

CSV からの一括投入例:

const people = await fetch("/data/people.csv")
  .then((r) => r.text())
  .then((t) => parse(t, { header: true }));

db.importRows(people, { mode: "nodes", label: "Person" });
db.importRows(edgesArr, { mode: "edges", edgeType: "KNOWS", source: "from", target: "to" });

4. Cypher で辿る

executeCypher に MATCH 文を投げると、ノードと関係を辿った結果が JSON で返ってくる:

const rows = db.executeCypher(`
  MATCH (a:Person)-[:WROTE]->(b:Book)-[:USES_TOPIC]->(t:Topic)
  WHERE t.name = 'graph'
  RETURN a.name AS author, b.title AS book, b.year AS year
  ORDER BY b.year DESC
`);
// [{author: "Alix", book: "Graph Internals", year: 2024}, ...]

JS オブジェクトの配列で返ってくる(columns: keys / values: any[] のような raw 形式が欲しい場合は executeRaw)。

特に強力なのが 可変長 path:

// 知人の知人(2 ホップ)
db.executeCypher(`
  MATCH (start:Person {name: 'Alix'})-[:KNOWS*1..2]->(p:Person)
  WHERE p.name <> 'Alix'
  RETURN DISTINCT p.name
`);

*1..2 で「1 ホップ以上 2 ホップ以下」、* だけだと無制限。SQL では再帰 CTE が要るパターンが 1 行で書ける。

5. 全文検索 + ベクトル検索

GrafeoDB は BM25 テキスト検索HNSW ベクトル検索 を「ノードのプロパティに対するインデックス」として持てる。

// BM25
db.createTextIndex("Article", "content");
const bm25 = db.textSearch("Article", "content", "graph database", 10);
// [{id: 42, score: 2.5}, ...]

// 埋め込みベクトル(384 次元)
db.createVectorIndex("Article", "embedding", {
  dimensions: 384,
  metric: "cosine",
  m: 16,
  efConstruction: 128,
});
const vec = db.vectorSearch("Article", "embedding", queryVec, 10, { ef: 200 });

// ハイブリッド(BM25 + vector の RRF / 加重融合)
const hybrid = db.hybridSearch("Article", "content", "embedding", "graph database", 10);

返ってくる idCypher で再ヒット させて関連するノードや出典を引っ張ってくれば、これだけで Browser-only RAG が成立する:

const hits = db.hybridSearch("Article", "content", "embedding", question, 5);
const ids = hits.map((h) => h.id);

const ctx = db.executeCypher(`
  MATCH (a:Article)-[:CITES]->(s:Source)
  WHERE id(a) IN $ids
  RETURN a.title AS title, a.content AS content, s.url AS source
`, { ids });   // ← パラメータ渡しは executeWithParams 系を使う

埋め込み生成は別途 transformers.js などをブラウザで動かす(MiniLM-L6 ~ 90MB / GTE-small ~ 130MB)。サーバ無しでセマンティック検索が動く のが新しい。

6. スナップショットで「ビルド時に DB 構築・ブラウザでロード」

毎回ブラウザで投入するのは無駄なので、ビルド時に 1 回だけグラフを構築 → 二進スナップショットを export、ブラウザは ロードするだけ にする。

import { Database } from "@grafeo-db/wasm";
import fs from "node:fs/promises";

const db = new Database();
// ... importLpg / importRows ...
db.createTextIndex("Article", "content");
db.createVectorIndex("Article", "embedding", { dimensions: 384, metric: "cosine", m: 16, efConstruction: 128 });
db.compact();   // 読み取り専用に最適化、~60x メモリ削減

const snapshot = db.exportSnapshot();   // Uint8Array
await fs.writeFile("public/graph.gsn", snapshot);
import { Database } from "@grafeo-db/wasm";

const buf = await fetch("/graph.gsn").then((r) => r.arrayBuffer());
const db = Database.importSnapshot(new Uint8Array(buf));

const rows = db.executeCypher("MATCH (n) RETURN count(n) AS n");

ポイント:

  • compact() = 読取専用 CompactStore に変換。~60x メモリ削減 / 100x+ 走査速度向上(write は不可になる)
  • 改ざん防止が要る場合:exportSnapshotSigned(key) / importSnapshotSigned(buf, key) で HMAC 検証
  • ビルド時 ↔ ブラウザで同一 wasm バージョンを使う(snapshot にバイナリ互換あり)

これで 数 MB の .gsn を CDN 配信するだけ で、サーバ立てずに辞書 / 知識グラフを配れる。

7. IndexedDB に永続化

スナップショットは Uint8Array なので IndexedDB にそのまま入る:

import { openDB } from "idb";

const idb = await openDB("graph-store", 1, {
  upgrade(db) { db.createObjectStore("snapshots"); },
});

async function save(db: Database) {
  await idb.put("snapshots", db.exportSnapshot(), "main");
}

async function load(): Promise<Database> {
  const buf = await idb.get("snapshots", "main");
  if (buf) return Database.importSnapshot(buf);
  return new Database();   // 初回
}

書込のたびに save するのではなく、「N 件書いたら save」「タブ閉じる前に save」(beforeunload) のような遅延保存が現実的。

8. 4 つのクエリ言語の使い分け

言語強み用途
Cypher(executeCypher)パターンマッチが直感的、Neo4j で流通関係を辿る、サブグラフ抽出
GQL(execute)ISO 標準のグラフ問合言語将来の互換性
SQL(executeSql)集計、JOIN数値計算、レポート
SPARQL(executeSparql)RDF / オントロジー既存 RDF データを取り込む時
GraphQL / Gremlinエコシステム互換既存ツールチェーンとの接続

辿る → Cypher集計 → SQL外部互換 → SPARQL/GraphQL」が判断軸。同じ DB 上で混ぜて使える。

9. 比較:他のブラウザ DB との位置取り

GrafeoDBDuckDB-WASMsqlite-wasmPGlite
データモデルグラフ + 表列指向行指向行指向
主クエリCypher / GQL / SQLSQL(分析)SQLSQL(Postgres)
全文検索◎ BM25△(拡張)△ FTS5
ベクトル検索◎ HNSW✕(拡張)◯ pgvector
サイズ感数 MB数 MB~1 MB~3 MB
強み辿る + RAG集計、parquet軽量互換

集計が要らず、関係性を辿りたい」のが GrafeoDB の最適領域。逆にユーザの売上を集計したいだけなら DuckDB-WASM が速い。

10. ユースケース整理

  • 静的サイトの「関連書籍」「次の記事」レコメンド:著者 → トピック → 著者で 2-3 ホップ辿る
  • オフライン辞書 / Wiki アプリ:ノード=ページ、エッジ=リンク、Cypher で「ここから辿れるすべて」
  • ブラウザ完結の RAG:hybridSearch でドキュメント検索 + Cypher で出典を表示
  • コードベース可視化:ファイル / 関数 / 依存関係をグラフ化、UI でクエリ
  • 個人知識管理(PKM):ノートグラフを完全ローカルで永続化(プライバシー保持)

「サーバ運用が許されない / コストかけられない」コンテキストで、動的に関係を辿る体験 を出せるのが価値。

つまずいたポイント

  • Vite の事前バンドルで壊れる:optimizeDeps.exclude: ["@grafeo-db/wasm"] 必須。これがないと dev server で __wbg_set_wasm 周りが死ぬ
  • Top-level await が必要:vite-plugin-top-level-await を入れないと build で fail。modern target なら不要だが、Vite はトランスパイル先が低いので必要
  • SSR で import するとサーバ側で wasm を読もうとして死ぬ:client:only="react" か React の lazy + Suspense でクライアント限定にする
  • 大量 import で OOM:WASM heap には上限がある。10 万ノード超は importRows で chunk 分け + 都度 compact() を検討
  • スナップショット互換性:wasm バージョンを跨ぐと snapshot が読めなくなる場合がある。ビルド時 / ブラウザで同一バージョンに固定
  • close() / [Symbol.dispose]:WASM heap を確実に解放するには db.close() を React の cleanup で呼ぶ。長時間使うアプリではメモリリーク注意

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784297144944

JavaScriptによるはじめてのアルゴリズム入門

河西 朝雄
詳細を tech-book.net で見る
tech-book.net /books/9784873118086

Python と JavaScriptではじめるデータビジュアライゼーション

Kyran Dale/嶋田 健志/木下 哲也
詳細を tech-book.net で見る
tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books/9784627857216

はじめてのWebデザイン&amp;プログラミング : HTML、CSS、JavaScript、PHPの基本

村上 祐治
詳細を tech-book.net で見る
tech-book.net /books/9784297138714

フロントエンドの知識地図ーー 一冊でHTML/CSS/JavaScriptの開発技術が学べる本

株式会社ICS 池田 泰延/西原 翼/松本 ゆき
詳細を tech-book.net で見る