GraphRAGを試したいと思い調べたところ、 Text2Cypher というものがあり自然言語を Cypher に変換できるらしい。 よくわかっていないのだが、GraphRAGでは この Text2CypherRetriever を使っているらしい: Effortless RAG With Text2CypherRetriever このページの説明では Neo4j にセットアップされたグラフ形式のデータベースが存在していることが前提で、それに対して自然言語で問い合わせを行うと、 Text2Cypher によって自然言語が Cypher のクエリに変換されて(ここで LLM が使用される)その Cypher クエリでデータベースに問い合わせて結果を得る・・・ という流れ。 つまり、そもそも前提として Neo4j に問い合わせしたいデータが入っている必要がある。
もちろん、Text2Cypher が意図通り機能するのであれば、 データの準備それ自体にも使うことで自然文から変換できるとは思うのだが。
ということで、急がば回れで、Neo4j に入門して基本から理解することにした。
$ docker run --rm \
--publish=7474:7474 --publish=7687:7687 \
--volume=$HOME/var/neo4j/data:/data \
--env=NEO4J_AUTH=none \
neo4j
--env=NEO4J_AUTH=none と指定することで ユーザー名とパスワードをなしで使うことができる。
データベースの保存先は $HOME/var/neo4j/data を指定。 これで docker stop してもデータベースを維持することができる。
起動できたら http://localhost:7474/ にブラウザでアクセスする。
Satoshi(Person) と Pikachu(Monster) を作成:
CREATE (a:Person {name: 'Satoshi'})
CREATE (m:Monster {name: 'Pikachu'})
Satoshi と Pikachu の関係(RelationShip)を定義:
MATCH (p:Person {name: 'Satoshi'}), (m:Monster {name: 'Pikachu'}) CREATE (p)-[:Keep]->(m)
Satoshi keeps Pikachu という関係として定義。
Zenigame を追加して Satoshi との関係を定義:
CREATE (m:Monster {name: 'Zenigame'})
MATCH (p:Person {name: 'Satoshi'}), (m:Monster {name: 'Zenigame'}) CREATE (p)-[:Keep]->(m)
Kasumi と Coduck も追加しましょう:
CREATE (a:Person {name: 'Kasumi'})
CREATE (m:Monster {name: 'Coduck'})
Satoshi と Pikachu のときと同じように彼らの関係を定義:
MATCH (p:Person {name: 'Kasumi'}), (m:Monster {name: 'Coduck'}) CREATE (p)-[:Keep]->(m)
さらに、Satoshi と Kasumi を Friend の関係として定義:
MATCH (p0: Person {name: 'Satoshi'}), (p1: Person {name: 'Kasumi'}) CREATE (p0)-[:Friend]->(p1)
有向グラフしか定義できないらしいので、逆方向にも設定:
MATCH (p0: Person {name: 'Satoshi'}), (p1: Person {name: 'Kasumi'}) CREATE (p1)-[:Friend]->(p0)
ここで一旦リセットして Python コードで同じことを実行してみます。
docker ps して container-id を確認して docker stop CONTAINER_ID にて neo4j をまず止めます。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7716bc96070e neo4j "tini -g -- /startup…" 16 minutes ago Up 16 minutes 0.0.0.0:7474->7474/tcp, :::7474->7474/tcp, 7473/tcp, 0.0.0.0:7687->7687/tcp, :::7687->7687/tcp upbeat_lamarr
$ docker stop 7716bc96070e
データの内容もリセットしたいので削除します。
$ sudo rm -rf ~/var
それでは再度 docker neo4j を起動:
$ docker run --rm \
--publish=7474:7474 --publish=7687:7687 \
--volume=$HOME/var/neo4j/data:/data \
--env=NEO4J_AUTH=none \
neo4j
続いて・・・Python でコードを書いていきます。
Python 環境を確認:
$ python --version
Python 3.12.9
venv します:
$ python -m venv ~/venv-neo4j
$ source ~/venv-neo4j/bin/activate
(venv-neo4j) $
このあたり https://neo4j.com/docs/python-manual/5/ を参考に neo4j を入れる:
(venv-neo4j) $ pip install neo4j
こて試しとして、Satoshi(Person) を追加して削除するコードを用意:
from neo4j import GraphDatabase
URI = "neo4j://localhost"
AUTH = ("", "")
with GraphDatabase.driver(URI, auth=AUTH) as driver:
driver.verify_connectivity()
records, summary, keys = driver.execute_query("""
CREATE (p:Person {name: 'Satoshi'})
RETURN p.name AS name
""")
for person in records:
print(person)
records, summary, keys = driver.execute_query("""
MATCH (p:Person {name: 'Satoshi'})
DETACH DELETE p
""")
print(f"Query counters: {summary.counters}.")
実行してみます:
(venv-neo4j) $ python main.py
<Record name='Satoshi'>
Query counters: {'_contains_updates': True, 'nodes_deleted': 1}.
うまくいきました。
それでは先ほどセットアップしたデータを再現するコードを書きます:
# main.py
from neo4j import GraphDatabase
def exec_query(auth, q):
with GraphDatabase.driver(URI, auth=AUTH) as driver:
return driver.execute_query(q)
URI = "neo4j://localhost"
AUTH = ("", "")
queries = [
"""
CREATE (p:Person {name: 'Satoshi'})
RETURN p.name AS name
""",
"""
CREATE (p:Person {name: 'Kasumi'})
RETURN p.name AS name
""",
"""
CREATE (m0:Monster {name: 'Pikachu'})
CREATE (m1:Monster {name: 'Zenigame'})
CREATE (m2:Monster {name: 'Coduck'})
RETURN
m0.name AS name0,
m1.name AS name1,
m1.name AS name2
""",
"""
MATCH
(p0:Person {name: 'Satoshi'}),
(p1:Person {name: 'Kasumi'}),
(m0:Monster {name: 'Pikachu'}),
(m1:Monster {name: 'Zenigame'}),
(m2:Monster {name: 'Coduck'})
CREATE (p0)-[:Keep]->(m0)
CREATE (p0)-[:Keep]->(m1)
CREATE (p1)-[:Keep]->(m2)
CREATE (p0)-[:Friend]->(p1)
CREATE (p1)-[:Friend]->(p0)
RETURN
p0.name AS name0,
p1.name AS name1
""",
]
for q in queries:
records, summary, keys = exec_query(AUTH, q)
for r in records:
print(r)
実行:
(venv-neo4j) $ python main.py
<Record name='Satoshi'>
<Record name='Kasumi'>
<Record name0='Pikachu' name1='Zenigame' name2='Zenigame'>
<Record name0='Satoshi' name1='Kasumi'>
意図通りできたか http://localhost:7474/ をブラウザで開いて確認:
意図通り再現できました。
それでは、Keep の関係を持ったグラフだけをリストアップしてみます。
Cypher:
MATCH
p=()-[r:Keep]->()
RETURN
p LIMIT 25
Python で書くとこれ:
# query.py
from neo4j import GraphDatabase
def exec_query(auth, q):
with GraphDatabase.driver(URI, auth=AUTH) as driver:
return driver.execute_query(q)
URI = "neo4j://localhost"
AUTH = ("", "")
queries = [
"""
MATCH
p=()-[r:Keep]->()
RETURN
p LIMIT 25
""",
]
for q in queries:
records, summary, keys = exec_query(AUTH, q)
for r in records:
print(r)
実行します(結果は見やすいように適当な部分で改行を入れて加工):
(venv-neo4j) $ python query.py
<Record
p=<Path start=<Node element_id='4:b0e3651b-ac95-4747-baea-cce709e9f220:0' labels=frozenset({'Person'})
properties={'name': 'Satoshi'}>
end=<Node element_id='4:b0e3651b-ac95-4747-baea-cce709e9f220:2' labels=frozenset({'Monster'}) properties={'name': 'Pikachu'}> size=1>>
<Record
p=<Path start=<Node element_id='4:b0e3651b-ac95-4747-baea-cce709e9f220:0' labels=frozenset({'Person'})
properties={'name': 'Satoshi'}>
end=<Node element_id='4:b0e3651b-ac95-4747-baea-cce709e9f220:3' labels=frozenset({'Monster'}) properties={'name': 'Zenigame'}> size=1>>
<Record
p=<Path start=<Node element_id='4:b0e3651b-ac95-4747-baea-cce709e9f220:1' labels=frozenset({'Person'})
properties={'name': 'Kasumi'}>
end=<Node element_id='4:b0e3651b-ac95-4747-baea-cce709e9f220:4' labels=frozenset({'Monster'}) properties={'name': 'Coduck'}> size=1>>
全ての Keep 関係がリストアップできました。
では、Satoshi の友達関係がある情報だけを取り出すには?
Cypher:
MATCH
r=( (p:Person WHERE p.name = 'Satoshi')-[:Friend]->() )
RETURN
r LIMIT 25
Python で書けば:
# query2.py
from neo4j import GraphDatabase
def exec_query(auth, q):
with GraphDatabase.driver(URI, auth=AUTH) as driver:
return driver.execute_query(q)
URI = "neo4j://localhost"
AUTH = ("", "")
queries = [
"""
MATCH
r=( (p:Person WHERE p.name = 'Satoshi')-[:Friend]->() )
RETURN
r LIMIT 25
""",
]
for q in queries:
records, summary, keys = exec_query(AUTH, q)
for r in records:
print(r)
実行:
(venv-neo4j) $ python query2.py
<Record
r=<Path start=<Node element_id='4:b0e3651b-ac95-4747-baea-cce709e9f220:0' labels=frozenset({'Person'})
properties={'name': 'Satoshi'}>
end=<Node element_id='4:b0e3651b-ac95-4747-baea-cce709e9f220:1' labels=frozenset({'Person'}) properties={'name': 'Kasumi'}> size=1>>
Satoshi と Kasumi の情報を得ることができました。
問い合わせ結果の標準出力だけではきついので、視覚的に結果を得る方法が提供されていた。
Transform to graph というのを試してみる。
pyvis が必要なので入れる:
(venv-neo4j) $ pip install pyvis
# graph.py
from neo4j import GraphDatabase
import neo4j
import pyvis
URI = "neo4j://localhost"
AUTH = ("", "")
query = """
MATCH
p=()-[r:Keep]->()
RETURN
p LIMIT 25
"""
with GraphDatabase.driver(URI, auth=AUTH) as driver:
query_graph = driver.execute_query(query, result_transformer_=neo4j.Result.graph)
nodes_text_properties = {}
visual_graph = pyvis.network.Network()
for node in query_graph.nodes:
#node_label = list(node.labels)[0]
node_text = node['name']
visual_graph.add_node(node.element_id, node_text, group=node_label)
for relationship in query_graph.relationships:
visual_graph.add_edge(
relationship.start_node.element_id,
relationship.end_node.element_id,
title=relationship.type
)
visual_graph.show('network.html', notebook=False)
実行:
(venv-neo4j) $ python graph.py
カレントディレクトリに network.html ができるのでそれをブラウザで開きます:
以上です。