GraphRAG とは何か ― ベクトルRAGとの違いから理解する
はじめに
ベクトルRAG(埋め込みベクトルによる類似検索)の基本は理解している前提で、GraphRAG が何を解決しようとしているのかを整理します。
ベクトルRAG の概要、デメリット
ベクトルRAGの処理フローを以下に示します。
① 事前処理:
文書 → チャンク分割 → ベクトル化 → ベクトルDB に保存
② 質問時:
質問 → ベクトル化 → 類似チャンクを検索 → LLM に渡す → 回答
しかし「意味的な近さ」で検索するため、チャンク間の関係・構造が失われるという弱点があります。
具体例
以下のような文書があるとします:
「Alice は Acme Corp のエンジニアで、Bob と共同でプロジェクトXを進めている。
プロジェクトXは Globex 社の製品と競合している。
Globex の CEO は Carol で、Alice の元同僚だ。」
質問: 「Aliceの競合相手のCEOは誰?」
ベクトルRAG:
チャンク①: Alice は Acme Corp のエンジニア...
チャンク②: プロジェクトXは Globex と競合...
チャンク③: Globex の CEO は Carol...
→ 各チャンクはバラバラに保存されているため
「Alice → プロジェクトX → Globex → Carol」
という繋がりを辿れない
GraphRAG が解決すること
文書からエンティティと関係を抽出し、グラフ構造として保存することで繋がりを保持します。
GraphRAG:
(Alice)-[:WORKS_ON]->(Project X)-[:COMPETES_WITH]->(Globex)
(Carol)-[:CEO_OF]->(Globex)
→ グラフを3ホップたどれば Carol に辿り着く
| ベクトルRAG | GraphRAG | |
|---|---|---|
| データ表現 | テキストの断片 | ノードとリレーションシップ |
| 得意な質問 | 「〇〇とは何か」的な局所的質問 | 関係・文脈・全体俯瞰が必要な質問 |
| 多ホップ推論 | 苦手 | 得意 |
| コスト | 低い | エンティティ抽出にLLMが必要 |
エンティティとは
エンティティとは文章中の固有の概念・物事のことです。
| 種類 | 例 |
|---|---|
| 人物 | Alice、Bob、Carol |
| 組織 | Acme Corp、Globex |
| 場所 | 東京、シリコンバレー |
| 製品 | プロジェクトX、iPhone |
| 概念 | 機械学習、規制、金融 |
GraphRAG ではこれらを LLM で自動抽出してグラフのノードとして格納します。
文章: 「Alice は Acme Corp でPythonを使って機械学習モデルを開発している」
抽出:
ノード: Alice(Person), Acme Corp(Company), Python(Skill), 機械学習(Concept)
関係: (Alice)-[:WORKS_AT]->(Acme Corp)
(Alice)-[:USES]->(Python)
(Alice)-[:WORKS_ON]->(機械学習)
Local Search と Global Search
GraphRAG の検索戦略は2種類あります。
Local Search
特定のエンティティを起点に、周辺情報を集める検索です。
質問: 「Aliceについて教えて」
① Alice というエンティティを特定
② Alice から k-hop で周辺を探索
(Alice)-[:RELATED_TO {relation: "WORKS_AT"}]->(Acme Corp)
(Alice)-[:RELATED_TO {relation: "COLLABORATES_WITH"}]->(Bob)
③ 収集した情報をLLMに渡す → 回答生成
Cypher の可変長パス [*1..2] がそのまま使われます。
Global Search
グラフ全体を俯瞰して答える検索です。
質問: 「このドキュメント群の主要テーマは?」
① 事前に Louvain法でコミュニティを検出・サマリーを生成済み
② 関連するコミュニティのサマリーをLLMに渡す → 全体を要約して回答
使い分け
| 質問の種類 | 向いている検索 |
|---|---|
| 「〇〇とは何か」「〇〇について教えて」 | Local Search |
| 「全体的なテーマは?」「共通点は?」 | Global Search |
| 「〇〇と〇〇の関係は?」 | Local Search(多ホップ) |
Louvain 法とは
Global Search の基盤となるコミュニティ検出アルゴリズムです。グラフ上で「密に繋がっているノードのグループ」を自動で見つけます。
Before:
(Alice)-(Bob)-(Carol) (Dave)-(Eve)-(Frank)
↑ 互いに密に繋がっている ↑ 互いに密に繋がっている
After Louvain:
[コミュニティA: Alice, Bob, Carol]
[コミュニティB: Dave, Eve, Frank]
「モジュラリティ」という指標を最大化することで、パラメータ指定なしに自動でグループ数と構成を決めます。
Microsoft GraphRAG の処理フロー
Indexing フェーズ(事前処理・1回だけ実行)
① 文書の読み込み
② エンティティ・関係の抽出(LLM使用)
テキスト → (Entity)-[:RELATION]->(Entity)
③ グラフDB(Neo4j等)に格納
④ Louvain法でコミュニティ検出
⑤ コミュニティごとにサマリー生成(LLM使用)
→ サマリーをベクトル化して保存
Query フェーズ(検索時)
Local Search:
質問 → エンティティ特定 → k-hop探索 → コンテキスト収集 → LLM → 回答
Global Search:
質問 → ベクトル類似度でサマリー検索 → 関連コミュニティのサマリー取得 → LLM → 回答
GraphRAG はベクトルとグラフのハイブリッド
GraphRAG は「グラフ OR ベクトル」ではなく、両方を組み合わせた構造です。
| 用途 | 手法 |
|---|---|
| エンティティ周辺の探索(Local) | グラフ traversal(k-hop) |
| コミュニティサマリーの検索(Global) | ベクトル類似度検索 |
| エンティティ自体の類似検索 | ベクトル類似度検索 |
ベクトルRAGの経験がある場合は「グラフ構造で関係を保持しつつ、検索にはベクトルも併用する拡張版RAG」と捉えると理解しやすいです。
Neo4j で動かしてみる
知識グラフの手動構築
以下のような文書を想定してエンティティグラフを構築します。
「Alice は Acme Corp のエンジニアで、Bob と共同でプロジェクトXを進めている。
プロジェクトXは Globex 社の製品と競合している。
Globex の CEO は Carol で、Alice の元同僚だ。」
GraphRAG では全エンティティを統一ラベル Entity で管理し、リレーションシップには context(元の文脈)と relation(関係種別)をプロパティとして持たせます。
// ノード作成
CREATE (alice:Entity {name: "Alice", type: "Person"})
CREATE (bob:Entity {name: "Bob", type: "Person"})
CREATE (carol:Entity {name: "Carol", type: "Person"})
CREATE (acme:Entity {name: "Acme Corp", type: "Organization"})
CREATE (globex:Entity {name: "Globex", type: "Organization"})
CREATE (projX:Entity {name: "Project X", type: "Product"})
// リレーションシップ作成(context に元の文脈を埋め込む)
MATCH (alice:Entity {name: "Alice"}), (acme:Entity {name: "Acme Corp"})
CREATE (alice)-[:RELATED_TO {relation: "WORKS_AT", context: "Alice は Acme Corp のエンジニア"}]->(acme);
MATCH (alice:Entity {name: "Alice"}), (bob:Entity {name: "Bob"})
CREATE (alice)-[:RELATED_TO {relation: "COLLABORATES_WITH", context: "Alice と Bob は共同でプロジェクトXを進めている"}]->(bob);
MATCH (alice:Entity {name: "Alice"}), (projX:Entity {name: "Project X"})
CREATE (alice)-[:RELATED_TO {relation: "WORKS_ON", context: "Alice は Project X に取り組んでいる"}]->(projX);
MATCH (projX:Entity {name: "Project X"}), (globex:Entity {name: "Globex"})
CREATE (projX)-[:RELATED_TO {relation: "COMPETES_WITH", context: "Project X は Globex の製品と競合"}]->(globex);
MATCH (carol:Entity {name: "Carol"}), (globex:Entity {name: "Globex"})
CREATE (carol)-[:RELATED_TO {relation: "CEO_OF", context: "Carol は Globex の CEO"}]->(globex);
MATCH (alice:Entity {name: "Alice"}), (carol:Entity {name: "Carol"})
CREATE (alice)-[:RELATED_TO {relation: "FORMER_COLLEAGUE", context: "Alice と Carol は元同僚"}]->(carol);
k-hop 探索でコンテキスト収集(Local Search の実装)
「Alice の競合相手の CEO は誰か?」という質問を、グラフ探索で解いてみます。
// Alice を起点に 3ホップ以内の Person を無向探索で収集
MATCH path = (alice:Entity {name: "Alice"})-[:RELATED_TO*1..3]-(target:Entity)
WHERE target.type = "Person" AND target.name <> "Alice"
RETURN
target.name AS answer,
[r IN relationships(path) | r.relation] AS path_taken,
length(path) AS hops
ORDER BY hops
実行結果:
| answer | path_taken | hops |
|---|---|---|
| Bob | [COLLABORATES_WITH] | 1 |
| Carol | [FORMER_COLLEAGUE] | 1 |
| Carol | [WORKS_ON, COMPETES_WITH, CEO_OF] | 3 |
Carol が2つの経路で見つかっていることが重要です。「元同僚」という直接の繋がりに加えて、「競合関係を辿った先のCEO」という文脈も収集できます。LLM にこの複数経路の context をまとめて渡すことで、より根拠のある回答が生成されます。
有向探索 -> と無向探索 - の使い分け
今回のグラフでは (Carol)-[:RELATED_TO]->(Globex) という向きでエッジを張りました。
有向 (->): Alice → ProjectX → Globex →❌ Carol
(Globex から Carol へのエッジが逆向きで存在しない)
無向 (-): Alice → ProjectX → Globex ←✅ Carol
(向きを無視して辿れる)
GraphRAG の Local Search では無向探索 - を使うのが定石です。エッジの向きに探索の正否を依存させると、データ投入の順序や設計次第で結果が変わってしまいます。エンティティ間の関係の方向性は relation プロパティの文字列("CEO_OF", "WORKS_AT" 等)で表現し、エッジの矢印は「同一ノードのペアを重複登録しないための管理上の向き」と割り切るのが実用的です。
Local Search のコンテキスト収集パターン
以下が GraphRAG の Local Search における標準的なクエリパターンです。
// エンティティ名でノードを特定し、k-hop で周辺コンテキストを収集
MATCH path = (start:Entity {name: $entity_name})-[:RELATED_TO*1..2]-(neighbor:Entity)
RETURN
neighbor.name AS entity,
neighbor.type AS type,
[r IN relationships(path) | r.context] AS context_chain
ORDER BY length(path)
収集した context_chain(文脈の連鎖)をそのまま LLM のプロンプトに組み込むことで、「グラフを辿って得た情報を根拠に回答する」という GraphRAG の基本動作が完成します。
GDS でコミュニティ検出(Global Search の基盤)
GDS(Graph Data Science) は Neo4j の公式プラグインで、Louvain・PageRank・Dijkstra などのグラフアルゴリズムをまとめて提供するライブラリです。
GDS の処理モデル
GDS はいきなり Neo4j のデータに対して処理するのではなく、一旦メモリ上にグラフをコピー(投影)してからアルゴリズムを実行します。
① project Neo4j(ディスク) → メモリ上グラフ
② 処理 メモリ上グラフ → アルゴリズム実行
③ write 計算結果 → Neo4j(ディスク)に書き戻し
何度もグラフを走査するアルゴリズムにとって、ディスクI/Oが発生するよりメモリ上で完結させた方が高速なためこの設計になっています。
Louvain でコミュニティを付与する
// ① グラフを投影(メモリ上にコピー)
CALL gds.graph.project(
'knowledge-graph',
'Entity',
{ RELATED_TO: { orientation: 'UNDIRECTED' } }
)
// ② Louvain を実行して communityId をノードに書き戻す
CALL gds.louvain.write('knowledge-graph', {
writeProperty: 'communityId'
})
YIELD communityCount, modularity
// ③ コミュニティごとにノードを確認
MATCH (n:Entity)
RETURN n.communityId AS community, collect(n.name) AS members
ORDER BY community
実行結果(例):
| community | members |
|---|---|
| 0 | [Alice, Bob, Acme Corp, Project X] |
| 1 | [Carol, Globex] |
Alice 側の業務上の密な繋がり(コミュニティ0)と、Carol・Globex の CEO 関係(コミュニティ1)が自動で分離されています。
Global Search との繋がり
コミュニティが検出できると、コミュニティごとにサマリーを生成することが可能になります。これが Global Search の基盤です。
コミュニティ0のノード: Alice, Bob, Acme Corp, Project X
→ LLM にまとめてサマリー生成
→ 「Acme Corp のエンジニアチームが Project X に取り組んでいる」
コミュニティ1のノード: Carol, Globex
→ LLM にまとめてサマリー生成
→ 「Carol は Globex の CEO として組織を率いている」
「このドキュメント群の主要テーマは?」のような全体俯瞰の質問が来たとき、LLM は各コミュニティのサマリーを読んで回答を生成します。個々のノードをすべて渡すのではなく、圧縮されたサマリーを使うことでトークン数を抑えつつ全体把握ができる点が Global Search の強みです。
ベクトル検索とグラフ探索のハイブリッド(Local Search の完成形)
Neo4j Vector Index とは
Neo4j に標準搭載されている機能で、ノードのプロパティにベクトルを格納し、コサイン類似度で近いノードを高速に検索できます。
// Vector Index の作成(次元数と類似度関数を指定)
CREATE VECTOR INDEX entity_embeddings IF NOT EXISTS
FOR (n:Entity)
ON n.embedding
OPTIONS {
indexConfig: {
`vector.dimensions`: 4,
`vector.similarity_function`: 'cosine'
}
}
実際の運用では、OpenAI や Cohere などの埋め込みモデルで生成した 1536 次元などのベクトルを embedding プロパティに格納します。
ノードにベクトルを付与する
ハンズオンでは 4 次元のダミーベクトルを使って動作を確認します。
// 各ノードに embedding プロパティとしてベクトルを付与
MATCH (n:Entity {name: "Alice"})
SET n.embedding = [0.12, 0.85, 0.42, 0.31];
MATCH (n:Entity {name: "Bob"})
SET n.embedding = [0.10, 0.82, 0.45, 0.29];
MATCH (n:Entity {name: "Carol"})
SET n.embedding = [0.72, 0.15, 0.88, 0.63];
MATCH (n:Entity {name: "Acme Corp"})
SET n.embedding = [0.20, 0.78, 0.50, 0.35];
MATCH (n:Entity {name: "Globex"})
SET n.embedding = [0.68, 0.22, 0.80, 0.55];
MATCH (n:Entity {name: "Project X"})
SET n.embedding = [0.30, 0.70, 0.60, 0.40];
実運用では、ノードの name や context などのテキストを埋め込みモデル(OpenAI text-embedding-3-small など)に渡してベクトルを生成し、同様に SET で格納します。
ハイブリッド検索の実装
GraphRAG の Local Search は「ベクトル検索でエンティティを特定 → グラフ探索でコンテキストを収集」の2段構えです。
// ① ベクトル検索で seed エンティティを特定
// ② 各 seed から 2-hop のグラフ探索でコンテキスト収集
CALL db.index.vector.queryNodes('entity_embeddings', 2, $query_vector)
YIELD node AS start, score
CALL (start) {
MATCH (start)-[:RELATED_TO*1..2]-(neighbor:Entity)
RETURN neighbor
}
RETURN start.name AS seed, score, collect(DISTINCT neighbor.name) AS context
実行結果(クエリベクトルが Alice 寄りの場合):
| seed | score | context |
|---|---|---|
| Alice | 0.9998 | [Carol, Project X, Bob, Acme Corp, Globex] |
| Bob | 0.9995 | [Alice, Carol, Project X, Acme Corp] |
Alice を seed にすると、2-hop 以内にグラフ全体のエンティティが収集できています。
なぜハイブリッドが強いか
純粋なベクトル検索:
質問 → 意味的に近いエンティティを発見
※ エンティティ同士の「繋がり」は見えない
ハイブリッド(GraphRAG):
質問 → 意味的に近いエンティティを発見(ベクトル検索)
↓
そこを起点にグラフを k-hop 探索
↓
関係・文脈を含むコンテキストを LLM に渡す → 回答
「意味的な近さで入口を見つけて、関係の繋がりで文脈を広げる」という2段構えが、多ホップ推論を可能にする核心です。
LLM との接続 ― GraphRAG における LLM の2つの役割
ここまでのハンズオンではグラフ構築・k-hop 探索・コミュニティ検出・ベクトル検索と「部品」を動かしてきました。これらが LLM とどう繋がるかを整理します。
GraphRAG において LLM は 2か所 で働きます。
① 入口(Indexing): テキスト → エンティティ・関係の抽出
② 出口(Query) : グラフから収集したコンテキスト → 回答生成
グラフ・ベクトル検索は、この2つの間にある「中間層」として機能します。
役割① 入口 ― Indexing フェーズでのエンティティ抽出
今回のハンズオンでは Cypher を手動で記述して知識グラフを構築しました。本来の GraphRAG では LLM がテキストを読んでエンティティと関係を自動抽出します。
LLM に文書テキストと出力形式を指示するプロンプトを与え、返ってきた構造化データを Cypher に変換して Neo4j に格納します。ハンズオンで手動でやった「知識グラフの手動構築」を LLM が自動化するイメージです。
役割② 出口 ― Query フェーズでの回答生成
グラフ探索・ベクトル検索で収集したコンテキストを LLM のプロンプトに組み込んで回答を生成します。
Local Search の場合
k-hop 探索で収集した context_chain(各エッジの context プロパティの連鎖)をプロンプトに渡します。グラフを辿った文脈が「回答の根拠」として機能し、LLM がそれをもとに回答を生成します。
Global Search の場合
Indexing 時に各コミュニティのノード群を LLM に渡してサマリーを生成・ベクトル化して保存しておき、Query 時にはそのサマリーを Vector Index で検索してプロンプトに渡します。個々のノードをすべて渡す代わりに圧縮されたサマリーを使うことで、トークン数を抑えつつ全体俯瞰の回答が得られます。
GraphRAG の End-to-end フロー
━━━━━━━━━━━━━━ Indexing フェーズ(1回のみ実行)━━━━━━━━━━━━━━
文書テキスト
↓
【LLM①】エンティティ・関係の抽出
↓
Cypher に変換 → Neo4j に格納(知識グラフ完成)
↓
【GDS】Louvain でコミュニティ検出 → communityId をノードに付与
↓
【LLM②】コミュニティごとにサマリー生成 → ベクトル化 → Vector Index に保存
━━━━━━━━━━━━━━ Query フェーズ(毎回実行)━━━━━━━━━━━━━━
質問テキスト
↓
埋め込みモデルでベクトル化
↓
┌──────────────┴──────────────┐
│ Local Search │ Global Search │
│ │ │
│ Vector Index で │ Vector Index で │
│ エンティティ検索(seed 特定)│ コミュニティサマリー検索 │
│ ↓ │ ↓ │
│ k-hop グラフ探索 │ 関連サマリーを収集 │
│ ↓ │ ↓ │
│ context_chain を収集 │【LLM④】まとめ回答生成 │
│ ↓ │ │
│【LLM③】回答生成 │ │
└────────────────────────────┴────────────────────────┘
LLM は入口(抽出)と出口(回答)の両方に存在し、グラフ・ベクトル検索はその中間でコンテキストを構造化・収集する役割を担っています。
まとめ
- GraphRAG はエンティティ・関係をグラフに落とすことで多ホップ推論を可能にする
- Local Search は Vector Index で seed を特定し、k-hop 探索で周辺コンテキストを収集して LLM に渡す
- Global Search は Louvain で検出したコミュニティのサマリーを Vector Index で検索し、LLM に渡して全体俯瞰の回答を得る
- LLM は「入口(エンティティ抽出)」と「出口(回答生成)」の2か所で働き、グラフ・ベクトル検索はその中間層を担う
参考リンク
GDS についてより深く学びたい場合は、以下のリソースが有用です。
公式ドキュメント
- GDS マニュアル v2.27 — アルゴリズム全種類のリファレンス。project/stream/write/stats の各モードの詳細もここ
- アルゴリズム一覧 — Louvain 以外の中心性・類似度・パス探索アルゴリズムの一覧
- Louvain 公式ドキュメント — パラメータ詳細・階層的クラスタリングの解説
- GDS 入門ガイド — インストールから最初のアルゴリズム実行まで
ハンズオン学習(GraphAcademy)
- Community Detection コース — インストール不要で動かせる無料のインタラクティブコース
- Louvain Deep Dive — 不正検知ユースケースを使った Louvain の詳細ハンズオン