【グラフDB】Amazon Neptuneで始めるGremlin入門
データ事業本部のueharaです。
今回はタイトルの通り、Amazon Neptuneを利用してGremlinに入門してみたいと思います。
前提
今回、Amazon Neptuneは既に構築済みであることを想定しています。
また、Amazon Neptuneに対してのクエリはAmazon Sagemaker AIのノートブックインスタンス経由で行うこととします。
基本概念
Gremlinを理解するには、まず「プロパティグラフモデル」というデータ構造の概念を知る必要があります。
具体的には、以下の3つの主要素で構成されます。
- Vertex (頂点/Node)
- 「実体(エンティティ)」を表す
- 後述する「Property」と「Label」を付与することができる
- Edge (辺/Relationship)
- 頂点と頂点の間の「関係」を表す
- 必ず方向性を持つ(Outbound/Inbound)
- 「Label」は必須で、「Property」も付与可能
- Property (プロパティ)
- VertexやEdgeに付与される「Key-Value形式のデータ」
上記3つの主要素に加え、Vertexを種類ごとにグループ化(カテゴリ化)するための「Label」という概念があります。
- Label (ラベル)
- VertexやEdgeの「種類」を表す識別子
- RDBMSでいう「テーブル」に近い役割を果たす
基本概念をクエリ実行で確認
基本概念について文字で説明してもピンと来ない部分もあるかと思いますので、実際にクエリを実行しグラフを作成してみます。
Vertex(頂点)の追加は addV() で可能です。
引数には「Label」を指定します。
g.addV('person')
プロパティの付与は property() で実行可能です。
したがって、 person ラベルの頂点を作り、プロパティとして id: 1, name: Alice, age: 30 を設定したい場合は以下になります。
※補足:idは全てのVertex(頂点)で一意である必要があります。
g.addV('person').property(T.id, '1').property('name', 'Alice').property('age', 30)
上記クエリを実行すると、以下のようにVertexが追加されます。

追加した内容を確認するため、以下を実行します。
g.V().hasLabel('person').elementMap().limit(5)
ここで elementMap() はvertex/edgeを全プロパティを含むマップ(辞書)に変換する処理です。
結果は次の通りで、先程作成したAliceのデータが確認できます。

同じ要領でBobを追加します。
g.addV('person').property(T.id, '2').property('name', 'Bob').property('age', 25)
先程の確認用クエリを再度実行すると、Personが1つ増えBobが追加されていることを確認できます。

次にEdge(辺)を追加します。
Edgeの付与はVertexの時と似たような感じで、 addE() で可能です。
g.addE('knows').from(V('1')).to(V('2')).property('weight', 0.5)
ここで、 from() と to() は方向性を表しています。
上記では Vertexの id=1 (Alice) から Vertexの id=2 (Bob) の方向になります。
Edgeにもプロパティを付与することは可能で、例として weight: 0.5 という形にしています。
このクエリを実行すると以下のようになり、Edgeが作成されたことが分かります。

Edgeの作成ができたので、実際に可視化して確認してみます。
g.V().outE().inV().path().by('name').by('weight').by('name')
これは、文字として表現すると「頂点 -> エッジ -> 頂点という構造のパスを返し、値はそれぞれname, weight, nameにする」という内容になります。
outE() や inV() の out や in は方向を表しています。
- out
outE():自分から出ていくエッジoutV():エッジの出発点(始点)である頂点out():自分から出ていくエッジの先にある 隣の頂点 へ直接ジャンプする(outE().inV()の短縮形)
- in
inE():自分に入ってくるエッジinV():エッジの 到着点(終点) である頂点へ降りる。in():自分に入ってくるエッジの元にある隣の頂点へ直接ジャンプする(inE().outV()の短縮形)
実際に実行すると結果は以下の通りです。

設定した通り、AliceからBobに0.5という矢印が伸びている結果が得られます。
※補足:ノートブックのセルの最上部で %%gremlin -p v,oute,inv としている "マジックワード" 箇所の -p v,oute,inv という部分は、結果の可視化におけるヒント指定になります。このヒントを利用することで、ノートブック上でクエリ出力を図示化する方法を制御できます。(参考)
ここまで来ると、何となくGremlinの利用イメージがついてきたのではないでしょうか。
より実践に即した内容
先程はVertexやEdgeを1つずつ作成しましたが、もちろんまとめて追加することも可能です。
g.addV('person')
.property(T.id, '3')
.property('name', 'Charlie')
.property('age', 45)
.addE('knows')
.from(V('1'))
.property('weight', 0.2)
上記を実行した後、先程の可視化クエリを再実行すると次のようになります。

今度はクエリで検索することを考えます。
例えば、「Aliceというnameの人物が0.3以上のweightで知っている人物の名前を検索する」というクエリは以下で可能です。
g.V().has('name', 'Alice') // Aliceを見つける
.outE('knows') // 'knows'エッジの上に乗る
.has('weight', gte(0.3)) // エッジのweightが0.3以上(gte)かチェック
.inV() // 条件に合えば、その先の頂点に移動
.values('name') // その人の名前を表示
この結果は次の通りで、Bobが抽出できています。

値のアップデートも可能で、例えば先程作成したCharlieのageを変えるには以下クエリで可能です。
g.V().has('name', 'Charlie').property('age', 50)
確認すると年齢が変更されていることが確認できます
g.V('3').property('age', 50).elementMap()

※補足:仮に、他に Chalie という名前の人がいた場合、該当するすべてのVertexのage が 50 に更新されてしまいます。更新を一意に実行したい場合は、複合的に一意となるプロパティの指定や、idでの更新が必要となります。
一歩踏み込んだ内容
以下のように、「地点」と「経路」を表現するグラフを作成します。
g.addV('location').property(T.id, 'a').as('a').
addV('location').property(T.id, 'b').as('b').
addV('location').property(T.id, 'c').as('c').
addV('location').property(T.id, 'd').as('d').
addV('location').property(T.id, 'e').as('e').
addV('location').property(T.id, 'f').as('f').
addV('location').property(T.id, 'g').as('g').
addV('location').property(T.id, 'h').as('h').
addE('route').from('a').to('b').property('distance', 1).
addE('route').from('a').to('c').property('distance', 7).
addE('route').from('a').to('d').property('distance', 2).
addE('route').from('b').to('a').property('distance', 1).
addE('route').from('b').to('e').property('distance', 2).
addE('route').from('b').to('f').property('distance', 4).
addE('route').from('c').to('a').property('distance', 7).
addE('route').from('c').to('f').property('distance', 2).
addE('route').from('c').to('g').property('distance', 3).
addE('route').from('d').to('a').property('distance', 2).
addE('route').from('d').to('g').property('distance', 5).
addE('route').from('e').to('b').property('distance', 2).
addE('route').from('e').to('f').property('distance', 1).
addE('route').from('f').to('b').property('distance', 4).
addE('route').from('f').to('c').property('distance', 2).
addE('route').from('f').to('e').property('distance', 1).
addE('route').from('f').to('h').property('distance', 6).
addE('route').from('g').to('c').property('distance', 3).
addE('route').from('g').to('d').property('distance', 5).
addE('route').from('g').to('h').property('distance', 2).
addE('route').from('h').to('f').property('distance', 6).
addE('route').from('h').to('g').property('distance', 2)
可視化すると以下の通りです。
g.V().hasLabel('location')
.outE('route')
.inV()
.path().by(T.id).by('distance')

このグラフに対し、「地点aから地点hまでの経路とその総コスト」は以下クエリで確認できます。
// IDが 'a' である頂点から探索を開始
g.V('a')
// ループによる繰り返しの探索
.repeat(
// 'route' というラベルのエッジ(outE)を通って、次の頂点(inV)へ移動する
// simplePath() は「一度通った頂点は二度と通らない」という制約(無限ループの防止)
outE('route').inV().simplePath()
)
// IDが 'h' (ゴール) の頂点にたどり着くまで、上記処理を繰り返す(終了条件)
.until(hasId('h'))
// 結果の整形 (Project)
// ここまでで見つかった経路ごとの結果を、'Path' と 'TotalCost' という2つの項目を持つMap(辞書)形式に変換する
.project('Path', 'TotalCost')
// 'Path' 項目の定義 (1つ目の by)
// 経路情報(path)から、通った頂点のIDリストを作る
.by(
path() // 現在の経路履歴を取得
.unfold() // リストをバラバラの要素に分解
.filter(label().is('location')) // ラベルが 'location' (頂点) のものだけを残し、エッジを捨てる
.id() // 残った頂点オブジェクトを、そのID ('a', 'b'...) に変換
.fold() // バラバラになったIDを、もう一度リストにまとめ直す (例: ['a', 'b', ...])
)
// 'TotalCost' 項目の定義 (2つ目の by)
// 経路情報(path)から、エッジの distance を合計する
.by(
path() // 現在の経路履歴を取得
.unfold() // リストをバラバラの要素に分解
.values('distance') // 'distance' プロパティを持つ要素(エッジ)から、その値を取り出す(頂点は無視)
.sum() // 取り出した数値(距離)をすべて足し合わせる
)
結果は次の通りで、通る経路とその総コストを確認できます。

最後に
今回は、Amazon Neptuneを利用してGremlinに入門してみました。
参考になりましたら幸いです。







