🌊

ナレッジグラフを構築してみた

2024/11/14に公開

Neo4jで始めるナレッジグラフ作成ガイド

はじめに

皆さんはインターネットで情報を検索するとき、関連する情報が繋がって表示される経験はありませんか?これは「ナレッジグラフ」という技術によるものです。ナレッジグラフは、データ同士の関係性を可視化し、情報をより理解しやすくするための強力なツールです。本記事では、初心者の方にもわかりやすいように、ナレッジグラフの基本概念から、その構築方法、そしてNeo4jを使った実践的な手法までを解説します。

ナレッジグラフについて

概念

ナレッジグラフとは、情報(ノード)とその関係性(エッジ)をグラフ構造で表現したものです。例えば、人間関係を示す家系図や、駅と路線で繋がった鉄道路線図をイメージすると分かりやすいでしょう。

重要性

大量のデータが溢れる現代、情報同士の関係性を理解することは非常に重要です。ナレッジグラフを用いることで、データ間の関連性を直感的に把握でき、新たな洞察や発見につなげることができます。

応用範囲

  • 検索エンジン:ユーザーの意図を理解し、関連情報を提供
  • 医療:症状と疾患、治療法の関係性を分析
  • SNS:ユーザー同士の関係や興味を把握し、コンテンツを推薦

ナレッジグラフ使用例・比較

  • Googleナレッジグラフ:検索結果に人物や場所、物事の詳細情報を表示
  • Facebook Graph:ユーザーの友人関係や興味を分析し、広告やコンテンツを最適化

これらは従来のリレーショナルデータベースと比較して、複雑な関係性を効率的に扱える点で優れています。

ナレッジグラフ構築サイトの紹介・比較

代表的な構築ツール

  • Wikidata:ウィキペディアのデータを構造化し、誰でも編集可能
  • DBpedia:ウィキペディアの情報を抽出し、機械が読み取れる形式で提供

比較

ツール名 特徴 利点
Wikidata オープンな共同編集プラットフォーム 多言語対応、豊富なデータ
DBpedia ウィキペディアからの情報抽出 自動化されたデータ収集、機械学習に適用可能

これらのツールを使うことで、大規模なナレッジグラフを容易に構築・利用できます。

Neo4jの解説・使用言語の解説

https://neo4j.com/product/auradb/

Neo4jとは

Neo4jは、世界的に利用されているグラフデータベース管理システムです。ノードとエッジを直感的に扱え、大量のデータでも高速にクエリを実行できます。

使用言語:Cypher

Neo4jでデータを操作する際には、**Cypher(サイファー)**というクエリ言語を使用します。これは、グラフ構造を視覚的に表現でき、学習コストが低いのが特徴です。

例:ノードの作成

CREATE (a:Person {name: 'Alice'})

例:関係の作成

MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:KNOWS]->(b)

Neo4jの使い方説明

基本的な操作

データの投入

  1. プロジェクトの作成:新しいプロジェクトを作成します。
  2. データベースの作成:プロジェクト内で新しいグラフデータベースを作成します。
  3. Cypherクエリの実行:データベースを起動し、Cypherクエリを実行します。

例:人物と関係性の追加

CREATE (alice:Person {name: 'Alice'})
CREATE (bob:Person {name: 'Bob'})
CREATE (alice)-[:FRIENDS_WITH]->(bob)

データのクエリ

例:友人関係の検索

MATCH (person:Person)-[:FRIENDS_WITH]->(friend)
RETURN person.name, friend.name

実践例:簡単な家系図の作成

  1. 家族ノードの作成
CREATE (john:Person {name: 'John'})
CREATE (jane:Person {name: 'Jane'})
CREATE (emma:Person {name: 'Emma'})
  1. 関係性の定義
CREATE (john)-[:MARRIED_TO]->(jane)
CREATE (john)-[:PARENT_OF]->(emma)
CREATE (jane)-[:PARENT_OF]->(emma)
  1. クエリの実行:子供を持つ人物の検索
MATCH (parent:Person)-[:PARENT_OF]->(child:Person)
RETURN parent.name, child.name

結果

Neo4jをいじるには、cypher言語を習得しないといけないのか・・・むずいです。
なんとか、私の知る言語でいじれないかなぁと努力したのが以下です。

Google ColaboratoryでNeo4jを操作するガイド

必要な環境の準備

まずは、作業に必要なアカウントやツールを準備しましょう。

  • Googleアカウント:Colabを使用するために必要です。
  • Neo4jアカウント:Neo4j AuraDB Freeを利用するために作成します。

Neo4j AuraDB Freeのセットアップ

1. Neo4jアカウントの作成

  1. **Neo4j AuraDB Free**のページにアクセスします。
  2. 画面右上の「Start Free」ボタンをクリックします。
  3. 必要な情報(メールアドレス、パスワードなど)を入力してアカウントを作成します。
  4. 登録したメールアドレスに確認メールが届くので、リンクをクリックして認証を完了します。

2. 無料データベースの作成

  1. アカウント作成後、Neo4j Auraのダッシュボードにアクセスします。
  2. Create a Free Database」ボタンをクリックします。
  3. データベースの名前(例:mydb)を入力します。
  4. リージョンを選択します。日本に近いリージョンを選ぶと接続が速くなります。
  5. Create」をクリックしてデータベースを作成します。数十秒で準備が完了します。

3. 接続情報の取得

  1. 作成したデータベースの「Connect」ボタンをクリックします。
  2. 接続情報が表示されます。
    • Bolt URL(例:neo4j+s://xxxx.databases.neo4j.io
    • Usernameneo4j
    • Password:自動生成されたパスワード(コピーしてメモしておきます)
  3. Importantと表示されるパスワードは一度しか表示されないため、必ず控えてください。

Colabでのライブラリインストール

1. Colabノートブックの作成

  1. **Google Colaboratory**にアクセスします。
  2. 新しいノートブック」をクリックします。

2. Neo4jドライバのインストール

最初のセルに以下のコマンドを入力し、Shift + Enterで実行します。

!pip install neo4j
  • これにより、PythonからNeo4jに接続するためのドライバがインストールされます。

PythonからNeo4jへの接続

1. 接続設定の記述

新しいセルを作成し、以下のコードを入力します。先ほど控えた接続情報を反映させます。

from neo4j import GraphDatabase

# 接続情報の設定
uri = "neo4j+s://<YOUR_BOLT_URL>"
username = "neo4j"
password = "<YOUR_PASSWORD>"

# ドライバの作成
driver = GraphDatabase.driver(uri, auth=(username, password))
  • <YOUR_BOLT_URL>:取得したBolt URLを貼り付けます。
  • <YOUR_PASSWORD>:取得したパスワードを貼り付けます。

2. ドライバのテスト

接続が正しく行えるかを確認します。

# ドライバのテスト
with driver.session() as session:
    greeting = session.run("RETURN 'Hello, Neo4j!' AS message").single()
    print(greeting["message"])
  • 正常に接続されていれば、Hello, Neo4j!と表示されます。

参考リンク


Google ColaboratoryでNeo4jを操作する:コード解説と実践ガイド

コードの解説と実行

それでは、基本的なcypher言語をpython版のコードに置き換えながら、基本的なNeo4j動作をColab上で確認していきます。

コード1:Rosaがベルリンに住んでいるデータの作成

  • Personノード(name: 'Rosa')とPlaceノード(city: 'Berlin', country: 'DE')を作成します。
  • RosaがBerlinにsince: 2020から住んでいることを示すLIVES_INリレーションシップを作成します。

CREATE (:Person { name: 'Rosa' })-[:LIVES_IN {since: 2020}]->(:Place {city: 'Berlin', country: 'DE'})

Colabでの実行方法

PythonコードでCypherクエリを実行します。

def create_rosa(tx):
    tx.run("""
        CREATE (:Person { name: 'Rosa' })-[:LIVES_IN {since: 2020}]->(:Place {city: 'Berlin', country: 'DE'})
    """)

with driver.session() as session:
    session.execute_write(create_rosa)

結果の確認

def get_rosa(tx):
    result = tx.run("""
        MATCH (p:Person { name: 'Rosa' })-[:LIVES_IN]->(place:Place)
        RETURN p.name AS person, place.city AS city, place.country AS country
    """)
    return result.single()

with driver.session() as session:
    record = session.execute_read(get_rosa)
    print(f"{record['person']} lives in {record['city']}, {record['country']}")
  • 出力

    Rosa lives in Berlin, DE
    

コード2:データベース内のノードとリレーションシップの削除

  • すべてのノードを削除しようとしますが、リレーションシップが存在する場合はエラーになります。
  • LIVES_INリレーションシップを削除します。
  • DETACH DELETEを使用して、すべてのノードとそのリレーションシップを削除します。

MATCH (n)
DELETE n

MATCH ()-[r:LIVES_IN]->()
DELETE r

MATCH (n)
DETACH DELETE n

Colabでの実行方法

エラーを避けるため、適切な順序で実行します。

def delete_all(tx):
    # まず、すべてのリレーションシップを削除
    tx.run("MATCH ()-[r]->() DELETE r")
    # 次に、すべてのノードを削除
    tx.run("MATCH (n) DELETE n")

with driver.session() as session:
    session.execute_write(delete_all)

または、DETACH DELETEを使用して一度に削除します。

def detach_delete_all(tx):
    tx.run("MATCH (n) DETACH DELETE n")

with driver.session() as session:
    session.execute_write(detach_delete_all)

結果の確認

def count_nodes(tx):
    result = tx.run("MATCH (n) RETURN count(n) AS count")
    return result.single()["count"]

with driver.session() as session:
    node_count = session.execute_read(count_nodes)
    print(f"Total nodes in the database: {node_count}")
  • 出力

    Total nodes in the database: 0
    

コード3:ロンドンに住むFredとKarlのデータの作成

  • Placeノード(city: 'London', country: 'UK')を作成またはマッチします。
  • Personノード(name: 'Fred'とname: 'Karl')を作成またはマッチします。
  • FredとKarlがLondonに住んでいることを示すLIVES_INリレーションシップを作成します。

MERGE (london:Place { city: 'London', country: 'UK' })
MERGE (fred:Person { name: 'Fred' })
MERGE (fred)-[:LIVES_IN]->(london)
MERGE (karl:Person { name: 'Karl' })
MERGE (karl)-[:LIVES_IN]->(london)

Colabでの実行方法

def create_london_residents(tx):
    tx.run("""
        MERGE (london:Place { city: 'London', country: 'UK' })
        MERGE (fred:Person { name: 'Fred' })
        MERGE (fred)-[:LIVES_IN]->(london)
        MERGE (karl:Person { name: 'Karl' })
        MERGE (karl)-[:LIVES_IN]->(london)
    """)

with driver.session() as session:
    session.execute_write(create_london_residents)

結果の確認

def get_london_residents(tx):
    result = tx.run("""
        MATCH (p:Person)-[:LIVES_IN]->(place:Place { city: 'London' })
        RETURN p.name AS person
    """)
    return [record["person"] for record in result]

with driver.session() as session:
    residents = session.execute_read(get_london_residents)
    print(f"Residents of London: {residents}")
  • 出力

    Residents of London: ['Fred', 'Karl']
    

コード4:ベルリンに住む人物の取得

  • city: 'ベルリン', country: 'DE'のPlaceに住むPersonノードをマッチします。
  • 該当するPersonノードを返します。

MATCH (p:Person)-[:LIVES_IN]->(:Place { city: 'ベルリン', country: 'DE' })
RETURN p

Colabでの実行方法

ベルリンの表記を'Berlin'に修正します(前述のコードではcity: 'Berlin'としています)。

def get_berlin_residents(tx):
    result = tx.run("""
        MATCH (p:Person)-[:LIVES_IN]->(:Place { city: 'Berlin', country: 'DE' })
        RETURN p.name AS person
    """)
    return [record["person"] for record in result]

with driver.session() as session:
    residents = session.execute_read(get_berlin_residents)
    print(f"Residents of Berlin: {residents}")
  • 出力

    Residents of Berlin: ['Rosa']
    

コード5:Rosaの友達の友達を取得(2ステップの友人関係)

  • name: 'Rosa'のPersonノードから、2ステップのFRIENDリレーションシップを辿って到達できるPersonノードをマッチします。
  • 該当する友達の友達(fof)を返します。

MATCH (:Person { name: 'Rosa' })-[:FRIEND*2..2]->(fof:Person)
RETURN fof

Colabでの実行方法

まず、FRIENDリレーションシップを持つデータを作成します。

def create_friendships(tx):
    # Rosa、Alice、Bobのノードを作成
    tx.run("MERGE (rosa:Person { name: 'Rosa' })")
    tx.run("MERGE (alice:Person { name: 'Alice' })")
    tx.run("MERGE (bob:Person { name: 'Bob' })")
    
    # RosaとAliceの間にFRIENDリレーションシップを作成
    tx.run("""
        MATCH (rosa:Person { name: 'Rosa' })
        MATCH (alice:Person { name: 'Alice' })
        MERGE (rosa)-[:FRIEND]->(alice)
    """)
    
    # AliceとBobの間にFRIENDリレーションシップを作成
    tx.run("""
        MATCH (alice:Person { name: 'Alice' })
        MATCH (bob:Person { name: 'Bob' })
        MERGE (alice)-[:FRIEND]->(bob)
    """)

with driver.session() as session:
    session.execute_write(create_friendships)

次に、クエリを実行します。

def get_friends_of_friends(tx):
    result = tx.run("""
        MATCH (:Person { name: 'Rosa' })-[:FRIEND*2..2]->(fof:Person)
        RETURN fof.name AS friend_of_friend
    """)
    return [record["friend_of_friend"] for record in result]

with driver.session() as session:
    fof = session.execute_read(get_friends_of_friends)
    print(f"Rosa's friends of friends: {fof}")
  • 出力

    Rosa's friends of friends: ['Bob']
    

コード6:Rosa自身を除く、Rosaの友達の友達を取得

  • name: 'Rosa'のPersonノードから、2ステップのFRIENDリレーションシップを辿って到達できるPersonノードをマッチします。
  • WHERE句でRosa自身を除外します。
  • 該当する友達の友達(fof)を返します。

MATCH (rosa:Person{ name: 'Rosa' })-[:FRIEND*2..2]->(fof:Person)
WHERE rosa <> fof
RETURN fof

Colabでの実行方法

既にデータは作成済みなので、クエリのみ実行します。

def get_friends_of_friends_excluding_self(tx):
    result = tx.run("""
        MATCH (rosa:Person{ name: 'Rosa' })-[:FRIEND*2..2]->(fof:Person)
        WHERE rosa <> fof
        RETURN fof.name AS friend_of_friend
    """)
    return [record["friend_of_friend"] for record in result]

with driver.session() as session:
    fof = session.execute_read(get_friends_of_friends_excluding_self)
    print(f"Rosa's friends of friends (excluding self): {fof}")
  • 出力

    Rosa's friends of friends (excluding self): ['Bob']
    

コード7:ベルリンの居住者から1〜2ステップ以内の友人を取得

  • city: 'Berlin'のPlaceに住むPersonノードをマッチします。
  • そのPersonノードから、1〜2ステップのFRIENDリレーションシップを辿って到達できる他のPersonノードをマッチします。
  • WHERE句で元のPerson自身を除外します。
  • 該当する友人(f)を返します。

MATCH (:Place { city: 'Berlin' })<-[:LIVES_IN]-(p:Person)<-[:FRIEND*1..2]-(f:Person)
WHERE f <> p
RETURN f

Colabでの実行方法

友人関係を拡張します。

def create_berlin_residents_and_friends(tx):
    # ベルリンのノードを作成
    tx.run("MERGE (berlin:Place { city: 'Berlin', country: 'DE' })")
    
    # Rosaを作成し、ベルリンに住む
    tx.run("MERGE (rosa:Person { name: 'Rosa' })")
    tx.run("""
        MATCH (rosa:Person { name: 'Rosa' })
        MATCH (berlin:Place { city: 'Berlin', country: 'DE' })
        MERGE (rosa)-[:LIVES_IN]->(berlin)
    """)
    
    # Rosaの友人Aliceを作成
    tx.run("MERGE (alice:Person { name: 'Alice' })")
    tx.run("""
        MATCH (rosa:Person { name: 'Rosa' })
        MATCH (alice:Person { name: 'Alice' })
        MERGE (rosa)-[:FRIEND]->(alice)
    """)
    
    # Aliceの友人Bobを作成
    tx.run("MERGE (bob:Person { name: 'Bob' })")
    tx.run("""
        MATCH (alice:Person { name: 'Alice' })
        MATCH (bob:Person { name: 'Bob' })
        MERGE (alice)-[:FRIEND]->(bob)
    """)
    
    # Bobの友人Charlieを作成し、ロンドンに住む
    tx.run("MERGE (charlie:Person { name: 'Charlie' })")
    tx.run("MERGE (london:Place { city: 'London', country: 'UK' })")
    tx.run("""
        MATCH (bob:Person { name: 'Bob' })
        MATCH (charlie:Person { name: 'Charlie' })
        MERGE (bob)-[:FRIEND]->(charlie)
    """)
    tx.run("""
        MATCH (charlie:Person { name: 'Charlie' })
        MATCH (london:Place { city: 'London', country: 'UK' })
        MERGE (charlie)-[:LIVES_IN]->(london)
    """)

with driver.session() as session:
    session.execute_write(create_berlin_residents_and_friends)

クエリを実行します。

def get_friends_near_berlin(tx):
    result = tx.run("""
        MATCH (:Place { city: 'Berlin' })<-[:LIVES_IN]-(p:Person)
        MATCH (p)-[:FRIEND*1..2]->(f:Person)
        WHERE f <> p
        RETURN DISTINCT f.name AS friend
    """)
    return [record["friend"] for record in result]

with driver.session() as session:
    friends = session.execute_read(get_friends_near_berlin)
    print(f"Friends within 1-2 steps of Berlin residents: {friends}")
  • 出力

    Friends within 1-2 steps of Berlin residents: ['Alice', 'Bob']
    

コード8:Fredの名前を'Freddy'に変更(文字列の連結)

  • name: 'Fred'のPersonノードをマッチします。
  • apoc.atomic.concat関数を使用して、nameプロパティに'dy'を連結します。
  • 変更前の値(oldValue)と変更後の値(newValue)を返します。

MATCH (p: Person { name: 'Fred' })
CALL apoc.atomic.concat(p, 'name', 'dy')
YIELD oldValue, newValue
RETURN oldValue, newValue

解説

  • 目的: Fredの名前に'dy'を追加して、Freddyにします。apoc.atomic.concatはAPOCプラグインの機能です。

Colabでの実行方法

注意: Neo4j AuraDB FreeではAPOCプラグインの一部機能が制限されています。このため、このコードは直接実行できません。

しかし、SETを使って同様の操作が可能です。

def rename_fred(tx):
    result = tx.run("""
        MATCH (p:Person { name: 'Fred' })
        WITH p, p.name AS oldValue
        SET p.name = p.name + 'dy'
        RETURN oldValue, p.name AS newValue
    """)
    return result.single()

with driver.session() as session:
    record = session.execute_write(rename_fred)
    print(f"Old name: {record['oldValue']}, New name: {record['newValue']}")
  • 出力

    Old name: Fred, New name: Freddy
    

コード9:現在の日時をエポック秒から日数に変換

  • datetime().epochSecondsで現在のエポック秒を取得します。
  • apoc.date.convert関数を使用して、秒数を日数に変換します。
  • 結果をoutputInDaysとして返します。

RETURN apoc.date.convert(datetime().epochSeconds, 'seconds', 'days') AS outputInDays

Colabでの実行方法

APOCプラグインが使用できない場合、Cypherの標準機能で代替します。

def get_current_date_in_days(tx):
        result = tx.run("""
            RETURN duration.between(date('1970-01-01'), date()).days AS outputInDays
        """)
        return result.single()["outputInDays"]

# 5. セッション内で関数を実行
with driver.session() as session:
        days = session.execute_read(get_current_date_in_days)
        print(f"Days since Unix epoch: {days}")
  • 出力

    Days since Unix epoch: <日数が表示されます>
    

終わりに

これをベースに、RAGを用いた機械学習に発展していきたいです。

Discussion