はじめに:次に足すべきなのは「関係性」を扱う層
前回の記事では、Claude Code × Obsidian を使って、一次情報を壊さずに蓄積・編集するための自律型ナレッジOS「2do BRAIN」の基本構成を紹介しました。
この構成によって、01_raw/ に原典を残しつつ、02_wiki/ に構造化知識を育てる土台は作れます。
ただし、実務で使い続けると次の壁に当たります。
それが、「知識同士のつながりを機械的に追えない」という問題です。
たとえば、MarkdownベースのWiki運用だけでも「A社の課題は何か」は読めます。
しかし、「その課題に対して、過去にどの解決策を提案し、どの技術を紐づけたか」を横断的に辿ろうとすると、人間が手動でリンクを追い続ける必要があります。
一般的なベクトル検索は曖昧検索や意味検索に強い一方で、複数の実体の関係性を明示的に辿る用途では、グラフ構造の方が扱いやすい場面があります。
本記事では、Vector RAG を置き換えるのではなく、関係探索の層として Neo4j を補助的に追加します。
今回やらないこと
この記事では、GraphRAGを本番運用できる状態までは扱いません。
特に以下は次回以降のテーマです。
- 複数ドキュメントを跨いだ完全な名寄せ
- APOC を用いた再統合バッチ
- 自然言語からのCypher生成
- 差分更新と再インジェスト戦略
今回は、あくまで「Neo4jに安全に通し、Cypherで物理検証できること」をゴールにしています。
今回の防衛線(抽出のルール化)
グラフDBは強力ですが、LLMに自由に抽出させると、すぐにグラフが壊れます。
特に危ないのが、次の3つです。
- スキーマの無秩序化
- 名寄せ不足によるノード分裂
- 検証前に自然言語クエリへ進んでしまうこと
Neo4jのグラフモデルでは、ノードと関係の両方にプロパティを持たせられ、関係は型付きです。
逆に言えば、型の設計をサボると、Company と Organization のような似た概念が混ざり始め、後から運用が急速に苦しくなります。
また、LangChainの知識グラフ構築では、チャンクごとに処理される都合上、別チャンク間で同じ人物や会社が別ノードになる可能性があるため、entity disambiguation(名寄せ)は後段で重要になります。
そのため、この導入編では次の防衛線を引きます。
- 抽出するノード型とリレーション型を最初から絞る
- 事前ルールで
canonical_nameとaliasesを意識した抽出に寄せる -
include_source=Trueを使う場合はmetadata.idを明示する - いきなり自然言語QAに行かず、固定Cypherで物理検証する
Neo4jセットアップ
まずは、受け皿となる Neo4j を立ち上げます。
今回は、M1 Mac などのローカル開発環境でも扱いやすいように、メモリを絞った docker-compose.yml で始めます。
補足:
本記事ではAPOCを将来の再統合バッチのためだけでなく、LangChainの Neo4jGraph が行うスキーマ更新とノード・リレーション投入でも利用します。そのため、この最小PoCでもAPOCは導入前提とします。
docker-compose.yml
version: "3.8"
services:
neo4j:
image: neo4j:5.18.1
container_name: 2do-brain-neo4j
restart: unless-stopped
ports:
- "7474:7474" # Browser UI
- "7687:7687" # Bolt
environment:
- NEO4J_AUTH=neo4j/strong_password_2026
- NEO4J_PLUGINS=["apoc"]
- NEO4J_apoc_export_file_enabled=true
- NEO4J_apoc_import_file_enabled=true
- NEO4J_apoc_import_file_use__neo4j__config=true
- NEO4J_dbms_memory_pagecache_size=512M
- NEO4J_dbms_memory_heap_initial__size=512M
- NEO4J_dbms_memory_heap_max__size=512M
volumes:
- ./neo4j/data:/data
- ./neo4j/logs:/logs
- ./neo4j/import:/var/lib/neo4j/import
- ./neo4j/plugins:/plugins
起動します。
docker compose up -d neo4j
続いて Python 側の .env を作ります。
.env
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=strong_password_2026
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ライブラリは次のように入れておきます。
pip install langchain langchain-community langchain-experimental langchain-openai neo4j python-dotenv
最小インジェスト(知識の抽出と投入)
ここから、実際にテキストをグラフとして投入します。
今回のPoCでは、LangChain の LLMGraphTransformer と Neo4jGraph を使います。
なぜ metadata.id を入れるのか
LangChain の add_graph_documents() は、include_source=True を付けると、ソース文書ノードを保存して各エンティティに紐づけることができます。
include_source=True は便利ですが、metadata.id を入れないとソース文書のマージキーが page_content のMD5ベースになります。
PoCではそれでも動きますが、実務では文面の微修正だけで別ソースとして扱われる可能性があるため、固定IDを明示した方が安全です。
抽出ルールの「ハック」について
補足:
LLMGraphTransformer には prompt 指定も可能ですが、実運用ではバージョン差や structured output 周辺の揺れで結果が変わることがあるため、本記事では再現性を優先して、対象テキスト先頭に抽出ルールを物理的に結合する方法を採用しました。
ingest_graph.py
import os
from dotenv import load_dotenv
from langchain_core.documents import Document
from langchain_openai import ChatOpenAI
from langchain_community.graphs import Neo4jGraph
from langchain_experimental.graph_transformers import LLMGraphTransformer
load_dotenv()
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME", "neo4j")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not NEO4J_PASSWORD or not OPENAI_API_KEY:
raise ValueError("環境変数 NEO4J_PASSWORD または OPENAI_API_KEY が未設定です。")
ALLOWED_NODES = ["Person", "Company", "Challenge", "Solution", "Technology"]
ALLOWED_RELATIONSHIPS = [
"HAS_CHALLENGE",
"PROPOSES",
"USES_TECHNOLOGY",
"SOLVES",
"RELATED_TO",
]
EXTRACTION_RULES = """
【重要:エンティティ抽出ルール(名寄せと正規化)】
以下のテキストから知識グラフを抽出する際、同一の対象は必ず1つの「正規化されたID(canonical_name)」に統一してください。
1. 会社名:「株式会社〇〇」「〇〇社」はすべて「〇〇社」をIDとしてください。(例:A社)
2. 表記揺れ:テキスト内にある別名や略称は、必ず `aliases` プロパティに配列として保存してください。
---
[対象テキスト開始]
"""
# ⚠️ 公開用に完全に匿名化された架空の業務メモ
RAW_TEXT = """
2026年3月、株式会社アルファ(A社)との面談に向けた戦略メモ。
A社の最大の課題は「属人的なCSVの手作業連携」と、SaaS間連携時の「メモリ不足による処理不安定化」である。
これに対し、私はn8nを用いた「防衛的アーキテクチャ」を提案する。またAIのハルシネーション対策にはLangGraphを採用し、情報漏洩リスクを抑える解決策を提示する。
"""
def main():
print("🔌 Neo4j に接続中...")
graph = Neo4jGraph(
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
)
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
api_key=OPENAI_API_KEY,
)
combined_text = EXTRACTION_RULES + RAW_TEXT.strip()
document = Document(
page_content=combined_text,
metadata={
"id": "doc_alpha_strategy_202603_001",
"source": "2do_brain_poc_markdown",
"title": "A社 面談戦略メモ",
"created_at": "2026-03",
},
)
print("🧠 LLM によるグラフ抽出を開始...")
transformer = LLMGraphTransformer(
llm=llm,
allowed_nodes=ALLOWED_NODES,
allowed_relationships=ALLOWED_RELATIONSHIPS,
node_properties=["description", "canonical_name", "aliases"],
relationship_properties=["description"],
)
graph_documents = transformer.convert_to_graph_documents([document])
print("=== 抽出ノードの検品 ===")
for node in graph_documents[0].nodes:
aliases = node.properties.get("aliases", "なし") if hasattr(node, "properties") else "なし"
print(f"[{node.type}] ID: {node.id} / aliases: {aliases}")
print("💾 Neo4j へ保存中...")
graph.add_graph_documents(
graph_documents,
baseEntityLabel=True,
include_source=True,
)
graph.refresh_schema()
print("✅ インジェスト完了")
if __name__ == "__main__":
main()
この段階でやっていること
このスクリプトでやっていることは、実はかなり地味です。ただ、その地味さが大事です。
- 抽出型を最小限に制限する
- 別名を
aliasesに寄せる - ソース文書に固定IDを付ける
- まずは単一ドキュメントでインジェストして挙動を見る
LangChain の知識グラフ構築では、チャンクごとに entity consistency が崩れる可能性があるため、導入編では「まず1本通す」ことに徹するのが安全です。
固定Cypherで検証する
データが入ったら、すぐに自然言語QAに進むのではなく、まず固定Cypherで「本当に線がつながっているか」を見ます。
グラフDBでは、ここを飛ばすと、抽出の失敗なのか、クエリ生成の失敗なのか、切り分けができなくなります。
query_graph.py
import os
from dotenv import load_dotenv
from langchain_community.graphs import Neo4jGraph
load_dotenv()
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME", "neo4j")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
def main():
graph = Neo4jGraph(
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
)
print("=== 検証: 会社 → 課題 → 解決策 → 技術 ===")
query = """
MATCH (c:Company)-[:HAS_CHALLENGE]->(ch:Challenge)<-[:SOLVES]-(s:Solution)-[:USES_TECHNOLOGY]->(t:Technology)
WHERE toLower(c.id) CONTAINS 'alpha' OR toLower(c.id) CONTAINS 'a社'
RETURN
c.id AS Company,
ch.id AS TargetChallenge,
s.id AS Solution,
collect(DISTINCT t.id) AS Technologies
"""
result = graph.query(query)
if not result:
print("⚠️ 戦略パスが繋がっていません。")
return
for record in result:
print(f"[{record['Company']}]")
print(f" └─(課題)→ {record['TargetChallenge']}")
print(f" └─(提案)→ {record['Solution']}")
print(f" └─(技術)→ {', '.join(record['Technologies'])}")
print()
if __name__ == "__main__":
main()
実行結果
このスクリプトを実行すると、ターミナルには以下のように出力されます。
=== 検証: 会社 → 課題 → 解決策 → 技術 ===
[A社]
└─(課題)→ 属人的なCSVの手作業連携
└─(提案)→ 防衛的アーキテクチャ
└─(技術)→ n8n, LangGraph
このように、会社を起点に「課題 → 解決策 → 技術」の経路が辿れれば、PoCとしては成功です。
おわりに:次回予告
今回の導入編でやったことは、GraphRAGを完成させることではありません。
Neo4jを接続し、LangChain経由で知識をグラフ化し、Cypherで物理検証する土台を作ることです。
ここまでできれば、前回作った 2do BRAIN は、
- 原典を守る
01_raw/ - 編集済み知識を育てる
02_wiki/ - つながりを扱う Neo4j
という3層で運用できるようになります。
なお、今回のPoCでは「単一ドキュメント内の事前正規化」までに留めています。複数ドキュメント運用で必ず問題になるノード分裂については、次回 APOC の apoc.refactor.mergeNodes を用いた再統合バッチとして扱います。
知識は、保存されるだけでは資産になりません。
「原典」と「整理済み知識」と「関係グラフ」を分けて運用して初めて、AIは検索ツールではなく、実務を横断して支える頭脳に近づいていきます。
導入支援について
この記事で紹介した「2do BRAIN」および「GraphRAG統合」の構成は、実務導入向けにテンプレート化して提供しています。
- 3層ストレージ構造の初期構築
- 業務専用グラフスキーマの設計
- Claude Code / Obsidian / n8n / Neo4j の運用導線整備
- 30日間の伴走支援
詳細は以下をご覧ください。
この記事を書いた人✏️ @YushiYamamoto
株式会社プロドウガ CEO / AIアーキテクト
Next.js / TypeScript / n8n / LangGraph を活用した自律型アーキテクチャ設計を専門としています。
より深い実装メモや運用設計は、以下でも発信しています。
ITproDX.com
note
