はじめに
グラフデータベースとは、グラフ構造でデータの関係性を表すデータベースです
ここでいう「グラフ」とはグラフ理論の「グラフ」であり、ノード(頂点)をエッジ(辺)で繋いだ構造のことを示します
本記事では、グラフデータベースである FalkorDB を Livebook から操作し、基本的な使い方を理解します
ちなみに Falkor (ファルコー)は映画ネバーエンディングストーリーに登場する幸運の竜です(日本語ではファルコンと呼ばれています)
実装したノートブックはこちら
環境構築
以下のような内容で docker-compose.with-falkor-db.yml
を作成します
services:
livebook_with_falkor_db:
image: ghcr.io/livebook-dev/livebook:0.14.5
container_name: livebook_with_falkor_db
ports:
- '8080:8080'
- '8081:8081'
falkor_db_for_livebook:
image: falkordb/falkordb:edge
container_name: falkor_db_for_livebook
tty: true
ports:
- 6379:6379
- 3000:3000
volumes:
- ./falkor_db/data:/data
以下のコマンドを実行すると、 Livebook と FalkorDB がそれぞれコンテナで起動します
docker compose --file docker-compose.with-falkor-db.yml up
Livebook にはコンテナ起動時に表示される URL (トークン付き)でアクセスします
右上の "+ New notebook" から新しいノートブックを開きます
FalkorDB Browser(FalkorDB の操作用コンソール)には http://localhost:3000/
でアクセスします
認証の設定をしていないので、 "User Name" と "Password" の欄を空にして "Connect" をクリックしてください
以下のような画面に遷移します
セットアップ
Livebook のセットアップセルに以下のコードを入力し、必要なモジュールをインストールします
Mix.install([
{:redisgraph, "~> 0.1.0"},
{:kino, "~> 0.14.2"}
])
alias RedisGraph.{Node, Edge, Graph, QueryResult}
FalkorDB への接続には RedisGraph を使用しています
データベースへの接続
コンテナで起動した FalkorDB に接続します
falkor_db_for_livebook
の部分はホスト名で、今回のようにコンテナで起動した場合、サービス名でアクセスできます
6379
はポート番号です
{:ok, conn} = Redix.start_link("redis://falkor_db_for_livebook:6379")
接続できると以下のような結果が表示されます
{:ok, #PID<0.239.0>}
グラフの作成
新しいグラフ "Sweets" を作成します
graph = Graph.new(%{name: "Sweets"})
実行結果
%RedisGraph.Graph{name: "Sweets", nodes: %{}, edges: []}
この時点では FalkorDB には何も作成されていません
最終的にコミットしたタイミングで作成されます
ノードの追加
新しいノードを作成します
node = Node.new(%{
label: "Sweet",
properties: %{
name: "チョコレートケーキ",
category: "ケーキ",
brand: "スイーツベーカリー",
price: 450
}
})
実行結果
%RedisGraph.Node{
id: nil,
alias: nil,
label: "Sweet",
properties: %{
name: "チョコレートケーキ",
category: "ケーキ",
brand: "スイーツベーカリー",
price: 450
}
}
label
はノードの型であり、例えば「Person」や「Country」など、ノードの種類を表すものです
この時点では id
と alias
が nil
になります
新しいノードをグラフに追加します
{graph, john} = Graph.add_node(graph, node)
graph
実行結果
%RedisGraph.Graph{
name: "Sweets",
nodes: %{
"sdrayosuel" => %RedisGraph.Node{
id: nil,
alias: "sdrayosuel",
label: "Sweet",
properties: %{
name: "チョコレートケーキ",
category: "ケーキ",
brand: "スイーツベーカリー",
price: 450
}
}
},
edges: []
}
グラフの nodes
にノードが追加されました
また、ノードの alias
にランダムな値が設定されています
残りのノードを一気に追加しておきます
nodes = %{"チョコレートケーキ" => node}
{graph, nodes} =
[
%{
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: "フルーツ"
}
}
]
|> Enum.reduce({graph, nodes}, fn entities, {acc_graph, acc_nodes} ->
{graph, node} = Graph.add_node(acc_graph, Node.new(entities))
nodes = Map.put(acc_nodes, entities.properties.name, node)
{graph, nodes}
end)
エッジの追加
"チョコレートケーキ" が "砂糖" を含んでいる、という関係性をエッジとして追加します
relation
に関係性を示します
edge = Edge.new(%{
src_node: Map.get(nodes, "チョコレートケーキ"),
dest_node: Map.get(nodes, "砂糖"),
relation: "CONTAINS"
})
{:ok, graph} = Graph.add_edge(graph, edge)
他のエッジも一気に追加します
graph =
[
{"チョコレートケーキ", "CONTAINS", "卵"},
{"チョコレートケーキ", "CONTAINS", "バター"},
{"チョコレートケーキ", "CONTAINS", "チョコレート"},
{"イチゴのチーズケーキ", "CONTAINS", "小麦粉"},
{"イチゴのチーズケーキ", "CONTAINS", "砂糖"},
{"イチゴのチーズケーキ", "CONTAINS", "卵"},
{"イチゴのチーズケーキ", "CONTAINS", "バター"},
{"イチゴのチーズケーキ", "CONTAINS", "牛乳"},
{"イチゴのチーズケーキ", "CONTAINS", "イチゴ"},
{"アップルパイ", "CONTAINS", "小麦粉"},
{"アップルパイ", "CONTAINS", "砂糖"},
{"アップルパイ", "CONTAINS", "バター"},
{"アップルパイ", "CONTAINS", "リンゴ"},
{"チョコチップクッキー", "CONTAINS", "小麦粉"},
{"チョコチップクッキー", "CONTAINS", "砂糖"},
{"チョコチップクッキー", "CONTAINS", "バター"},
{"チョコチップクッキー", "CONTAINS", "チョコレート"},
{"ストロベリーキャンディー", "CONTAINS", "砂糖"},
{"ストロベリーキャンディー", "CONTAINS", "イチゴ"}
]
|> Enum.reduce(graph, fn {src_name, relation, dest_name}, acc_graph ->
{:ok, graph} =
Graph.add_edge(
acc_graph,
Edge.new(%{
src_node: Map.get(nodes, src_name),
dest_node: Map.get(nodes, dest_name),
relation: relation
})
)
graph
end)
コミット
ここまでの操作を FalkorDB に反映します
{:ok, commit_result} = RedisGraph.commit(conn, graph)
実行結果
{:ok,
%RedisGraph.QueryResult{
conn: #PID<0.239.0>,
graph_name: "Sweets",
raw_result_set: [
["Labels added: 2", "Nodes created: 13", "Properties set: 36", "Relationships created: 20",
"Cached execution: 0", "Query internal execution time: 3.757917 milliseconds"]
],
header: nil,
result_set: nil,
statistics: %{
"Labels added" => "2",
"Nodes created" => "13",
"Nodes deleted" => nil,
"Properties set" => "36",
"Query internal execution time" => "3.757917",
"Relationships created" => "20",
"Relationships deleted" => nil
},
labels: nil,
property_keys: nil,
relationship_types: nil
}}
ラベルが2種類、ノードが13個、エッジが20個つくられました
FalkorDB での確認
FalkorDB Browser で以下のクエリを実行します
MATCH (n)-[r]->(m) RETURN n,r,m
このクエリは OpenCypher というグラフデータベース用の言語で書いています
実行結果
ノードやエッジがつながってグラフになっていることが分かります
"チョコチップクッキー"の素材のグラフは以下のクエリで参照できます
MATCH (n {name: "チョコチップクッキー"})-[r]->(m)
RETURN n,r,m
"イチゴ" を含んでいるお菓子のグラフは以下のクエリで参照できます
MATCH (n)-[r]->(m {name: "イチゴ"})
RETURN n,r,m
クエリの実行
「イチゴを含むお菓子の名前と価格」は以下のようにして取得できます
{:ok, query_result} = RedisGraph.query(conn, graph.name, """
MATCH (n:Sweet)-[r:CONTAINS]->(m {name: "イチゴ"})
RETURN n.name AS name, n.price AS price
""")
query_result
実行結果
%RedisGraph.QueryResult{
conn: #PID<0.239.0>,
graph_name: "Sweets",
raw_result_set: [
[[1, "name"], [1, "price"]],
[
[[2, "ストロベリーキャンディー"], [3, 100]],
[[2, "イチゴのチーズケーキ"], [3, 520]]
],
["Cached execution: 0", "Query internal execution time: 1.732500 milliseconds"]
],
header: ["name", "price"],
result_set: [
["ストロベリーキャンディー", 100],
["イチゴのチーズケーキ", 520]
],
statistics: %{
"Labels added" => nil,
"Nodes created" => nil,
"Nodes deleted" => nil,
"Properties set" => nil,
"Query internal execution time" => "1.732500",
"Relationships created" => nil,
"Relationships deleted" => nil
},
labels: ["Sweet", "Ingredient"],
property_keys: ["name", "category", "brand", "price", "type"],
relationship_types: ["CONTAINS"]
}
このままの形式だと扱いにくいので、マップに変換します
QueryResult.results_to_maps(query_result)
実行結果
[
%{"name" => "ストロベリーキャンディー", "price" => 100},
%{"name" => "イチゴのチーズケーキ", "price" => 520}
]
表として表示する機能もありますが、日本語だと表示が崩れるので微妙です
query_result
|> QueryResult.pretty_print()
|> Kino.Text.new(terminal: true)
実行結果
Livebook 上であれば Kino.DataTable
を使いましょう
query_result
|> QueryResult.results_to_maps()
|> Kino.DataTable.new()
実行結果
もっと複雑な例として、乳製品不使用のお菓子を取得してみましょう
# 乳製品不使用のお菓子
{:ok, query_result} = RedisGraph.query(conn, graph.name, """
MATCH (s:Sweet)
WHERE NOT (s)-[:CONTAINS]->(:Ingredient {type: "乳製品"})
RETURN s.name AS name
""")
query_result
|> QueryResult.results_to_maps()
|> Kino.DataTable.new()
実行結果
データの削除
以下のコードで全てのデータを削除します
RedisGraph.query(conn, graph.name, "MATCH (n) DETACH DELETE n")
実行結果
{:ok,
%RedisGraph.QueryResult{
conn: #PID<0.239.0>,
graph_name: "Sweets",
raw_result_set: [
["Nodes deleted: 13", "Relationships deleted: 20", "Cached execution: 0",
"Query internal execution time: 2.147084 milliseconds"]
],
header: nil,
result_set: nil,
statistics: %{
"Labels added" => nil,
"Nodes created" => nil,
"Nodes deleted" => "13",
"Properties set" => nil,
"Query internal execution time" => "2.147084",
"Relationships created" => nil,
"Relationships deleted" => "20"
},
labels: nil,
property_keys: nil,
relationship_types: nil
}}
まとめ
Livebook から FalkorDB を操作することができました
次はグラフデータベースを利用してグラフRAGを構築してみます