2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

グラフデータベースとは、グラフ構造でデータの関係性を表すデータベースです

ここでいう「グラフ」とはグラフ理論の「グラフ」であり、ノード(頂点)をエッジ(辺)で繋いだ構造のことを示します

本記事では、グラフデータベースである Neo4j を Livebook から操作し、基本的な使い方を理解します

実装したノートブックはこちら

環境構築

以下のような内容で docker-compose.with-neo4j.yml を作成します

services:
  livebook_with_neo4j:
    image: ghcr.io/livebook-dev/livebook:0.14.5
    container_name: livebook_with_neo4j
    ports:
      - '8080:8080'
      - '8081:8081'

  neo4j-for-livebook:
    image: neo4j:community
    container_name: neo4j-for-livebook
    tty: true
    ports:
      - 7474:7474
      - 7687:7687
    volumes:
      - ./neo4j/data:/data
      - ./neo4j/logs:/logs
      - ./neo4j/conf:/conf
      - ./certs:/var/lib/neo4j/certificates/bolt
    environment:
      - NEO4J_AUTH=none
    user: '1000'
    group_add:
      - '1000'

neo4j-for-livebook というように Neo4j 側のサービス名はハイフン区切り(ケバブケース)にしています

_ を使っていると、接続時にエラーが発生するためです

以下のコマンドを実行すると、 Livebook と Neo4j がそれぞれコンテナで起動します

docker compose --file docker-compose.with-neo4j.yml up

Livebook にはコンテナ起動時に表示される URL (トークン付き)でアクセスします

スクリーンショット 2025-01-06 14.18.04.png

右上の "+ New notebook" から新しいノートブックを開きます

スクリーンショット 2025-01-06 14.22.19.png

Neo4j Browser(Neo4j の操作用コンソール)には http://localhost:7474/ でアクセスします

認証なしの設定で起動しているため、 "Authentication type" に "No authentication" を選択して "Connect" をクリックしてください

スクリーンショット 2025-01-08 10.56.42.png

接続できると、 Neo4j の操作用コンソールが表示されます

スクリーンショット 2025-01-08 11.08.14.png

"Try the new Browser preview!" カードの "Let's go" ボタンをクリックすると、新しい UI に切り替わり、改めて接続を求められます

"Connection URL" に "localhost:7687" を入力し、 "Password" は空欄のまま "Connect" をクリックしてください

スクリーンショット 2025-01-08 10.50.16.png

新しい UI のコンソールが表示されます

スクリーンショット 2025-01-08 11.12.03.png

以降、コンテナ起動後の接続時には新しい UI の画面が表示されるようになります

セットアップ

Livebook のセットアップセルに以下のコードを入力し、必要なモジュールをインストールします

Mix.install([
  {:boltx, "~> 0.0.6"},
  {:kino, "~> 0.14.2"}
])

Neo4j への接続には Boltx を使用しています

データベースへの接続

コンテナで起動した Neo4j に接続します

neo4j-for-livebook の部分はホスト名で、今回のようにコンテナで起動した場合、サービス名でアクセスできます

opts = [
    hostname: "neo4j-for-livebook",
    scheme: "bolt",
    auth: [username: "neo4j", password: ""],
    user_agent: "boltxTest/1",
    pool_size: 15,
    max_overflow: 3,
    prefix: :default
]

{:ok, conn} = Boltx.start_link(opts)

接続できると以下のような結果が表示されます

{:ok, #PID<0.1591.0>}

グラフデータベースでは Cypher というクエリ言語を使用します

単純なクエリを発行してみましょう

何も参照せずに、単に 1 を "number" という名前で返します

conn
|> Boltx.query!("RETURN 1 AS number")
|> Boltx.Response.first()

実行結果

%{"number" => 1}

ノードの追加

一つノードを追加してみましょう

node =
  conn
  |> Boltx.query!("""
  CREATE
  (node:Sweet {
    name: "チョコレートケーキ",
    category: "ケーキ",
    brand: "スイーツベーカリー",
    price: 450
  })
  RETURN node
  """)
  |> Map.get(:results)
  |> hd()

実行結果

%{
  "node" => %Boltx.Types.Node{
    id: 0,
    properties: %{
      "brand" => "スイーツベーカリー",
      "category" => "ケーキ",
      "name" => "チョコレートケーキ",
      "price" => 450
    },
    labels: ["Sweet"],
    element_id: "4:105fdad8-4452-402a-af21-4889ba749cc3:0"
  }
}

ノードが Neo4j 上に追加され、 id や element_id の値が付与されました

Neo4j Browser を再読込すると、左上 "Nodes" の中に "Sweet" というラベルが追加されています

"Sweet" をクリックすると、以下のように "チョコレートケーキ" のノードが表示されます

スクリーンショット 2025-01-08 11.30.46.png

他のノードも一括追加します

entities =
  [
    %{
      label: "Sweet",
      properties: %{
        name: "イチゴのチーズケーキ",
        category: "ケーキ",
        brand: "チーズハウス",
        price: 520
      }
    },
    %{
      label: "Sweet",
      properties: %{
        name: "アップルパイ",
        category: "パイ",
        brand: "パイファクトリー",
        price: 400
      }
    },
    %{
      label: "Sweet",
      properties: %{
        name: "チョコチップクッキー",
        category: "クッキー",
        brand: "クッキーランド",
        price: 300
      }
    },
    %{
      label: "Sweet",
      properties: %{
        name: "ストロベリーキャンディー",
        category: "キャンディー",
        brand: "キャンディーガーデン",
        price: 100
      }
    },
    %{
      label: "Ingredient",
      properties: %{
        name: "小麦粉",
        type: "粉類"
      }
    },
    %{
      label: "Ingredient",
      properties: %{
        name: "砂糖",
        type: "粉類"
      }
    },
    %{
      label: "Ingredient",
      properties: %{
        name: "卵",
        type: "液体"
      }
    },
    %{
      label: "Ingredient",
      properties: %{
        name: "バター",
        type: "乳製品"
      }
    },
    %{
      label: "Ingredient",
      properties: %{
        name: "チョコレート",
        type: "粉類"
      }
    },
    %{
      label: "Ingredient",
      properties: %{
        name: "牛乳",
        type: "乳製品"
      }
    },
    %{
      label: "Ingredient",
      properties: %{
        name: "イチゴ",
        type: "フルーツ"
      }
    },
    %{
      label: "Ingredient",
      properties: %{
        name: "リンゴ",
        type: "フルーツ"
      }
    }
  ]

Boltx.transaction によって、一つのトランザクション内でクエリを実行することが出来ます

Boltx.transaction(conn, fn conn ->
  entities
  |> Enum.each(fn entity ->
    properties =
      entity.properties
      |> Enum.map(fn {key, value} -> "#{key}: \"#{value}\"" end)
      |> Enum.join(",")

    query =
      """
      CREATE
      (node:#{entity.label} {
        #{properties}
      })
      """

    Boltx.query!(conn, query)
  end)
end)

お菓子と素材のノードが全て追加されました

スクリーンショット 2025-01-08 13.06.20.png

スクリーンショット 2025-01-08 13.07.10.png

制約の追加

お菓子の名前、素材の名前は一意なので、一意制約を付与します

一意制約を付けると、インデックスが自動的に張られます

Boltx.query!(conn, """
CREATE CONSTRAINT FOR (s:Sweet) REQUIRE (s.name) IS UNIQUE
""")

実行結果

%Boltx.Response{
  results: [],
  fields: [],
  records: [],
  plan: nil,
  notifications: [],
  stats: %{"constraints-added" => 1, "contains-updates" => true},
  profile: nil,
  type: "s",
  bookmark: "FB:kcwQEF/a2ERSQCqvIUiJunScwy2Q"
}
Boltx.query!(conn, """
CREATE CONSTRAINT FOR (i:Ingredient) REQUIRE (i.name) IS UNIQUE
""")

インデックスの追加

項目を指定してインデックスを追加することもできます

以下のクエリでは Sweet ラベルの price にインデックスを張っています

Boltx.query!(conn, """
CREATE INDEX FOR (s:Sweet) ON (s.price)
""")

実行結果

Boltx.query!(conn, """
CREATE INDEX FOR (s:Sweet) ON (s.price)
""")

エッジの追加

以下のようにクエリを実行することで、チョコレートと小麦粉の間に "CONTAINS" というエッジ(関係性)が追加されます

edge =
  conn
  |> Boltx.query!("""
  MATCH (s:Sweet {name:"チョコレートケーキ"})
  MATCH (i:Ingredient {name:"小麦粉"})
  CREATE (s)-[r:CONTAINS]->(i)
  RETURN r
  """)
  |> Map.get(:results)
  |> hd()

実行結果

%{
  "r" => %Boltx.Types.Relationship{
    id: 0,
    properties: %{},
    start: 13,
    end: 18,
    type: "CONTAINS",
    element_id: "5:105fdad8-4452-402a-af21-4889ba749cc3:0",
    start_node_element_id: "4:105fdad8-4452-402a-af21-4889ba749cc3:13",
    end_node_element_id: "4:105fdad8-4452-402a-af21-4889ba749cc3:18"
  }
}

他のエッジを一括追加します

relations =
  [
    {"チョコレートケーキ", "CONTAINS", "卵"},
    {"チョコレートケーキ", "CONTAINS", "バター"},
    {"チョコレートケーキ", "CONTAINS", "チョコレート"},
    {"イチゴのチーズケーキ", "CONTAINS", "小麦粉"},
    {"イチゴのチーズケーキ", "CONTAINS", "砂糖"},
    {"イチゴのチーズケーキ", "CONTAINS", "卵"},
    {"イチゴのチーズケーキ", "CONTAINS", "バター"},
    {"イチゴのチーズケーキ", "CONTAINS", "牛乳"},
    {"イチゴのチーズケーキ", "CONTAINS", "イチゴ"},
    {"アップルパイ", "CONTAINS", "小麦粉"},
    {"アップルパイ", "CONTAINS", "砂糖"},
    {"アップルパイ", "CONTAINS", "バター"},
    {"アップルパイ", "CONTAINS", "リンゴ"},
    {"チョコチップクッキー", "CONTAINS", "小麦粉"},
    {"チョコチップクッキー", "CONTAINS", "砂糖"},
    {"チョコチップクッキー", "CONTAINS", "バター"},
    {"チョコチップクッキー", "CONTAINS", "チョコレート"},
    {"ストロベリーキャンディー", "CONTAINS", "砂糖"},
    {"ストロベリーキャンディー", "CONTAINS", "イチゴ"}
  ]
Boltx.transaction(conn, fn conn ->
  relations
  |> Enum.each(fn {src_name, relation, dest_name} ->
    query =
      """
      MATCH (s:Sweet {name:"#{src_name}"})
      MATCH (i:Ingredient {name:"#{dest_name}"})
      CREATE (s)-[r:#{relation}]->(i)
      """

    Boltx.query!(conn, query)
  end)
end)

Neo4j Browser を再読込すると、左上 Relationships に "CONTAINS" が追加されています

"CONTAINS" をクリックすると、お菓子と素材が CONTAINS で繋がったグラフが確認できます

スクリーンショット 2025-01-08 13.16.24.png

クエリの実行

チョコレート(素材)のタイプを取得する場合、以下のようにクエリを実行します

結果を表として表示するため、 Kino.DataTable を利用しています

conn
|> Boltx.query!("""
MATCH (i:Ingredient {name: "チョコレート"})
RETURN i.type AS タイプ
""")
|> Map.get(:results)
|> Kino.DataTable.new()

実行結果

スクリーンショット 2025-01-08 14.03.36.png

価格が 400 を超えるお菓子の名前と価格は以下のようにして取得できます

conn
|> Boltx.query!("""
MATCH (s:Sweet) WHERE s.price > 400
RETURN s.name AS お菓子名, s.price AS 価格
""")
|> Map.get(:results)
|> Kino.DataTable.new()

スクリーンショット 2025-01-08 14.07.32.png

チョコレートを使っているお菓子の名前とブランドは以下のようにして取得できます

conn
|> Boltx.query!("""
MATCH (s:Sweet)-[:CONTAINS]->(i:Ingredient {name: "チョコレート"})
RETURN s.name AS お菓子名, s.brand AS ブランド
""")
|> Map.get(:results)
|> Kino.DataTable.new()

スクリーンショット 2025-01-08 14.09.02.png

乳製品を使っていないお菓子は以下のようにして取得できます

conn
|> Boltx.query!("""
MATCH (s:Sweet)
WHERE NOT (s)-[:CONTAINS]->(:Ingredient {type: "乳製品"})
RETURN s.name AS 乳製品不使用のお菓子
""")
|> Map.get(:results)
|> Kino.DataTable.new()

スクリーンショット 2025-01-08 14.11.33.png

データの削除

全てのデータを削除します

Boltx.query!(conn, "MATCH (n) DETACH DELETE n")

実行結果

%Boltx.Response{
  results: [],
  fields: [],
  records: [],
  plan: nil,
  notifications: [],
  stats: %{"contains-updates" => true, "nodes-deleted" => 13, "relationships-deleted" => 19},
  profile: nil,
  type: "w",
  bookmark: "FB:kcwQEF/a2ERSQCqvIUiJunScwzCQ"
}

制約を削除します

constraints =
  conn
  |> Boltx.query!("SHOW CONSTRAINTS YIELD name")
  |> Map.get(:results)

実行結果

[%{"name" => "constraint_64f87830"}, %{"name" => "constraint_bdcef13f"}]
constraints
|> Enum.each(fn %{"name" => name} ->
  Boltx.query!(conn, "DROP CONSTRAINT #{name}")
end)

インデックスを削除します

indexes =
  conn
  |> Boltx.query!("SHOW INDEXES YIELD name")
  |> Map.get(:results)

実行結果

[%{"name" => "index_343aff4e"}, %{"name" => "index_cdfa3f1b"}, %{"name" => "index_f7700477"}]
indexes
|> Enum.each(fn %{"name" => name} ->
  Boltx.query!(conn, "DROP INDEX #{name}")
end)

全てを削除しました

まとめ

Boltx を使うことで Livebook から Neo4j を操作できました

次は Neo4j によるグラフ RAG を実装してみます

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?