0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SurrealDBマルチモデルスキーマ設計実践:グラフ×ドキュメント×ベクトルを1DBで統合する

0
Last updated at Posted at 2026-06-27

SurrealDBマルチモデルスキーマ設計実践:グラフ×ドキュメント×ベクトルを1DBで統合する

この記事でわかること

  • マルチモデルデータベースの3つのリレーション設計パターン(Record Links / Record References / Graph Edges)の使い分け
  • SurrealDB 3.1のSCHEMAFULL/SCHEMALESSを組み合わせたハイブリッドスキーマ設計の具体例
  • BM25全文検索とHNSWベクトル検索をRRFで融合するハイブリッド検索の実装方法
  • FaunaDB終了(2025年5月)の教訓を踏まえたDBaaS依存リスクの設計上の対策
  • AIエージェント向けナレッジグラフ+ベクトル検索を統合したRAGパイプラインの構築例

対象読者

  • 想定読者: MLエンジニアでデータ基盤の設計・実装に関わる方
  • 必要な前提知識:
    • SQLの基礎文法(SELECT、INSERT、JOIN程度)
    • ベクトル検索の基本概念(embedding、コサイン類似度)— PyTorchやSentence Transformersの使用経験があると理解しやすい
    • NoSQLデータベース(MongoDB等)の基本概念
    • グラフデータベースの概念(ノード、エッジ)— 未経験でも記事内で解説

結論・成果

マルチモデルデータベースSurrealDBを使うと、従来「PostgreSQL+MongoDB+Neo4j+Elasticsearch」の4つで構成していたMLデータ基盤を1つのDBに統合できます。SurrealDB 3.xの公式ベンチマークでは、PostgreSQL比でWrite性能が約1.5倍、Neo4j比でCRUD全体が2〜3.5倍の性能が報告されています。一方、単一レコードのRead性能ではPostgreSQLが上回り(327k vs 254k ops/sec)、単一レコードWriteではMongoDBが優位(183k vs 122k ops/sec)というトレードオフがあります。

本記事では、SurrealDB 3.1(2026年6月時点の最新版v3.1.5)のスキーマ設計パターンを実践的に解説します。「何を選ぶか」ではなく**「どう設計するか」**に焦点を当て、グラフ・ドキュメント・ベクトル検索を1つのDBで統合するスキーマ設計の具体例を示します。

マルチモデルDBのリレーション設計パターンを理解する

SurrealDBでスキーマを設計する際、最初に理解すべきは3つのリレーションモデリング手法です。RDB(リレーショナルデータベース)の外部キーに相当する概念ですが、それぞれ異なるユースケースに適しています。

Record Links:軽量な直接参照

Record Linksは、レコードIDを直接フィールドに埋め込む方法です。RDBの外部キーに近い概念ですが、JOINなしで参照先のデータを取得できる点が異なります。PyTorchでいえば、torch.Tensorのビュー(view)のように、データのコピーを作らず元データへの直接参照を保持するイメージです。

-- ユーザーが所属するチームへの直接参照
CREATE user:alice SET
    name = "Alice",
    team = team:ml_platform,
    skills = ["Python", "PyTorch", "SurrealDB"];

CREATE team:ml_platform SET
    name = "ML Platform Team",
    department = "Engineering";

-- JOINなしでチーム名を取得(ディスクへの直接アクセス)
SELECT name, team.name AS team_name FROM user:alice;
-- 結果: { name: "Alice", team_name: "ML Platform Team" }

-- 多段階の参照も可能(チーム → 部門 → 会社)
SELECT team.department FROM user:alice;

Record Linksの特徴:

特性 詳細
参照方式 レコードIDの直接埋め込み
参照整合性 なし(参照先が削除されてもリンクは残る)
パフォーマンス テーブルスキャン不要の直接ディスクアクセス
適用場面 1対1・1対多の単純な参照関係

注意点:

Record Linksは参照整合性を保証しません。参照先のレコードが削除されても、リンク元のフィールドには古いIDが残り続けます。RDBのON DELETE CASCADEのような自動クリーンアップが必要な場合は、次に紹介するRecord Referencesを使ってください。

Record References:双方向追跡つき参照

Record Referencesは、SurrealDB 3.x(v2.1.0以降)で導入された参照方式です。Record Linksとの違いは、逆方向の参照を自動的に追跡できる点です。スキーマ定義でREFERENCEキーワードを指定すると、<~構文で逆引きが可能になります。

-- スキーマ定義:双方向参照を設定
DEFINE TABLE experiment SCHEMAFULL;
DEFINE FIELD name ON experiment TYPE string;
DEFINE FIELD models ON experiment TYPE option<array<record<ml_model>>> REFERENCE;

DEFINE TABLE ml_model SCHEMAFULL;
DEFINE FIELD name ON ml_model TYPE string;
DEFINE FIELD framework ON ml_model TYPE string;
-- 逆方向の参照を計算フィールドとして定義
DEFINE FIELD used_in ON ml_model COMPUTED <~(experiment FIELD models);

-- データ挿入
CREATE ml_model:bert_v2 SET name = "BERT-v2", framework = "PyTorch";
CREATE ml_model:gpt_ft SET name = "GPT-finetuned", framework = "PyTorch";
CREATE experiment:exp_001 SET
    name = "Sentiment Analysis Comparison",
    models = [ml_model:bert_v2, ml_model:gpt_ft];

-- 逆引き:このモデルはどの実験で使われている?
SELECT name, used_in.name AS experiments FROM ml_model:bert_v2;
-- 結果: { name: "BERT-v2", experiments: ["Sentiment Analysis Comparison"] }

Record Referencesでは、参照先レコードの削除時の動作をON DELETE句で制御できます。

DEFINE FIELD models ON experiment
    TYPE option<array<record<ml_model>>>
    REFERENCE ON DELETE REJECT;
-- REJECT: 参照されているモデルの削除を拒否
-- CASCADE: 実験ごと削除
-- UNSET: フィールドをnullに設定
-- IGNORE: リンクを残す(デフォルト)

Graph Edges:メタデータ付きリレーション

Graph Edgesは、RELATE文で作成するリレーション専用のテーブルです。Record LinksやReferencesとの違いは、リレーション自体にデータを持てる点です。「誰が」「何を」だけでなく、「いつ」「どのように」「どの程度」といったメタデータをエッジに格納できます。

-- エッジテーブルの定義(型制約付き)
DEFINE TABLE trained_on TYPE RELATION FROM ml_model TO dataset SCHEMAFULL;
DEFINE FIELD epochs ON trained_on TYPE int;
DEFINE FIELD loss ON trained_on TYPE float;
DEFINE FIELD trained_at ON trained_on TYPE datetime;
DEFINE FIELD hyperparams ON trained_on TYPE object;

-- データセットとモデルの作成
CREATE dataset:imdb_reviews SET name = "IMDB Reviews", size = 50000;
CREATE ml_model:sentiment_v1 SET name = "Sentiment-v1", framework = "PyTorch";

-- グラフエッジの作成(学習履歴をメタデータとして格納)
RELATE ml_model:sentiment_v1->trained_on->dataset:imdb_reviews SET
    epochs = 10,
    loss = 0.023,
    trained_at = d"2026-06-15T10:00:00Z",
    hyperparams = {
        learning_rate: 0.0001,
        batch_size: 32,
        optimizer: "AdamW"
    };

-- グラフ走査:モデルの学習履歴を取得
SELECT
    name,
    ->trained_on AS training_history,
    ->trained_on.loss AS losses,
    ->trained_on->dataset.name AS datasets
FROM ml_model:sentiment_v1;

3パターンの使い分け判断フロー

パターン 適用場面 パフォーマンス 参照整合性
Record Links 単純な1対1/1対多参照 高(直接ディスクアクセス) なし
Record References 双方向参照、削除制御が必要 中(逆引きインデックス) あり(ON DELETE制御)
Graph Edges メタデータ付きリレーション 低〜中(エッジテーブル経由) あり(TYPE RELATION制約)

よくある間違い: 最初から全リレーションをGraph Edgesで設計してしまうケースがあります。Graph Edgesはエッジごとに専用テーブルのレコードが作られるため、単純な参照関係には過剰です。「ユーザー → チーム」のような静的な所属関係にはRecord Linksで十分であり、「ユーザー →(いつ、どの役割で)→ プロジェクト」のようにメタデータが必要な場合にGraph Edgesを使うのが適切です。

SCHEMAFULL×SCHEMALESSのハイブリッド設計を実装する

SurrealDBでは、テーブルごとにSCHEMAFULL(厳密なスキーマ強制)とSCHEMALESS(柔軟なスキーマ)を選択できます。さらに、SCHEMAFULLテーブル内でもFLEXIBLE TYPEを使って特定フィールドだけスキーマレスにできます。この組み合わせが、マルチモデルDB設計の要です。

設計原則:クエリパターンでスキーマモードを選ぶ

スキーマモードの選択は「データの性質」ではなく「クエリパターン」で決めます。

-- ■ SCHEMAFULL:構造化データ + 頻繁にフィルタ/集計するテーブル
DEFINE TABLE ml_model SCHEMAFULL;
DEFINE FIELD name ON ml_model TYPE string;
DEFINE FIELD version ON ml_model TYPE string;
DEFINE FIELD framework ON ml_model TYPE string
    ASSERT $value IN ["PyTorch", "TensorFlow", "JAX", "ONNX"];
DEFINE FIELD status ON ml_model TYPE string
    ASSERT $value IN ["training", "evaluating", "deployed", "archived"];
DEFINE FIELD created_at ON ml_model TYPE datetime
    VALUE time::now() READONLY;

-- ユニークインデックス
DEFINE INDEX idx_model_name_version ON ml_model
    FIELDS name, version UNIQUE;

-- ■ SCHEMALESS:構造が変動するデータ(実験ログ、メトリクス)
DEFINE TABLE experiment_log SCHEMALESS;
-- 最低限のフィールドだけ定義
DEFINE FIELD experiment_id ON experiment_log TYPE record<experiment>;
DEFINE FIELD logged_at ON experiment_log TYPE datetime
    VALUE time::now() READONLY;
-- それ以外のフィールド(metrics, config, notes等)は自由に追加可能

-- ■ ハイブリッド:SCHEMAFULLテーブルに柔軟なフィールドを混在
DEFINE TABLE dataset SCHEMAFULL;
DEFINE FIELD name ON dataset TYPE string;
DEFINE FIELD format ON dataset TYPE string
    ASSERT $value IN ["parquet", "csv", "jsonl", "arrow", "tfrecord"];
DEFINE FIELD size ON dataset TYPE int;
-- metadataフィールドだけスキーマレス(国ごとに異なるラベル体系等)
DEFINE FIELD metadata ON dataset FLEXIBLE TYPE object;

なぜこの設計を選んだか:

  • ml_modelテーブル: ステータスやフレームワークで頻繁にフィルタするため、ASSERTで値を制約してクエリの安全性を確保
  • experiment_logテーブル: 実験ごとに記録するメトリクスの種類が異なるため、スキーマレスが適切
  • datasetテーブル: 基本構造は固定だが、メタデータは柔軟性が必要なためFLEXIBLE TYPEで対応

実践例:MLモデル管理システムのスキーマ

MLパイプラインで必要なテーブル群を統合設計した例を示します。

-- ========================================
-- 名前空間とデータベースの作成
-- ========================================
DEFINE NAMESPACE ml_platform;
USE NS ml_platform;
DEFINE DATABASE model_registry;
USE DB model_registry;

-- ========================================
-- コアテーブル定義
-- ========================================

-- モデルテーブル(SCHEMAFULL)
DEFINE TABLE ml_model SCHEMAFULL;
DEFINE FIELD name ON ml_model TYPE string;
DEFINE FIELD version ON ml_model TYPE string;
DEFINE FIELD framework ON ml_model TYPE string
    ASSERT $value IN ["PyTorch", "TensorFlow", "JAX", "ONNX"];
DEFINE FIELD architecture ON ml_model TYPE string;
DEFINE FIELD parameters ON ml_model TYPE int;
DEFINE FIELD status ON ml_model TYPE string
    ASSERT $value IN ["training", "evaluating", "deployed", "archived"];
DEFINE FIELD created_at ON ml_model TYPE datetime VALUE time::now() READONLY;
DEFINE FIELD tags ON ml_model TYPE array<string>;
-- embedding:ベクトル検索用(モデルカードの意味表現)
DEFINE FIELD description ON ml_model TYPE string;
DEFINE FIELD embedding ON ml_model TYPE array<float> ASSERT array::len($value) = 768;

-- ベクトルインデックス(モデル類似検索用)
DEFINE INDEX idx_model_embedding ON ml_model
    FIELDS embedding HNSW DIMENSION 768 DIST COSINE TYPE F32;

-- データセットテーブル(ハイブリッド)
DEFINE TABLE dataset SCHEMAFULL;
DEFINE FIELD name ON dataset TYPE string;
DEFINE FIELD format ON dataset TYPE string;
DEFINE FIELD size ON dataset TYPE int;
DEFINE FIELD split ON dataset TYPE string
    ASSERT $value IN ["train", "validation", "test"];
DEFINE FIELD metadata ON dataset FLEXIBLE TYPE object;

-- 実験テーブル(SCHEMAFULL)
DEFINE TABLE experiment SCHEMAFULL;
DEFINE FIELD name ON experiment TYPE string;
DEFINE FIELD hypothesis ON experiment TYPE string;
DEFINE FIELD status ON experiment TYPE string
    ASSERT $value IN ["planned", "running", "completed", "failed"];
DEFINE FIELD started_at ON experiment TYPE option<datetime>;
DEFINE FIELD completed_at ON experiment TYPE option<datetime>;
DEFINE FIELD models ON experiment TYPE option<array<record<ml_model>>> REFERENCE;

-- 実験ログ(SCHEMALESS:メトリクスの種類が実験ごとに異なる)
DEFINE TABLE experiment_log SCHEMALESS;
DEFINE FIELD experiment_id ON experiment_log TYPE record<experiment>;
DEFINE FIELD epoch ON experiment_log TYPE int;
DEFINE FIELD logged_at ON experiment_log TYPE datetime VALUE time::now() READONLY;

-- ========================================
-- グラフエッジ定義(リレーション)
-- ========================================

-- モデル → データセット:学習関係
DEFINE TABLE trained_on TYPE RELATION FROM ml_model TO dataset SCHEMAFULL;
DEFINE FIELD epochs ON trained_on TYPE int;
DEFINE FIELD final_loss ON trained_on TYPE float;
DEFINE FIELD duration_sec ON trained_on TYPE int;
DEFINE FIELD trained_at ON trained_on TYPE datetime;
DEFINE FIELD hyperparams ON trained_on FLEXIBLE TYPE object;

-- モデル → モデル:派生関係(fine-tuning元の追跡)
DEFINE TABLE derived_from TYPE RELATION FROM ml_model TO ml_model SCHEMAFULL;
DEFINE FIELD method ON derived_from TYPE string
    ASSERT $value IN ["fine-tune", "distillation", "pruning", "quantization"];
DEFINE FIELD delta_params ON derived_from TYPE option<int>;

-- ========================================
-- イベントトリガー定義
-- ========================================

-- モデルのステータスが"deployed"に変わったら通知
DEFINE EVENT model_deployed ON TABLE ml_model
    WHEN $before.status != "deployed" AND $after.status = "deployed"
    THEN {
        CREATE notification SET
            type = "model_deployed",
            model = $after.id,
            model_name = $after.name,
            message = "Model " + $after.name + " v" + $after.version + " deployed",
            created_at = time::now();
    };

制約条件:

このスキーマ設計はSurrealDB 3.1.x以降を前提としています。v2.x以前ではREFERENCEキーワードやCOMPUTEDフィールドが未対応のため、Record Referencesパターンは使用できません。

ハイブリッド検索を実装する:BM25×HNSW×RRFの統合

マルチモデルDBの強みが発揮される場面の1つが、全文検索とベクトル検索の統合です。従来はElasticsearchで全文検索、Pineconeでベクトル検索と別々のDBを運用していたところを、SurrealDBでは単一のSurrealQLクエリで両方を実行し、RRF(Reciprocal Rank Fusion)で結果を融合できます。

インデックス定義:BM25とHNSWの両立

-- アナライザー定義(日本語対応は限定的、英語のSnowballステミング)
DEFINE ANALYZER model_analyzer
    TOKENIZERS blank, class, camel, punct
    FILTERS lowercase, snowball(english);

-- BM25全文検索インデックス
DEFINE INDEX idx_model_ft_name ON ml_model
    FIELDS name FULLTEXT ANALYZER model_analyzer BM25;
DEFINE INDEX idx_model_ft_desc ON ml_model
    FIELDS description FULLTEXT ANALYZER model_analyzer BM25(1.2, 0.75);
DEFINE INDEX idx_model_ft_arch ON ml_model
    FIELDS architecture FULLTEXT ANALYZER model_analyzer BM25;

-- HNSWベクトルインデックス(既に定義済み、再掲)
DEFINE INDEX idx_model_embedding ON ml_model
    FIELDS embedding HNSW DIMENSION 768 DIST COSINE TYPE F32;

BM25の引数(1.2, 0.75)は、パラメータk1(単語頻度の飽和度)とb(文書長の正規化度)です。デフォルト値のままで多くの場合十分ですが、短いテキスト(モデル名など)ではb=0.3程度に下げると短文への偏りを軽減できます。

ハイブリッド検索クエリの実装

-- ハイブリッド検索:BM25 + HNSW + RRF fusion
-- 入力: $query(テキスト), $embedding(768次元ベクトル)

-- Step 1: BM25全文検索(キーワードマッチ)
LET $ft_results = SELECT
    id, name, description, architecture, status,
    (
        (search::score(0) * 20) +
        (search::score(1) * 10) +
        (search::score(2) * 15)
    ) AS ft_score
FROM ml_model
WHERE name @0@ $query
   OR description @1@ $query
   OR architecture @2@ $query
ORDER BY ft_score DESC
LIMIT 20;

-- Step 2: HNSWベクトル検索(意味的類似性)
LET $vs_results = SELECT
    id, name, description, architecture, status,
    vector::distance::knn() AS distance
FROM ml_model
WHERE embedding <|20,100|> $embedding
ORDER BY distance ASC
LIMIT 20;

-- Step 3: RRF融合(k=60で上位30件)
LET $fused = search::rrf([$ft_results, $vs_results], 60, 30);

RETURN $fused;

<|20,100|>は、HNSWインデックスから20件を返し、探索時に100候補を考慮するという意味です。第2引数(ef_search)を大きくすると精度が上がりますが、検索時間も増加します。SurrealDB公式ブログの実例では、<|30,100|>の設定でBM25 + HNSW融合クエリが約1.35秒で実行されたと報告されています。

なぜRRFを選んだか:

  • RRF(Reciprocal Rank Fusion)は各ランキングリストの順位のみを使うため、BM25スコアとベクトル距離のスケールが異なっても正規化不要
  • 代替手法であるConvex Combination(重み付き線形結合)はスコアの正規化が必要で、チューニングコストが高い
  • search::rrf()はSurrealDB組み込み関数のため、アプリケーション側での実装が不要

ハマりポイント:

HNSWインデックスのメモリキャッシュサイズはデフォルト256MiBです。大量のベクトルを扱う場合はSURREAL_HNSW_CACHE_SIZE環境変数で調整が必要です。キャッシュ不足時にはディスクフォールバックが発生し、検索レイテンシが数倍に増加します。

ナレッジグラフRAGへの応用

ハイブリッド検索にグラフ走査を組み合わせると、単純なベクトル検索では得られない構造化されたコンテキストをLLMに渡せます。SurrealDBの公式ブログで紹介されているKnowledge Graph RAGの2つのパターンを、MLモデル管理に適用した例を示します。

-- パターン1: コンセプトベース検索
-- ベクトル検索でモデルを見つけ、グラフ走査で関連情報を収集

-- Step 1: 類似モデルをベクトル検索で取得
LET $similar_models = SELECT
    id, name, version, status,
    (1 - vector::distance::knn()) AS similarity
FROM ml_model
WHERE embedding <|5,40|> $query_embedding;

-- Step 2: グラフ走査で学習履歴・派生関係を取得
LET $context = SELECT
    name, version, status,
    ->trained_on.{ epochs, final_loss, trained_at } AS training,
    ->trained_on->dataset.{ name, size } AS datasets,
    <-derived_from<-ml_model.{ name, version } AS parent_models,
    ->derived_from->ml_model.{ name, version } AS child_models
FROM $similar_models.id;

-- Step 3: LLMに渡すコンテキストとして整形
RETURN $context;

FaunaDB終了から学ぶDBaaS依存リスクへの設計対策を整理する

2025年3月、FaunaDBはサービス終了を発表し、同年5月30日にサービスを停止しました。80,000以上の開発チームが影響を受け、DBaaS(Database as a Service)への依存リスクが改めて浮き彫りになりました。

FaunaDB終了の経緯と教訓

FaunaDBの終了理由は「技術的な問題」ではなく「資金調達の困難」でした。公式発表では「グローバルなDBaaSの広範な採用を推進するには多大な資本が必要であり、現在の市場環境では独立してその目標を達成するための資金調達が困難」と説明されています

この事例から得られるMLエンジニア向けの教訓は以下の通りです。

教訓 詳細 設計上の対策
技術的優位性≠事業継続性 FaunaDBはCalvin論文ベースの分散トランザクションで技術的に先進的だったが、収益化に失敗 DB選定時にGitHubスター数・コミュニティサイズ・資金調達状況も評価指標に含める
プロプライエタリDBaaSのロックイン FaunaQL(独自クエリ言語)への依存が移行コストを増大 標準SQL互換性の高いDBを選ぶか、DAL(データアクセスレイヤー)で抽象化
OSS化の約束は保証ではない FaunaDBはOSS化を計画したが、2026年6月時点でコア技術の公開は限定的 セルフホスト可能なDBを優先的に選択

SurrealDBのリスク評価

SurrealDBも同様のリスクを持つスタートアップです。2026年2月に2300万ドルの資金調達を完了していますが、FaunaDBの教訓を踏まえると以下のリスク軽減策が有効です。

-- リスク軽減策1: DAL(データアクセスレイヤー)パターン
-- アプリケーションコードとDB固有の構文を分離

-- SurrealDB固有のクエリをリポジトリパターンで抽象化(Python例)
# repository.py - DB固有の実装を隠蔽するDALパターン
from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class Model:
    id: str
    name: str
    version: str
    framework: str
    status: str

class ModelRepository(ABC):
    @abstractmethod
    async def find_similar(self, embedding: list[float], limit: int = 5) -> list[Model]:
        raise NotImplementedError

    @abstractmethod
    async def get_training_history(self, model_id: str) -> list[dict]:
        raise NotImplementedError

class SurrealDBModelRepository(ModelRepository):
    """SurrealDB固有の実装"""

    def __init__(self, client):
        self.client = client

    async def find_similar(self, embedding: list[float], limit: int = 5) -> list[Model]:
        result = await self.client.query(
            f"""SELECT id, name, version, framework, status,
                   (1 - vector::distance::knn()) AS similarity
               FROM ml_model
               WHERE embedding <|{limit},100|> $embedding
               ORDER BY similarity DESC""",
            {"embedding": embedding}
        )
        return [Model(**r) for r in result]

    async def get_training_history(self, model_id: str) -> list[dict]:
        result = await self.client.query(
            """SELECT
                   ->trained_on.{ epochs, final_loss, trained_at } AS training,
                   ->trained_on->dataset.name AS dataset_name
               FROM $model_id""",
            {"model_id": model_id}
        )
        return result

# 将来PostgreSQL+pgvectorに移行する場合も、
# PostgresModelRepository を実装するだけでアプリケーションコードは変更不要

トレードオフ: DALパターンを導入するとSurrealDB固有の強力なグラフ走査構文(->trained_on->dataset.nameのような連鎖)を抽象化しにくくなります。グラフクエリが中心のユースケースでは、完全な抽象化よりも「移行時にリライトするクエリの一覧を管理する」アプローチが現実的です。

セルフホスト vs クラウド:デプロイ戦略

-- リスク軽減策2: セルフホストデプロイ
-- SurrealDBはシングルバイナリでセルフホスト可能
-- Docker / Kubernetes での運用が公式サポートされている
# docker-compose.yml - SurrealDBセルフホスト構成
services:
  surrealdb:
    image: surrealdb/surrealdb:v3.1.5
    command: start --log info --user root --pass root file:/data/srdb.db
    ports:
      - "8000:8000"
    volumes:
      - surrealdb_data:/data
    environment:
      - SURREAL_HNSW_CACHE_SIZE=512MiB
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "surreal", "isready", "--conn", "http://localhost:8000"]
      interval: 10s
      timeout: 5s
      retries: 3

volumes:
  surrealdb_data:
    driver: local

注意点:

SurrealDBのファイルストレージバックエンド(RocksDB)はシングルノード構成のみです。分散構成が必要な場合は、SurrealDB Enterprise版のSurrealDS(分散ストレージエンジン)が必要になります。個人プロジェクトや小〜中規模のチームであれば、シングルノードで十分な性能(CRUD 141k ops/sec)が得られます。

AIエージェントのコンテキスト管理基盤を構築する

SurrealDBは2026年に入り、「AIエージェント向けコンテキストレイヤー」としてのポジショニングを強化しています。グラフ(関係性)×ドキュメント(非構造化データ)×ベクトル(意味検索)を1つのACIDトランザクション内で扱えるため、エージェントの記憶管理に適しています。

エージェントメモリのスキーマ設計

SurrealDBの公式ドキュメントでは、エージェントメモリを6種類に分類しています。これをSurrealQLのスキーマとして実装する例を示します。

-- ========================================
-- エージェントメモリ スキーマ
-- ========================================

-- セマンティックメモリ(知識・事実のグラフ)
DEFINE TABLE knowledge SCHEMAFULL;
DEFINE FIELD content ON knowledge TYPE string;
DEFINE FIELD category ON knowledge TYPE string;
DEFINE FIELD confidence ON knowledge TYPE float
    ASSERT $value >= 0.0 AND $value <= 1.0;
DEFINE FIELD source ON knowledge TYPE string;
DEFINE FIELD embedding ON knowledge TYPE array<float>
    ASSERT array::len($value) = 768;
DEFINE FIELD created_at ON knowledge TYPE datetime VALUE time::now() READONLY;
DEFINE FIELD valid_from ON knowledge TYPE datetime;
DEFINE FIELD valid_until ON knowledge TYPE option<datetime>;

-- ベクトル + 全文検索インデックス
DEFINE ANALYZER knowledge_analyzer TOKENIZERS blank, class FILTERS lowercase;
DEFINE INDEX idx_knowledge_vec ON knowledge
    FIELDS embedding HNSW DIMENSION 768 DIST COSINE TYPE F32;
DEFINE INDEX idx_knowledge_ft ON knowledge
    FIELDS content FULLTEXT ANALYZER knowledge_analyzer BM25;

-- エピソードメモリ(過去のインタラクション記録)
DEFINE TABLE episode SCHEMAFULL;
DEFINE FIELD agent_id ON episode TYPE string;
DEFINE FIELD user_query ON episode TYPE string;
DEFINE FIELD response ON episode TYPE string;
DEFINE FIELD outcome ON episode TYPE string
    ASSERT $value IN ["success", "partial", "failure", "unknown"];
DEFINE FIELD occurred_at ON episode TYPE datetime VALUE time::now() READONLY;
DEFINE FIELD embedding ON episode TYPE array<float>
    ASSERT array::len($value) = 768;

DEFINE INDEX idx_episode_vec ON episode
    FIELDS embedding HNSW DIMENSION 768 DIST COSINE TYPE F32;

-- 知識間の関係(グラフエッジ)
DEFINE TABLE relates_to TYPE RELATION FROM knowledge TO knowledge SCHEMAFULL;
DEFINE FIELD relation_type ON relates_to TYPE string
    ASSERT $value IN ["causes", "contradicts", "supports", "extends", "replaces"];
DEFINE FIELD strength ON relates_to TYPE float
    ASSERT $value >= 0.0 AND $value <= 1.0;

-- エピソードから学んだ知識への参照
DEFINE TABLE learned_from TYPE RELATION FROM knowledge TO episode SCHEMAFULL;
DEFINE FIELD extraction_method ON learned_from TYPE string;

マルチホップ検索:グラフ走査×ベクトル検索の統合クエリ

エージェントが「Transformerのattention機構に関する知識」を検索する場合、単純なベクトル検索では直接的に類似した知識しか取得できません。グラフ走査を組み合わせると、関連する知識を芋づる式に取得できます。

-- マルチホップ知識検索
-- Step 1: ベクトル検索で起点となる知識を取得
LET $seeds = SELECT
    id, content, category, confidence,
    (1 - vector::distance::knn()) AS similarity
FROM knowledge
WHERE embedding <|3,50|> $query_embedding
  AND confidence >= 0.7
  AND (valid_until IS NONE OR valid_until > time::now());

-- Step 2: 1ホップ先の関連知識を取得(グラフ走査)
LET $related = SELECT
    id, content, category, confidence,
    ->relates_to.relation_type AS relation,
    ->relates_to.strength AS edge_strength,
    ->relates_to->knowledge.{
        id, content, category, confidence
    } AS neighbors
FROM $seeds.id;

-- Step 3: 矛盾する知識がないかチェック
LET $contradictions = SELECT
    in.content AS claim_a,
    out.content AS claim_b,
    strength
FROM relates_to
WHERE relation_type = "contradicts"
  AND (in IN $seeds.id OR out IN $seeds.id)
  AND strength >= 0.8;

RETURN {
    direct_matches: $seeds,
    related_knowledge: $related,
    contradictions: $contradictions
};

制約条件:

この設計はSurrealDBのシングルノード構成での利用を想定しています。知識ベースが100万レコードを超える規模では、HNSWインデックスのメモリ消費量が問題になる可能性があります。SurrealDBのHNSWキャッシュサイズ(デフォルト256MiB)を超えるとディスクフォールバックが発生するため、大規模運用時はSURREAL_HNSW_CACHE_SIZEの適切な設定と、不要な知識の定期的なアーカイブ(valid_untilフィールドでの期限管理)が必要です。

よくある問題と解決方法

問題 原因 解決方法
RELATEで「table not found」エラー エッジテーブルをTYPE RELATIONで定義していない DEFINE TABLE edge_name TYPE RELATION FROM a TO b;を先に実行
SCHEMAFULLテーブルへのINSERTが拒否される 未定義フィールドを含むデータを挿入 柔軟性が必要なフィールドにFLEXIBLE TYPEを指定
ベクトル検索の結果が空 HNSWインデックスが未構築または次元数の不一致 DEFINE INDEXDIMENSIONがembeddingの次元数と一致しているか確認
グラフ走査が遅い 深い再帰走査(5ホップ以上)の実行 @.{1..3}のように深さを制限、またはLIMIT句を追加
search::rrf()の結果が期待と異なる BM25とHNSWの結果数が極端に異なる 両方のLIMITを揃える(例: 両方20件)
Record Referencesの逆引きが動作しない REFERENCEキーワードの付け忘れ フィールド定義にREFERENCEを追加(v2.1.0+必須)

まとめと次のステップ

まとめ:

  • SurrealDBの3つのリレーションパターン(Record Links / Record References / Graph Edges)は、用途に応じて使い分けることでパフォーマンスと保守性のバランスを取れる
  • SCHEMAFULL×SCHEMALESSのハイブリッド設計により、ML実験ログのような柔軟なデータとモデル管理のような厳密なデータを1つのDBで共存させられる
  • BM25 + HNSW + RRFのハイブリッド検索は、単一SurrealQLクエリで実行可能であり、従来の2DB構成(Elasticsearch + ベクトルDB)を置き換えられる
  • FaunaDBの終了事例は「技術的優位性≠事業継続性」を示しており、DALパターンやセルフホスト戦略でロックインリスクを軽減すべき
  • SurrealDB 3.1のグラフ走査性能は前バージョン比8〜22倍に改善されているが、単一レコードReadではPostgreSQLに劣るトレードオフがある

次にやるべきこと:

参考


注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?