Amazon Web Services ブログ

Amazon Neptune のための Gremlin クエリヒントの紹介

Amazon Neptune は、高度に連結されたデータの保存とクエリのために最適化された、高速で信頼性に優れた完全マネージド型のグラフデータベースで、データにおける連結のナビゲートと活用に依存するオンラインアプリケーションに最適です。

Amazon Neptune は、SPARQL クエリ言語を使用してクエリできる W3C RDF グラフをサポートします。また、Gremlin グラフトラバース/クエリ言語を使用してクエリできる Apache TinkerPop プロパティグラフもサポートしています。

この記事では、Gremlin ユーザーが使用できるいくつかの新しい機能について検討していきます。今回は特に、クエリがどのように実行されるかをより良く制御できる新しいクエリヒント機能に注目します。

Gremlin クエリヒント

Gremlin クエリヒントは、クエリがデータをトラバースする方法をより良く制御し、クエリ順序を最適化するかどうかを指定するために設計されています。このクエリヒントは、特にデータのシェイプを熟知している場合に便利です。

クエリヒントを適用するには、以下の withSideEffect Gremlin ステップを使用します。ここにある <hint-name> は適用されるヒントの名前に置き換え、<hint-setting> は適切な値に置き換えます。新しいヒントは、すべて Neptune# プレフィックスで始まります。

g.withSideEffect('Neptune#<hint-name>','<hint-setting>')

現在利用可能なヒントとヒント設定を、以下の表に示します。各ヒントについては、この記事で後から詳しく見ていきます。

ヒント名 ヒント設定 アクション
noReordering true Neptune はクエリの実行順序の最適化を試みません。これは、データがどのように編成されているかをユーザーが理解しており、Neptune が順序を変更して制約が適用されることがないようにしたい場合に最も有効です。
noReordering false Neptune は、最初に最も厳しい制約を適用するために、クエリの実行順序の最適化を試みます。これはデフォルトの noReordering 設定です。
repeatMode BFS 幅優先の方法でグラフをトラバースします。これは Gremlin の repeat ステップの使用時における Amazon Neptune のデフォルトバースモードで、多くのユースケースにとって望ましいモードです。ただし、多数の頂点を調べなければならない場合には、かなり多くのメモリを消費する場合があります。
repeatMode DFS 深さ優先の方法でグラフをトラバースします。これは、開始点から数レベル深い場所にある可能性がある結果を検索するが、各深度レベルにある頂点のすべてを調査する必要はないというクエリを実施する場合に最も有効なモードです。取得する必要がある頂点の数が少ないため、このモードは通常必要となるメモリが少なく、最高のパフォーマンスを提供できます。
repeatMode CHUNKED_DFS トラバースの各深度で最大 1,000 個の頂点を調査するハイブリッドモードです。このモードは、トラバースの各深度にある頂点をいくつか調べたいが、すべての頂点を調べる必要はないという場合に役立ちます。大規模のグラフでは、このモードがパフォーマンスとメモリ使用量の優れたバランスを提供します。

新しいクエリヒントの使用方法についての詳しい説明と追加の例については、Amazon Neptune ユーザーガイドにある「Gremlin クエリヒント」を参照してください。

noReordering ヒント

Gremlin クエリを評価する場合、Neptune のクエリエンジンはデフォルトでそのクエリを分析し、行わなければならない作業の量を最小限に抑えるクエリプランの構築を試みて、クエリを効率的な実行のために最適化します。

この最適化のため、制約が実施される正確な順序が、クエリで指定されている順序と異なる場合があります。

ケースによっては、この最適化を無効化したほうが良い場合もあります。連結された has ステップを使用して 2 つの制約が指定されている以下の例を検討してください。

g.V().has('phone_number','512-555-0123').
      has('name','Kelvin')

最高のパフォーマンスを実現し、取得して分析しなければならないデータの量を最小限に抑えるため、Neptune のエンジンは、以下のように記述されているかのようにクエリを実行する場合があります。

g.V().has('name', 'Kelvin').
      has('phone_number', '512-555-0123')

ただし、Neptune は、特定のデータとユースケースに最適ではないクエリ順序を選択する可能性があります。データの特徴を理解しており、クエリに対して厳格な順序を指定したい場合は、noReordering ヒントを使ってそれを行うことができます。前述の例では、グラフ内のすべての頂点全体で phone_number プロパティが固有であることはわかるかもしれませんが、名前は固有でない場合があります。このため、名前の前に電話番号を使う探索は、関心がある特定の番号に関して電話番号をチェックする前に「Kelvin」という名前の人物全員を取得することと比べて、取得するデータが少なくなる可能性があります。

以下の例には、Neptune にクエリの順序を変更しようとしないよう指示する noReordering ヒントが含まれています。

g.withSideEffect('Neptune#noReordering',true).
  V().has('phone_number','512-555-0123')
      has('name’,’Kelvin').

repeatMode ヒント

repeatMode ヒントは、3 つの可能な戦略のうち、どれをグラフのトラバースに使用したいかを Neptune に指示することを可能にします。

デフォルトで、Neptune は幅優先探索 (BFS) 戦略を使用します。BFS 探索では、現時点での深度にあるすべての頂点を調べてから、ひとつ下の深度の頂点に移動します。この戦略は多くの状況において有効です。しかし、探索の各深度で維持する必要がある大量の状態データが原因で、BFS 戦略は最もメモリ集約的な戦略です。

最も関心がある頂点が、グラフのより深い部分を最初に調査することによって見つけられる可能性が高い場合は、深さ優先探索 (DFS) 戦略を使用することができます。DFS 探索では、所定の各深度で 1 つの頂点だけを調査してから、次の深度に移動します。このプロセスは、ターゲットが見つかるまで、またはクエリで指定された最大深度などのその他制約に到達するまで繰り返されます。DFS プロシージャはその後、可能なパスのすべてが調査されるまで、またはクエリで指定された制約 (limit ステップなど) に到達するまで繰り返されます。必要な結果がグラフのより深い場所にある場合、DFS アプローチは BFS 探索よりも大幅に速く呼び出し側アプリケーションに結果を返し始めることが予想されます。

3 番目のユースケースには、ハイブリッドアプローチが必要です。このユースケースでは、各深度で頂点の良いサンプルを取得したいものの、次の深度に進む前に必ずしもすべての頂点を調べたいわけではありません。このハイブリッド戦略は、BFS 探索よりもメモリの使用量が少なくなりますが、各深度で DFS 探索よりも多くのデータを調べます。Neptune は、このハイブリッドアプローチでグラフをトラバースするために CHUNKED_DFS ヒントを提供します。クエリが進むに従って、各深度で最大 1,000 個の頂点が調査されます。

Neptune#repeatMode クエリヒントを説明できるように、ここでは順序付けされたバイナリツリーを表す小規模グラフを使用します。以下の Gremlin コード (Gremlin コンソールが Amazon Neptune エンドポイントに接続されている場合、コンソールに直接貼り付けることができるもの) がグラフを作成します。repeatMode ヒントはどのシェイプのグラフデータでも機能しますが、明確な深度があるツリーをモデル化するデータは、これらのヒントが機能する方法の視覚化を容易にしてくれます。

.addV('root').property('data',9).as('root').
  addV('leaf').property('data',5).as('b').
  addV('leaf').property('data',2).as('c').
  addV('leaf').property('data',11).as('d').
  addV('leaf').property('data',15).as('e').
  addV('leaf').property('data',10).as('f').
  addV('leaf').property('data',1).as('g').
  addV('leaf').property('data',8).as('h').
  addV('leaf').property('data',22).as('i').
  addV('leaf').property('data',16).as('j').
  addE('left').from('root').to('b').
  addE('left').from('b').to('c').
  addE('right').from('root').to('d').
  addE('right').from('d').to('e').
  addE('right').from('e').to('i').
  addE('left').from('i').to('j').
  addE('left').from('d').to('f').
  addE('right').from('b').to('h').
  addE('left').from('c').to('g').iterate()

以下の図は、前述の Gremlin コードが作成する小規模のバイナリツリーを図で示したものです。

以下の Gremlin クエリは、Gremlin repeat ステップの使用時における Amazon Neptune のデフォルト設定である幅優先 (BFS) モードで、グラフにある節点のすべてを取得します。Apache TinkerGraph を使用した Gremlin での経験がある場合、このモードは、クエリで repeat ステップを使用するときに TinkerGraph がどのように機能するかを最も綿密にモデル化します。

gg.withSideEffect('Neptune#repeatMode', 'BFS').
  V().hasLabel('root').
      emit().repeat(out()).until(not(outE())).
      values('data')

Neptune インスタンスに接続された Gremlin コンソールを使って前述のクエリが実行されると、以下の結果が得られます。この出力をバイナリツリーの図と比較すると、各深度からの値のすべてが、次の深度に進む前に見つけられていることがわかります。

==>9
==>5
==>11
==>2
==>8
==>10
==>15
==>22
==>1
==>16

ここで、深さ優先 (DFS) モードでグラフの節点を取得するようにクエリを変更しましょう。

g.withSideEffect('Neptune#repeatMode', 'DFS').
  V().hasLabel('root').
      emit().repeat(out()).until(not(outE())).
      values('data')

今回の結果は、前回と大幅に異なります。ご覧のとおり、クエリはルート節点 (9) から始めて、まず 5、2、および 1 を見つけました。つまり、クエリはツリーの最初のブランチでルート節点から可能な限り深いレベルに進み、その後ルートに戻って次のブランチに移動しています。

==>9
==>5
==>2
==>1
==>8
==>11
==>10
==>15
==>22
==>16

今回のグラフ例は、探索中に多量のメモリを消費するほど大きくないため、CHUNKED_DFS ヒントを使用する必要はありません。しかし、一部の深度に 1,000 をはるかに上回る数の節点を有する大型ツリーがあったとしたら、メモリの使用量が少ないモードが役に立ちます。

それでは、DFS が最も効率的なトラバースを提供するユースケースを検討してみましょう。グラフ内で値「1」を表す節点を見つけたいとします。以下のクエリは、repeat…until トラバースを使って見つけたい節点を探索します。クエリに emit ステップを含め、最後に path ステップを追加することによって、探索がどのように行われるのかを明確に見ることができます。

g.withSideEffect('Neptune#repeatMode', 'DFS').
  V().hasLabel('root').
      emit().repeat(out()).until(has('data',1)).
      path().by('data')

以前の例からも予測できるように、1 節点はすぐに見つかりました。

==>[9]
==>[9, 5]
==>[9, 5, 2]
==>[9, 5, 2, 1]         ← 任務完了、1 が見つかりました。
==>[9, 5, 8]
==>[9, 11]
==>[9, 11, 10]
==>[9, 11, 15]
==>[9, 11, 15, 22]
==>[9, 11, 15, 22, 16]

それでは、今度は BFS モードで同じクエリを実行しましょう。

g.withSideEffect('Neptune#repeatMode', 'BFS').
  V().hasLabel('root').
      emit().repeat(out()).until(has('data',1)).
      path().by('data')

以下の結果から分かるように、幅優先で探索したことから、今回は「1」の値を表す節点を見つける前に、より多くの節点が調べられています。もちろん、このような小さなグラフでは、DFS または BFS のどちらのモードを使っても、比較的迅速に結果を見つけることができます。しかし、各深度に調査すべき節点が何千個もあるグラフでは、DFS モードのほうがはるかに効率的です。

==>[9]
==>[9, 5]
==>[9, 11]
==>[9, 5, 2]
==>[9, 5, 8]
==>[9, 11, 10]
==>[9, 11, 15]
==>[9, 11, 15, 22]
==>[9, 5, 2, 1]         ← 探していた 1 がようやく見つかりました。
==>[9, 11, 15, 22, 16]

まとめ

この記事では、Amazon Neptune で利用できる新しい Gremlin クエリヒントをご紹介しました。これらのヒントはクエリの作成時に使用でき、Neptune のクエリエンジンがクエリをどのように実行するかをさらに制御できるというメリットがあります。

参考資料

Amazon Neptune リソースウェブページで、追加の記事、動画、および例をご覧いただけます。


著者について

Kelvin Lawrence は、Amazon Neptune とその他多くの関連サービスに重点を置く Database Services Customer Advisory Team のプリンシパルデータアーキテクトです。Kelvin はグラフデータベースを扱った仕事に長年携わっており、「Practical Gremlin」という本の著者で、Apache TinkerPop プロジェクトのコミッターです。

 

 

 

Ian Robinson は Database Services Customer Advisory Team のアーキテクトです。Ian は「Graph Databases」および「REST in Practice」(どちらもオライリー社から出版) の共著者で、「REST: From Research to Practice」(シュプリンガー社) と「Service Design Patterns」(Addison-Wesley 社) の寄稿者でもあります。