Home About Contact
Neo4j , Graph Database

Neo4j に入門した

GraphRAGを試したいと思い調べたところ、 Text2Cypher というものがあり自然言語を Cypher に変換できるらしい。 よくわかっていないのだが、GraphRAGでは この Text2CypherRetriever を使っているらしい: Effortless RAG With Text2CypherRetriever このページの説明では Neo4j にセットアップされたグラフ形式のデータベースが存在していることが前提で、それに対して自然言語で問い合わせを行うと、 Text2Cypher によって自然言語が Cypher のクエリに変換されて(ここで LLM が使用される)その Cypher クエリでデータベースに問い合わせて結果を得る・・・ という流れ。 つまり、そもそも前提として Neo4j に問い合わせしたいデータが入っている必要がある。

もちろん、Text2Cypher が意図通り機能するのであれば、 データの準備それ自体にも使うことで自然文から変換できるとは思うのだが。

ということで、急がば回れで、Neo4j に入門して基本から理解することにした。

Docker で 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/ にブラウザでアクセスする。

ブラウザの UI から Cypher コマンドでデータ投入

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 という関係として定義。

Satoshi keeps Pikachu

Zenigame を追加して Satoshi との関係を定義:

CREATE (m:Monster {name: 'Zenigame'})
MATCH (p:Person {name: 'Satoshi'}), (m:Monster {name: 'Zenigame'}) CREATE (p)-[:Keep]->(m)

Satoshi keeps Pikachu and Zenigame

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 keeps Pikachu and Zenigame / Kasumi keeps Coduck

さらに、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)

Satoshi keeps Pikachu and Zenigame / Kasumi keeps Coduck / Satoshi and Kasumi are friend

Python を使う

ここで一旦リセットして 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/ をブラウザで開いて確認:

Satoshi keeps Pikachu and Zenigame / Kasumi keeps Coduck / Satoshi and Kasumi are friend

意図通り再現できました。

それでは、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 の情報を得ることができました。

Visualizing と Transformers

問い合わせ結果の標準出力だけではきついので、視覚的に結果を得る方法が提供されていた。

https://neo4j.com/docs/python-manual/current/transformers/

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 ができるのでそれをブラウザで開きます:

Network

以上です。