GraphRAG 入門 #3 Bedrock Knowledge Bases のマネージド GraphRAG とベクトル RAG を同じデータで精度を比較してみた【Amazon Neptune】
はじめに
こんにちは!AI 事業本部のこーすけです。
GraphRAG 入門シリーズも 3 本目です。
- 1 本目では、グラフ DB の基本概念と Amazon Neptune Analytics を扱いました。
- 2 本目では、グラフを活用する RAG を、文書からグラフを自動構築する**パターン ①(GraphRAG)と、構築済みのナレッジグラフに自然言語で問い合わせるパターン ②(KGQA)**の 2 つに整理しました。
今回は、パターン ① の GraphRAG を実際に動かす回です。Amazon Bedrock Knowledge Bases のマネージド GraphRAG 機能を使い、架空のスペシャルティコーヒー商社「サンプル珈琲商事」の業務文書群を用意して、通常のベクトル RAG(チャンク類似検索のみ)と回答を比較しながら、多ホップ質問への強さと、#2 で整理した「グラフ構築段階のリスク」が実際にどう現れるかを検証してみます。
Bedrock Knowledge Bases GraphRAG とは
GraphRAG は、Amazon Bedrock Knowledge Bases に組み込まれたマネージド機能で、S3 に置いたドキュメントを取り込む際に、LLM が自動でエンティティ・関係を抽出し、Amazon Neptune Analytics 上にナレッジグラフを構築してくれます。グラフのモデリングを自分で設計する必要は一切ありません。
通常のベクトル RAG(チャンク類似検索のみ)との違いをまとめるとこうなります。
| 通常のベクトル RAG | GraphRAG | |
|---|---|---|
| 検索の仕組み | チャンク単位の埋め込みベクトルの類似度検索のみ | ベクトル類似検索 + グラフ探索(共通のエンティティを介してチャンクを辿る) |
| 得意な質問 | 単一の文書・チャンクで答えが完結する質問 | 複数の文書にまたがる事実をつなぎ合わせる必要がある質問 |
| 裏側のストレージ | ベクトルストア(OpenSearch Serverless 等) | Amazon Neptune Analytics(ベクトル + グラフを両方保持) |
作成時には、ドキュメントからエンティティ・関係を抽出するための グラフ構築モデル(Model for graph construction) を選びます。選べるモデルには Claude 3 Haiku・Claude Haiku 4.5・Amazon Nova 系がありますが、Claude 3 Haiku は Legacy 化しているため、Claude Haiku 4.5 など現行サポートされているモデルを選ぶようにしましょう。
GraphRAG が複数文書をまたぐ質問に強い理由
比較に入る前に、GraphRAG が複数文書をまたぐ質問に強いとされる仕組みを整理しておきます。
通常のベクトル RAG の検索は、クエリを埋め込みベクトルに変換し、チャンクに対して類似度検索を行い、上位 N 件を取得するという処理です。チャンク同士の関係は保持していないため、取得されるのは「クエリと意味的に近い」という基準だけで選ばれたチャンクの集合になります。クエリとは似ていないが、別の文書を経由すると答えにつながる文書は、この方式では取得できません。
GraphRAG は、この検索にグラフ探索による取得経路を追加しています。
データソースの取り込み(Sync)時に、グラフ構築モデルが各チャンクからエンティティ(農園名・ロット番号・商品名など)を抽出し、チャンクと紐づけて Neptune Analytics 上にグラフとして保存します。別々の文書に登場する同じエンティティは、グラフ上で同一のノードに対応づけられます。
検索時には、まずベクトル類似検索でクエリに近いチャンクを特定し、次にそのチャンクに含まれるエンティティを起点として、同じエンティティを含む別のチャンクをたどって追加取得します。

つまり、「クエリに似ているか」というベクトル検索の基準に加えて、「取得済みチャンクに登場するエンティティとグラフ上でつながっているか」という第 2 の取得基準が加わります。クエリとの類似度が低い文書でも、共通のエンティティ(今回のデータでは出荷 ID やロット番号など)を介してつながっていれば、検索結果に含められるようになります。
今回の検証データセット: 「サンプル珈琲商事」を文書として用意する
検証データには、架空のスペシャルティコーヒー商社 「サンプル珈琲商事」 を題材にした業務文書群を使います。農園 → 収穫ロット → 焙煎バッチ → 商品 → 注文 → 出荷 → 品質インシデントという多段のつながりを持つ世界観で、多ホップ質問の検証に向いた設計にしています。なお、このシリーズでは #4 以降で、同じ事実関係を構造化グラフとしても構築します。
用意する文書は次の 6 種類・66 件です。
| 文書タイプ | 件数 | 書く内容 | 意図的に書かない情報 |
|---|---|---|---|
| 農園プロフィール | 5 | 農園名・所在地・保有認証・取引輸出業者 | — |
| 収穫・輸出記録 | 15 | ロット番号・農園名・収穫日・数量・輸出業者名 | 認証名(農園プロフィールを見ないと分からない) |
| 焙煎バッチ記録 | 10 | バッチ番号・焙煎日・焙煎度・使用ロット番号(複数可) | 農園名・認証名 |
| 商品仕様書 | 7 | 商品名・カテゴリ・使用焙煎バッチ番号 | 農園名・認証名 |
| 注文・出荷記録 | 25 | 顧客名・商品名・数量・注文日・出荷 ID | — |
| 品質インシデント報告書 | 4 | 報告日・重大度・対象出荷 ID・内容 | 農園名・認証名・ロット番号・商品名は一切登場しない |
また、それぞれの文書タイプの一例を以下に示します。
農園プロフィール(farm_F04.txt)
# 農園プロフィール: 北風高地農園(F04)
所在地: Kenya、Nyeri地方(標高1900m)
保有認証: FairTrade、RainforestAlliance
取引輸出業者: African Highlands Trading
北風高地農園は、標高1900mの気候を活かして高品質なアラビカ種を栽培しています。
収穫・輸出記録(harvest_L10.txt)
# 収穫・輸出記録: ロットL10
農園: 北風高地農園
収穫日: 2025-11-05
数量: 870kg
グレード: AA
輸出業者: African Highlands Trading
焙煎バッチ記録(roast_RB04.txt)
# 焙煎バッチ記録: RB04
焙煎日: 2026-01-08
焙煎度: medium
重量: 38kg
使用ロット: L10
商品仕様書(product_P04.txt)
# 商品仕様書: 北風高地シングルオリジン(P04)
カテゴリ: single_origin
使用焙煎バッチ: RB04, RB10
注文・出荷記録(order_O04.txt)
# 注文・出荷記録: O04
顧客: サンプルカフェ4号店
商品: 北風高地シングルオリジン
数量: 13kg
注文日: 2026-02-22
出荷ID: S04
品質インシデント報告書(incident_QI01.txt)
# 品質インシデント報告書: QI01
報告日: 2026-02-10
重大度: 中
対象出荷: S04
内容: 対象出荷分の商品について、焙煎ムラが確認されたとの報告が顧客からありました。
これらのデータは、1 つの文書だけでは全体のつながりが追えないように設計しています。文書同士の関係は、文書の中には一切書かれていません。
例えば、「焙煎のムラが発生した事例がある商品は何?」ということが知りたいとき、このように文書が細かく分かれており、文脈が散らばっているデータセットの場合、通常の RAG ではうまくいかないことが多いです。
実際に構築してみる
前提
- AWS アカウント(Bedrock・S3・IAM を利用できる権限)
- Bedrock のモデルアクセスが有効化済みであること(埋め込みモデル・グラフ構築モデル・生成モデル)
💡 コストについて: 比較のため Knowledge Base を 2 つ(OpenSearch Serverless + Neptune Analytics)同時に稼働させることになります。特に OpenSearch Serverless は OCU(OpenSearch Compute Unit)単位の時間課金で、検証用の小さなデータでも最低利用量の関係で比較的高額になりがちです。検証が終わったらリソースの削除を忘れずに行いましょう。
ステップ 1: 文書データを生成する
以下の Python スクリプトを実行すると、上記のデータセットを 66 件の文書(テキストファイル)として生成できます。下敷きの事実関係はスクリプト冒頭のデータ定義にまとまっており、#4 で構造化グラフを構築する際にも同じ定義を使います。
import os
OUT = "coffee_docs"
os.makedirs(OUT, exist_ok=True)
def write_doc(filename, content):
with open(os.path.join(OUT, filename), "w", encoding="utf-8") as f:
f.write(content.strip() + "\n")
# ---------- 下敷きデータ(シリーズ共通の事実関係) ----------
farms = [
("F01", "エスペランサ農園", "Colombia", "Huila", 1800),
("F02", "モンテベルデ農園", "Costa Rica", "Tarrazu", 1700),
("F03", "サンライズヒル農園", "Ethiopia", "Yirgacheffe", 2000),
("F04", "北風高地農園", "Kenya", "Nyeri", 1900),
("F05", "リオドラド農園", "Brazil", "Cerrado", 1100),
]
has_certification = {
"F01": ["Organic", "FairTrade"],
"F02": ["RainforestAlliance"],
"F03": ["Organic"],
"F04": ["FairTrade", "RainforestAlliance"],
"F05": [],
}
farm_exporter = {"F01": "Andes Export Co.", "F05": "Andes Export Co.",
"F02": "Pacific Bean Logistics",
"F03": "African Highlands Trading", "F04": "African Highlands Trading"}
# ---------- 1. 農園プロフィール(5件)※認証・輸出業者を書く ----------
for fid, name, country, region, alt in farms:
certs = has_certification[fid]
cert_text = "、".join(certs) if certs else "現時点で保有認証なし"
write_doc(f"farm_{fid}.txt", f"""
# 農園プロフィール: {name}({fid})
所在地: {country}、{region}地方(標高{alt}m)
保有認証: {cert_text}
取引輸出業者: {farm_exporter[fid]}
{name}は、標高{alt}mの気候を活かして高品質なアラビカ種を栽培しています。
""")
# ---------- 2. 収穫・輸出記録(15件)※認証は書かない ----------
harvest_dates = ["2025-11-05", "2025-12-10", "2026-01-15"]
grades = ["AA", "A", "Specialty"]
lot_id_counter = 1
for fid, name, *_ in farms:
for i in range(3):
lot_id = f"L{lot_id_counter:02d}"
qty = 500 + lot_id_counter * 37
write_doc(f"harvest_{lot_id}.txt", f"""
# 収穫・輸出記録: ロット{lot_id}
農園: {name}
収穫日: {harvest_dates[i]}
数量: {qty}kg
グレード: {grades[i]}
輸出業者: {farm_exporter[fid]}
""")
lot_id_counter += 1
# ---------- 3. 焙煎バッチ記録(10件)※使用ロット番号のみ、農園名は書かない ----------
roast_batches = [
("RB01", "2026-01-05", "medium", 40, ["L01"]),
("RB02", "2026-01-06", "light", 35, ["L04"]),
("RB03", "2026-01-07", "medium", 45, ["L02", "L07"]),
("RB04", "2026-01-08", "medium", 38, ["L10"]),
("RB05", "2026-01-09", "dark", 42, ["L13"]),
("RB06", "2026-01-10", "medium", 40, ["L01"]),
("RB07", "2026-01-11", "light", 30, ["L08"]),
("RB08", "2026-01-12", "medium", 33, ["L10", "L05"]),
("RB09", "2026-01-13", "medium", 36, ["L14"]),
("RB10", "2026-01-14", "dark", 28, ["L11"]),
]
for bid, d, lvl, w, lots in roast_batches:
write_doc(f"roast_{bid}.txt", f"""
# 焙煎バッチ記録: {bid}
焙煎日: {d}
焙煎度: {lvl}
重量: {w}kg
使用ロット: {", ".join(lots)}
""")
# ---------- 4. 商品仕様書(7件)※使用焙煎バッチ番号のみ ----------
products = [
("P01", "エスペランサシングルオリジン", "single_origin", ["RB01", "RB06"]),
("P02", "モンテベルデシングルオリジン", "single_origin", ["RB02"]),
("P03", "サンライズヒルシングルオリジン", "single_origin", ["RB07"]),
("P04", "北風高地シングルオリジン", "single_origin", ["RB04", "RB10"]),
("P05", "リオドラドシングルオリジン", "single_origin", ["RB05", "RB09"]),
("P06", "サンライズブレンド", "blend", ["RB03"]),
("P07", "モーニングブレンド", "blend", ["RB08"]),
]
product_name_by_id = {pid: name for pid, name, *_ in products}
for pid, name, cat, batches in products:
write_doc(f"product_{pid}.txt", f"""
# 商品仕様書: {name}({pid})
カテゴリ: {cat}
使用焙煎バッチ: {", ".join(batches)}
""")
# ---------- 5. 注文・出荷記録(25件) ----------
customers = [f"サンプルカフェ{i}号店" for i in range(1, 11)]
order_dates = ["2026-02-01", "2026-02-08", "2026-02-15", "2026-02-22", "2026-03-01"]
product_ids = [p[0] for p in products]
for i in range(1, 26):
order_id, ship_id = f"O{i:02d}", f"S{i:02d}"
cust = customers[(i - 1) % 10]
product_id = product_ids[(i - 1) % len(product_ids)]
qty = 5 + (i % 8) * 2
odate = order_dates[(i - 1) % len(order_dates)]
write_doc(f"order_{order_id}.txt", f"""
# 注文・出荷記録: {order_id}
顧客: {cust}
商品: {product_name_by_id[product_id]}
数量: {qty}kg
注文日: {odate}
出荷ID: {ship_id}
""")
# ---------- 6. 品質インシデント報告書(4件)※農園名・認証名・ロット番号・商品名は一切書かない ----------
quality_incidents = [
("QI01", "2026-02-10", "中", "S04", "焙煎ムラが確認されたとの報告が顧客からありました。"),
("QI02", "2026-02-17", "低", "S06", "香りが弱いとの指摘が顧客からありました。"),
("QI03", "2026-02-24", "高", "S05", "異物混入の報告がありました。"),
("QI04", "2026-03-03", "低", "S01", "賞味期限表示に誤りがあったとの指摘がありました。"),
]
for qid, d, sev, ship, desc in quality_incidents:
write_doc(f"incident_{qid}.txt", f"""
# 品質インシデント報告書: {qid}
報告日: {d}
重大度: {sev}
対象出荷: {ship}
内容: 対象出荷分の商品について、{desc}
""")
print("done. files:", len(os.listdir(OUT)))
このスクリプトを実行すると coffee_docs/ フォルダに 66 件のテキストファイルが生成されます(農園 5・収穫記録 15・焙煎記録 10・商品仕様書 7・注文記録 25・品質インシデント 4)。
ステップ 2: 文書を S3 にアップロードする
aws s3 mb s3://example-bedrock-graphrag-demo --region ap-northeast-1
aws s3 cp coffee_docs/ s3://example-bedrock-graphrag-demo/coffee_docs/ --recursive
S3 バケット名はグローバルで一意である必要があるため、自分の環境用の名前に置き換えてください。
66 件のドキュメントが格納されていれば OK です。

ステップ 3: 従来型(ベクトルのみ)Knowledge Base を作成する
比較のベースラインとして、通常のベクトル RAG の Knowledge Base を先に作ります。
-
Bedrock コンソールの左メニューから ナレッジベース → Unstructured Vector Store KBを選択します。

-
ナレッジベースの詳細を指定 画面で、以下の項目を指定します。
- ナレッジベース名:
coffee-docs-vector-onlyを入力します。 - IAM 許可: 新しいサービスロールを作成して使用 を選択します(ロール名は自動で
AmazonBedrockExecutionRoleForKnowledgeBase_xxxxになります)。 - データソースを選択: Amazon S3 を選択します。

- データソースを設定 画面で、以下の項目を指定します。
- データソース名: 任意の名前を入力します(例:
knowledge-base-coffee-docs) - S3 の URI: ステップ 2 でアップロードした URI(
s3://example-bedrock-graphrag-demo/coffee_docs/)を指定します。 - 解析戦略: Amazon Bedrock デフォルトパーサー のままにします。
- チャンキング戦略: チャンキングなし を選択します。

- データストレージと処理を設定 画面で、以下の項目を指定します。
- 埋め込みモデル: Titan Text Embeddings v2.0 を選択します。
- ベクトルデータベース: 新しいベクトルストアをクイック作成 - 推奨 を選び、ベクトルストアのプルダウンから Amazon OpenSearch Serverless を選択します。

-
設定を確認して ナレッジベースを作成 をクリックします。
-
作成が完了したら、データソースを選択して 同期 を実行します。
ステップ 4: GraphRAG 対応 Knowledge Base を作成する
同じ S3 データソースに対して、今度は GraphRAG を有効にした Knowledge Base を作ります。
-
Bedrock コンソールの左メニューから ナレッジベース → Unstructured Vector Store KBを選択します。
-
ナレッジベースの詳細を指定 画面で、以下の項目を指定します(ナレッジベース名以外ステップ 3 と同じです。)
- ナレッジベース名:
coffee-docs-graphragを入力します。 - IAM 許可: 新しいサービスロールを作成して使用 を選択します(ロール名は自動で
AmazonBedrockExecutionRoleForKnowledgeBase_xxxxになります)。 - データソースを選択: Amazon S3 を選択します。

- データソースを設定 画面で、以下の項目を指定します(ステップ 3 と全く同じです。)
- データソース名: 任意の名前を入力します(例:
knowledge-base-coffee-docs) - S3 の URI: ステップ 3 と 同じ URI(
s3://example-bedrock-graphrag-demo/coffee_docs/)を指定します。 - 解析戦略: Amazon Bedrock デフォルトパーサー のままにします。
- チャンキング戦略: チャンキングなし を選択します。
- データストレージと処理を設定 画面で、以下の項目を指定します。
- 埋め込みモデル: Titan Text Embeddings v2.0 を選択します。
- ベクトルデータベース: 新しいベクトルストアをクイック作成 - 推奨 を選び、ベクトルストアのプルダウンから Amazon Neptune Analytics (GraphRAG) を選択します。ここがステップ 3 との唯一の違いで、これを選ぶと GraphRAG が有効になります
- Model for graph construction: 文書からエンティティ・関係を抽出するグラフ構築モデルを選択します。今回は Claude Haiku 4.5 を選びました。

-
設定を確認して ナレッジベースを作成 をクリックします。
-
作成が完了したら、データソースを選択して 同期 を実行します。
が、そのまま同期を実行すると、jp.anthropic.claude-haiku-4-5の実行権限が無い旨のエラーが発生します。
そのため、作成された実行ロールに以下のインラインポリシーをアタッチしてから同期を行ってください。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BedrockInvokeModelInferenceProfile",
"Effect": "Allow",
"Action": ["bedrock:InvokeModel"],
"Resource": [
"arn:aws:bedrock:*:123456789012:inference-profile/jp.anthropic.claude-haiku-4-5-20251001-v1:0",
"arn:aws:bedrock:*::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0"
]
}
]
}
回答精度を比較する
両方の Knowledge Base の「ナレッジベースをテスト」から、同じ 3 つの質問を投げて回答を比較します。取得するチャンク数は、デフォルトの 5 件では少ない可能性があるため、両方とも 20 件に設定しました。
Q1:QI01(品質インシデント)の対象商品は、どの農園のどの認証のロットに由来しますか?
この問題に回答するには、該当するインシデントファイルに記載の出荷 ID から、商品、バッチ、ロット、農園、認証までをたどる必要があります。これを網羅する単一のチャンクは存在しないため、通常の RAG では回答ができないはずです。
正解のルートは、incident_QI01(出荷 S04)→ order_O04(商品: 北風高地シングルオリジン)→ product_P04(バッチ RB04, RB10)→ roast_RB04 / roast_RB10(ロット L10, L11)→ harvest_L10 / harvest_L11(農園: 北風高地農園)→ farm_F04(認証: FairTrade, RainforestAlliance)と、6 種類・計 8 件の文書をまたぐ必要があります。
Q2:モンテベルデ農園の保有認証は?
この問題は対照的に、1 文書だけで完結する質問です。両方とも正しく答えられるはずです。
Q3:RainforestAlliance 認証の農園由来の豆を使った商品を、直近で発注した顧客は?
RainforestAlliance 認証の農園由来の豆を使った商品を、直近で発注した顧客は?
RainforestAlliance 認証を持つのはモンテベルデ農園・北風高地農園の 2 つです。そこから連なる商品(モンテベルデシングルオリジン・北風高地シングルオリジン・モーニングブレンド)を経由した注文の中から、最新の注文日を特定する必要があります。
Q1 よりもさらに広い範囲のネットワーク(複数農園・複数商品)をたどった上で、日付の新旧比較という一段別種の処理も必要になるより難しい質問です。
実行結果
まず、通常のベクトル RAG の回答です。

次に、GraphRAG の回答です。

まとめると以下の通りです。
| 質問 | 通常のベクトル RAG | GraphRAG |
|---|---|---|
| Q1 | S04 の出荷詳細が検索結果に含まれず、特定できず | 同様に S04 とロット・農園の紐付けが取得できず、特定できず |
| Q2 | 正解(RainforestAlliance) | 正解(RainforestAlliance) |
| Q3 | 認証情報が取得できず、商品・顧客を特定できず | RainforestAlliance 認証の 2 農園(モンテベルデ・北風高地)を特定し、北風高地シングルオリジンの発注顧客 2 件(サンプルカフェ 8 号店 2026-02-15、サンプルカフェ 5 号店 2026-03-01)まで到達 |
想定通り Q2 は両者とも回答できた一方、Q1 は両者とも回答できませんでした。
Q3 は、通常のベクトル RAG が全く回答できなかったのに対し、GraphRAG は「北風高地」を起点にして、認証 → 農園 → 商品 → 注文までたどり、発注顧客(サンプルカフェ 8 号店・5 号店)の割り出しまでできています。回答は完全ではないものの、グラフ探索の効果が一定確認できる結果となりました。
Q1 と Q3 で文書の関連付けに差が出た要因は、たどる際のキーとなる語が「北風高地〇〇」のような自然言語の固有名詞か、「S04」のような ID 文字列かの違いにあるのかもしれません。文書から自動でエンティティを抽出する際に、自然言語の固有名詞のほうがうまく処理された可能性があります。
この仮説を、実際にグラフの中身を見て確かめてみます。
構築されたナレッジグラフを確認する
Neptune の Analytics → Graphs を開くと、GraphRAG 用に自動作成されたグラフを確認することができます。

このグラフに openCypher でクエリを投げるために、プライベートグラフエンドポイントとノートブックを用意します。
プライベートグラフエンドポイントを作成する
自動作成されたグラフはパブリック接続が無効なため、VPC 内のノートブックから接続するためのプライベートグラフエンドポイントを作成します。
- Neptune コンソールで対象のグラフを開きます。
- 接続設定のタブから プライベートグラフエンドポイントを作成 を選択します。
- ノートブックを配置する VPC・サブネット・セキュリティグループ(これまでと同じデフォルト VPC の
defaultで OK です)を指定して作成します。

ノートブックを作成して接続する
続いて、このグラフに紐づく Neptune ノートブック(Workbench)を作成します。ノートブックの実体は SageMaker がホストする Jupyter で、SageMaker の通常利用料金が別途かかります(詳細はシリーズ 1 本目を参照してください)。
- Neptune コンソールの左メニューから Notebooks → ノートブックを作成 を選択します。
- Neptune サービス で Analytics を選択し、グラフ のプルダウンから自動作成されたグラフ(
bedrock-knowledge-base-xxxx)を選択します。 - その他の項目は以下の通りの設定で作成します。
- ノートブックインスタンスタイプ: 検証用のため ml.t2.mediumを選択します。
- IAM ロール名: 1 本目で作成した
AWSNeptuneNotebookRole-ロールがあれば 既存の IAM ロールを選択 で流用し、なければ IAM ロールを作成 を選択します。 - ネットワーク設定: VPC・サブネットはプライベートグラフエンドポイントを作成したものと同じものを選択し、セキュリティグループは
default、インターネットアクセスは Amazon SageMaker を介した直接アクセス にします。
作成が完了したら Jupyter Lab を開き、%status で疎通を確認します。

なお、このグラフは Knowledge Base が管理しているリソースです。クエリでの参照は問題ありませんが、データの追加・変更・削除は行わないようにしてください。グラフを直接書き換えると、Knowledge Base 側の動作との整合が保証されなくなります。
自動構築されたナレッジグラフを覗いてみる
ノートブックから接続できたら、まずグラフの規模とノードのラベルの内訳を確認します。
グラフクエリには openCypher を使用します。
%%oc
MATCH (n) RETURN count(n) AS node_count
%%oc
MATCH (n)
RETURN labels(n) AS label, count(*) AS cnt
ORDER BY cnt DESC

グラフの構成: 全 259 ノードで、内訳は Entity が 127、DocumentId が 66、Chunk が 66 でした。チャンキングなしの設定をしたので、ドキュメント数とチャンク数があっているのは想定通りですね。
Entity は 127 でした。想定した 107 個(農園 5・認証 3・ロット 15・輸出業者 3・焙煎バッチ 10・商品 7・顧客 10・注文 25・出荷 25・インシデント 4)よりも 20 個ほど多くなっています。
Entity の中身を確認する
Entity ノードがどんな構造をしているかをサンプルで確認します。
%%oc
MATCH (n:Entity)
RETURN n
LIMIT 5

結果を見ると、Entity は properties が空で、ノード ID(~id)そのものに x-amz-bedrock-kb-rb10、x-amz-bedrock-kb-サンプルカフェ6号店 のような形式でエンティティ名が埋め込まれている構造になっているようです。ID は小文字化されている点(RB10 → rb10、L01 → l01)も確認できました。
また、tarrazu地方 のような、想定外の概念も抽出されていることがわかります。Entity が多かった理由はこういった差分によるものと考えられます。
Chunk の中身を確認する
Chunk 側も確認しておきます。
%%oc
MATCH (c:Chunk)
RETURN c
LIMIT 2
Chunk はプロパティにソース情報のメタデータ(metadata_x-amz-bedrock-kb-source-uri など)を持っており、どの S3 文書由来のチャンクかを判別できるようになっています。
{
"~id": "d56d41ed-01a4-4198-ba9a-4263170f62fd",
"~entityType": "node",
"~labels": ["Chunk"],
"~properties": {
"metadata_x-amz-bedrock-kb-source-uri": "s3://example-bedrock-graphrag-demo/coffee_docs/order_O05.txt",
"AMAZON_BEDROCK_METADATA": "{\"sourceUrl\":\"s3://example-bedrock-graphrag-demo/coffee_docs/order_O05.txt\"}",
"AMAZON_BEDROCK_TEXT": "# 注文・出荷記録: O05\n\n顧客: サンプルカフェ5号店\n商品: リオドラドシングルオリジン\n数量: 15kg\n注文日: 2026-03-01\n出荷ID: S05",
"metadata_x-amz-bedrock-kb-data-source-id": "YAHRVLX5NP",
"metadata_x-amz-bedrock-kb-source-file-modality": "TEXT"
}
}
Entity と Chunk の中身を見比べると、このグラフの役割分担が読み取れます。Entity は実際のデータ(プロパティ)を持っておらず、ノード ID に名前を埋め込んだだけの、他のノードと接続するための結節点です。一方、文書の本文という実データを持っているのは Chunk 側(AMAZON_BEDROCK_TEXT プロパティ)です。検索時は、ベクトル検索でヒットしたチャンクに含まれるエンティティを経由して、同じエンティティにつながる別のチャンクへたどり着くことで、クエリと似ていない文書のチャンクも取得できる、という仕組みです。最終的に LLM へ渡るのはたどり着いた先の Chunk のテキストであり、#2 で整理した「グラフは検索の索引、情報源は文書」という構図が、ノードの設計にそのまま表れています。
エッジの種類を確認する
ノードの役割分担が分かったところで、エッジの種類も全数集計で確認しておきます。
%%oc
MATCH (a)-[r]->(b)
RETURN labels(a) AS from_label, type(r) AS edge_type, labels(b) AS to_label, count(*) AS cnt
ORDER BY cnt DESC
結果は次の 2 種類だけでした。
| from | エッジ | to | 本数 |
|---|---|---|---|
| Chunk | CONTAINS | Entity | 226 |
| Chunk | FROM | DocumentId | 66 |
つまりこのグラフのエッジは、「チャンクがエンティティを含む(CONTAINS)」と「チャンクが元文書に属する(FROM)」の 2 系統だけで、Entity 同士を直接つなぐエッジは存在しません。エンティティ間の関係はエッジとしては保存されておらず、「同じエンティティを含むチャンク同士」という間接的な形でのみ表現されます。
全てのグラフを可視化すると以下のようになっていました(一枚目:全体、二枚目:一部拡大)。


黄色のノードが Entity、青色のノードが Chunk、赤色のノードが DocumentId になります。上述の様子が確認できますね。
「S04」はグラフ上でどうなっているか
Entity の名前が ID に埋め込まれていることが分かったので、id() 関数で S04 を探します。ID は小文字化されるため、検索文字列は小文字の s04 にします。
%%oc
MATCH (n:Entity)
WHERE id(n) CONTAINS 's04'
RETURN n

S04 は 期待通り Entity として抽出されているようでした。よって、このノード周辺を可視化してネットワークを確認し、接続を見てみます。
%%oc -d {"Entity":"~id","Chunk":"metadata_x-amz-bedrock-kb-source-uri"} --label-max-length 100
MATCH p=(n:Entity)-[r]-(m)
WHERE id(n) CONTAINS 's04'
RETURN p
LIMIT 50
Entity:S04 の周辺のノードを可視化した結果がこちらになります。incident_QI01.txt 由来の Chunk が CONTAINS エッジでつながっているのみで、他の Chunk からのエッジは存在していませんでした。

order_O04.txt の本文には「出荷 ID: S04」と記載されているため、order_O04.txt の Chunk にもつながっていることを期待しましたが、注文記録側の抽出では S04 がエンティティとして拾われなかったようです。
つまり、Q1 でうまく回答できなかった要因のうちの一つには、グラフ構築段階の問題が考えられます。
「北風高地」はグラフ上でどうなっているか
つづいて「北風高地」の周辺をみてみます。
%%oc -d {"Entity":"~id","Chunk":"metadata_x-amz-bedrock-kb-source-uri"} -l 100
MATCH p=(n:Entity)-[r]-(m)
WHERE id(n) CONTAINS '北風高地'
RETURN p
LIMIT 50

たくさんヒットしましたね。「北風高地農園」、「北風高地シングルオリジン」、「北風高地」の 3 つの Entity が抽出され、それぞれが独立してチャンクと紐づくネットワークを持っているようです。



農園プロフィールや収穫記録から抽出されたのは「北風高地農園」 の Entity でしたが、商品仕様書から抽出されたのは「北風高地」 です。抽出時の表記ゆれで Entity が分裂してしまっているようです。
RainforestAlliance とモンテベルデ側も確認する
つづいて Q3 の探索経路の入口である RainforestAlliance と、回答で商品を特定できなかったモンテベルデ側も見ておきます。
まず、「RainforestAlliance」周辺を見てみます。
%%oc -d {"Entity":"~id","Chunk":"metadata_x-amz-bedrock-kb-source-uri"} -l 100
MATCH p=(n:Entity)-[r]-(m)
WHERE id(n) CONTAINS 'rainforest'
RETURN p
LIMIT 50

このように、farm_F02.txt と farm_F04.txtがつながっています。それぞれモンテベルデ農園と北風高地農園を説明している文書なので、認証 → 農園の経路があることが確認できました。
続いて、「モンテベルデ」の周辺をみてみます。
%%oc -d {"Entity":"~id","Chunk":"metadata_x-amz-bedrock-kb-source-uri"} -l 100
MATCH p=(n:Entity)-[r]-(m)
WHERE id(n) CONTAINS 'モンテベルデ'
RETURN p
LIMIT 50

「モンテベルデシングルオリジン」、「モンテベルデ農園」の 2 つの Entity が抽出されています。


表記ゆれはなく、非常に正確に関係を読み取れています。
先ほどの回答結果からは、モンテベルデ農園に紐づく商品が特定できなかったことが不思議ですが、、
ともかく、構築されたグラフを確認すると Q3 の回答は求めることが構造的には可能だと思われます。
改善作業: リランキングを有効にする
ここまでの調査で、Q3 に必要な情報はグラフに表現されていることが分かりました。そうあれば、取りこぼしの原因はグラフ側ではなく、取得したチャンクの精選・整理の側にありそうです。
そこで、公式の GA アナウンス(参考文献参照)で推奨されている改善を試します。ブログには「GraphRAG の機能を最大限に活用するため、リランキングの有効化を推奨する。リランキングにより GraphRAG は検索結果を精選・最適化できる」という記載があります。リランキングは、取得したチャンク候補を質問との関連度で並べ直し、関連度の高いものを優先して生成に渡す仕組みです。
テスト画面の検索設定からリランカーモデル(Amazon Rerank 1.0)を有効にして、同じ質問を再実行しました。
Q1「QI01(品質インシデント)の対象商品は、どの農園のどの認証のロットに由来しますか?」

これまで回答できなかった Q1 が、農園・認証まで回答できるようになりました。回答は「QI01 の対象出荷は S04 であり、S04 は注文 O04 に対応。商品は北風高地シングルオリジンで、北風高地農園に由来し、FairTrade および RainforestAlliance の認証を保有」というもので、正解(農園: 北風高地農園、認証: FairTrade・RainforestAlliance)に一致します。ソースの詳細を見ると、これまで取得できていなかった order_O04(出荷 ID: S04 を含む注文記録)が含まれるようになっていました。
Q3「RainforestAlliance 認証の農園由来の豆を使った商品を、直近で発注した顧客は?」

Q3 も、RainforestAlliance 認証の 2 農園の特定から「サンプルカフェ 5 号店(2026-03-01 に北風高地シングルオリジンを注文)」という正解の特定までできています。
なお補足として、Q1 の回答には正解ルートに含まれるロット番号(L10・L11)への言及はありません。回答後半の商品 → 農園の接続は、「北風高地」という名前の共通性を手がかりにしている可能性があり、文書チェーンを完全にたどり切ったとまでは言えない点は把握しておく必要があります。それでも、リランキングの有効化によって多ホップ質問の回答品質が変わることが確認できました。
わかったこと
- GraphRAG の文書横断は、複数文書に同一表記で登場する固有名詞が Entity の結節点になることで機能する。
- 一方、ID の参照やエンティティ抽出時に表記ゆれのある語は連結されず、グラフが分断されて回答できないケースがある。
- 裏側のグラフは Neptune Analytics 上にあり、openCypher で直接調査できる。想定外の結果が出たときの原因切り分けに有効。
- リランキングを有効にすると取得チャンクが精選され、回答できなかった Q1 が農園・認証まで回答できるようになった。GraphRAG を使う際はリランキングの併用が前提と考えてよさそう。
後片付け
今回は Knowledge Base を 2 つ作成した関係で、これまでで一番削除対象が多くなっています。忘れずに削除しておきましょう。
- Knowledge Base × 2(
coffee-docs-vector-only・coffee-docs-graphrag)— 削除時に、自動作成した Vector Database(OpenSearch Serverless コレクション / Neptune Analytics グラフ)も一緒に削除するか選べます。 - Neptune Analytics グラフに作成したプライベートグラフエンドポイント
- Neptune ノートブックインスタンス
- S3 バケット(
example-bedrock-graphrag-demo) - IAM ロール(各 Knowledge Base 用の実行ロール、ノートブック用の
AWSNeptuneNotebookRole-ロール、追加したインラインポリシー)
Knowledge Base 側で Vector Database を「削除しない」で残した場合は、OpenSearch Serverless コレクションと Neptune Analytics グラフを個別に削除する必要があるので注意してください。
おわりに
今回は Bedrock Knowledge Bases のマネージド GraphRAG を、通常のベクトル RAG と同じデータ・同じ条件で比較しました。通常の RAG で回答できなかった多ホップの質問で GraphRAG の効果を確認できたことは感動しました笑。
一方でエンティティ抽出の表記ゆれや ID の未連結によってグラフが分断され、回答できないケースがあることも分かりました。
次回以降は、パターン ②、つまり同じ事実関係のナレッジグラフが整備されている前提に立ちます。#4 でそのグラフを Amazon Neptune Database 上に自分の手で構築し、#5 では構築したグラフに byokg-rag で自然言語の質問を投げて、今回答えられなかった質問に回答できるかを検証します。
最後まで読んでいただきありがとうございました!
参考文献
- Build a knowledge base with Amazon Neptune Analytics graphs(AWS)
- Create an Amazon Bedrock knowledge base with Amazon Neptune Analytics graphs(AWS) — コンソール手順、グラフ構築モデルの選択、Sync の流れ
- Announcing general availability of Amazon Bedrock Knowledge Bases GraphRAG with Amazon Neptune Analytics(AWS Blog)
- Build GraphRAG applications using Amazon Bedrock Knowledge Bases(AWS Blog)
- Model lifecycle(AWS) — グラフ構築モデルの Legacy 状況確認
- Create an empty Neptune graph(AWS) — プライベートグラフエンドポイントの作成手順
- Using magics in Amazon Neptune notebooks(AWS) —
%status・%%ocなど本記事で使用したマジックの一覧





