平凡な社会人の日記

平凡な社会人の日記です。怠惰な毎日を送っております。

Amazon Neptune 触ってみた。~グラフDB楽しい。~

はじめに

本稿の目的

  • グラフDBについて知る
  • グラフDBの一つであるAmazon Neptuneに触ってみる
f:id:physics-heibon:20210821080140p:plain
こんなグラフの書き方を説明する。

本稿の対象者

  • グラフDBについて興味がある人
  • グラフがどう使われるか知りたい人
  • Amazon Neptune をサービスに取り入れたい人

自己紹介

こんにちは。4月から東京にて仕事でパソコンをぽちぽちしているさっちゃんです。開発未経験でIT企業に入って5ヶ月(研修3ヶ月、配属されて2ヶ月)、普段はバックエンドでAPIの設計、実装(Java, Spring boot)、テストの自動化をアジャイルで開発してます。二週間ほど前に突然AWSに関する仕事があったので参加させてもらって色々勉強していたところ、Amazon Neptuneが面白かったので共有したいと思います。AWSについては何も知らなかったんですが便利ですね。

本稿の構成

Amazon Neptune はフルマネージドのグラフDBなんですが、話の流れとしてはまずグラフの基本的な用語と身の回りのグラフ構造について説明します。その後でグラフDBについて簡単に言及した後でAmazon Neptuneとその query言語である Gremlin について書きます。ここまで読めばjupyter notebookを使ってAmazon Neptuneに触ることは可能なのですが、サービスに組み込む際やAWS Lambdaを使う際にはpythonJavaなどで扱えた方がいいと思います。ここではpythonを使ってグラフDBを触るためにgremlin_pythonというライブラリの紹介をします。

グラフとは何か。我々の身の回りにあるグラフ構造について。

本章では本稿で言うグラフとは何かと、グラフの例について挙げます。

グラフとは

本稿で言うグラフとは次の画像のようなものです。

f:id:physics-heibon:20210821054118j:plain
グラフの例。グラフGはvertexとedge, それぞれのpropetyから成る。後でこれを再現する

もう少し詳しく説明すると、グラフ理論におけるグラフとは大雑把に言うと頂点(vertex, node)と辺(edge)から成る集合(有向グラフ Gは順序対(V, A), where V: 集合, A: 順序対 (V, V)のこと)ですが、本稿で言うグラフはそれに加えて各頂点と辺に「どういう種類のノード、辺か」(上の例で言う person, cat, food, Loves, Knows)というラベルと、それぞれの頂点と辺に対してpropertyという情報を持たせたものです。これを用いると、現実世界にある物(人間、猫、アカウントなど)とそれらの関係を表現することができます。

グラフの例

我々の身の回りにはグラフでモデリングできるものに溢れています。

  • SNSにおけるフォロー
  • 鉄道の駅と駅、かかる時間
  • ネットワーク
  • ディレクトリとファイルの関係

グラフを使ってモデリングすることで、例えば最短距離を求めたいとなれば良く知られているアルゴリズムダイクストラやbitDPなど)で解く事ができるので、グラフは問題解決の手段としてよく使われています。

永続化の手段としてのグラフDBについて

ここまでの話をまとめると、グラフとは頂点(vertex)と辺(edge)とpropertyから成る、何らかの関係性を表す図のことでした。本節では永続化の手段としてグラフDBを使用する意義について述べます。

私が(そしておそらく多くの方が)永続化の手段として真っ先に思い浮かぶのはRelational DBです。物の関係性には1対1、1対多、多対多がありますが、1対1ならもちろんAmazon Dynamo DBなどのkey-value形式で保存できるDBを使えばいいですし、1対多はRDBが得意とするところだと思います。ここで多対多の場合について考えると、RDBだと中間テーブルを使うなどして多対多を1対多に落とし込む方法が一般的だと思いますが、テーブルが多くなるにつれて実現したいことに対してクエリが複雑になったりパフォーマンスが落ちるということがあります。そのような問題を回避する策として、保存したい対象がグラフ構造を持っているならばグラフDBという選択肢があります。

グラフDBとは、上にあげた例のようなモデリングされたグラフ構造を"そのまま"保存してしまうという方法です。これによって複雑な関係を持つデータでも比較的簡単なクエリで直感的に操作できます。また、パフォーマンスについては保存したい対象に寄ると思いますが、データが少ないうちはRDBの方が速いがデータ量が多くなるとグラフDBの方がデータの探索は速くなるという論文もありました。どうやらデータ数が増えるとテーブルのJoinがボトルネックになるようです。
Comparison of Graph Databases and Relational Databases When Handling Large-Scale Social Data

Amazon Neptuneとgremlinについて

グラフDBの選択肢の一つとして Amazon Neptune があります。Amazon Neptune はAWSのサービスの一つで、フルマネージドのグラフDBです。詳しくは公式サイトを見てほしいのですが、個人的には他のAWSサービスと連携させやすい点や使おうと思えばすぐに使い始められるところ、jupyter notebookから操作できることが気に入っています。Amazon Neptune ML を使えば機械学習を使ってノードを分類したりプロパティを推論することができるようです。

また、Neptune はクエリ言語としてApache TinkerPop Gremlin というものを使っていますが、これが慣れれば直感的にグラフを操作できるのでとても気に入っています。(クエリを流すのに少し癖がありますが...。)また多くの言語に対してdriverが用意されており、自分はAWS Lambdaの中でgremlin_pythonというライブラリを使ってグラフを生成したり探索しています。本章の残りでは実際にjupyter notebookからグラフを作ってみます。

jupyter notebookでグラフを触ってみる

AWS アカウントは作っている前提で進めます。aws consoleにいって、Neptune を検索します。あとは流れに乗って適当にぽちぽちやって(notebookの名前は適当にtestとかにしておく)数分待つとnotebookが生成されます。左側のナビゲーションのNotebooksからopen notebookを選択するとjupyter notebookに入れます。Gremlinの使い方はこのNotebooksに載っていますが、試しに本稿の例であげたグラフを作ってみましょう。

f:id:physics-heibon:20210821054118j:plain
再掲

適当なディレクトリで、右側にあるNewボタンからPython3を選択してnotebookを作ります。Untitledをクリックするとノートの名前を変えられるので、testとかでもしておきましょう。

頂点を作ってみる

では早速グラフを生成してみます。次のコマンドを実行してみてください。

%%gremlin
g.addV("person").property("name", "Toshiaki").property("age", 24)
f:id:physics-heibon:20210821070304p:plain
実行結果。

%%gremlinはGremlinというクエリ言語を使うという宣言で、この宣言がされるとgというのは暗黙のうちにグラフ全体を表すオブジェクトになります。このコマンドでname: Toshiaki, age: 24をpropertyに持つ頂点をグラフに加えることができます。

頂点ができているか確認する

本当にできているかは次のコマンドで確認できます。

%%gremlin 
g.V().hasLabel("person").has("name", "Toshiaki")
f:id:physics-heibon:20210821070554p:plain
何か頂点があることはわかる。

このコマンドについて補足すると、g.V()まででグラフ上の頂点全てを取ってきます。g.V().hasLabel("person")でpersonというラベルを持つ頂点に絞り、.hasでプロパティを使って絞る事ができます。ただこれだと分かりにくいので、最後に.valueMap()をつけると頂点のプロパティの中を見る事ができます。

%%gremlin 
g.V().hasLabel("person").has("name", "Toshiaki").valueMap()
f:id:physics-heibon:20210821071058p:plain
ちゃんと頂点が生成されている事が確認できる

また、次のコマンドでもっと見やすい形にできます。

%%gremlin -p v,oute,inv
g.V().path().by(elementMap())
f:id:physics-heibon:20210821072456p:plain
見やすい。すごい。
頂点を消す

頂点を消すにはdropを使います。

%%gremlin
g.V().drop()
一つのセルで複数のクエリを実行する

頂点を二つ生成しようとしても、次のようなコマンドはfoodラベルを持った頂点しか生成されません。(g.V()コマンドで確認してみてください)

%%gremlin
g.addV("cat").property("name", "Azuki").property("age", 3)
g.addV("food").property("type", "cat-food").property("company", "HOGEHOGE-FOOD")

これを思ったように実行するには、次の二通りがあります。

%%gremlin
g.addV("cat").property("name", "Azuki").property("age", 3).next()
g.addV("food").property("type", "cat-food").property("company", "HOGEHOGE-FOOD")
%%gremlin
g.addV("cat").property("name", "Azuki").property("age", 3)
.addV("food").property("type", "cat-food").property("company", "HOGEHOGE-FOOD")

これらの違いとしては、前者はクエリをnext()の部分で分けて流しているのに対して後者は一つのクエリとして流しているらしいです。

辺を描く

ここまでで頂点が三つ生成できるようになりました。

f:id:physics-heibon:20210821074139p:plain
person, cat, food というラベルの頂点が生成されている

では続いて辺を描いてみましょう。

%%gremlin
g.addE("Likes").from(g.V().hasLabel("person").has("name", "Toshiaki"))
.to(g.V().hasLabel("cat").has("name", "Azuki"))

構文的には g.addE("Label").from(頂点).to(頂点)と直感的です。辺ができたかどうかは次のコマンドで確認できます。

%%gremlin -p v,oute,inv
g.V().outE().inV().path()
f:id:physics-heibon:20210821075946p:plain
確かに辺が作られている

あとは同じように辺を作れば完成です。お疲れ様でした。

%%gremlin
g.addE("Loves").from(g.V().hasLabel("person").has("name", "Toshiaki"))
.to(g.V().hasLabel("cat").has("name", "Azuki")).next()
g.addE("Knows").to(g.V().hasLabel("person").has("name", "Toshiaki"))
.from(g.V().hasLabel("cat").has("name", "Azuki")).next()
g.addE("Loves").to(g.V().hasLabel("food"))
.from(g.V().hasLabel("cat").has("name", "Azuki"))
f:id:physics-heibon:20210821080140p:plain
完成。
辺を辿って頂点を見つける

personというラベルを持つ頂点からLovesというラベルを持つ辺の先にある頂点を探すには次のようにします。

%%gremlin
g.V().hasLabel("person").out("Loves").valueMap()
f:id:physics-heibon:20210821215243p:plain
人間が好きなのは猫
f:id:physics-heibon:20210821215328p:plain
人間が好きな猫が好きなのはキャットフード
次にやること
もっと触ってみたいと思ったら、Notebookにあるgetting-startedやサンプルをやっていくと楽しいです。また、pythonなどで扱うためにgremlin_pythonというライブラリもあります。例えばAmazon API Gateway のオーソライザーとしてcognitoを使って認証を行い、そのユーザー情報を使ってLambdaの中でログインしてきたユーザーの頂点を作って、アプリケーションの中でフォローなどの関係ができたらNeptuneに辺を作成しにいく。そしてグラフを使ってリコメンデーションする。みたいな使い方もできそうですね。

Pythonを使ってNeptuneを操作する

最後にpythonを使って上記と同じことを行うコードを置いておきます。ここではAWS Lambdaからグラフを操作することを想定しています。 gremlin_pythonは少し癖があるますが、慣れれば使うのは簡単です。接続の設定などは調べてもらう必要がありますが、雰囲気はこんな感じになると思います。toList()でクエリを流します。 ちなみにlocalで書いたコードをAWS Lambdaにデプロイするための方法の一つとして、インストールしたパッケージごとzipで固めてaws cliでぽちぽちやる方法がありますが、ここではAWSコンソール上でコードを書いてみてください。(ある程度コード量が多くなるとコンソール上ではコードをいじれなくなる。) ちゃんと書けているかはprintしてCloutWatchでログを見るか、jupyter notebookで見ると良いと思います。

from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection
from gremlin_python.process.anonymous_traversal import traversal
from gremlin_python.process.traversal import T


def lambda_handler(event, context):
    # 接続の設定は調べてね。
    g = traversal().withRemote(DriverRemomteConnection("wss://(endpoint_name):(port_number)/gremlin','g'"))

    # 頂点の生成
    person_vertex = g.addV("person")\
        .property("name", "Toshiaki")\
        .property("age", 24).toList()[0]
    cat_vertex = g.addV("cat")\
        .property("name", "Azuki")
        .property("age", 3).toList()[0]
    food_vertex = g.addV("food")\
        .property("type", "cat-food")\
        .property("company", "HOGEHOGE-FOOD").toList()[0]

    # 辺の生成
    g.addE("Loves").from_(person_vertex).to(cat_vertex).toList()
    g.addE("Knows").from_(cat_vertex).to(person_vertex).toList()
    g.addE("Loves").from_(cat_vertex).to(food_vertex).toList()

    # グラフを探索
    g.V().hasLabel("person")\
        .has("name", "Toshiaki")\
        .out("Loves").toList()
    g.V().hasLabel("person")\
        .has("name", "Toshiaki")\
        .out("Loves")\
        .out("Loves").toList()

    # 頂点のidを自分で指定する(idは頂点を作成するときにのみ指定でき、作った後で変更することはできない)
    g.addV("person")\
        .property(T.id, 1)\
        .property("name", "hogehoge").toList()
    hogehoge_vertex = g.V().has(T.id, 1).toList()[0]

終わりに

今回はAmazon Neptuneを触ってグラフを生成、探索してみました。より詳しくはAmazon NeptuneのドキュメントやApache TinkerPopのドキュメントを参照してください。 AWSはコンソールでぽちぽちやれば簡単にサーバーレスで動く物を作る事ができるので楽しいですね。次はAPI Gatewayかcognitoの話を書くかもしれません。何か間違いやアドバイスがあればぜひお願いします。