5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Livebook でグラフデータベース FalkorDB を操作する

Last updated at Posted at 2025-01-06

はじめに

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

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

本記事では、グラフデータベースである 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 (トークン付き)でアクセスします

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

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

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

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

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

認証の設定をしていないので、 "User Name" と "Password" の欄を空にして "Connect" をクリックしてください

以下のような画面に遷移します

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

セットアップ

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」など、ノードの種類を表すものです

この時点では idaliasnil になります

新しいノードをグラフに追加します

{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 というグラフデータベース用の言語で書いています

https://opencypher.org/

実行結果

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

ノードやエッジがつながってグラフになっていることが分かります

"チョコチップクッキー"の素材のグラフは以下のクエリで参照できます

MATCH (n {name: "チョコチップクッキー"})-[r]->(m)
RETURN n,r,m

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

"イチゴ" を含んでいるお菓子のグラフは以下のクエリで参照できます

MATCH (n)-[r]->(m {name: "イチゴ"})
RETURN n,r,m

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

クエリの実行

「イチゴを含むお菓子の名前と価格」は以下のようにして取得できます

{: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)

実行結果

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

Livebook 上であれば Kino.DataTable を使いましょう

query_result
|> QueryResult.results_to_maps()
|> Kino.DataTable.new()

実行結果

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

もっと複雑な例として、乳製品不使用のお菓子を取得してみましょう

# 乳製品不使用のお菓子
{: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()

実行結果

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

データの削除

以下のコードで全てのデータを削除します

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を構築してみます

5
1
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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?