グラフデータベース入門: Amazon NeptuneとGremlinを使ったトラバーサル

グラフデータベース入門: Amazon NeptuneとGremlinを使ったトラバーサル

岩佐 孝浩
岩佐 孝浩
14 min read
Graph Database Neptune

はじめに

先日、「Graph Database 入門 ~Amazon Neptune によるデータの「つながり」探索~」 というタイトルの勉強会を開催しました(イベントリンク)。このブログ記事では、そのプレゼンテーションの内容を共有し、グラフデータベース を理解し、Amazon Neptune の機能を探るお手伝いをしたいと思います。

この記事で使用されたサンプルコードは、GitHub リポジトリ で見つけることができます。

前提条件

対象読者

この投稿は以下の読者を対象としています。

  • グラフデータベースAmazon Neptune に興味がある。
  • リレーショナルデータベースAWS の基本的な理解がある。

目的

  • グラフデータベース と Amazon Neptune の概要を提供。
  • Apache TinkerPop Gremlin を使用したグラフのトラバーサルを実演。

グラフデータベースとは

キーコンセプト

グラフデータベースは、エンティティ間のリレーションシップの保存とクエリの最適化に特化しています。リレーショナルデータベースや NoSQL データベースとは異なり、グラフデータベースは関係が複雑でパフォーマンスが重要なシナリオで優れています。

グラフデータベースでは:

  • 頂点 (ノード) はエンティティを表します。
  • エッジ は頂点間の関係を定義します。
  • 頂点とエッジの両方にプロパティ(キーと値のペア)を持たせることができます。
    • 頂点とエッジにラベルが付けられたグラフは プロパティグラフ と呼ばれます。

Graph Representation

グラフデータベースの用途

ソーシャルネットワーク

グラフデータベースは、Facebook や LinkedIn のようなプラットフォームで見られるように、人々の間の関係をモデル化します。

ナレッジグラフ

これらのグラフは、データに文脈的な意味を追加し、例えば「リンゴ」という果物と「Apple Inc.」という会社を区別します。Introducing the Knowledge Graph: things, not strings で、ナレッジグラフという用語が広く採用されました。

アイデンティティグラフ

アイデンティティグラフは、複数のユーザー識別子をプラットフォーム間でリンクし、包括的なユーザー行動分析を可能にします。有名なアプリケーションの例として、Customer 360 が挙げられ、すべての接点にわたる顧客の統一ビューを提供します。

不正検出

不正グループは正当な取引と違法行為を混在させることが多く、検出が困難です。グラフデータベースは、関係を視覚化し、共通属性を特定することで、不正検出に優れています。これにはクレジットカード番号、電話番号、社会保障番号などが含まれます。

実世界の例:パナマ文書

パナマ文書の調査では、グラフデータベースを使用して漏洩した文書を効率的に分析しました。詳細は Forbes の記事 をご覧ください。

データベースの比較:グラフ DB、RDB、NoSQL

特徴グラフ DBRDBNoSQL
データ構造グラフテーブル多様
スキーマゆるい厳密ゆるい
用途リレーションシップエンティティストレージ多様
クエリ方法トラバーサルジョイン / サブクエリ多様
パフォーマンス非常に高い平均 1非常に高い
クエリ言語Gremlin
SPARQL
openCypher 2
Cypher
GQL
SQL-
  1. リレーショナルデータベース (RDB) のパフォーマンスは、Amazon Redshift のようなカラム型データベースを使用することで向上します。
  2. Amazon Neptune は、エンジンリリース 1.1.1.0 以降、openCypher を本番環境でサポート しています。ただし、Cypher および GQL は現在サポートされていません。

Amazon Neptune の紹介

Amazon Neptune は、完全マネージド型のグラフデータベースサービスであり、数十億のリレーションシップをミリ秒単位でクエリできます。高可用性耐久性低レイテンシーを提供します。

主な特徴

  • クラスターのスケーラビリティ:最大 15 のリードレプリカ。
  • 自動スケーリング:ストレージは最大 64 TiB まで自動拡張。
  • データの冗長性3 つのアベイラビリティゾーンにまたがる 6 コピーを保持。

Neptune におけるトランザクション

Neptune は、リードオンリークエリとミューテーションクエリをそれぞれ異なるトランザクション分離レベルで管理します。詳細はこちらのドキュメント をご参照ください。

リードオンリークエリ

リードオンリークエリでは、Neptune は スナップショット分離を使用して マルチバージョンコンカレンシーコントロール (MVCC) を実現します。これにより、ダーティリード非再現リード、およびファントムリードがスナップショットを用いて防止されます。

ミューテーションクエリ

ミューテーションクエリでは、Neptune は READ COMMITTED 分離を使用してダーティリードを防ぎます。さらに、Neptune は読み取られるレコードセットをロックし、非再現リードファントムリードを回避します。

Gremlin を使用したクエリ

Amazon Neptune は、オープンソースのグラフトラバーサル言語である Gremlin によるクエリをサポートしています。また、Graph Notebook を使用して、クエリや可視化を行うことも可能です。

トラバーサルの例

サンプルグラフデータ

この例では、頂点に age プロパティ を持ち、エッジに weight プロパティ を持つグラフを使用します。

上記のサンプルデータをロードするには、以下のコマンドを使用してください。%%gremlin は、Neptune Workbench 専用の Jupyter Notebook マジックコマンドです。

%%gremlin
// 既存データの削除
g.V().drop()
%%gremlin
// 頂点の追加
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
// エッジの追加
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
// 頂点の抽出
g.V()
 .project('id', 'properties') // 投影
 .by(id()).by(valueMap()) // valueMap は頂点のプロパティを返します。

結果:

Rowデータ
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
// A から 2階層以内で接続されている 25 歳以上の人物を抽出
g.V('A')
 .repeat(outE().inV()).times(2).emit() // 隣接する頂点のトラバーサルを 2 回繰り返す
 .has('age', gte(25)) // 25 歳以上
 .project('id', 'age')
 .by(id()).by(values('age'))

結果:

Rowデータ
1{'id': 'B', 'age': 25}
2{'id': 'C', 'age': 35}
3{'id': 'F', 'age': 25}

例3: 重みでフィルタリング

重みの積が 0.5 を超える人物を見つけます。

%%gremlin
// A から開始し、重みの積が 0.5 を超える頂点を抽出
g.withSack(1.0f).V('A') // 一時的なデータを格納するための Sack を使用
 .repeat(outE().sack(mult).by('weight').inV().simplePath()).emit()
 .where(sack().is(gt(0.5))) // Sack 値が 0.5 を超える
 .dedup() // 重複を削除
 .project('id', 'weight')
 .by(id).by(sack())

結果:

Rowデータ
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 は、グラフをインタラクティブに可視化するツールを提供しています。詳細は公式ドキュメントをご参照ください:Neptune Visualization

以下のコマンドを実行して、グラフを可視化します。

%%gremlin -d T.id -de weight
// -d は表示する頂点プロパティを指定
// -de は表示するエッジプロパティを指定

// 例3のトラバーサルを実行
g.withSack(1.0f).V('A') // Sack を使用して一時データを保存
 .repeat(outE().sack(mult).by('weight').inV().simplePath()).emit() // 重み付きエッジのトラバース
 .where(sack().is(gt(0.5))) // Sack 値が 0.5 を超えるパスをフィルタリング
 .dedup() // 重複パスを削除
 .path() // パスデータを抽出
 .by(elementMap()) // 頂点およびエッジのプロパティを表示

出力例:

Rowデータ
1path[{<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}]
2path[{<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}]

実行後、Neptune Workbench の Graph タブに移動して結果を可視化します。このインターフェースは、ドラッグやズームイン・ズームアウト操作をサポートしており、直感的にグラフを探索できます。

まとめ

グラフデータベースは、リレーションシップの処理において大きな利点を提供し、Amazon Neptune は AWS 上でそのようなデータベースをシームレスに展開できます。Gremlin を使用することで、複雑なデータリレーションシップを効率的にトラバースおよび分析できます。

グラフデータベースを探求するなら、Amazon Neptune は素晴らしいスタート地点です。

Happy Coding! 🚀

Appendix 1: リレーショナルデータベース (RDB) におけるツリー構造の表現

ツリー構造は、リレーショナルデータベース (RDB) で様々なアプローチを使用してモデル化できます。一部のシナリオでは、グラフデータベースを使用する必要がない場合もあります。ただし、Naive Tree モデル は多くのケースで非最適であることが多く、“SQL Antipatterns” で紹介されています。

モデル説明長所短所
Naive Treet1.id = t2.parent_id
  • 簡単な実装 (隣接リスト)
  • 非隣接ノードの抽出が困難
  • 複雑な SQL
  • 低いパフォーマンス
Path Enumerationpath LIKE '1/2%'
  • 非隣接ノードの抽出が容易
  • INSERT/UPDATE/DELETE 操作が複雑
  • カラムの最大長に制限される
Nested SetsLeft > 1 AND Right < 6
  • 非隣接ノードのクエリに効率的
  • INSERT/UPDATE 操作が複雑
  • カラムの最大長に制限される
  • 直感的でない構造
Closure Tableツリー用の別テーブル
  • すべてのノードのクエリに効率的
  • INSERT/UPDATE/DELETE 操作が容易
  • データサイズが大幅に増加する可能性
  • INSERT/UPDATE/DELETE 操作が低パフォーマンスになる場合がある

各アプローチにはそれぞれの長所と制限があり、アプリケーションの具体的な要件に依存して選択が決まります。

Appendix 2: トランザクション

ダーティリード (Dirty Read)

Tx1 が行を更新し、Tx2 がその行を読み、Tx1 が失敗またはロールバックする場合、Tx2 は反映されていないデータを読むことになります。

非再現リード (Non-repeatable Read)

Tx1 が行を読み、Tx2 がその行を更新または削除してコミットし、Tx1 が再度その行を読む場合、Tx1 は前回とは異なるコミット済みデータを読むことになります。

ファントムリード (Phantom Read)

Tx1 がレコードを読み、Tx2 がいくつかの行を挿入または削除してコミットし、Tx1 が再度その行を読む場合、Tx1 は前回とは異なる行を読むことになります。

分離レベル

レベルダーティリード非再現リードファントムリード
READ UNCOMMITTED生じる生じる生じる
READ COMMITTED生じない生じる生じる
REPEATABLE READ生じない生じない生じる
SERIALIZABLE生じない生じない生じない

Appendix 3: リレーショナルデータベース (RDB) を使用したクエリ

RDB にデータを格納する場合、リレーションシップをクエリすることはますます複雑になる可能性があります。SQL クエリはしばしば複雑になり、行と列の構造内で相互に接続されたデータを表現することは直感的ではありません。

岩佐 孝浩

岩佐 孝浩

Software Developer at KAKEHASHI Inc.
AWS を活用したクラウドネイティブ・アプリケーションの要件定義・設計・開発に従事。 株式会社カケハシで、処方箋データ収集の新たな基盤の構築に携わっています。 Japan AWS Top Engineers 2020-2023