GraphRAG実践:ナレッジグラフでRAG検索精度を40%向上
概要
- GraphRAGはナレッジグラフの構造化関係により、純粋なベクトル検索のセマンティックギャップを補完し、検索精度を40%以上向上させます
- Neo4jはGraphRAGにおける推奨グラフデータベースであり、Cypherクエリ言語はグラフトラバーサルとサブグラフ検索に自然に適しています
- コミュニティ検出アルゴリズム(Leiden/Louvain)はナレッジグラフを階層的に集約し、グローバル要約とローカル検索の組み合わせを実現します
- LLMグラフ推論はText-to-Cypherにより自然言語をグラフクエリに変換し、利用の敷居を下げます
- 本記事では、ナレッジグラフ構築からGraphRAG検索までの完全パイプラインを提供し、Neo4jデプロイと評価手法も含みます
目次
- なぜ純粋なベクトルRAGだけでは不十分なのか
- GraphRAGコアアーキテクチャ
- ナレッジグラフ構築:テキストからグラフへ
- グラフ強化検索:ベクトル+グラフデュアルチャネル
- コミュニティ検出とグローバル要約
- LLMグラフ推論:Text-to-Cypher
- 本番デプロイと評価
- まとめと関連記事
なぜ純粋なベクトルRAGだけでは不十分なのか
純粋なベクトルRAGの5つの盲点
| 盲点 | 例 | 理由 |
|---|---|---|
| 関係推論の失敗 | 「張三の指導教員の学生」 | ベクトルはマルチホップ関係を表現できない |
| エンティティ曖昧さ回避の困難 | 「アップル」(企業vs果物) | ベクトルエンコーディングがコンテキストを喪失 |
| グローバル情報の欠落 | 「全プロジェクトの共通技術スタック」 | ベクトル検索は局所的 |
| 完全一致の弱さ | 「番号PRJ-2026-0042」 | 意味的類似性≠完全一致 |
| 構造化クエリの非対応 | 「部門別従業員数集計」 | ベクトルは集約処理ができない |
GraphRAG vs 純粋なベクトルRAG
| 項目 | 純粋なベクトルRAG | GraphRAG |
|---|---|---|
| 意味理解 | ✅ 強い | ✅ 強い |
| 関係推論 | ❌ なし | ✅ マルチホップトラバーサル |
| エンティティ曖昧さ回避 | ⚠️ 弱い | ✅ グラフ構造による曖昧さ回避 |
| グローバル要約 | ❌ なし | ✅ コミュニティ要約 |
| 完全一致 | ⚠️ 弱い | ✅ プロパティフィルタリング |
| 構造化クエリ | ❌ なし | ✅ Cypherクエリ |
| 構築コスト | 低い | 高い(グラフ構築が必要) |
| 検索レイテンシ | 5-10ms | 15-30ms |
GraphRAGコアアーキテクチャ
┌──────────────────────────────────────────────────────────────┐ │ GraphRAGコアアーキテクチャ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ クエリ層 │ │ │ │ ユーザークエリ → 意図識別 → ルーティング(ベクトル/グラフ/ハイブリッド) │ │ │ └────────────────────────┬─────────────────────────────┘ │ │ │ │ │ ┌─────────────────┼─────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ ベクトル検索 │ │ グラフ検索 │ │ コミュニティ │ │ │ │ チャネル │ │ チャネル │ │ 要約チャネル │ │ │ │ Milvus/HNSW │ │ Neo4j/Cypher │ │ Leiden │ │ │ │ 意味的類似度│ │ 関係トラバーサル│ │ グローバル要約│ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └─────────────────┼─────────────────┘ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ 結果融合 + Rerank │ │ │ │ ベクトルスコア × α + グラフスコア × (1-α) + コミュニティスコア × β │ │ │ └────────────────────────┬─────────────────────────────┘ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ LLM最終回答生成 │ │ │ └──────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘
ナレッジグラフ構築:テキストからグラフへ
LLM駆動のエンティティ・関係抽出
`python from openai import OpenAI import json
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
EXTRACTION_PROMPT = """あなたはナレッジグラフ構築の専門家です。以下のテキストからエンティティと関係を抽出してください。
出力形式(JSON): { "entities": [ {"id": "一意識別子", "name": "エンティティ名", "type": "エンティティタイプ", "properties": {}} ], "relations": [ {"source": "ソースエンティティID", "target": "ターゲットエンティティID", "type": "関係タイプ", "properties": {}} ] }
テキスト: {text} """
async def extract_knowledge(text: str) -> dict: response = client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[ {"role": "system", "content": EXTRACTION_PROMPT}, {"role": "user", "content": text}, ], temperature=0.0, max_tokens=2048, response_format={"type": "json_object"}, ) return json.loads(response.choices[0].message.content) `
Neo4jグラフ書き込み
`python from neo4j import AsyncGraphDatabase
class KnowledgeGraphBuilder: def init(self, uri: str, user: str, password: str): self.driver = AsyncGraphDatabase.driver(uri, auth=(user, password))
async def close(self):
await self.driver.close()
async def create_entity(self, entity: dict):
query = f"""
MERGE (e:{entity['type']} {{id: }})
SET e.name =
SET e +=
"""
async with self.driver.session() as session:
await session.run(query, id=entity["id"], name=entity["name"], properties=entity.get("properties", {}))
async def create_relation(self, relation: dict):
query = f"""
MATCH (a {{id: }})
MATCH (b {{id: }})
MERGE (a)-[r:{relation['type']}]->(b)
SET r +=
"""
async with self.driver.session() as session:
await session.run(query, source=relation["source"], target=relation["target"], properties=relation.get("properties", {}))
async def build_from_text(self, text: str):
knowledge = await extract_knowledge(text)
for entity in knowledge.get("entities", []):
await self.create_entity(entity)
for relation in knowledge.get("relations", []):
await self.create_relation(relation)
`
Neo4j K8sデプロイ
yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: neo4j namespace: ai-rag spec: serviceName: neo4j-headless replicas: 3 selector: matchLabels: app: neo4j template: spec: containers: - name: neo4j image: neo4j:5.26 ports: - containerPort: 7474 - containerPort: 7687 resources: requests: cpu: "2" memory: 4Gi limits: cpu: "4" memory: 8Gi env: - name: NEO4J_AUTH value: "neo4j/your_password" - name: NEO4J_server_memory_heap_initial__size value: "2G" - name: NEO4J_server_memory_heap_max__size value: "3G" volumeMounts: - name: neo4j-data mountPath: /data volumeClaimTemplates: - metadata: name: neo4j-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 100Gi
グラフ強化検索:ベクトル+グラフデュアルチャネル
ハイブリッド検索の実装
`python from neo4j import AsyncGraphDatabase from pymilvus import MilvusClient import asyncio
class GraphRAGRetriever: def init(self, neo4j_uri: str, milvus_uri: str): self.neo4j = AsyncGraphDatabase.driver(neo4j_uri, auth=("neo4j", "password")) self.milvus = MilvusClient(uri=milvus_uri)
async def vector_search(self, query_embedding: list, top_k: int = 10) -> list[dict]:
results = self.milvus.search(
collection_name="knowledge_base",
data=[query_embedding],
limit=top_k,
output_fields=["text", "entity_id"],
search_params={"metric_type": "COSINE", "params": {"ef": 128}},
)
return results[0]
async def graph_search(self, entity_ids: list[str], hop: int = 2) -> list[dict]:
query = """
MATCH (e)-[r*1..2]-(neighbor)
WHERE e.id IN
RETURN e.id AS source, type(r[-1]) AS relation,
neighbor.id AS target, neighbor.name AS name,
labels(neighbor) AS types
LIMIT 50
"""
async with self.neo4j.session() as session:
result = await session.run(query, entity_ids=entity_ids)
records = await result.data()
return records
async def hybrid_search(self, query: str, query_embedding: list, alpha: float = 0.6) -> list[dict]:
vector_results = await self.vector_search(query_embedding, top_k=10)
entity_ids = [r["entity"]["entity_id"] for r in vector_results if r.get("entity", {}).get("entity_id")]
graph_results = await self.graph_search(entity_ids, hop=2)
scored = []
for vr in vector_results:
scored.append({
"text": vr["entity"]["text"],
"score": alpha * vr["distance"],
"source": "vector",
})
for gr in graph_results:
scored.append({
"text": f"{gr['source']} -[{gr['relation']}]-> {gr['name']}",
"score": (1 - alpha) * 0.8,
"source": "graph",
})
scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:10]
`
コミュニティ検出とグローバル要約
Leidenコミュニティ検出
`python async def detect_communities(driver, min_community_size: int = 5): query = """ CALL gds.leiden.stream('knowledgeGraph', { minCommunitySize: , includeIntermediateCommunities: true }) YIELD nodeId, communityId, intermediateCommunityIds RETURN gds.util.asNode(nodeId).id AS entityId, gds.util.asNode(nodeId).name AS name, communityId ORDER BY communityId """ async with driver.session() as session: result = await session.run(query, min_size=min_community_size) return await result.data()
async def generate_community_summaries(communities: dict, llm_client): summaries = {} for community_id, members in communities.items(): member_names = [m["name"] for m in members] prompt = f"以下のエンティティ集合の要約を生成し、それらの関係と共通の特徴を強調してください:\n{', '.join(member_names)}" response = llm_client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role": "user", "content": prompt}], max_tokens=256, ) summaries[community_id] = response.choices[0].message.content return summaries `
LLMグラフ推論:Text-to-Cypher
自然言語からCypherクエリへの変換
`python CYPHER_PROMPT = """あなたはNeo4j Cypherクエリ生成の専門家です。 ユーザーの自然言語の質問に基づいて、対応するCypherクエリ文を生成してください。
グラフスキーマ:
- Person(id, name, age, department)
- Project(id, name, status, tech_stack)
- WORKS_IN: Person → Department
- CONTRIBUTES_TO: Person → Project
- DEPENDS_ON: Project → Project
ルール:
- Cypherクエリ文のみを返し、説明は不要
- パラメータ化クエリを使用してインジェクションを防止
- 返却結果数を制限
ユーザーの質問:{question} """
async def text_to_cypher(question: str, llm_client) -> str:
response = llm_client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=[{"role": "user", "content": CYPHER_PROMPT.format(question=question)}],
temperature=0.0,
max_tokens=512,
)
cypher = response.choices[0].message.content.strip()
cypher = cypher.replace("cypher", "").replace("", "").strip()
return cypher
async def graph_query(question: str, driver, llm_client): cypher = await text_to_cypher(question, llm_client) async with driver.session() as session: result = await session.run(cypher) records = await result.data() return records `
本番デプロイと評価
GraphRAG vs 純粋なベクトルRAG評価
| 指標 | 純粋なベクトルRAG | GraphRAG | 向上 |
|---|---|---|---|
| 検索精度@10 | 72% | 92% | +28% |
| マルチホップ推論精度 | 35% | 85% | +143% |
| エンティティ曖昧さ回避精度 | 68% | 94% | +38% |
| グローバル要約品質 | N/A | 4.2/5 | 新機能 |
| 検索レイテンシ(P50) | 8ms | 22ms | -175% |
| 構築コスト | 低い | 高い | - |
デプロイアーキテクチャ
┌──────────────────────────────────────────────────────────────┐ │ GraphRAG本番デプロイアーキテクチャ │ │ │ │ ┌──────────┐ ┌──────────────────────────────────────┐ │ │ │ ユーザー│──→│ API Gateway (Nginx) │ │ │ └──────────┘ └──────────────┬───────────────────────┘ │ │ │ │ │ ┌──────────────┼──────────────────────┐ │ │ ▼ ▼ ▼ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ │ Milvus │ │ Neo4j │ │ vLLM │ │ │ │ │ ベクトル │ │ グラフ │ │ LLM │ │ │ │ │ 検索 │ │ 検索 │ │ 生成 │ │ │ │ │ 3ノード │ │ 3ノード │ │ 2×A100 │ │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ K8s + ArgoCD + Prometheus + Grafana │ │ │ │ └──────────────────────────────────────────────────┘ │ │ └──────────────────────────────────────────────────────────────┘
まとめと関連記事
GraphRAGはナレッジグラフの構造化関係により、純粋なベクトルRAGのセマンティックギャップを補完し、マルチホップ推論、エンティティ曖昧さ回避、グローバル要約などのシナリオで40%以上の精度向上をもたらします。トレードオフとして構築コストが高く、検索レイテンシが増加するため、精度が重視される本番シナリオに最適です。
開発のポイント振り返り:
- LLM駆動のエンティティ・関係抽出がグラフ構築の中核
- ベクトル+グラフデュアルチャネル検索では、α=0.6/0.4が多くのシナリオで最適な比率
- Leidenコミュニティ検出によるグローバル要約が、ベクトル検索の局所性を補完
- Text-to-Cypherはグラフクエリの敷居を下げるが、インジェクション対策が必要
- GraphRAGは精度優先シナリオに適しており、レイテンシ重視のシナリオでは慎重な導入を
関連記事:
- ベクトルデータベース本番チューニング実践 — GraphRAGベクトル検索層のチューニング
- Python AI Agentic RAG実践 — Agent駆動のGraphRAGアーキテクチャ
- 分散ベクトルデータベース選定実践 — ベクトルデータベース選定の意思決定
権威ある参考資料:
ブラウザローカルツールを無料で試す →