Amazon Web Services ブログ

MongoDB 互換の新しい Amazon DocumentDB 集計パイプライン演算子: $objectToArray、$arrayToObject、$slice、$mod、$range



Amazon DocumentDB (MongoDB 互換) は、MongoDB のワークロードをサポートする高速でスケーラブル、かつ可用性に優れた完全マネージド型のドキュメント データベース サービスです。お客様は、基盤となるインフラストラクチャの管理を気にすることなく、現在ご使用のものと同じ MongoDB 向けのアプリケーションコード、ドライバー、ツールを、そのまま Amazon DocumentDB 上で実行や管理をしたり、処理負荷を調整したりするのに使えます。

今日、Amazon DocumentDB は、 5 つの集計パイプライン機能を新たにサポートするようになりました。これにより、ドキュメントで強力な集計を作成できます。新しい機能には、$objectToArray$arrayToObject$slice$mod$range 集計パイプライン演算子が含まれます。

AWS は日頃からお客様の声に耳を傾け、最もニーズの高い機能を構築し、実用的な機能の互換性を高めています。Amazon DocumentDB のサポート対象の MongoDB API と集計パイプライン機能の詳細については、ドキュメントを参照してください。

このブログ記事では、一般的なユースケースを使用してこれらの新しい集計演算子の一部を紹介し、クイックスタートガイドを提供します。これにより、Amazon DocumentDB でこれらの機能をすぐにご利用開始できます。

配列集計演算子

Amazon DocumentDB では、配列データ型を持つフィールドを含むドキュメントを作成できます。データベースのクエリ言語内でネイティブに配列をクエリおよび操作できると、配列操作をデータベースにプッシュすることでパフォーマンスを向上させ、アプリケーションコードを簡素化できます。このセクションでは、4 つの新しい配列集計演算子 ($arrayToObject、$slice、$objectToArray、$range) の使用方法を説明します。

$objectToArray

$objectToArray 集計演算子は、オブジェクト (またはドキュメント) を配列に変換します。演算子への入力はドキュメントで、出力は入力ドキュメントの各フィールドと値のペアの配列要素で構成されます。

$objectToArray の仕組みを理解するために、アイオワ州の架空のビデオレンタルチェーン店のビデオテープの在庫を追跡するデータセットを例に取ります。最初にデータをモデル化して、特定のビデオタイトルごとに店舗在庫を分類しましたが、どの店舗のビデオ在庫が最も多いかを知りたい場合にはどうしたらよいでしょうか。その問いに答えるため、$objectToArray 集計演算子を使用します。

Input:

サンプルデータセットの各ドキュメントは個別のビデオタイトルで、各レンタルビデオ店の所在地の在庫を追跡する埋め込みドキュメントが含まれています。

db.videos.insertMany([
{ "_id":1, "name":"Live Soft", "inventory": {"Des Moines": 1000, "Ames" : 500}},
{ "_id":2, "name":"Top Pilot", "inventory": {"Mason City": 250, "Des Moines": 1000}},
{ "_id":3, "name":"Romancing the Rock", "inventory": {"Mason City": 250, "Ames" : 500}},
{ "_id":4, "name":"Bravemind", "inventory": {"Mason City": 250, "Des Moines": 1000, "Ames" : 500}}
])

以下のクエリでは、複数の集計ステージを利用して、ビデオの在庫が最も多い店舗など、ここで提起した問いに答えています。最初の段階では、$objectToArray を使用して在庫ドキュメントを配列に変換します。これにより、店舗在庫をより簡単に集計できるようにします。以下は、最初のステージの出力です。ご覧のとおり、ドキュメントはキーと値のペアの配列になりました。

Query:

db.videos.aggregate([
   { $project: {videos: { $objectToArray: "$inventory" }}}
])

Result:

{ "_id" : 1, "videos" : [ { "k" : "Des Moines", "v" : 1000 }, { "k" : "Ames", "v" : 500 } ] }
{ "_id" : 2, "videos" : [ { "k" : "Mason City", "v" : 250 }, { "k" : "Des Moines", "v" : 1000 } ] }
{ "_id" : 3, "videos" : [ { "k" : "Mason City", "v" : 250 }, { "k" : "Ames", "v" : 500 } ] }
{ "_id" : 4, "videos" : [ { "k" : "Mason City", "v" : 250 }, { "k" : "Des Moines", "v" : 1000 }, { "k" : "Ames", "v" : 500 } ] }

どの店舗のビデオの在庫が最も多いかという元の質問に答えるために、最初の段階から配列を $unwind し、都市ごとに配列 (キーや「k」など) をグループ化して、数量 (値や「v」など) を合算します。次に、最大の在庫を持つ店舗を返すために、次の段階で合計値の降順 $sort を実行し、最後の $limit 段階を使用して上位の結果のみを返します。

Query:

db.videos.aggregate(
   [{ $project: {videos: { $objectToArray: "$inventory" }}},
    { $unwind: "$videos" },
    { $group: { _id: "$videos.k", total: { $sum: "$videos.v" } } },
    { $sort: {"total": -1}},
    { $limit : 1 }
   ]
)

Result:

{ "_id" : "Des Moines", "total" : 3000 }

最終結果は、Des Moines 店のビデオの在庫が 3000 本で最大であることを示しています。すべての店舗の在庫を確認したい場合、集計クエリから最後の $limit ステージを省略します。

$ArrayToObject

$ObjectToArray と同様に、$ArrayToObject 集計演算子は、キーと値のペアの配列を単一のドキュメントに変換します。入力として、$ArrayToObject は、配列がすでに 1 つ以上のキーと値のペアとして表されていることを想定しています。たとえば、前の例で見たように:

“videos" : [ { "k" : "Des Moines", "v" : 1000 }, { "k" : "Ames", "v" : 500 } ]

$arrayToObject 演算子は、$objectToArray 演算子を反映しています。$arrayToObject を使用して、上記のクエリから出力を取得し、元の形式で返すことができます。

Query:

db.videos.aggregate(
   [ { $project: {name:1, videos: { $objectToArray: "$inventory" }}},
     { $project: {name:1, inventory: { $arrayToObject: "$videos" }}}
   ]
)

Result:

{ "_id" : 1, "name" : "Live Soft", "inventory" : { "Des Moines" : 1000, "Ames" : 500 } }
{ "_id" : 2, "name" : "Top Pilot", "inventory" : { "Mason City" : 250, "Des Moines" : 1000 } }
{ "_id" : 3, "name" : "Romancing the Rock", "inventory" : { "Mason City" : 250, "Ames" : 500 } }
{ "_id" : 4, "name" : "Bravemind", "inventory" : { "Mason City" : 250, "Des Moines" : 1000, "Ames" : 500 } }

さらに、キーと値のペアとして整理された配列を取り、それらをドキュメントに変換できます。以下は、Odell Lake で 1 日釣りをしたレポートのサンプルデータセットです。

Input:

db.fish.insertMany([
{ "_id" : 1, "report": [{"k":"Whitefish", "v":10}]},
{ "_id" : 2, "report": [{"k": "Dolly Varden", "v": 2}]},
{ "_id" : 3, "report": [{"k": "Rainbox Trout", "v": 33}]},
{ "_id" : 4, "report": [{"k": "Blueback Salmon", "v": 9}]}
])

$arrayToObject 演算子を使用して、捕獲した魚とその数のキーと値のペアの配列を単一のドキュメントに変換します。

Query:

db.fish.aggregate(
	[
     { $project: {caught: { $arrayToObject: "$report" }}}
   ]
)

Result:

{ "_id" : 1, "caught" : { "Whitefish" : 10 } }
{ "_id" : 2, "caught" : { "Dolly Varden" : 2 } }
{ "_id" : 3, "caught" : { "Rainbox Trout" : 33 } }
{ "_id" : 4, "caught" : { "Blueback Salmon" : 9 } }

$slice

$slice 集計演算子を使用すると、配列の先頭または末尾から配列をトラバースすることにより、配列のサブセットを返すことができます。$slice の有用性を説明するために、一握りのシェフのお気に入りのスイーツを含む以下のデータセットを検討します。

Input:

db.sweets.insertMany([
{ "_id" : 1, "name" : "Alvin", favorites: [ "chocolate", "cake", "toffee", "beignets" ] },
{ "_id" : 2, "name" : "Tom", favorites: [ "donuts", "pudding", "pie" ] },
{ "_id" : 3, "name" : "Jessica", favorites: [ "fudge", "smores", "pudding", "cupcakes" ] },
{ "_id" : 4, "name" : "Rachel", favorites: [ "ice cream" ] }
])

各シェフの最初の 2 つのお気に入りのみを返したい場合、次の集計クエリで $slice が使えます。

Query:

db.sweets.aggregate([
   { $project: { name: 1, threeFavorites: { $slice: [ "$favorites", 2] } } }
])

Result:

{ "_id" : 1, "name" : "Alvin", "threeFavorites" : [ "chocolate", "cake" ] }
{ "_id" : 2, "name" : "Tom", "threeFavorites" : [ "donuts", "pudding" ] }
{ "_id" : 3, "name" : "Jessica", "threeFavorites" : [ "fudge", "smores" ] }
{ "_id" : 4, "name" : "Rachel", "threeFavorites" : [ "ice cream" ] }

クエリの出力では、シェフのお気に入りのスイーツごとに最大 2 つのアイテムが生成されます。クエリ結果は配列の先頭から始まり、最初の 2 つの項目を選択したことに注意してください。配列の最後の 2 つの項目を選択する場合、2 番目のパラメータに負の数を使用して、配列の最後の要素から開始して右から左にトラバースすることを指定できます。たとえば、以下は上記と同じクエリですが、$slice の配列トラバースの順序が逆になっています。

Query:

db.sweets.aggregate([
   { $project: { name: 1, threeFavorites: { $slice: [ "$favorites", -2] } } }
])

Result:

{ "_id" : 1, "name" : "Alvin", "threeFavorites" : [ "toffee", "beignets" ] }
{ "_id" : 2, "name" : "Tom", "threeFavorites" : [ "pudding", "pie" ] }
{ "_id" : 3, "name" : "Jessica", "threeFavorites" : [ "pudding", "cupcakes" ] }
{ "_id" : 4, "name" : "Rachel", "threeFavorites" : [ "ice cream" ] }

結果からわかるように、配列の最後の 2 つの項目が選択されました。2 つに満たない項目を持つ配列の場合、1 つの値のみが返されることに注意してください。

$range

$range 集計演算子では、シーケンス番号の配列を作成できます。集計演算子への入力は、目的の数値範囲の開始値と終了値、およびオプションのゼロ以外の増分値です。これらの機能をわかりやすく説明するために、長距離の自転車レースのためにエイドステーションを配置する方法の例を用います。次のレースとそれぞれの距離を検討します。私はレースディレクターとして、ライダーのために 20 マイル (約32 km) ごとに給水所を設け、確実に水分補給できるようにしたいと思います。$range 演算子を使用して、エイドステーションを配置する距離マーカーを指定します。

Input:

db.races.insertMany([
{ _id: 0, race: "STP", distance: 206 },
{ _id: 1, race: "RSVP", distance: 160 },
{ _id: 2, race: "Chilly Hilly", distance: 33 },
{ _id: 3, race: "Flying Wheels", distance: 100 },
])

Query:

db.races.aggregate([
	{$project: {race: 1, "waterStations": { $range: [ 20, "$distance", 20 ] }}}
])

Result:

{ "_id" : 0, "race" : "STP", "waterStations" : [ 20, 40, 60, 80, 100, 120, 140, 160, 180, 200 ] }
{ "_id" : 1, "race" : "RSVP", "waterStations" : [ 20, 40, 60, 80, 100, 120, 140 ] }
{ "_id" : 2, "race" : "Chilly Hilly", "waterStations" : [ 20 ] }
{ "_id" : 3, "race" : "Flying Wheels", "waterStations" : [ 20, 40, 60, 80 ] }

結果から、給水所の設置とスタッフの配置に必要な距離マーカーを確認できます。レースごとに必要な総給水所の数を知りたい場合は、$addFields 集計ステージを使用してドキュメントに別のフィールドを追加し、さらに $size 集計演算子が使えます。

Query:

db.races.aggregate([
	{$project: {race: 1, "waterStations": { $range: [ 20, "$distance", 20 ] }}},
	{$addFields: {totalStations: {$size:"$waterStations"}}}
])

Result:

{ "_id" : 0, "race" : "STP", "waterStations" : [ 20, 40, 60, 80, 100, 120, 140, 160, 180, 200 ], "totalStations" : 10 }
{ "_id" : 1, "race" : "RSVP", "waterStations" : [ 20, 40, 60, 80, 100, 120, 140 ], "totalStations" : 7 }
{ "_id" : 2, "race" : "Chilly Hilly", "waterStations" : [ 20 ], "totalStations" : 1 }
{ "_id" : 3, "race" : "Flying Wheels", "waterStations" : [ 20, 40, 60, 80 ], "totalStations" : 4 }

結果は、「totalStations」というタイトルのドキュメント内の別のフィールドで、配列のサイズ、つまり必要な給水所の数量を示しています。

算術演算子

このリリースでは、$mod も追加しました。これは、お客様から最も要望の多かった算術演算子の 1 つです。

$mod

$mod 算術演算子を使用すると、モジュラー演算を実行できます。$mod の一般的な使用例には、数が奇数か偶数か (偶数 % 2 が 0 を返す) の決定、または有限数のグループ間での人またはアイテムの分散があります。以下のデータセットでは、100 個のパッケージでウィジェットを出荷した場合に残っているウィジェットの数を判断します。

Input:

db.widgets.insertMany([
{ "_id" : 1, "widget" : "A", "count" : 80372},
{ "_id" : 2, "widget" : "B", "count" : 409282},
{ "_id" : 3, "widget" : "C", "count" : 60739}
])

次の集計クエリでは、$addFields 集計ステージも使用して、剰余演算操作の残りを既存のドキュメントに追加フィールドとして単純に追加します。

Query:

db.widgets.aggregate(
   [
     { $addFields: {leftOver: { $mod: [ "$count", 100 ] } } }
   ]
)

Result:

{ "_id" : 1, "widget" : "A", "count" : 80372, "leftOver" : 72 }
{ "_id" : 2, "widget" : "B", "count" : 409282, "leftOver" : 82 }
{ "_id" : 3, "widget" : "C", "count" : 60739, "leftOver" : 39 }

結果から、leftOver フィールドは 100 個のカウントの残りの % を計算したものを示していることがわかります。

まとめ

AWS は、お客様からのフィードバックを基に、お客様がお求めになる機能をこれからも構築していきます。このリリースでは、$objectToArray$arrayToObject$slice$mod$range の 5 つの新しい集計パイプライン機能が追加されました。

Amazon DocumentDB の使用を開始するには、Amazon DocumentDB 入門ガイド、または次のビデオをご覧ください。次に、現在 MongoDB で使用しているものと同じアプリケーションコード、ドライバ、およびツールを使って、Amazon DocumentDB での開発を開始できます。詳細については、Amazon DocumentDB 製品ページを参照してください。移行の詳細については、移行ガイドを参照し、FINRA が Amazon DocumentDB に移行した方法をご覧ください。

 


著者について

 

Joseph Idziorek は、アマゾン ウェブ サービスのプリンシパルプロダクトマネージャーです。