Amazon Neptune 触ってみた。~グラフDB楽しい。~
はじめに
本稿の目的
- グラフDBについて知る
- グラフDBの一つであるAmazon Neptuneに触ってみる
本稿の対象者
- グラフ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を使う際にはpythonやJavaなどで扱えた方がいいと思います。ここではpythonを使ってグラフDBを触るためにgremlin_pythonというライブラリの紹介をします。
グラフとは何か。我々の身の回りにあるグラフ構造について。
本章では本稿で言うグラフとは何かと、グラフの例について挙げます。
グラフとは
本稿で言うグラフとは次の画像のようなものです。
もう少し詳しく説明すると、グラフ理論におけるグラフとは大雑把に言うと頂点(vertex, node)と辺(edge)から成る集合(有向グラフは順序対, where : 集合, : 順序対 のこと)ですが、本稿で言うグラフはそれに加えて各頂点と辺に「どういう種類のノード、辺か」(上の例で言う person, cat, food, Loves, Knows)というラベルと、それぞれの頂点と辺に対してpropertyという情報を持たせたものです。これを用いると、現実世界にある物(人間、猫、アカウントなど)とそれらの関係を表現することができます。
永続化の手段としてのグラフ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に載っていますが、試しに本稿の例であげたグラフを作ってみましょう。
適当なディレクトリで、右側にあるNewボタンからPython3を選択してnotebookを作ります。Untitledをクリックするとノートの名前を変えられるので、testとかでもしておきましょう。
頂点を作ってみる
では早速グラフを生成してみます。次のコマンドを実行してみてください。
%%gremlin g.addV("person").property("name", "Toshiaki").property("age", 24)
%%gremlinはGremlinというクエリ言語を使うという宣言で、この宣言がされるとgというのは暗黙のうちにグラフ全体を表すオブジェクトになります。このコマンドでname: Toshiaki, age: 24をpropertyに持つ頂点をグラフに加えることができます。
頂点ができているか確認する
本当にできているかは次のコマンドで確認できます。
%%gremlin g.V().hasLabel("person").has("name", "Toshiaki")
このコマンドについて補足すると、g.V()まででグラフ上の頂点全てを取ってきます。g.V().hasLabel("person")でpersonというラベルを持つ頂点に絞り、.hasでプロパティを使って絞る事ができます。ただこれだと分かりにくいので、最後に.valueMap()をつけると頂点のプロパティの中を見る事ができます。
%%gremlin g.V().hasLabel("person").has("name", "Toshiaki").valueMap()
また、次のコマンドでもっと見やすい形にできます。
%%gremlin -p v,oute,inv g.V().path().by(elementMap())
一つのセルで複数のクエリを実行する
頂点を二つ生成しようとしても、次のようなコマンドは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()の部分で分けて流しているのに対して後者は一つのクエリとして流しているらしいです。
辺を描く
ここまでで頂点が三つ生成できるようになりました。
では続いて辺を描いてみましょう。
%%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()
あとは同じように辺を作れば完成です。お疲れ様でした。
%%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"))
辺を辿って頂点を見つける
personというラベルを持つ頂点からLovesというラベルを持つ辺の先にある頂点を探すには次のようにします。
%%gremlin g.V().hasLabel("person").out("Loves").valueMap()
次にやること
もっと触ってみたいと思ったら、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]