Amazon Web Services ブログ

Amazon DocumentDB の新しい集約パイプライン機能を使いパワフルな集約型クエリを記述する



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

2 月のブログで解説した機能を基に、 今回、Amazon DocumentDB に新たな集約パイプライン処理に関するサポートが追加されました。この新機能には以下のものが含まれます。

  • 7 個の集約文字列演算子 ($indexOfCP$indexOfBytes$strLenCP$strLenBytes$toLower$toUpper$split)
  • 9 個の日付時間演算子 ($dayOfYear$dayOfMonth$dayOfWeek$year$month$hour$minute$second$millisecond)
  • $sample 集約パイプラインステージ

このブログでは、一般的なユースケースとして上記演算子の使用法を示しながら、そこに新しく加わった機能をご紹介していくことにします。

Amazon DocumentDB の使用開始

Amazon DocumentDB の初心者の方は、次のいずれかから使用を開始することができます。

 新機能

Amazon DocumentDB クラスターを作成して接続したら、次の例をよく理解して拡張することができます。

1.集約文字列演算子

ドキュメント内に格納された文字列は、多様な利用法に応じ、必要とされる部分ごとに分けて取り出す操作が可能です。この文字列では、追加の処理や比較、あるいはデータのクリーンアップなどが行えます。アプリケーションコードで文字列処理を記述しなくても、集約文字列演算子を用いてデータ処理をデータベース内に落とし込むことができます。現状で、Amazon DocumentDB は、MongoDB 3.6 の全文字列演算子をサポートしています。

これらの新しい演算子がどう機能するかご理解いただくには、例えば、従業員の役割、管理者、勤務地などを検索するために使うアプリケーションを、ご自身で直接記述したことを想像していただくと良いでしょう。今、ドイツ国内で働く従業員についての資料を格納したドキュメントが、Amazon DocumentDB 内に収集されていると仮定します。「都市 – 建物 – 座席」という形で所在地を表示するにしても、その分類基準は、各個人によってある程度は差異が生じるものです。

今回の新しい文字列演算子を用い、アプリケーションでデータの利用性を高められる文字列情報の取り出し方を、ここで示していきたいと思います。

入力

今回の例で使う各ドキュメントは、それぞれ 1 人の個人を扱っています。その中には、従業員名、管理職名、役割、そして勤務地に関するその企業独特の識別子が登録されています。

db.people.insertMany([
{ "_id":1, "name":"John Doe", "Manager":"Jane Doe", "Role":"Developer", "Desk": " Düsseldorf-BVV-021"},
{ "_id":2, "name":"John Stiles", "Manager":"Jane Doe", "Role":"Manager", "Desk": " Munich-HGG-32a"},
{ "_id":3, "name":"Richard Roe", "Manager":"Jorge Souza", "Role":"Product", "Desk": " Cologne-ayu-892.50"},
{ "_id":4, "name":"Mary Major", "Manager":"Jane Doe", "Role":"Solution Architect", "Desk": " Dortmund-Hop-78"}])

$indexOfCP

Desk のフィールドにある文字列を解析する時、その文字列内にある特定のキャラクターが最初に登場するコードポイント (CP) を、インデックスとして見つけ出すことが有用です。そのためには、$indexOfCP 演算子を使います。次に示すクエリでは、最初に登場するハイフンを検索しています。

クエリ:

db.people.aggregate(
[
 { $project: { stateLocation: { $indexOfCP: [ "$Desk", "-"] } } }
])

結果:

{ "_id" : 1, "stateLocation" : 11 }
{ "_id" : 2, "stateLocation" : 7 }
{ "_id" : 3, "stateLocation" : 8 }
{ "_id" : 4, "stateLocation" : 9 }

この結果は、最初のハイフン (“-“) がある場所をインデックスとして列挙しています。こうして得た情報を、$substrCP$substrBytes といった演算子とともに利用することで、さらに解析を行ったり文字列から中身を抽出することに利用できます。

$indexOfBytes

$indexOfBytes は、インデックスをバイト数で返す以外は、$indexofCP と同じです。次の例から、2 つの間で判断基準が違っていることを確認してください。

クエリ:

db.people.aggregate(
[
 { $project: { stateLocation: { $indexOfBytes: [ "$Desk", "-"] } } }
]) 

結果:

{ "_id" : 1, "stateLocation" : 12 }
{ "_id" : 2, "stateLocation" : 7 }
{ "_id" : 3, "stateLocation" : 8 }
{ "_id" : 4, "stateLocation" : 9 }

ご覧になってわかるとおり、(バイト数で示された) 各文字列内での位置には、少し違いが現れています。「Düsseldorf」の中にある「ü」というキャラクターが 2 バイト文字であるため、$indexOfBytes が返したインデックスは 12 に、$indexOfCP が返したインデックスは 11 になっています。

$strLenCP

文字列内にあるコードポイントの数は、$strLenCP 演算子を使って知ることができます。

クエリ:

db.people.aggregate(
[{
  $project: {"Desk": 1, "length": { $strLenCP: "$Desk" }}}
])

結果:

{ "_id" : 1, "Desk" : " Düsseldorf-BVV-021", "length" : 19 }
{ "_id" : 2, "Desk" : " Munich-HGG-32a", "length" : 15 }
{ "_id" : 3, "Desk" : " Cologne-ayu-892.50", "length" : 19 }
{ "_id" : 4, "Desk" : " Dortmund-Hop-78", "length" : 16 }

$strLenBytes

これと似た感じで、$strLenBytes 演算子では、 文字列の長さをバイト数として得ることができます。

クエリ:

db.people.aggregate(
[{
  $project: {"Desk": 1, "length": { $strLenBytes: "$Desk" }}}
])

結果:

{ "_id" : 1, "Desk" : "Düsseldorf-BVV-021", "length" : 20 }
{ "_id" : 2, "Desk" : "Munich-HGG-32a", "length" : 15 }
{ "_id" : 3, "Desk" : "Cologne-ayu-892.50", "length" : 19 }
{ "_id" : 4, "Desk" : "Dortmund-Hop-78", "length" : 16 }

ここでも、「Düsseldorf-BVV-021」の文字列について、コードポイントとバイト数の間で長さの判断基準が違うことを確認してください。

$toLower

文字列をクリーンアップしたり、2 つの間で同等性を比較する場合、全てのキャラクターを大文字か小文字に統一しておくことは有用です。次に示すクエリでは、$toLower 演算子を使い Desk フィールドの文字列を、すべて小文字に変換しています。

クエリ:

db.people.aggregate([
 {$project:{item: { $toLower: "$Desk" }}}
])

結果:

{ "_id" : 1, "item" : " düsseldorf-bvv-021" }
{ "_id" : 2, "item" : " munich-hgg-32a" }
{ "_id" : 3, "item" : " cologne-ayu-892.50" }
{ "_id" : 4, "item" : " dortmund-hop-78" }

$toUpper

$toLower と似たように、$toUpper は文字列内の全キャラクターを大文字に変換します。

クエリ:

db.people.aggregate([
 {$project:{item: { $toUpper: "$Desk" }}}
])

結果:

{ "_id" : 1, "item" : " DüSSELDORF-BVV-021" }
{ "_id" : 2, "item" : " MUNICH-HGG-32A" }
{ "_id" : 3, "item" : " COLOGNE-AYU-892.50" }
{ "_id" : 4, "item" : " DORTMUND-HOP-78" }

$split

文字列を使い特定のインデックスを見つけ出すことは、処理目的の助けになりますが、同時に、デリミッターを使い文字列を配列の形に分解することも有用な手段となります。それぞれのデリミッターを検出するため、複雑なクエリを記述することも可能ですが、$split という 1 つのコマンドにより、このタスクを行うことができます。次に示すクエリでは、勤務地についての各コンポーネントを、配列の形で分解しています。こうすると、文字列内のデータを処理しやすくなります。

クエリ:

db.people.aggregate([
 {$project:{parts: { $split: ["$Desk","-"]}}}
])

結果:

{ "_id" : 1, "parts" : [ " Düsseldorf", "BVV", "021" ] }
{ "_id" : 2, "parts" : [ " Munich", "HGG", "32a" ] }
{ "_id" : 3, "parts" : [ " Cologne", "ayu", "892.50" ] }
{ "_id" : 4, "parts" : [ " Dortmund", "Hop", "78" ] }

$split は、アプリケーション側が従業員情報を表示する目的で使うのに適した配列を、作成および出力します。

2.日付集約演算子

日付集約演算子は、与えられた日付けやタイムスタンプにある個別の部分を取り出す手段を与えます。この演算子は、アプリケーションが日付を処理する際に必要なデータだけを、日付情報から取り出すのに有用です。たとえば、誕生月や、注文が入った曜日だけを取り出したい、ということがあり得るでしょう。あるいは、アプリケーションの仕様に従って、日付情報の一部を表示したいということも考えられます。今回、日付時間演算子として、$dayOfYear$dayOfMonth$dayOfWeek$year$month$hour$minute$second$millisecond の 9 個が追加されました。

ここでは、気象観測所から届く測定値を保存する場合を考えて見ましょう。一つの測定値には、次の入力データが示すような要素が挿入されています。

入力:

db.weather.insert({
  "temperature" : 97.5,
  "humidity": 0.60,
  "date" : new Date() 
})

今回追加された日付集約演算子を使用して、タイムスタンプ情報のコンポーネントを、いくつかの違った形で取り出すことができます。

クエリ:

db.weather.aggregate([
{$project:{
   year: { $year: "$date" },
   month: { $month: "$date" },
   day: { $dayOfMonth: "$date" },
   hour: { $hour: "$date" },
   minutes: { $minute: "$date" },
   seconds: { $second: "$date" },
   milliseconds: { $millisecond: "$date" },
   dayOfYear: { $dayOfYear: "$date" },
   dayOfWeek: { $dayOfWeek: "$date" }}}
]).pretty()

結果:

{
	"_id" : ObjectId("5c85a290f9d5147c8653f952"),
	"year" : 2019,
	"month" : 1,
	"day" : 9,
	"hour" : 9,
	"minutes" : 45,
	"seconds" : 22,
	"milliseconds" : 981,
	"dayOfYear" : 9,
	"dayOfWeek" : 4
}

この例から分かるとおり、年、月、日、時、分、秒、ミリ秒、年の内の日数、週の内の日数、そして週の数をタイムスタンプから取り出すことができます。これにより、データを利用する上でかなりな柔軟性が与えられます。

3.サンプリング

引き続き、気象情報の例を使いながら、各データポイントを解析することなく、データをサンプリングしたい場合について考えて見ましょう。これは、測定値やテレメトリーを扱う時に、特に有用な方法です。Amazon DocumentDB 内のデータをサンプリングするには、$sample 集約パイプラインステージを使用します。

入力:

db.temp.insertMany([
{ "_id": 1, "temperature" : 97.5, "humidity": 0.61, "timestamp" : new Date() },
{ "_id": 2, "temperature" : 97.2, "humidity": 0.60, "timestamp" : new Date() },
{ "_id": 3, "temperature" : 97.4, "humidity": 0.61, "timestamp" : new Date() },
{ "_id": 4, "temperature" : 97.9, "humidity": 0.61, "timestamp" : new Date() },
{ "_id": 5, "temperature" : 97.6, "humidity": 0.61, "timestamp" : new Date() },
{ "_id": 6, "temperature" : 97.5, "humidity": 0.62, "timestamp" : new Date() },
{ "_id": 7, "temperature" : 97.2, "humidity": 0.62, "timestamp" : new Date() },
{ "_id": 8, "temperature" : 97.1, "humidity": 0.63, "timestamp" : new Date() },
{ "_id": 9, "temperature" : 96.9, "humidity": 0.62, "timestamp" : new Date() },
{ "_id": 10, "temperature" : 97.4, "humidity": 0.63, "timestamp" : new Date()}
])

次に示すクエリは、収集された気温に関するデータから、ランダムに 2 つのドキュメントを選び出します。ここでランダムに返してほしいドキュメントの数は、「size」を増加もしくは減少し変更することができます。

クエリ:

db.temp.aggregate(
   [ { $sample: { size: 2 } } ]
)

結果:

{ "_id" : 4, "temperature" : 97.9, "humidity" : 0.61, "timestamp" : ISODate("2019-03-21T21:17:22.425Z") }
{ "_id" : 9, "temperature" : 96.9, "humidity" : 0.62, "timestamp" : ISODate("2019-03-21T21:17:22.425Z") }

この結果では、10 個のうち 2 個のドキュメントが、ランダムにサンプリングされているのが分かります。こうして得たドキュメントを使い、平均値の算出や、最大あるいは最小値の計算が行えます。

まとめ

弊社は、Amazon DocumentDB に関して、広範囲のアプリケーションを構築、管理できる体制を追加整備しながら、お客様の後方支援となる作業を続けます。Amazon DocumentDB の詳細については、Amazon DocumentDB のホームページおよび入門ガイドを参照してください。

今回のブログ、もしくは新たに追加された集約演算子について何か疑問点やコメントをお持ちの場合は、このページのコメント覧をご利用いただければ幸いです。Amazon DocumentDB に関する疑問については、「FAQ」か「developer forum」をご参照ください。


著者について

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