fold()/coalesce()/unfold() による効率的な Gremlin アップサートの実行 - Amazon Neptune

翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

fold()/coalesce()/unfold() による効率的な Gremlin アップサートの実行

アップサート (または条件付き挿入) は、頂点やエッジが既に存在する場合は再利用し、存在しない場合は作成します。効率的なアップサートは、Gremlin クエリのパフォーマンスに大きな違いをもたらすことができます。

このページでは、fold()/coalesce()/unfold() Gremlin パターンを使用して効率的なアップサートを行う方法を説明します。ただし、エンジン TinkerPop バージョン 1.2.1.0 で Neptune で導入されたバージョン 3.6.x のリリースでは、ほとんどの場合、新しい mergeV()および mergeE() ステップが推奨されます。ここで説明する fold()/coalesce()/unfold() パターンは、一部の複雑な状況ではまだ役に立つかもしれませんが、一般的な用途では、Gremlin mergeV() および mergeE() ステップによる効率的なアップサートの実行 で説明されているように、できれば mergeV() および mergeE() を使用してください。

アップサートを使用すると、冪等挿入操作、つまり、そのような操作を何回実行しても、全体的な結果は同じ操作を記述できます。これは、グラフの同じ部分に同時に変更を加えると、1 つ以上のトランザクションが強制的に ConcurrentModificationException でロールバックされ、再試行が必要になるような、同時書き込みの多いシナリオに役立ちます。

例えば、次のクエリは、最初にデータセット内の指定された頂点を探し、その結果をリストにまとめることで頂点を更新します。coalesce() ステップで最初に指定されたトラバーサルで、クエリはこのリストを展開します。展開されたリストが空でない場合、結果は coalesce() から出力されます。ただし、頂点が現在存在しないために unfold() が空のコレクションを返した場合、coalesce() は、その頂点が指定された 2 番目のトラバーサルの評価に移り、この 2 番目のトラバーサルで、クエリは欠落している頂点を作成します。

g.V('v-1').fold() .coalesce( unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org') )

アップサートには最適化された coalesce() の形式を使用する

Neptune は fold().coalesce(unfold(), ...) イディオムを最適化して高スループットの更新を行うことができますが、この最適化は、coalesce() の両方の部分が頂点またはエッジのいずれかを返し、それ以外は何も返さない場合にのみ機能します。coalesce() のいずれかの部分からプロパティなど異なるものを返そうとした場合、Neptune の最適化は行われません。クエリは成功するかもしれませんが、特に大きなデータセットに対しては、最適化されたバージョンほどには機能しません。

最適化されていないアップサートクエリは実行時間を増加させ、スループットを低下させるため、Gremlin explain エンドポイントを使用して Upsert クエリが完全に最適化されているかどうかを判断する価値があります。explain プランを見直すときは、+ not converted into Neptune stepsWARNING: >> で始まる行を探してください。例:

+ not converted into Neptune steps: [FoldStep, CoalesceStep([[UnfoldStep], [AddEdgeSte... WARNING: >> FoldStep << is not supported natively yet

これらの警告は、クエリが完全に最適化されない原因となっている部分を特定するのに役立ちます。

クエリを完全に最適化できない場合もあります。このような場合は、最適化できないステップをクエリの最後に配置して、エンジンができるだけ多くのステップを最適化できるようにする必要があります。この手法はバッチアップサートの例の一部で使用されています。バッチアップサートの例では、ある頂点またはエッジのセットに対して最適化されたアップサートをすべて実行してから、最適化されていない可能性のある追加の修正を同じ頂点またはエッジに適用します。

アップサートをバッチ処理してスループットを向上させる

高スループットの書き込みシナリオでは、アップサートのステップを連鎖させて頂点とエッジをバッチでアップサートできます。バッチ処理を行うと、多数の頂点やエッジをアップサートすることによるトランザクションのオーバーヘッドが軽減されます。その後、複数のクライアントを使用してバッチリクエストを並行して更新することで、スループットをさらに高めることができます。

経験則として、バッチリクエストごとに約 200 件のレコードをアップサートすることをお勧めします。レコードは 1 つの頂点、またはエッジラベル、またはプロパティです。例えば、1 つのラベルと 4 つのプロパティを持つ頂点では、5 つのレコードが作成されます。1 つのラベルと 1 つのプロパティを持つエッジでは、2 つのレコードが作成されます。それぞれ 1 つのラベルと 4 つのプロパティを持つ頂点のバッチをアップサートしたい場合、200 / (1 + 4) = 40 のため、バッチサイズは 40 から始める必要があります。

バッチサイズを試してみることもできます。バッチあたり 200 レコードから始めるのが適切ですが、理想的なバッチサイズは作業負荷に応じて大きくなったり小さくなったりします。ただし、Neptune ではリクエストごとの Gremlin ステップの総数を制限する場合があることに注意してください。この制限は文書化されていませんが、念のため、リクエストに含まれるグレムリンステップ数は 1500 を超えないようにしてください。Neptune は、1,500 ステップを超える大規模なバッチリクエストを拒否する場合があります。

スループットを高めるには、複数のクライアントを使用して、バッチを並行してアップサートできます (「効率的なマルチスレッドの Gremlin 書き込みの作成」を参照)。クライアント数は、Neptune ライターインスタンスのワーカースレッドの数と同じである必要があります。通常、サーバー vCPUs 上の の数の 2 倍です。例えば、r5.8xlargeインスタンスには 32 vCPUs および 64 のワーカースレッドがあります。r5.8xlarge を使用する高スループットの書き込みシナリオでは、64 台のクライアントが Neptune にバッチアップサートを並行して書き込みます。

各クライアントはバッチリクエストを送信し、リクエストが完了するのを待ってから、別のリクエストを送信する必要があります。複数のクライアントは並行して実行されますが、個々のクライアントはそれぞれ順番にリクエストを送信します。これにより、サーバー側のリクエストキューがフラッディングすることなく、すべてのワーカースレッドを占有するリクエストのストリームがサーバーに安定して供給されます (「Neptune DB クラスターでの DB インスタンスのサイジング」を参照)。

複数のトラバーサーを生成するステップは避ける

Gremlin ステップを実行すると、入力トラバーサーが 1 つ取得され、1 つ以上の出力トラバーサーが出力されます。1 つのステップで発生するトラバーサーの数によって、次のステップが実行される回数が決まります。

通常、バッチ操作を実行するときは、頂点 A のアップサートなどの各操作を 1 回実行する必要があります。これにより、一連の操作は、頂点 A をアップサート、頂点 B をアップサート、頂点 C をアップサートするというようになります。ステップが 1 つの要素のみを作成または変更する限り、そのステップはトラバーサーを 1 つだけ出力し、次の操作を表すステップは 1 回だけ実行されます。一方、1 つの操作で複数の要素を作成または変更すると、複数のトラバーサーが出力され、それ以降のステップは、発行されたトラバーサーごとに 1 回ずつ、というように複数回実行されます。その結果、データベースが不必要な追加作業を行うことになり、場合によっては不要な頂点、エッジ、またはプロパティ値が作成されることもあります。

問題が発生する例としては、g.V().addV() のようなクエリがあります。この単純なクエリは、グラフ内の頂点ごとに頂点を追加します。これは、V() がグラフ内の頂点ごとにトラバーサーを発行し、それらのトラバーサーのそれぞれが addV() への呼び出しをトリガーするためです。

複数のトラバーサーを発行する操作を処理する方法については、「アップサートと挿入の混合」を参照してください。

頂点のアップサート

頂点 ID を使用して、対応する頂点が存在するかどうかを判断できます。Neptune は に関する同時実行性の高いユースケースに合わせてアップサートを最適化するため、これが推奨されるアプローチですIDs。例として、次のクエリは、特定の頂点 ID を持つ頂点がまだ存在しない場合はその頂点を作成し、存在する場合はそれを再利用します。

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .id()

このクエリは id() ステップで終わることに注意してください。頂点をアップサートする目的では必ずしも必要ではありませんが、アップサートクエリの最後に id() ステップを追加すると、サーバーはすべての頂点プロパティをクライアントにシリアル化し直さなくなるため、クエリのロックコストを削減できます。

あるいは、頂点プロパティを使用して頂点が存在するかどうかを調べることもできます。

g.V() .hasLabel('Person') .has('email', 'person-1@example.org') .fold() .coalesce(unfold(), addV('Person').property('email', 'person-1@example.org')) .id()

可能であれば、ユーザーが指定した独自の を使用して頂点IDsを作成し、これらIDsを使用してアップサートオペレーション中に頂点が存在するかどうかを判断します。これにより、Neptune は のアップサートを最適化できますIDs。ID ベースのアップサートは、同時変更が頻繁に行われる場合、プロパティベースのアップサートよりもはるかに効率的です。

頂点アップサートの連鎖

頂点アップサートを連鎖して、まとめて挿入することができます。

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .id()

エッジのアップサート

エッジを使用してIDs、カスタム頂点 を使用して頂点をアップサートするのと同じ方法でエッジをアップサートできますIDs。繰り返しますが、Neptune がクエリを最適化できるようになるため、この方法が推奨されます。例えば、次のクエリは、エッジ ID がまだ存在しない場合はそのエッジ ID に基づいてエッジを作成し、存在する場合は再利用します。このクエリでは、新しいエッジを作成する必要がある場合、 fromおよび to 頂点IDsの も使用します。

g.E('e-1') .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2')) .property(id, 'e-1')) .id()

多くのアプリケーションはカスタム頂点 を使用しますがIDs、Neptune を離れてエッジ を生成しますIDs。エッジの ID がわからないが、 fromおよび to 頂点 がわかっている場合はIDs、次の式を使用してエッジをアップサートできます。

g.V('v-1') .outE('KNOWS') .where(inV().hasId('v-2')) .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2'))) .id()

where() 句の頂点ステップは otherV() ではなく inV() (または、inE() を使用してエッジを見つけた場合は outV()) でなければならないことに注意してください。ここでは otherV() を使用しないでください。使用すると、クエリが最適化されず、パフォーマンスが低下します。例えば、Neptune は次のクエリを最適化しません。

// Unoptimized upsert, because of otherV() g.V('v-1') .outE('KNOWS') .where(otherV().hasId('v-2')) .fold() .coalesce(unfold(), addE('KNOWS').from(V('v-1')) .to(V('v-2'))) .id()

エッジまたは頂点がIDs事前にわからない場合は、頂点プロパティを使用してアップサートできます。

g.V() .hasLabel('Person') .has('name', 'person-1') .outE('LIVES_IN') .where(inV().hasLabel('City').has('name', 'city-1')) .fold() .coalesce(unfold(), addE('LIVES_IN').from(V().hasLabel('Person') .has('name', 'person-1')) .to(V().hasLabel('City') .has('name', 'city-1'))) .id()

頂点アップサートと同様に、Neptune がアップサートを完全に最適化できるようにIDs、プロパティベースのアップサートではなく、エッジ ID または to fromと頂点 のいずれかを使用して ID ベースのエッジアップサートを使用することをお勧めします。

from および to 頂点の有無の確認

新しいエッジを作成するステップの構造 addE().from().to() に注意してください。この構造により、クエリは fromto の両方の頂点の存在を確実にチェックします。これらのいずれかが存在しない場合、クエリは次のようなエラーを返します。

{ "detailedMessage": "Encountered a traverser that does not map to a value for child... "code": "IllegalArgumentException", "requestId": "..." }

from または to のいずれかの頂点が存在しない可能性がある場合は、頂点間のエッジをアップセットする前に、頂点をアップサートしてみてください。「頂点とエッジのアップサートの組み合わせ」を参照してください。

エッジを作成するには、使用すべきではない別の方法 V().addE().to() があります。from 頂点が存在する場合にのみ、エッジを追加します。to 頂点が存在しない場合、前述のようにクエリはエラーを生成しますが、from 頂点が存在しない場合、エラーを発生せずに、エッジの挿入に失敗します。例えば、次のアップサートは、from 頂点が存在しない場合、エッジをアップサートせずに完了します。

// Will not insert edge if from vertex does not exist g.V('v-1') .outE('KNOWS') .where(inV().hasId('v-2')) .fold() .coalesce(unfold(), V('v-1').addE('KNOWS') .to(V('v-2'))) .id()

エッジのアップサートの連鎖

エッジアップサートを連鎖してバッチリクエストを作成する場合は、エッジ がわかっていても、各アップサートを頂点ルックアップで開始する必要がありますIDs。

アップサートするエッジIDsの と、頂点fromと頂点IDsの to が既にわかっている場合は、次の式を使用できます。

g.V('v-1') .outE('KNOWS') .hasId('e-1') .fold() .coalesce(unfold(), V('v-1').addE('KNOWS') .to(V('v-2')) .property(id, 'e-1')) .V('v-3') .outE('KNOWS') .hasId('e-2').fold() .coalesce(unfold(), V('v-3').addE('KNOWS') .to(V('v-4')) .property(id, 'e-2')) .V('v-5') .outE('KNOWS') .hasId('e-3') .fold() .coalesce(unfold(), V('v-5').addE('KNOWS') .to(V('v-6')) .property(id, 'e-3')) .id()

おそらく最も一般的なバッチエッジアップサートのシナリオは、 fromto 頂点 はわかっているがIDs、アップサートするエッジIDsの はわからないことです。その場合は、次の式を使用してください。

g.V('v-1') .outE('KNOWS') .where(inV().hasId('v-2')) .fold() .coalesce(unfold(), V('v-1').addE('KNOWS') .to(V('v-2'))) .V('v-3') .outE('KNOWS') .where(inV().hasId('v-4')) .fold() .coalesce(unfold(), V('v-3').addE('KNOWS') .to(V('v-4'))) .V('v-5') .outE('KNOWS') .where(inV().hasId('v-6')) .fold() .coalesce(unfold(), V('v-5').addE('KNOWS').to(V('v-6'))) .id()

アップサートしたいエッジIDsはわかっているが、 fromおよび 頂点IDsの to がわからない場合 (これは異常です)、次の式を使用できます。

g.V() .hasLabel('Person') .has('email', 'person-1@example.org') .outE('KNOWS') .hasId('e-1') .fold() .coalesce(unfold(), V().hasLabel('Person') .has('email', 'person-1@example.org') .addE('KNOWS') .to(V().hasLabel('Person') .has('email', 'person-2@example.org')) .property(id, 'e-1')) .V() .hasLabel('Person') .has('email', 'person-3@example.org') .outE('KNOWS') .hasId('e-2') .fold() .coalesce(unfold(), V().hasLabel('Person') .has('email', 'person-3@example.org') .addE('KNOWS') .to(V().hasLabel('Person') .has('email', 'person-4@example.org')) .property(id, 'e-2')) .V() .hasLabel('Person') .has('email', 'person-5@example.org') .outE('KNOWS') .hasId('e-1') .fold() .coalesce(unfold(), V().hasLabel('Person') .has('email', 'person-5@example.org') .addE('KNOWS') .to(V().hasLabel('Person') .has('email', 'person-6@example.org')) .property(id, 'e-3')) .id()

頂点とエッジのアップサートの組み合わせ

頂点とそれらを接続するエッジの両方をアップサートしたい場合があります。ここで紹介したバッチサンプルをミックスしてもかまいません。次の例では、3 つの頂点と 2 つのエッジをアップサートしています。

g.V('p-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-1') .property('email', 'person-1@example.org')) .V('p-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-2') .property('name', 'person-2@example.org')) .V('c-1') .fold() .coalesce(unfold(), addV('City').property(id, 'c-1') .property('name', 'city-1')) .V('p-1') .outE('LIVES_IN') .where(inV().hasId('c-1')) .fold() .coalesce(unfold(), V('p-1').addE('LIVES_IN') .to(V('c-1'))) .V('p-2') .outE('LIVES_IN') .where(inV().hasId('c-1')) .fold() .coalesce(unfold(), V('p-2').addE('LIVES_IN') .to(V('c-1'))) .id()

アップサートと挿入の混合

頂点とそれらを接続するエッジの両方をアップサートしたい場合があります。ここで紹介したバッチサンプルをミックスしてもかまいません。次の例では、3 つの頂点と 2 つのエッジをアップサートしています。

アップサートは通常、一度に 1 つの要素を処理します。ここで説明したアップサートパターンに従うと、アップサート操作ごとにトラバーサーが 1 回発生し、それ以降の操作は 1 回だけ実行されます。

ただし、アップサートと挿入を混在させたい場合もあります。例えば、エッジを使用してアクションやイベントのインスタンスを表す場合などが該当します。リクエストでは、必要な頂点がすべて存在することを確認するためにアップサートを使用し、エッジを追加するためにインサートを使用する場合があります。この種のリクエストでは、各操作で発生する可能性のあるトラバーサーの数に注意してください。

アップサートとインサートを組み合わせて、イベントを表すエッジをグラフに追加する次の例を考えてみましょう。

// Fully optimized, but inserts too many edges g.V('p-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-1') .property('email', 'person-1@example.org')) .V('p-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-2') .property('name', 'person-2@example.org')) .V('p-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-3') .property('name', 'person-3@example.org')) .V('c-1') .fold() .coalesce(unfold(), addV('City').property(id, 'c-1') .property('name', 'city-1')) .V('p-1', 'p-2') .addE('FOLLOWED') .to(V('p-1')) .V('p-1', 'p-2', 'p-3') .addE('VISITED') .to(V('c-1')) .id()

クエリは、2 つのエッジと 3 つのFOLLOWEDエッジの 5 つのVISITEDエッジを挿入する必要があります。ただし、書き込まれたクエリは、2 FOLLOWEDと 6 の 8 つのエッジを挿入しますVISITED。この理由は、2 つのFOLLOWEDエッジを挿入するオペレーションが 2 つのトラバーサーを発し、3 つのエッジを挿入する後続の挿入オペレーションが 2 回実行されるためです。

この問題を解決するには、複数のトラバーサーが発生する可能性のある各操作の後に fold() ステップを追加します。

g.V('p-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-1') .property('email', 'person-1@example.org')) .V('p-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-2'). .property('name', 'person-2@example.org')) .V('p-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'p-3'). .property('name', 'person-3@example.org')) .V('c-1') .fold(). .coalesce(unfold(), addV('City').property(id, 'c-1'). .property('name', 'city-1')) .V('p-1', 'p-2') .addE('FOLLOWED') .to(V('p-1')) .fold() .V('p-1', 'p-2', 'p-3') .addE('VISITED') .to(V('c-1')). .id()

ここでは、FOLLOWEDエッジを挿入する操作の後にfold()ステップを挿入しました。これにより、1 つのトラバーサーが発生し、それ以降の操作は 1 回だけ実行されます。

この方法の欠点は、fold() が最適化されていないため、クエリが完全には最適化されないことです。fold() の後に続く挿入操作は最適化されなくなります。

fold() を使用して、後続のステップの代わりにトラバーサーの数を減らす必要がある場合は、最もコストの低いものがクエリの最適化されていない部分を占めるように操作を順序付けてください。

既存の頂点とエッジを変更するアップサート

頂点やエッジが存在しない場合は作成し、その頂点やエッジが新規か既存かに関わらず、プロパティを追加または更新したい場合があります。

プロパティを追加または変更するには、property() ステップを使用します。このステップは coalesce() ステップの外部で使用してください。coalesce() ステップ内で既存の頂点またはエッジのプロパティを変更しようとすると、クエリはクエリエンジンによって最適化されない場合があります。

次のクエリは、アップサートされた各頂点のカウンタープロパティを追加または更新します。各 property() ステップには単一のカーディナリティがあり、新しい値が既存の値のセットに追加されるのではなく、既存の値と置き換えられるようにします。

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .property(single, 'counter', 1) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .property(single, 'counter', 2) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .property(single, 'counter', 3) .id()

lastUpdated タイムスタンプ値など、アップサートされたすべての要素に適用されるプロパティ値がある場合は、クエリの最後にそのプロパティ値を追加または更新できます。

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org')) .V('v-2'). .fold(). .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org')) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org')) .V('v-1', 'v-2', 'v-3') .property(single, 'lastUpdated', datetime('2020-02-08')) .id()

頂点やエッジをさらに変更するかどうかを決める条件が他にもある場合は、has() ステップを使用して、変更を適用する要素をフィルタリングできます。次の例では、has() ステップを使用して、アップサートされた頂点を version プロパティの値に基づいてフィルタリングしています。次に、クエリは version が 3 未満のすべての頂点の version を 3 倍に更新します。

g.V('v-1') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-1') .property('email', 'person-1@example.org') .property('version', 3)) .V('v-2') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-2') .property('email', 'person-2@example.org') .property('version', 3)) .V('v-3') .fold() .coalesce(unfold(), addV('Person').property(id, 'v-3') .property('email', 'person-3@example.org') .property('version', 3)) .V('v-1', 'v-2', 'v-3') .has('version', lt(3)) .property(single, 'version', 3) .id()