1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Neo4j導入編】Claude Code × Obsidian製「2do BRAIN」をGraphRAG対応にする最小PoC

1
Posted at

Robotic_librarian_connecting_dat…_202605052031.jpeg

はじめに:次に足すべきなのは「関係性」を扱う層

前回の記事では、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のグラフモデルでは、ノードと関係の両方にプロパティを持たせられ、関係は型付きです。
逆に言えば、型の設計をサボると、CompanyOrganization のような似た概念が混ざり始め、後から運用が急速に苦しくなります。
また、LangChainの知識グラフ構築では、チャンクごとに処理される都合上、別チャンク間で同じ人物や会社が別ノードになる可能性があるため、entity disambiguation(名寄せ)は後段で重要になります。

そのため、この導入編では次の防衛線を引きます。

  1. 抽出するノード型とリレーション型を最初から絞る
  2. 事前ルールで canonical_namealiases を意識した抽出に寄せる
  3. include_source=True を使う場合は metadata.id を明示する
  4. いきなり自然言語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 の LLMGraphTransformerNeo4jGraph を使います。

なぜ 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日間の伴走支援

詳細は以下をご覧ください。

2do BRAIN | 実務用ナレッジOS

この記事を書いた人✏️ @YushiYamamoto
株式会社プロドウガ CEO / AIアーキテクト
Next.js / TypeScript / n8n / LangGraph を活用した自律型アーキテクチャ設計を専門としています。
より深い実装メモや運用設計は、以下でも発信しています。
ITproDX.com
note

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?