Gremlin mergeV() および mergeE() ステップによる効率的なアップサートの実行 - Amazon Neptune

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

Gremlin mergeV() および mergeE() ステップによる効率的なアップサートの実行

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

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

例えば、次のクエリは、指定された Map を使用して、T.id"v-1" の頂点を最初に探すことによって、頂点をアップサートします。その頂点が見つかると、その頂点が返されます。見つからなかった場合は、その id とプロパティを持つ頂点が onCreate 句によって作成されます。

g.mergeV([(id):'v-1']). option(onCreate, [(label): 'PERSON', 'email': 'person-1@example.org'])

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

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

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

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

スループットを高めるには、複数のクライアントを使用して、バッチを並行してアップサートできます (「効率的なマルチスレッドの Gremlin 書き込みの作成」を参照)。クライアントの数は、Neptune ライターインスタンスのワーカースレッドの数と同じである必要があり、通常、これはサーバー上の vCPU の数の 2 倍です。例えば、r5.8xlarge インスタンスには 32 個の vCPU と 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() への呼び出しをトリガーするためです。

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

頂点のアップサート

mergeV() ステップは、頂点をアップサートするために特別に設計されています。グラフ内の既存の頂点と一致する要素を表す Map を引数として取り、要素が見つからない場合は、その Map を使用して新しい頂点を作成します。このステップでは、作成時やマッチ時の動作を変更することも可能であり、option() モジュレータに Merge.onCreate および Merge.onMatch トークンを適用して、それぞれの動作を制御できます。このステップの使用方法の詳細については、 TinkerPop リファレンスドキュメントを参照してください

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

g.mergeV([(T.id): 'v-1']). option(onCreate, [(T.label): 'PERSON', email: 'person-1@example.org', age: 21]). option(onMatch, [age: 22]). id()

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

あるいは、頂点プロパティを使用して頂点を識別することもできます。

g.mergeV([email: 'person-1@example.org']). option(onCreate, [(T.label): 'PERSON', age: 21]). option(onMatch, [age: 22]). id()

可能であれば、ユーザー指定の独自の ID を使用して頂点を作成し、これらの ID を使用してアップサート操作中に頂点が存在するかどうかを判断します。これにより、Neptune はアップサートを最適化できます。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()

または、次の mergeV() 構文を使用することもできます。

g.mergeV([(T.id): 'v-1', (T.label): 'PERSON', email: 'person-1@example.org']). mergeV([(T.id): 'v-2', (T.label): 'PERSON', email: 'person-2@example.org']). mergeV([(T.id): 'v-3', (T.label): 'PERSON', email: 'person-3@example.org'])

ただし、この形式のクエリには、id による基本的な検索方法では不要な要素が検索条件に含まれているため、前のクエリほど効率的ではありません。

エッジのアップサート

mergeE() ステップは、エッジをアップサートするために特別に設計されています。グラフ内の既存のエッジと一致する要素を表す Map を引数として取り、要素が見つからない場合は、その Map を使用して新しいエッジを作成します。このステップでは、作成時やマッチ時の動作を変更することも可能であり、option() モジュレータに Merge.onCreate および Merge.onMatch トークンを適用して、それぞれの動作を制御できます。このステップの使用方法の詳細については、 TinkerPop リファレンスドキュメントを参照してください

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

g.mergeE([(T.id): 'e-1']). option(onCreate, [(from): 'v-1', (to): 'v-2', weight: 1.0]). option(onMatch, [weight: 0.5]). id()

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

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

g.mergeE([(from): 'v-1', (to): 'v-2', (T.label): 'KNOWS']). id()

ステップでエッジを作成するには、mergeE() によって参照されるすべての頂点が存在している必要があります。

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

頂点アップサートの場合と同様、mergeE() ステップを連鎖して、バッチリクエストにするのは簡単です。

g.mergeE([(from): 'v-1', (to): 'v-2', (T.label): 'KNOWS']). mergeE([(from): 'v-2', (to): 'v-3', (T.label): 'KNOWS']). mergeE([(from): 'v-3', (to): 'v-4', (T.label): 'KNOWS']). id()

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

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

g.mergeV([(id):'v-1']). option(onCreate, [(label): 'PERSON', 'email': 'person-1@example.org']). mergeV([(id):'v-2']). option(onCreate, [(label): 'PERSON', 'email': 'person-2@example.org']). mergeV([(id):'v-3']). option(onCreate, [(label): 'PERSON', 'email': 'person-3@example.org']). mergeE([(from): 'v-1', (to): 'v-2', (T.label): 'KNOWS']). mergeE([(from): 'v-2', (to): 'v-3', (T.label): 'KNOWS']). id()

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

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

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

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

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

// Fully optimized, but inserts too many edges g.mergeV([(id):'v-1']). option(onCreate, [(label): 'PERSON', 'email': 'person-1@example.org']). mergeV([(id):'v-2']). option(onCreate, [(label): 'PERSON', 'email': 'person-2@example.org']). mergeV([(id):'v-3']). option(onCreate, [(label): 'PERSON', 'email': 'person-3@example.org']). mergeV([(T.id): 'c-1', (T.label): 'CITY', 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()

クエリでは 5 つのエッジを挿入する必要があります。2 つは FOLLOWED エッジであり、3 つは VISITED エッジです。ただし、記述されているクエリでは、8 つのエッジが挿入されます (2 つは FOLLOWED、6 つは VISITED)。これは、2 つの FOLLOWED エッジを挿入する操作で 2 つのトラバーサーが発生し、3 つのエッジを挿入する後続の挿入操作が 2 回実行されるためです。

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

g.mergeV([(T.id): 'v-1', (T.label): 'PERSON', email: 'person-1@example.org']). mergeV([(T.id): 'v-2', (T.label): 'PERSON', email: 'person-2@example.org']). mergeV([(T.id): 'v-3', (T.label): 'PERSON', email: 'person-3@example.org']). mergeV([(T.id): 'c-1', (T.label): 'CITY', 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() を使用して、後続のステップの代わりにトラバーサーの数を減らす必要がある場合は、最もコストの低いものがクエリの最適化されていない部分を占めるように操作を順序付けてください。