
GraphRAGを構築してみた
はじめに
データ事業本部の藤川です。プロンプトエンジニアリング、コンテキストエンジニアリング、ハーネスエンジニアリング、ループエンジニアリングの次はグラフエンジニアリング(Graph Engineering)と言われています。昨日の話題が今日には古くなってしまう生成AI界隈です。グラフエンジニアリングの解説は他者にお任せするとして、実際に、構築して試してみました。
社内文書やマニュアルをRAG(Retrieval-Augmented Generation:検索拡張生成)で検索できるようにする取り組みは、すっかり一般的になりました。質問に近い文章を探してきて、それを根拠に回答させる。この仕組みは「この規程の第3条には何と書いてあるか」といった、答えがどこか一箇所に書いてある問いには非常によく効きます。
しかしながら、実際に運用してみると、うまく答えられない質問があることに気づきます。「このマニュアル全体で最も強調されている方針は何か」「この一年の議事録を通して、繰り返し議論されている論点は何か」といった問いです。
これらが難しいのには理由があります。答えがどこにも書かれていないからです。文書全体を読み通して初めて見えてくる内容は、どの一節とも似ていません。類似度で文章を探す仕組みでは、そもそも探しようがないわけです。
この課題に対する Microsoft社のアプローチが GraphRAG です。今回は、GraphRAGを実際に動かし、生成されたナレッジグラフを可視化するところまで試してみたのでご紹介します。
概要
GraphRAGとは
GraphRAGは、文書からナレッジグラフを構築してRAGに活用する手法です。通常のRAGとの違いは、インデックスを作る段階にあります。
通常のRAGは、文書をチャンクに分割して埋め込みベクトルを計算し、ベクトルDBに格納します。ここまでです。
GraphRAGはこれに加えて、LLMに各チャンクを読ませてエンティティと関係を抽出します。抽出した関係からグラフを構築し、Leidenアルゴリズムでコミュニティ(密に結びついたノードの集まり)を検出します。そして各コミュニティの要約を、これもLLMに書かせます。
つまり、質問される前に、あらかじめ文書の要約を階層的に作っておくわけです。「全体のテーマは何か」と聞かれたときには、この作り置きの要約を読んで答えます。検索するのではなく、事前に用意した答えを合成する。ここが決定的に違う点です。
つまりGraphRAGは、通常のRAGが検索時に行っていた仕事を、インデックス作成時に前倒しする設計になっています。
今回の構成
GraphRAGはLiteLLM経由でモデルを呼ぶため、100以上のプロバイダを選べます。ただし、chatモデルとembeddingモデルの両方が必要です。
今回はGeminiを選びました。APIキー1つでchatとembeddingの両方をまかなえ、無料枠があるためです。
| 役割 | モデル |
|---|---|
| chat | gemini-3.5-flash-lite |
| embedding | gemini-embedding-001 |
Python環境はmiseとuvで構築します。
題材には夏目漱石の『坊っちゃん』を使います。公式チュートリアルでは『クリスマス・キャロル』が使われていますが、日本語の作品にした理由は2つあります。1つは、著作権が切れており青空文庫から自由に入手できること。もう1つは、多くの方が登場人物と筋を既にご存じであることです。生成されたグラフが正しいかどうかを、ご自身の知識で判断していただけます。
準備
GeminiのAPIキーを Google AI Studio で発行しておきます。AIzaで始まる文字列です。
作業ディレクトリを作り、.mise.tomlでPythonのバージョンを固定します。GraphRAGが要求するPythonは >=3.11,<3.14 です。ここでは、3.12を使用します。
mkdir ~/graphrag_quickstart && cd ~/graphrag_quickstart
cat <<'EOF' > .mise.toml
[tools]
python = "3.12"
uv = "latest"
EOF
mise trust
mise install
仮想環境を作り、graphragをインストールします。GraphRAGはPyPIで公開されているため、リポジトリのクローンは不要です。
uv venv --python 3.12
source .venv/bin/activate
uv pip install graphrag
graphragコマンドに--versionオプションはありませんので、パッケージのメタデータからバージョン情報を取得します。今回使用したgraphragはバージョンが3.1.1です。頻繁に更新されているので、この記事の内容と合わない場合は、ブランチを3.1.1にしてください。
python -c "from importlib.metadata import version; print(version('graphrag'))"
やってみた
プロジェクトを初期化する
graphrag init でプロジェクトの雛形を作ります。実行するとchatモデルと埋め込みモデルを対話で聞かれますので、今回使うモデル名を入力します。
graphrag init
Specify the default chat model to use [gpt-4.1]: gemini-3.5-flash-lite
Specify the default embedding model to use [text-embedding-3-large]: gemini-embedding-001
settings.yaml、.env、input/、prompts/ が生成されます。
Gemini用に設定を書き換える
ここで一点、注意が必要でした。生成されたsettings.yamlを確認すると、モデル名は反映されているものの、model_providerはopenaiのままでした。
completion_models:
default_completion_model:
model_provider: openai # ← モデル名しか反映されていない
model: gemini-3.5-flash-lite
対話で聞かれるのはモデル名だけで、プロバイダは常に既定値が入る仕様のようです。2箇所ともopenaiをgeminiに書き換えます。
sed -i 's/^ model_provider: openai$/ model_provider: gemini/' settings.yaml
続いて.envのGRAPHRAG_API_KEYに、発行しておいたGeminiのAPIキーを設定します。
設定を検証する
--dry-run オプションで設定ファイルの構文チェックをしておきます。これはYAMLの構文チェックにとどまらず、実際に各モデルへテストリクエストを1回ずつ投げてくれます。設定ミスやAPIキーの誤りは、ここで判明します。
graphrag index --dry-run
ところが、実行してもコンソールには何も表示されません。少し戸惑いましたが、成功時は無音で、失敗時のみメッセージが出る作りでした。結果はログに記録されています。
grep "Validated\|Dry run" logs/indexing-engine.log
INFO - graphrag.index.validate_config - LLM Config Params Validated
INFO - graphrag.index.validate_config - Embedding LLM Config Params Validated
INFO - graphrag.cli.index - Dry run complete, exiting...
この3行が出ていれば、プロバイダ・APIキー・モデル名すべて正しく設定できています。
レート制限に対処する
検証が通りましたので、題材のテキストを用意します。
青空文庫の『坊っちゃん』はzip形式で、中身はShift_JISのテキストです。ルビが《》、注記が[#]という独自記法で埋め込まれていますので、これらを取り除いてUTF-8で保存します。
curl -sL -o /tmp/botchan.zip "https://www.aozora.gr.jp/cards/000148/files/752_ruby_2438.zip"
unzip -o -q /tmp/botchan.zip -d /tmp
python -c "
import re
raw = open('/tmp/bocchan.txt', encoding='shift_jis').read()
body = re.split(r'-{20,}\n', raw)[2] # 冒頭の凡例を除去
body = re.split(r'\n底本[::]', body)[0] # 末尾の底本情報を除去
body = re.sub(r'《[^》]*》', '', body).replace('|','') # ルビを除去
body = re.sub(r'[#[^]]*]', '', body).strip() # 注記を除去
open('input/book.txt','w',encoding='utf-8').write(body)
print('文字数:', len(body))
"
文字数: 88997
準備ができましたので、インデックスを作成します。
graphrag index
しかし、途中で停止してしまいました。
Starting workflow: extract_graph
Pipeline error: litellm.BadRequestError: GeminiException BadRequestError - {
"error": {
"code": 429,
"message": "You exceeded your current quota, ...
"quotaValue": "15"
Geminiの無料枠は 15リクエスト/分 です。対してGraphRAGの既定の並列度は 25同時リクエストでしたので、開始早々に超過したというわけです。
厄介なのは、リトライ設定では解決しない点でした。LiteLLMがGeminiの429をBadRequestErrorとしてラップしており、GraphRAGはこの例外名をリトライ対象外にしているためです。エラーメッセージに Please retry in 25s と親切に書かれていても、リトライせずパイプラインが落ちます。
そこで、送信側を絞る方針で対処します。まずモデル定義にレート制限を追加します。
sed -i '/^ api_key: \${GRAPHRAG_API_KEY}/a\ rate_limit:\n type: sliding_window\n period_in_seconds: 60\n requests_per_period: 12' settings.yaml
あわせて並列度も1に落とします。こちらはルート直下の設定ですので、ファイル末尾に追記します。
echo -e "\nconcurrent_requests: 1" >> settings.yaml
これで1分あたり12リクエストに絞られました。再度実行します。
graphrag index
なお、GraphRAGはLLMの応答をcache/に保存しているため、429で中断しても、成功した分の呼び出しはやり直しになりません。失敗するたびにキャッシュが積み上がりますので、再実行を繰り返せばいずれ完走できます。
『坊っちゃん』は69チャンクに分割され、28分52秒で完了しました。output/にparquetファイル群が生成されています。
$ ls output/
communities.parquet entities.parquet stats.json
community_reports.parquet lancedb/ text_units.parquet
context.json relationships.parquet
documents.parquet
検索してみる
では、冒頭で挙げた「文書全体を俯瞰する問い」を投げてみます。
graphrag query "この物語の主要なテーマは何ですか?"
夏目漱石の小説『坊っちゃん』の世界観や登場人物を基にしたこの物語では、学校内における
派閥争いや人間関係の対立が主要なテーマの一つとなっています [Data: Reports (1, 2, 14, +more)]。
### 学校内の対立と政治的陰謀
物語では、主人公である「おれ(坊っちゃん)」や数学教師の山嵐と、校長や教頭(赤シャツ)
といった学校行政側との間に生じる摩擦や政治的な陰謀が描かれています [Data: Reports (1, 2, 11, +more)]。
### 恋愛模様とメディアの影響
さらに、マドンナやうらなり君(古賀さん)を巡る恋愛模様や、うらなり君の転任に伴う送別会の開催、
さらには不当な噂や虚偽の記事を流す地方新聞によるメディアの影響なども、
物語の展開に深く絡んでいます [Data: Reports (1, 2, 14, +more)]。
作品全体を俯瞰した回答が返ってきました。
注目していただきたいのは、各段落の末尾にある [Data: Reports (0, 6, 15, +more)] です。これは原文の抜粋を指しているのではなく、インデックス作成時にLLMが書いたコミュニティレポートの番号を指しています。作り置きの要約が、そのまま回答の根拠になっているわけです。
GraphRAGには検索方式が4つ用意されており、--method basic を指定すると通常のRAG(チャンクのベクトル検索)になります。同じ質問を投げ比べると、両者の性格の違いがよく分かります。
ナレッジグラフを可視化する
せっかくグラフを構築したのですから、目で見てみたいところです。settings.yamlでgraphmlのスナップショット出力を有効にします。
sed -i 's/^ graphml: false$/ graphml: true/' settings.yaml
graphrag index
再実行になりますが、LLM応答はキャッシュから返るため、API消費なく1分15秒で終わりました。初回の28分52秒と比べれば、キャッシュの効きは一目瞭然です。output/graph.graphml が生成されました。
可視化ツールには、インストール不要でブラウザから使える Gephi Lite を選びました。ところが、生成されたgraph.graphmlを読み込ませるとエラーになってしまいます。
Unexpected token '<', "<graphml x"... is not valid JSON
Gephi Liteのソースを確認したところ、原因が分かりました。ファイル形式の判定が、1行目が<?xmlで始まるかどうかで分岐しており、そうでなければJSONパーサに回される実装だったのです。GraphRAGはnetworkxでGraphMLを書き出しますが、これはXML宣言を付けません。そのためJSON側に落ちていたのでした。
XML宣言を持つGEXF形式に変換すれば回避できます。networkxはgraphragの依存に含まれていますので、追加インストールは不要です。
.venv/bin/python -c "
import networkx as nx
g = nx.read_graphml('output/graph.graphml')
print('ノード数:', g.number_of_nodes(), '/ エッジ数:', g.number_of_edges())
nx.write_gexf(g, 'output/graph.gexf')
"
ノード数: 132 / エッジ数: 233
生成されたgraph.gexfをGephi Liteで開き、見た目を整えていきます。
まず左パネルの Metrics → Nodes → Louvain community detection を選択し、 Compute metric ボタンをクリックします。次のメッセージが表示されます。
The nodes metric "modularityClass" has been added to the data.

各ノードにmodularityClass属性が付きましたので、Appearance → Nodes → Color でmodularityClass属性を選ぶと、コミュニティごとに色分けされます。


続いて Metrics → Degree を選択し、Comute metric ボタンをクリックします。次のメッセージが表示されます。
The nodes metric "degree" has been added to the data.

Appearance → Nodes → Size でdegreeを選びます。関係の多いエンティティほど大きく表示されるようになります。

最後にレイアウトです。Layout → ForceAtlas2 を選び、パラメータの Adjust sizes? をONにします。
Start ボタンをクリックし、...

ノードの動きが止まってきたら、Stop ボタンをクリックします。これをONにすると、ノードの大きさを考慮して重なりを避けてくれますので、大きなノードが密集して潰れる事態を防げます。

次数の上位はこのようになりました。
45 山嵐
41 赤シャツ
38 おれ
26 主人公
18 清
18 野だ
17 校長
16 狸
13 学校
9 うらなり
9 うらなり君
8 古賀
山嵐と赤シャツが最上位に来ています。作品を読まれた方であれば、この2人が物語の対立軸そのものであることに納得いただけるはずです。文書を読まずとも人物の関係構造が一望できるのは、なかなか気持ちのよいものです。

さいごに
Geminiの無料枠だけでGraphRAGを一通り動かし、可視化まで到達できました。しかも、Gemini 3.5 Flash-LiteというGeminiの中で安価なモデルを使用していて、このアウトプットが得られるのです。
実際に触ってみると、いくつか気づいたことがあります。
-
可視化はインデックス品質の点検になる
上の次数ランキングをもう一度ご覧ください。校長と狸が別ノードになっています。うらなり・うらなり君・古賀も3つに分かれています。おれと主人公に至っては、語り手が2つのノードに割れてしまっています。作品をご存じの方には説明不要でしょうが、狸は校長のあだ名、古賀はうらなり君の本名です。つまり同一人物が別々のノードとして抽出されているわけです。日本語の作品を選んだ狙いはここにありました。読者が「これは同じ人物だ」と即座に判定できるため、インデックスの精度が肌感覚で分かります。
あだ名で人物を呼ぶ作品は日本語に限らずありますが、業務文書でも「株式会社○○」と「○○社」のような表記ゆれは日常茶飯事です。実務で使うのであれば、
graphrag prompt-tuneで抽出プロンプトを自社データに合わせて調整する工程は欠かせないと考えています。 -
アルゴリズムの違いに注意
Gephi Liteのコミュニティ検出はLouvainですが、GraphRAGが内部で使うのはLeidenです。別物ですので、可視化で見えたクラスタとcommunities.parquetの内容は一致しません。GraphRAGが算出したコミュニティで色分けしたい場合は、GraphML経由ではなくentities.parquetを直接読む必要があります。
膨大なデータを前に、途方に暮れる場面は、業務でもよくあります。その問いにLLMが答えられるよう、あらかじめ全体を俯瞰したデータを用意しておく。GraphRAGの発想はそういうものだと理解しました。
どなたかのお役に立てれば幸いです。








