Gremlin で Amazon Neptune をクエリする
Amazon Neptune にプロパティグラフを登録し、Gremlin のトラバーサルで関係をクエリする方法を解説します。
Amazon Neptune は、Gremlin と SPARQL に対応したマネージドグラフデータベースです。以下の例では、Gremlin によるトラバーサルに焦点を当てます。
グラフデータベースとは
グラフデータベースは、エンティティ間の関係の格納とクエリに最適化されています。リレーショナルモデルでは複雑な結合が必要になるような、相互に接続されたデータのトラバーサルに適しています。
グラフデータベースでは、
- 頂点(Vertices) はエンティティを表します。
- エッジ(Edges) は頂点間の関係を定義します。
- 頂点とエッジのどちらにもプロパティ(キーと値のペア)を設定できます。このモデルをプロパティグラフと呼びます。

Neptune におけるトランザクション
Neptune は、読み取り専用クエリと更新クエリで異なるトランザクション分離レベルを使用します。詳細は公式ドキュメントを参照してください。
読み取り専用クエリでは、Neptune は MultiVersion Concurrency Control(MVCC) によるスナップショット分離を使用します。トランザクション開始時点のスナップショットを参照するため、ダーティリード、非再現リード、ファントムリードを防止できます。
更新クエリでは、Neptune は READ COMMITTED 分離レベルでダーティリードを防止します。さらに、読み取り対象のレコード範囲をロックし、非再現リードとファントムリードを防ぎます。
トラバーサルの例
グラフデータの準備
この例では、頂点に age プロパティ、エッジに weight プロパティを持つグラフを使用します。

以下のコマンドでサンプルデータをロードします。%%gremlin は、Neptune Workbench で利用できる Jupyter Notebook のマジックコマンドです。
%%gremlin// Drop existing datag.V().drop()%%gremlin// Add Verticesg.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 Edgesg.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 Verticesg.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: つながりをたどる
'A' から 2 ホップ以内でつながっている、25 歳以上の人物を取得します。
%%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: weight によるフィルタリング
'A' からの経路にある各エッジの weight を乗算し、その値が 0.5 を超える頂点を取得します。
%%gremlin// Start traversal at A which extracts vertices that have a multiplied weight value greater than 0.5g.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 specifies the vertex property to display// -de specifies the edge property to display
// Execute traversal from example 3g.withSack(1.0f).V('A') // Sack is used to store temporary data .repeat(outE().sack(mult).by('weight').inV().simplePath()).emit() // Traverse with edge weight .where(sack().is(gt(0.5))) // Filter paths where the sack value > 0.5 .dedup() // Remove duplicate paths .path() // Extract path data .by(elementMap()) // Display properties of vertices and edges出力例:
| 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}] |
クエリを実行したら、Neptune Workbench の Graph タブを開いて結果を可視化します。ドラッグとズームを使ってグラフを探索できます。

付録 1: リレーショナルデータベース(RDB)におけるツリー構造
ツリー構造は、複数の方法でリレーショナルデータベース(RDB)上にモデル化できるため、すべての階層データにグラフデータベースが必要なわけではありません。
ただし、“SQL Antipatterns” で論じられているように、Naive Tree モデルには制約があります。
- Naive Tree
- 表現:
t1.id = t2.parent_id - 利点
- 実装がシンプル(隣接リスト)
- 欠点
- 隣接していないノードの抽出が難しい
- SQL が複雑になる
- パフォーマンスが低い
- 表現:
- Path Enumeration
- 表現:
path LIKE '1/2%' - 利点
- 隣接していないノードの抽出が簡単になる
- 欠点
- INSERT/UPDATE/DELETE 操作が複雑になる
- カラムの最大長による制限がある
- 表現:
- Nested Sets
- 表現:
Left > 1 AND Right < 6 - 利点
- 隣接していないノードのクエリに効率的
- 欠点
- INSERT/UPDATE 操作が複雑になる
- カラムの最大長による制限がある
- 構造が直感的でない
- 表現:
- Closure Table
- 表現: ツリー用の別テーブル
- 利点
- すべてのノードのクエリに効率的
- INSERT/UPDATE/DELETE を簡単に扱える
- 欠点
- データサイズが大きく増加する可能性がある
- INSERT/UPDATE/DELETE のパフォーマンスが低くなることがある
リレーショナルモデルでは、関係の階層が深くなるほどクエリが複雑になります。相互に接続されたデータを行と列で表現してたどるには、再帰クエリや複数回の結合が必要になる場合があります。

付録 2: トランザクション
ダーティリード
Tx1 が行を更新し、コミット前の値を Tx2 が読み取った後、Tx1 が失敗またはロールバックすると、Tx2 はコミットされなかったデータを読み取ったことになります。

非再現リード
Tx1 が行を読み取った後、Tx2 がその行を更新または削除してコミットし、Tx1 が再度読み取ると、最初とは異なるコミット済みの値が返されます。

ファントムリード
Tx1 が行の集合を読み取った後、Tx2 が条件に一致する行を挿入または削除してコミットし、Tx1 が同じクエリを再実行すると、最初とは異なる行の集合が返されます。

分離レベル
| Level | Dirty Read | Non-repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | Possible | Possible | Possible |
| READ COMMITTED | Not possible | Possible | Possible |
| REPEATABLE READ | Not possible | Not possible | Possible |
| SERIALIZABLE | Not possible | Not possible | Not possible |
まとめ
この例では、小さなプロパティグラフを Neptune にロードし、Gremlin で複数ホップの関係とエッジの累積 weight をクエリしました。
例 2 の repeat(outE().inV()).times(2).emit() は頂点 A から 2 ホップ以内をたどり、例 3 の withSack(1.0f) は各経路の累積 weight を保持します。
リレーショナルデータベースのツリーモデルには、それぞれ異なるトレードオフがあります。隣接リストは更新が単純ですが、離れたノードのクエリが複雑になります。Closure Table は検索を効率化できる一方、追加のストレージと書き込み処理が必要です。
関係そのものが主なクエリ対象である場合、グラフモデルを使うと、リレーショナルモデルで結合を繰り返すよりもトラバーサルを直接表現できます。
Related posts
Cognito User Pools と OIDC で Slack サインインを実装する
Cognito user pool を OIDC 経由で Slack と連携させ、"Sign in with Slack" を Amplify で Next.js アプリケーションに組み込みます。
Lambda Web Adapter で FastAPI を AWS Lambda にデプロイする
FastAPI で書いた API バックエンドをコンテナ化し、Lambda Web Adapter と AWS CDK を使って単一の Lambda 関数へデプロイします。
API Gateway WebSocket:モック統合の実装
バックエンドの Lambda を使わず、モック統合のみで API Gateway WebSocket API を構築し、あらかじめ用意したレスポンスを返します。
CloudFront 署名付き URL 経由で S3 にアップロードする
CloudFront の署名付き URL を使い、独自ドメイン経由で S3 にアップロードする方法を紹介します。S3 の署名付き URL を直接使えない場合に有用です。
AWS EventBridge Scheduler:スケジュールに沿って EC2 を起動・停止する
Lambda を介さず、EventBridge Scheduler から EC2 API を直接呼び出し、cron スケジュールに従って EC2 インスタンスを起動・停止します。
