LangChainとNeo4jでグラフデータベースQAシステム構築
この記事のハイライト
サザエの夫の義父の妻の孫の叔父の妹の父の娘の甥
% python sazae.py サザエの夫の義父の妻の孫の叔父の妹の父の娘の甥は?
> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (s:Actor {name: 'サザエ'})-[:夫]->(h:Actor)-[:義父]->(f:Actor)-[:妻]->(w:Actor)-[:孫]->(g:Actor)-[:叔父]->(u:Actor)-[:妹]->(sister:Actor)-[:父]->(d:Actor)-[:娘]->(n:Actor)-[:甥]->(nephew:Actor) RETURN nephew.name
Full Context:
[{'nephew.name': 'タラオ'}]
> Finished chain.
タラオはサザエの夫の義父の妻の孫の叔父の妹の父の娘の甥です。
なぜグラフデータベース?
グラフデータベースは、ノード(エンティティ)とエッジ(リレーションシップ)で構成され、関係性を分かりやすく表現できるデータベースです。サザエさん一家のような複雑な家族関係も、グラフデータベースならスッキリと整理できます。
今回使用するツール
Neo4j: わりと昔からある人気のグラフデータベース
LangChain: LLMアプリ開発のためのフレームワーク
ChatOpenAI: 安いので gpt-4o-mini を使います
コード解説
それでは、実際にコードを見ていきましょう
sazae.pyのコード全文
from langchain.chains import GraphCypherQAChain
from langchain_community.graphs import Neo4jGraph
from langchain_openai import ChatOpenAI
from langchain_core.prompts.prompt import PromptTemplate
import sys
#############################################################
# Neo4jの設定
graph = Neo4jGraph(url="bolt://localhost:7687", username="your_username", password="your_password")
#############################################################
# 新しいノードを作成するクエリ
new_nodes_queries = [
"MERGE (:Actor {name:'サザエ', sex:'女', age:24, hobby:['読書','推理小説']})",
"MERGE (:Actor {name:'マスオ', sex:'男', age:28, hobby:['バイオリン','ゴルフ','読書','麻雀','飲酒','絵を描くこと']})",
"MERGE (:Actor {name:'波平', sex:'男', age:54, hobby:['囲碁', '盆栽', '釣り', '俳句', '骨董品の収集']})",
"MERGE (:Actor {name:'フネ', sex:'女', age:54, hobby:'芝居見物'})",
"MERGE (:Actor {name:'カツオ', sex:'男', age:11, hobby:['野球','サッカー','イタズラ','つまみ食い']})",
"MERGE (:Actor {name:'ワカメ', sex:'女', age:9, hobby:['読書','おしゃれ','人形遊び','絵本や詩を書くこと']})",
"MERGE (:Actor {name:'タラオ', sex:'男', age:3, hobby:['三輪車に乗ること','リカちゃんと遊ぶこと']})",
]
# 新しいノードを作成するクエリを実行
for query in new_nodes_queries:
graph.query(query)
#############################################################
# リレーションを作成するクエリ
relationships_queries = [
"MATCH (sazae {name:'サザエ'}), (masuo {name:'マスオ'}) MERGE (sazae)-[:夫]->(masuo)",
"MATCH (sazae {name:'サザエ'}), (masuo {name:'マスオ'}) MERGE (sazae)<-[:妻]-(masuo)",
"MATCH (masuo {name:'マスオ'}), (katsuo {name:'カツオ'}) MERGE (masuo)-[:義弟]->(katsuo)",
"MATCH (masuo {name:'マスオ'}), (katsuo {name:'カツオ'}) MERGE (masuo)<-[:義兄]-(katsuo)",
"MATCH (masuo {name:'マスオ'}), (wakame {name:'ワカメ'}) MERGE (masuo)-[:義妹]->(wakame)",
"MATCH (masuo {name:'マスオ'}), (wakame {name:'ワカメ'}) MERGE (masuo)<-[:義兄]-(wakame)",
"MATCH (masuo {name:'マスオ'}), (namihei {name:'波平'}) MERGE (masuo)-[:義父]->(namihei)",
"MATCH (masuo {name:'マスオ'}), (namihei {name:'波平'}) MERGE (masuo)<-[:娘婿]-(namihei)",
"MATCH (masuo {name:'マスオ'}), (fune {name:'フネ'}) MERGE (masuo)-[:義母]->(fune)",
"MATCH (masuo {name:'マスオ'}), (fune {name:'フネ'}) MERGE (masuo)<-[:娘婿]-(fune)",
"MATCH (sazae {name:'サザエ'}), (wakame {name:'ワカメ'}) MERGE (sazae)-[:妹]->(wakame)",
"MATCH (sazae {name:'サザエ'}), (wakame {name:'ワカメ'}) MERGE (sazae)<-[:姉]-(wakame)",
"MATCH (sazae {name:'サザエ'}), (katsuo {name:'カツオ'}) MERGE (sazae)-[:弟]->(katsuo)",
"MATCH (sazae {name:'サザエ'}), (katsuo {name:'カツオ'}) MERGE (sazae)<-[:姉]-(katsuo)",
"MATCH (sazae {name:'サザエ'}), (tarao {name:'タラオ'}) MERGE (sazae)-[:息子]->(tarao)",
"MATCH (sazae {name:'サザエ'}), (tarao {name:'タラオ'}) MERGE (sazae)-[:子]->(tarao)",
"MATCH (sazae {name:'サザエ'}), (tarao {name:'タラオ'}) MERGE (sazae)<-[:母]-(tarao)",
"MATCH (sazae {name:'サザエ'}), (tarao {name:'タラオ'}) MERGE (sazae)<-[:親]-(tarao)",
"MATCH (masuo {name:'マスオ'}), (tarao {name:'タラオ'}) MERGE (masuo)-[:息子]->(tarao)",
"MATCH (masuo {name:'マスオ'}), (tarao {name:'タラオ'}) MERGE (masuo)-[:子]->(tarao)",
"MATCH (masuo {name:'マスオ'}), (tarao {name:'タラオ'}) MERGE (masuo)<-[:父]-(tarao)",
"MATCH (masuo {name:'マスオ'}), (tarao {name:'タラオ'}) MERGE (masuo)<-[:親]-(tarao)",
"MATCH (katsuo {name:'カツオ'}),(tarao {name:'タラオ'}) MERGE (katsuo)-[:甥]->(tarao)",
"MATCH (katsuo {name:'カツオ'}),(tarao {name:'タラオ'}) MERGE (katsuo)<-[:叔父]-(tarao)",
"MATCH (wakame {name:'ワカメ'}),(tarao {name:'タラオ'}) MERGE (wakame)-[:甥]->(tarao)",
"MATCH (wakame {name:'ワカメ'}),(tarao {name:'タラオ'}) MERGE (wakame)<-[:叔母]-(tarao)",
"MATCH (namihei {name:'波平'}), (fune {name:'フネ'}) MERGE (namihei)-[:妻]->(fune)",
"MATCH (namihei {name:'波平'}), (fune {name:'フネ'}) MERGE (namihei)<-[:夫]-(fune)",
"MATCH (katsuo {name:'カツオ'}),(wakame {name:'ワカメ'}) MERGE (katsuo)-[:妹]->(wakame)",
"MATCH (katsuo {name:'カツオ'}),(wakame {name:'ワカメ'}) MERGE (katsuo)<-[:兄]-(wakame)",
"MATCH (fune {name:'フネ'}), (sazae {name:'サザエ'}) MERGE (fune)-[:娘]->(sazae)",
"MATCH (fune {name:'フネ'}), (sazae {name:'サザエ'}) MERGE (fune)-[:子]->(sazae)",
"MATCH (fune {name:'フネ'}), (sazae {name:'サザエ'}) MERGE (fune)<-[:母]-(sazae)",
"MATCH (fune {name:'フネ'}), (sazae {name:'サザエ'}) MERGE (fune)<-[:親]-(sazae)",
"MATCH (fune {name:'フネ'}), (katsuo {name:'カツオ'}) MERGE (fune)-[:息子]->(katsuo)",
"MATCH (fune {name:'フネ'}), (katsuo {name:'カツオ'}) MERGE (fune)-[:子]->(katsuo)",
"MATCH (fune {name:'フネ'}), (katsuo {name:'カツオ'}) MERGE (fune)<-[:母]-(katsuo)",
"MATCH (fune {name:'フネ'}), (katsuo {name:'カツオ'}) MERGE (fune)<-[:親]-(katsuo)",
"MATCH (fune {name:'フネ'}), (wakame {name:'ワカメ'}) MERGE (fune)-[:娘]->(wakame)",
"MATCH (fune {name:'フネ'}), (wakame {name:'ワカメ'}) MERGE (fune)-[:子]->(wakame)",
"MATCH (fune {name:'フネ'}), (wakame {name:'ワカメ'}) MERGE (fune)<-[:母]-(wakame)",
"MATCH (fune {name:'フネ'}), (wakame {name:'ワカメ'}) MERGE (fune)<-[:親]-(wakame)",
"MATCH (namihei {name:'波平'}), (sazae {name:'サザエ'}) MERGE (namihei)-[:娘]->(sazae)",
"MATCH (namihei {name:'波平'}), (sazae {name:'サザエ'}) MERGE (namihei)-[:子]->(sazae)",
"MATCH (namihei {name:'波平'}), (sazae {name:'サザエ'}) MERGE (namihei)<-[:父]-(sazae)",
"MATCH (namihei {name:'波平'}), (sazae {name:'サザエ'}) MERGE (namihei)<-[:親]-(sazae)",
"MATCH (namihei {name:'波平'}), (katsuo {name:'カツオ'}) MERGE (namihei)-[:息子]->(katsuo)",
"MATCH (namihei {name:'波平'}), (katsuo {name:'カツオ'}) MERGE (namihei)-[:子]->(katsuo)",
"MATCH (namihei {name:'波平'}), (katsuo {name:'カツオ'}) MERGE (namihei)<-[:父]-(katsuo)",
"MATCH (namihei {name:'波平'}), (katsuo {name:'カツオ'}) MERGE (namihei)<-[:親]-(katsuo)",
"MATCH (namihei {name:'波平'}), (wakame {name:'ワカメ'}) MERGE (namihei)-[:娘]->(wakame)",
"MATCH (namihei {name:'波平'}), (wakame {name:'ワカメ'}) MERGE (namihei)-[:子]->(wakame)",
"MATCH (namihei {name:'波平'}), (wakame {name:'ワカメ'}) MERGE (namihei)<-[:父]-(wakame)",
"MATCH (namihei {name:'波平'}), (wakame {name:'ワカメ'}) MERGE (namihei)<-[:親]-(wakame)",
"MATCH (namihei {name:'波平'}), (tarao {name:'タラオ'}) MERGE (namihei)-[:孫]->(tarao)",
"MATCH (namihei {name:'波平'}), (tarao {name:'タラオ'}) MERGE (namihei)<-[:祖父]-(tarao)",
"MATCH (fune {name:'フネ'}), (tarao {name:'タラオ'}) MERGE (fune)-[:孫]->(tarao)",
"MATCH (fune {name:'フネ'}), (tarao {name:'タラオ'}) MERGE (fune)<-[:祖母]-(tarao)",
]
# リレーションを作成するクエリを実行
for query in relationships_queries:
graph.query(query)
#############################################################
# LLMの設定
llm = ChatOpenAI(
temperature=0.3,
model="gpt-4o-mini"
)
# Cypher生成用のテンプレートを設定
CYPHER_GENERATION_TEMPLATE = """
タスク:
- グラフデータベースにクエリーするためのCypherステートメントを生成する
制限:
- スキーマで提供されているリレーションシップ・タイプとプロパティのみを使用する
- 提供されていない他のリレーションシップ・タイプやプロパティを使用しないこと
スキーマ:
{schema}
注意点:
- 回答には説明や謝罪を含めないこと
- Cypher文を作成すること以外を問うような質問には答えないこと
- 生成されたCypher文以外のテキストを含めないこと
- 単語のみで回答すること
例:
- 以下は、特定の質問に対して生成されたCypherステートメントの例です:
# タラオの父の義妹は?
MATCH (t:Actor {{name: 'タラオ'}})-[:父]->(f:Actor)-[:義妹]->(a:Actor) RETURN a.name
# 妻がいないのは誰?
MATCH (a:Actor {{sex: '男'}}) WHERE NOT (a)-[:妻]->(:Actor) RETURN a.name
# カツオと一番歳が近いのは?(リレーションは指定せずに検索する)
MATCH (k:Actor {{name: 'カツオ'}})-[]->(a:Actor) RETURN a.name abs(k.age - a.age) AS diff ORDER BY abs(k.age - a.age) LIMIT 1
# 一番歳が離れているのは?
MATCH (a:Actor)-[]->(b:Actor) RETURN a.name, b.name abs(a.age - b.age) AS diff ORDER BY abs(a.age - b.age) DESC LIMIT 1
# 成人男性は何人?
MATCH (a:Actor {{sex: '男'}}) WHERE a.age > 19 RETURN count(a)
# 成人女性の平均年齢は?
MATCH (a:Actor {{sex: '女'}}) WHERE a.age > 19 RETURN avg(a.age) AS average_age
# マスオより年上なのは?(リレーションは指定せずに検索する)
MATCH (m:Actor {{name: 'マスオ'}})<-[]-(a:Actor) WHERE a.age > m.age RETURN a.name, a.age
質問はこれです:
{question}
"""
# テンプレートを適用
CYPHER_GENERATION_PROMPT = PromptTemplate(
input_variables=["schema", "question"], template=CYPHER_GENERATION_TEMPLATE
)
# Chainの設定
chain = GraphCypherQAChain.from_llm(
llm,
graph=graph,
verbose=True,
cypher_prompt=CYPHER_GENERATION_PROMPT,
)
# 引数で質問を設定(引数がない場合は既定の質問)
input = sys.argv[1] if len(sys.argv) > 1 else "サザエの弟は?"
# 結果を出力
result = chain.invoke({"query": input})
print(result['result'])
実行方法
Neo4jをインストールし、起動します。
必要なPythonライブラリをインストールします (`pip install langchain langchain-community langchain-openai`)など
OpenAIのAPIキーを設定します(参考記事)
Neo4jにAPOCプラグインをインストールしておきます
上記のコードを保存し、実行します。
※ APOCプラグインの有効化の手順
実行する
例えば、`python sazae.py "サザエの弟は?"` と実行すると、"カツオ"と出力されます。
verbose=True, cypher_prompt=CYPHER_GENERATION_PROMPT,を書いておくと、途中の処理を出力してくれるようになります
ちゃんとCypherクエリを生成・実行してくれていることがわかります
% python sazae.py サザエの弟は?
> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (s:Actor {name: 'サザエ'})-[:弟]->(a:Actor) RETURN a.name
Full Context:
[{'a.name': 'カツオ'}]
> Finished chain.
カツオがサザエの弟です。
サザエの夫の義父の妻の孫の叔父の妹の父の娘の甥
% python sazae.py サザエの夫の義父の妻の孫の叔父の妹の父の娘の甥は?
> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (s:Actor {name: 'サザエ'})-[:夫]->(h:Actor)-[:義父]->(f:Actor)-[:妻]->(w:Actor)-[:孫]->(g:Actor)-[:叔父]->(u:Actor)-[:妹]->(sister:Actor)-[:父]->(d:Actor)-[:娘]->(n:Actor)-[:甥]->(nephew:Actor) RETURN nephew.name
Full Context:
[{'nephew.name': 'タラオ'}]
> Finished chain.
タラオはサザエの夫の義父の妻の孫の叔父の妹の父の娘の甥です。
ちゃんとCypherクエリを生成して探してくれてます
ChatGPTに直接聞いても多分正解率低いと思います
ChatGPT 4oの回答例
まとめ
LangChainとNeo4jを使えば、複雑な関係性を持つデータに対しても、自然言語で質問を投げかけるだけで簡単に答えを得ることができます。
今回はサザエさん一家を例に紹介しましたが、顧客データやソーシャルネットワークデータなど、様々なデータに適用可能です。ぜひ、皆さんも試してみてくださいー
このブログ記事が、皆さんのグラフデータベースとLLM活用の一助になれば幸いです
by unco3