MongoDBの集計をやってみた話
-
2024年2月13日
はじめまして、ココネでサーバサイドエンジニアをやっているYOです。
ココネのサービスでは、データベースは主にNoSQLのMongoDBを使っています。
私は今までの職歴の中ではRDBを扱うことが多かったため、最近ココネに入社するまでは、MongoDBをほとんどさわったことがありませんでした。つい先日、MongoDBのデータを集計したいなと思うことがありました。プログラムを書くほどではなく、さくっと集計するにはどうしたらよいだろうといろいろ調べて試してみたので、今回はそのときのことを題材にお話ししたいと思います。
具体的には、MongoDBの1つのドキュメントの中に複数言語に翻訳した文字列が登録されているデータがあって、各言語の文字数の平均を求めたいというものです。
データの作成例を記載いたします。
また、Mongoのバージョンは6として話を進めます。
db.getCollection("SAMPLE").insertMany([
{
"_id" : "000",
"memo" : {
"en" : "hello world",
"id" : "Halo Dunia",
"vi" : "Chào thế giới"
}
},
{
"_id" : "001",
"memo" : {
"en" : "Hello",
"id" : "Halo",
"vi" : "Chào"
}
}]);
今回のポイントは、文字数をどうやってカウントするのか、平均値はどうやって求めるのかという点になるかと思います。RDBであればSQLが使えます。Oracleにしろ、MySQLにしろ文字列の数はLENGTH関数、平均値はAVG関数のような機能があるので簡単に集計することができます。
NoSQLのMongoDBはどうなんだろう、同じような機能があればいいなと思いつつ調べてみたところ、集計のやり方自体にいくつか方法がありましたので、下記の2つについて具体例を見ていきたいと思います。
- aggregateを使った方法
- map/reduce
aggregateを使った方法
まずaggregateを使った集計処理では、集計したいプロパティを$groupで指定して、計算を行うという流れでした。
$matchを使えば、対象にするデータも絞れます。
集計に便利な機能もいろいろ用意されていて、その中から文字数は$strLenCP、平均は$avgを使えば簡単に結果が取得できます。
今回やりたかった集計は、下記のようなクエリで実現可能です。
▼ クエリ
db.getCollection("SAMPLE").aggregate([
{
$group: {
_id: "average",
"en":{$avg: {$strLenCP: "$memo.en"}},
"id":{$avg: {$strLenCP: "$memo.id"}},
"vi":{$avg: {$strLenCP: "$memo.vi"}}
}
}
]);
▼ 結果
{
"_id" : "average",
"en" : 8.0,
"id" : 7.0,
"vi" : 8.5
}
map/reduce
次にmap/reduceを使ったやり方についてです。
map関数はキーと値を生成し(emit)、reduce関数は、map関数で作ったキーと値を参照します。
よって、map関数で言語毎の文字列長のデータを作成し、reduce関数で平均を求めるという流れで集計可能です。細かい説明は省きますが、下記のような感じで集計できます。
▼ クエリ
db.getCollection("SAMPLE").mapReduce(map, reduce, {
out: { inline:1 }
});
map = function() {
const key = "average"
emit(key, {
"en" : this.memo["en"].length,
"id" : this.memo["id"].length,
"vi" : this.memo["vi"].length
});
}
reduce = function(key, values) {
const lang_list = ["en", "id", "vi"];
var count = [0, 0 ,0];
values.forEach(function(v) {
for (let i = 0; i < lang_list.length; i++) {
count[i] += v[lang_list[i]];
}
});
return {
"en": count[0] / values.length,
"id": count[1] / values.length,
"vi": count[2] / values.length
};
}
▼ 結果(抜粋)
"results" : [
{
"_id" : "average",
"value" : {
"en" : 8.0,
"id" : 7.0,
"vi" : 8.5
}
}
]
余談ですが、map/reduceといえば、むかしアクセスログの集計にHadoopを使っていたことがあるのですが、そのときもmap/reduceを使って集計をやっていました。Hadoopとは大量データを解析するためのフレームワークのことです。
それまでは、アクセスログの集計といえばgrepコマンドを使ってやっており、ログが大量になるとコマンドを打ってから結果が返ってくるまで何時間もかかっていました。そんな状況を解消するために、Hadoopを導入したという経緯です。ただ、使いこなすのはそれなりに大変で、あまり浸透しなかったと記憶しています。
そんな中、GCPのBigQueryやAmazon Athenaが登場して、状況ががらりと変わりました。
まだクラウドを使い始めた頃で、最初はセキュリティ面の対応や構築に時間がかかりましたが、いざ使ってみると、今まで何時間もかかっていた集計処理が数分あれば結果が返ってきます。使った分だけ料金がかかるという点には注意が必要ですが、手軽さと処理速度がとても衝撃的でした。SQLが使えるのも良い点で、エンジニア以外の方も含め多くの人にとってハードルが低いのではないかと思います。
まとめ
aggregateを使えば、便利な関数が用意されているので比較的かんたんに集計ができると思います。
たいていの場合であればaggregateで事足りそうなので、map/reduceを使うことはあまりないとは思いますが、複雑な集計が必要な場合はmap/reduceの出番かもしれません。