グラフデータベース紹介 - Amazon Neptune と Gremlin を使用したつながりのトラバーサル
AWS のグラフデータベースを紹介する勉強会を開催しました。「雲勉@オンライン【勉強会】Graph Database 入門 ~Amazon Neptune によるデータの「つながり」探索~【開発エンジニア向け】」
プレゼンテーションの内容を紹介し、グラフデータベースと Amazon Neptune を知るきっかけになれば幸いです。この投稿のサンプルは、 GitHub リポジトリから取得できます。
前提条件
参加者は以下が期待されます:
- グラフデータベースと Amazon Neptune に興味を持っていること
- RDB と AWS の基本知識
この投稿の目標:
- グラフデータベースと Amazon Neptune の概要把握
- Apache TinkerPop Gremlin を使用したトラバーサルのデモ
グラフデータベース概要
概念と技術用語
グラフデータベースは、データ内の関係を保存およびクエリすることに最適化されています。リレーショナルデータベース (RDB) や NoSQL データベースも関係の保存とクエリは可能ですが、特定のユースケースにおいて性能と拡張性の点でグラフデータベースのほうが適しています。グラフデータベースは数学的なグラフ理論の原則に基づいており、それに対してリレーショナルデータベースは集合論に基づいています。
グラフデータベースでは、データは頂点 (vertices) またはノード (nodes) として表現され、それらの間の関係はエッジ (edges) として表現されます。頂点とエッジにはラベルが付けられることがあります。エッジには有向と無向の2種類があります。頂点とエッジがキーと値のペアを持つことができるグラフは、プロパティグラフ (property graph) と呼ばれます。
代表的なグラフ
Social Network グラフ
グラフデータベースは、 Facebook, LinkedIn などの SNS に見られるような人との関係を表現するためによく使用されます。
ナレッジグラフ
データを文脈で理解することにより、検索エンジンは同じ名前を共有する異なる概念を区別できます。たとえば、「りんご」という果物とテクノロジー企業の「Apple Inc.」などです。ナレッジグラフの用語は、 “Introducing the Knowledge Graph: things, not strings” の公開以来、一般的になりました。
Identity グラフ
グラフデータベースは、異なる ID 間の関係を表現するためにも使用できます。たとえば、複数のサイトやデバイスでのユーザーの行動を単一のユーザーに関連付けるなどがあります。これにより、ユーザーの行動分析が可能になります。これに関連するよく知られたユースケースの一例が Customer 360 です。
Fraud グラフ
不正活動に共同参加する Fraud Rings を構成し、一部に合法的な取引を含めることで、詐欺行為を検出するのを難しくすることがあります。グラフデータベースは、クレジットカード番号、電話番号、社会保障番号などの共通の属性のような Fraud Rings で共有されるパターンを特定することにより、詐欺行為を容易に視覚化するのに役立つことがあります。
パナマ文書
最近では、グラフデータベースの多くのユースケースがありますが、その中でも最も有名なものの一つがパナマ文書です。2016年に、パナマの法律事務所から大量の内部文書が流出しました。国際調査ジャーナリスト連合 (ICIJ) は、データの分析にグラフデータベースを使用しました。
RDB および NoSQL との比較
ユースケースに応じて異なるデータベースを使用することは非常に重要です。基本的に、単一のタイプのデータベースに頼るべきではありません。
Item | Graph DB | RDB | NoSQL |
---|---|---|---|
データ構造 | Graph | Table | 様々 (Document, Key-Value, etc.) |
スキーマ定義 | 緩い | 厳格 | 緩い |
目的 | エンティティ間の関係保存 | エンティティの保存 | 様々 (Primary data storage, caching, etc.) |
関連データのクエリ | Traversal | Join/Sub Query | 様々 |
パフォーマンス | 非常に高い | 平均 (他と比べて) | 非常に高い |
言語 | Gremlin SPARQL openCypher Cypher (Neo4j) GQL | SQL | - |
RDB のパフォーマンスは、 Amazon Redshift のような列指向データベースを使用することで向上する可能性があります。
Amazon Neptune は現在、プロダクション環境で openCypher をサポートしています。 Cypher と GQL はサポートされていないことに留意してください。
Starting with engine release 1.1.1.0, openCypher is available for production use in Neptune.
RDB におけるツリー構造
以下のモデルを使用して、 RDB でツリーを表現できます。一部のケースでは、グラフデータベースは必要ないかもしれません。ただし、 Naive Tree は多くの場合、最適でないことに注意が必要です。これに関する詳細は “SQL Antipatterns” に記載されています。
Model | Description | Pros | Cons |
---|---|---|---|
ナイーブツリー | t1.id = t2.parent_id | シンプルな実装 (隣接リスト) | |
パス列挙 | path LIKE ‘1/2%’ | 直近のノード以外の抽出が容易 | |
入れ子集合 | Left > 1 AND right < 6 | 直近のノード以外の抽出が容易 | |
閉包テーブル | ツリーのためのテーブル |
Amazon Neptune 概要
Amazon Neptune は、フルマネージドのグラフデータベースサービスで、ミリ秒単位で数十億のリレーションシップをクエリできます。 Aurora と同様に高い可用性、耐久性、低遅延を提供します。価格については、公式ページをご参照ください。
クラスターの機能は次のとおりです。
Feature | Description |
---|---|
プライマリインスタンス | 1 |
リードレプリカインスタンス | 最大15 |
レプリケーション遅延 | 大抵の場合において 100ms 未満 |
フェイルオーバー | リードレプリカ昇格 |
ストレージの機能は次のとおりです。
Feature | Description |
---|---|
ボリューム | クラスターを跨いで一つの論理ボリューム |
冗長性 | 3AZ に 6つのコピー |
サイズ | 64TiB まで自動でスケール |
クエリ環境
様々な SDK を使用して、グラフをクエリできます。例えば、 Gremlin-Java, Gremlin-Python, Gremlin-JavaScript などがあります。また、 REST API も利用可能です。
AWS は、 Neptune Workbench というフルマネージドの環境を提供しています。 これには、 Graph Notebook が事前にインストールされており、 Gremlin クエリを実行しグラフを可視化できます。また、自分のローカル環境でグラフノートブックを起動することもできます。
トランザクション
Dirty Read
Tx1 が行を更新し、 Tx2 がその行を読み取り、 Tx1 が失敗またはロールバックした場合、 Tx2 は反映されていないデータを読み取ったことになります。
Non-repeatable Read
Tx1 が行を読み取り、 Tx2 がその行を更新または削除してコミット、 Tx1 がその行を読み取ると、 Tx1 は以前の結果と異なるコミットされたデータを読み取ります。
Phantom Read
Tx1 が複数行を読み取り、 Tx2 がいくつかの行を挿入または削除してコミット、 Tx1 が再度複数行を読み取ると、 Tx1 は以前の結果と異なる行を読み取ります。
分離レベル
Level | Dirty | Non-repeatable | Phantom |
---|---|---|---|
READ UNCOMMITTED | ありえる | ありえる | ありえる |
READ COMMITTED | ありえない | ありえる | ありえる |
REPEATABLE READ | ありえない | ありえない | ありえる |
SERIALIZABLE | ありえない | ありえない | ありえない |
Neptune におけるトランザクション
Neptune は、読み取り専用クエリとミューテーションクエリで、異なるトランザクション分離レベルを使用します。
読み取り専用クエリに関して、 Neptune は MultiVersion Concurrency Control (MVCC) でスナップショット分離を採用しています。これにより、トランザクションの開始時にスナップショットを取得して、 Dirty read, Non-repeatable read, Phantom read を防ぎます。
ミューテーションクエリに関して、 Neptune は Dirty read を防ぐために READ COMMITTED 分離を採用しています。さらに、 Neptune はデータの読み取り時にレコードセットをロックして、 Non-repeatable read と Phantom read を防ぎます。
Gremlin による Traversal
Gremlin は、 Apache TinkerPop が提供するグラフトラバーサル言語で、 TinkerPop はグラフデータベースのためのオープンソースフレームワークです。
Gremlin は Start Steps に続いて Traversal Steps を経てトラバースします。 Traversal Steps は、 General Steps と Terminal Steps の2つのステップで構成されています。
サンプルデータ
この投稿では、以下のデータをサンプルとして使用しています。頂点には age プロパティが含まれ、エッジには頂点間の親密さを表す weight プロパティを含みます。
もしデータが RDB に格納されている場合、関係データのクエリは難しくなる可能性があります。 SQL クエリは複雑になりがちで、行列のテーブルで関係を表現することは直感的でないかもしれません。
上記のサンプルデータを以下のコマンドで入力してください。 %%gremlin
は、 Neptune Workbench が提供する Jupyter Notebook のマジックコマンドです。
%%gremlin
// Drop existing data
g.V().drop()
%%gremlin
// Add Vertices
g.addV('person').property(id, 'A').property('age', 30)
.addV('person').property(id, 'B').property('age', 25)
.addV('person').property(id, 'C').property('age', 35)
.addV('person').property(id, 'D').property('age', 20)
.addV('person').property(id, 'E').property('age', 18)
.addV('person').property(id, 'F').property('age', 25)
.addV('person').property(id, 'G').property('age', 10)
.addV('person').property(id, 'H').property('age', 15)
%%gremlin
// Add Edges
g.V('A').addE('know').to(g.V('B')).property('weight', 1.0)
.V('A').addE('know').to(g.V('C')).property('weight', 0.9)
.V('A').addE('know').to(g.V('H')).property('weight', 0.5)
.V('B').addE('know').to(g.V('D')).property('weight', 0.8)
.V('B').addE('know').to(g.V('E')).property('weight', 0.4)
.V('C').addE('know').to(g.V('F')).property('weight', 0.5)
.V('C').addE('know').to(g.V('G')).property('weight', 0.6)
.V('D').addE('know').to(g.V('E')).property('weight', 0.7)
.V('H').addE('know').to(g.V('E')).property('weight', 1.0)
.V('H').addE('know').to(g.V('G')).property('weight', 1.0)
探索例1
以下のコマンドで全ての人を探索してみましょう。
%%gremlin
// Extract Vertices
g.V()
.project(‘id’, ‘properties’) // Projection
.by(id()).by(valueMap()) // valueMap returns properties of vertices.
Row | Data |
---|---|
1 | {'id': 'A', 'properties': {'age': [30]}} |
2 | {'id': 'B', 'properties': {'age': [25]}} |
3 | {'id': 'C', 'properties': {'age': [35]}} |
4 | {'id': 'D', 'properties': {'age': [20]}} |
5 | {'id': 'E', 'properties': {'age': [18]}} |
6 | {'id': 'F', 'properties': {'age': [25]}} |
7 | {'id': 'G', 'properties': {'age': [10]}} |
8 | {'id': 'H', 'properties': {'age': [15]}} |
探索例2
25歳以上で A から2番目までの人を探索してみましょう。
%%gremlin
// Extract persons (entities) which are older than 25 years old and connected from A up to 2nd.
g.V('A’)
.repeat(outE().inV()).times(2).emit() // Repeat traversal of adjacent vertices twice
.has(‘age’, gte(25)) // Greater than or equal 25 years old
.project('id', 'age’)
.by(id()).by(values('age'))
Row | Data |
---|---|
1 | {'id': 'B', 'age': 25} |
2 | {'id': 'C', 'age': 35} |
3 | {'id': 'F', 'age': 25} |
探索例3
重みの合計が 0.5 よりも大きい人を探索してみましょう。
%%gremlin
// Start traversal at A which extracts vertices that have a multiplied weight value greater than 0.5
g.withSack(1.0f).V(‘A’) // Sack can be used to store temporary data
// Multiply a weight value of an out-directed edge by a sack value, and traverse all in-directed vertices
.repeat(outE().sack(mult).by(‘weight’).inV().simplePath()).emit()
.where(sack().is(gt(0.5))) // A sack value greater than 0.5
.dedup() // deduplication
.project('id', 'weight’)
.by(id).by(sack())
Row | Data |
---|---|
1 | {'id': 'B', 'weight': 1.0} |
2 | {'id': 'C', 'weight': 0.9} |
3 | {'id': 'D', 'weight': 0.8} |
4 | {'id': 'G', 'weight': 0.54} |
5 | {'id': 'E', 'weight': 0.5599…} |
グラフ可視化
Neptune Workbench でグラフを可視化できます。
次のコマンドでグラフを可視化してみましょう。
%%gremlin -d T.id -de weight
// -d means that a property to be displayed in vertices
// -de means that a property to be displayed in edges
// Execute the traversal example 3
g.withSack(1.0f).V(‘A’) // Sack can be used to store temporary data
// Multiply a weight value of an out-directed edge by a sack value, and traverse all in-directed vertices
.repeat(outE().sack(mult).by(‘weight’).inV().simplePath()).emit()
.where(sack().is(gt(0.5))) // A sack value greater than 0.5
.dedup() // deduplication
.path() // Extract path data
.by(elementMap())
Row | Data |
---|---|
1 | path[{<T.id: 1>: 'A', <T.label: 4>: 'person', 'age': 30}, {<T.id: 1>: '8ebe47fa-901b-c6d3-a11f-0a9bf0ce8aa2', <T.label: 4>: 'know', <Direction.IN: 2>: {<T.id: 1>: 'B', <T.label: 4>: 'person'}, <Direction.OUT: 3>: {<T.id: 1>: 'A', <T.label: 4>: 'person'}, 'weight': 1.0}, {<T.id: 1>: 'B', <T.label: 4>: 'person', 'age': 25}] |
2 | path[{<T.id: 1>: 'A', <T.label: 4>: 'person', 'age': 30}, {<T.id: 1>: '7abe47fa-901c-c394-4bed-6dce7defa3f9', <T.label: 4>: 'know', <Direction.IN: 2>: {<T.id: 1>: 'C', <T.label: 4>: 'person'}, <Direction.OUT: 3>: {<T.id: 1>: 'A', <T.label: 4>: 'person'}, 'weight': 0.9}, {<T.id: 1>: 'C', <T.label: 4>: 'person', 'age': 35}] |
Graph
タブでグラフを表示でき、ドラッグ、ズームイン、ズームアウトで、グラフをすぐに理解できるようになっています。