はじめに
グラフDB、名前は聞いたことあるけど実際に触ったことない人向けに書きます。
この記事では:
- グラフDBって何なのかを理解する
- Neo4jをDockerで立ち上げる
- Pythonから繋いでデータを操作する
- LLMからNeo4jに自然言語で問い合わせる
ここまでを一本で通します。
1. グラフDBって何が違うの?
まず普通のRDB(MySQLとかPostgres)と何が違うのか。
RDBの場合
ユーザーと友達関係を表現するとき、こうなる。
users テーブル
| id | name |
|----|-------|
| 1 | Alice |
| 2 | Bob |
| 3 | Carol |
friends テーブル
| user_id | friend_id |
|---------|-----------|
| 1 | 2 |
| 2 | 3 |
「Aliceの友達の友達は誰?」を取得するには JOIN を2回かける必要がある。さらに「3ホップ先の友達」となると JOIN が増えて、深くなるほど重くなる。
グラフDBの場合
グラフDBはデータをノード(点) と エッジ(線) で表現する。
(Alice) -[友達]-> (Bob) -[友達]-> (Carol)
「3ホップ先の友達を全員出して」みたいなクエリが、グラフの構造をそのままトレースするだけなので速い。
向いてるユースケース
| 向いてる | 向いてない |
|---|---|
| 人間関係・SNSグラフ | 単純な CRUD |
| 知識グラフ・RAG | 集計・分析系 |
| 推薦システム | RDBで十分なもの |
| サプライチェーン | - |
LLMとの相性がいい理由は「知識グラフ」として使えるから。エンティティ同士の関係性をそのまま持てる。
2. Neo4jをDockerで立ち上げる
compose.yml を作る:
services:
neo4j:
image: neo4j:5
ports:
- "7474:7474"
- "7687:7687"
environment:
- NEO4J_AUTH=neo4j/password
- NEO4J_PLUGINS=["apoc"]
volumes:
- ./data:/data
NEO4J_PLUGINS=["apoc"] はLangChainがスキーマ取得に使うため必須。ないと接続時にエラーになる。
起動:
docker compose up -d
停止:
docker compose down
ブラウザで http://localhost:7474 にアクセスするとNeo4jのWebコンソールが開く。
- ユーザー名:
neo4j - パスワード:
password
ログインできたらOK。
3. Pythonからつなぐ
プロジェクトセットアップ
mkdir neo4j-llm-tutorial
cd neo4j-llm-tutorial
uv init
uv add neo4j langchain-neo4j langchain-ollama
接続確認
connect.py:
from neo4j import GraphDatabase
URI = "bolt://localhost:7687"
AUTH = ("neo4j", "password")
driver = GraphDatabase.driver(URI, auth=AUTH)
with driver.session() as session:
result = session.run("RETURN 'Hello, Neo4j!' AS message")
print(result.single()["message"])
driver.close()
uv run connect.py
# Hello, Neo4j!
4. データを入れてみる
Neo4jのクエリ言語は Cypher という独自の言語を使う。SQLと雰囲気は似てる。
ノードを作る
from neo4j import GraphDatabase
URI = "bolt://localhost:7687"
AUTH = ("neo4j", "password")
driver = GraphDatabase.driver(URI, auth=AUTH)
def create_sample_data(tx):
# ノード(人物)を作成
tx.run("CREATE (a:Person {name: 'Alice', age: 30})")
tx.run("CREATE (b:Person {name: 'Bob', age: 25})")
tx.run("CREATE (c:Person {name: 'Carol', age: 28})")
# 関係(エッジ)を作成
tx.run("""
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:FRIENDS_WITH]->(b)
""")
tx.run("""
MATCH (b:Person {name: 'Bob'}), (c:Person {name: 'Carol'})
CREATE (b)-[:FRIENDS_WITH]->(c)
""")
with driver.session() as session:
session.execute_write(create_sample_data)
print("データ作成完了")
driver.close()
uv run seed.py
seed.py を複数回実行するとデータが重複する。やり直す場合はWebコンソールで MATCH (n) DETACH DELETE n を実行してからもう一度。
データを確認する
WebコンソールでCypherを直接打って確認できる。
MATCH (n) RETURN n
ノードと関係がグラフとして描画されるのが見える。これがグラフDBの直感的なとこ。
Cypherで問い合わせ
def find_friends_of_friends(tx, name):
result = tx.run("""
MATCH (p:Person {name: $name})-[:FRIENDS_WITH*2]->(fof)
RETURN fof.name AS name
""", name=name)
return [record["name"] for record in result]
with driver.session() as session:
result = session.execute_read(find_friends_of_friends, "Alice")
print(f"Aliceの友達の友達: {result}")
# Aliceの友達の友達: ['Carol']
[:FRIENDS_WITH*2] の *2 が「2ホップ」を意味する。*1..5 と書けば1〜5ホップ以内を全部たどれる。これがグラフDBの強み。
5. LLMからNeo4jを使う
いよいよ本題。自然言語でNeo4jに問い合わせる。
APIキー不要でローカルで動く Ollama を使う。
Ollamaのセットアップ
Ollama公式サイト からインストールしてモデルを取得する。
ollama pull llama3.1:8b
llama3.2:3b などの軽量モデルでも動くが、回答の精度が落ちやすい。4GB以上のVRAMがあれば llama3.1:8b 推奨。
GraphCypherQAChainを使う
LangChainには GraphCypherQAChain というNeo4j用のチェーンが用意されている。自然言語をCypherに変換して実行してくれる。
llm_query.py:
from langchain_neo4j import Neo4jGraph, GraphCypherQAChain
from langchain_ollama import ChatOllama
from langchain_core.prompts import PromptTemplate
# Neo4jに接続
graph = Neo4jGraph(
url="bolt://localhost:7687",
username="neo4j",
password="password"
)
# グラフのスキーマを確認(LLMに渡すため)
print(graph.schema)
# 回答生成用のプロンプト(日本語で答えさせる)
CYPHER_QA_PROMPT = PromptTemplate(
input_variables=["context", "question"],
template="""以下のNeo4jクエリの結果をもとに、質問に日本語で答えてください。
結果が空でなければ必ず答えを述べてください。「わからない」とは言わないでください。
クエリ結果: {context}
質問: {question}
回答:"""
)
# LLM + Chainをセットアップ
llm = ChatOllama(model="llama3.1:8b", temperature=0)
chain = GraphCypherQAChain.from_llm(
llm=llm,
graph=graph,
verbose=True, # 生成されたCypherを表示
allow_dangerous_requests=True,
qa_prompt=CYPHER_QA_PROMPT,
)
# 自然言語で問い合わせ
questions = [
"AliceはBobと友達ですか?",
"Aliceの友達の友達は誰ですか?",
"25歳の人は誰ですか?",
]
for q in questions:
print(f"\n質問: {q}")
result = chain.invoke({"query": q})
print(f"回答: {result['result']}")
uv run llm_query.py
実行するとこんな感じの出力になる:
質問: 25歳の人は誰ですか?
> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (p:Person) WHERE p.age = 25 RETURN p.name
Full Context:
[{'p.name': 'Bob'}]
> Finished chain.
回答: Bobです。
verbose=True にしておくと、LLMがどんなCypherを生成したか見えるのがおもしろい。
動作の仕組み
ユーザーの自然言語
↓
LLM(グラフのスキーマ情報も渡す)
↓ Cypherクエリを生成
Neo4j
↓ クエリ実行結果
LLM(結果を自然言語に変換)
↓
回答
GraphCypherQAChain は内部でグラフのスキーマ(どんなノードと関係が存在するか)をLLMに渡している。これがあるからLLMが正しいCypherを生成できる。
まとめ
| やったこと | 使ったもの |
|---|---|
| Neo4j起動 | Docker + APOC |
| Python接続・データ操作 | neo4j ドライバ、Cypher |
| 自然言語で問い合わせ | LangChain GraphCypherQAChain + Ollama |
次回はアダプターを自作する。今回使った GraphCypherQAChain の中身を自分で実装することで、グラフDBとLLMを繋ぐ仕組みを深く理解する。


