今回は MongoDB にある集計関数の1つ Map-Reduce の使い方についてまとめます。
MongoDB で集計を行おうとすると Aggregation Pipeline を利用しますが、それだけではできないような複雑なクエリ実行するときに Map-Reduce を利用します。
今回はその Map-Reduce の簡単な使い方を見ていきます。
Map-Reduce の 動作概要
Map-Reduce を使うと MongoDB のデータに対して複雑な統計処理を行ってその結果を取得することができます。
Map-Reduce の動作は大きく2段階になっています。
最初の処理は map で コレクション をグルーピングする処理になります。
次の処理は reduce で map でグルーピングしたコレクションに対して集計を行います。
以下の図は Map-Reduce の動作例です。

上図では、コレクションに対して map 処理 で type をキーとする ドキュメント の グルーピング を行い、 reduce 処理 で グルーピングされた ドキュメント に対して 合計値 を演算する 集計処理 を行っています。
この後のサンプルコードでは上図の処理を実際に実装してみます。
Map-Reduce を使ってみる (サンプルコード)
前提データ
コマンドからでもデータの投入はできますが、以下は JavaScript コードを利用して投入するようにしています。
投入しているデータは上で図示していたサンプルコレクションと同じです。
test データベース の items コレクション に投入しています。
insert.js
var CONNECTION_URL = "mongodb://localhost:27017/test";
var moment = require("moment");
var MongoClient = require("mongodb").MongoClient;
MongoClient.connect(CONNECTION_URL).then((db) => {
db.collection("items").insertMany([
{ type: "A", value: 20 },
{ type: "B", value: 30 },
{ type: "A", value: 10 },
{ type: "C", value: 40 },
{ type: "D", value: 20 },
{ type: "B", value: 10 },
{ type: "C", value: 20 },
], (result) => {
db.close();
});
});
コマンド実行
> node .\insert.js
Map-Reduce してみる
では、データ投入できたところで実際に Map-Reduce で集計を行ってみます。
index.js
var CONNECTION_URL = "mongodb://localhost:27017/test";
var MongoClient = require("mongodb").MongoClient;
MongoClient.connect(CONNECTION_URL).then((db) => {
// Map function
var map = function () {
emit(this.type, this.value);
};
// Reduce function.
var reduce = function (key, values) {
var sum = 0;
for (let value of values) {
sum += value;
}
return sum;
};
// Map-Reduce options.
var options = {
out: { inline: 1 }
};
// Execute Map-Reduce process.
db.collection("items").mapReduce(map, reduce, options).then((docs) => {
console.log(JSON.stringify(docs));
});
});
collection.mapReduce() 関数は map 関数 , reduce 関数 , options オブジェクト の 3引数 をとります。
第4引数にコールバック関数を指定することができますが、指定しなければ戻り値の Promise を利用して結果を取得することができます。
上記のサンプルコードでは Promise を利用した方法で記載しています。
map 関数 中では必ず emit(key, value) を呼びます。
emit() は 2引数 をとります。
第1引数 はグルーピングするためのキーを指定します(オブジェクトも可)。
第2引数には reduce() の 第2引数 の配列要素として引き渡す値(オブジェクトも可)を指定します。
また、 map 関数 内における this は 探索中のドキュメント自身 を指すので、this.type などとすれば 値 を取得することができます。
reduce 関数 は key と values の 2引数を取ります。
第1引数の key は map 関数 でグルーピングした際に利用した キー が引き渡されます。
第2引数の values は 配列 で、map 関数 内の emit() の第2引数に渡された値またはオブジェクトが配列として渡されます。
reduce 関数 では 第2引数 で渡された配列のデータを集計した結果を戻り値として return します。
options オブジェクト にはいくつか指定ができますが…詳細は後の節に回します。
最低限 out オプションだけは指定しておかないとエラーになります。
out オプションは Map-Reduce の出力先を指定できるのですが、普通に使う(コード内で利用する)には { out: {inline: 1} } でよいかと思います。
では、実際に上記コード index.js を実行してみましょう。
コマンド実行
> node .\index.js
実行結果
[{"_id":"A","value":30},{"_id":"B","value":40},{"_id":"C","value":60},{"_id":"D","value":20}]
Map-Reduce するとグルーピングキーは _id へ入り、集計結果(reduce 関数 の戻り値)は value というプロパティの値に入ります。
reduce 関数 の戻り値にオブジェクトを指定していた場合、 value もオブジェクトになります。
Map-Reduce にもう少し踏み込んでみる
以下に挙げる内容はいずれも options オブジェクト のプロパティとして指定することで実現できます。
個別に詳細を見ていきましょう。
対象を絞り込む
{ query: <検索条件> }
query オプションを指定すると、Map-Reduce の対象とするコレクションをあらかじめ絞り込むことができます。
結果に対する絞り込みではない点には注意です。
検索条件には find で指定するときと同様のクエリを指定できます。
検索クエリで利用可能なオペレーターは mongodb - Query and Projection Operators に記載があります。
サンプル
db.collection.mapReduce(map, reduce, {
out: { inline: 1 },
query: { value: { $gte: 20 } }
});
対象をソートする
{ sort: <ソート条件> }
sort オプションを指定すると Map-Reduce の対象とするコレクションに対してあらかじめソートをしておくことができます。
結果に対するソートではない点が残念ですが…。。
db.collection.mapReduce(map, reduce, {
out: { inline: 1 },
sort: { value: 1 }
});
結果をソートする
上記の通り結果をソートするオプションは存在しないので、結果として取得される配列を単純に sort するくらいしかありません。
aggregate() には $sort があるので、aggregate で実現できないか検討しなおしてみるのも方法の1つかとは思います。
db.collection("items").mapReduce(map, reduce, options).then((docs) => {
docs.sort((a, b) => {
return a.value > b.value ? 1 : -1;
})
console.log(JSON.stringify(docs));
});
最後に処理する
{ finalize: <処理> }
各グループに対して reduce 処理 が終わった後に実施したい処理があれば finalize オプションに関数を指定して実行することができます。
…ただ、この finalize オプションで指定できる関数は指定した関数の外に対して影響するような操作ができません。
例えば、コンソールにログ出したり、スコープ外のオブジェクトを操作したりなどはできません。
しかもグループごとに呼び出されるので、reduce() の return 直前に処理を入れるのと同じです。
ちなみにすべての reduce 処理 が終わった後に何か処理したいのであれば、それはそもそも mapReduce のコールバックまたは Promise で続く処理の先頭で処理すればよいはずです。
以下にサンプルコードを記載しますが…正直このオプションは使えないオプションかと。。
db.collection.mapReduce(map, reduce, {
out: { inline: 1 },
finalize: function (key, reduced) {
return {
key: key,
val: reduced
};
}
});
今回は MongoDB の Map-Reduce についてまとめました。
ポイントは以下の通りです。
参考になったでしょうか?
本記事がお役に立っていると嬉しいです!!