MongoDB : サルでもわかるMapReduce

まずは「集計ができる」と覚えておきましょう。
最初はこれだけで十分でしょう。

利用事例

ある日、ピカチューがつぶやきサービスでつぶやき始めました。
ピカチューのつぶやきに対して、仲間からたくさんの「いいね!」ボタンが押されました。

あなたはつぶやきサービス管理人として、ピカチューのつぶやき数と、ピカチューがゲットした「いいね!」のトータル数を集計してみましょう。

準備

Mongoクライアントを起動して、DBを作成しましょう。

./mongodb/bin/mongo
MongoDB shell version: 2.0.1
connecting to: test
> use sample;
switched to db sample
サンプルデータ
ピカチューが3回つぶやいて、「いいね!」がたくさんつきました。
> db.entries.save( {username:'pikachu', text:'Pikaaa!' ,     iine:10 } );
> db.entries.save( {username:'pikachu', text:'PikPika!' ,    iine:20 } );
> db.entries.save( {username:'pikachu', text:'PikPikchuuu' , iine:30 } );

MongoDB用語でいうと、entriesコレクションにドキュメントを3つ保存したということになります。
(RDBMS用語でいうと、entriesテーブルにレコードを3件insertしたことに相当します。)

集計する

ユーザ名をキーにして、つぶやき件数といいねトータル数を集計してみましょう。
期待する結果

ユーザ"pikachu"の集計結果: { count : 3, iine : 60 }

Map関数とReduce関数という2つの関数を作ることによって、これを実現します。
Map関数を作成する

Map関数では、何を集計したいかを定義して、ドキュメントの中から集計に必要な項目を抜き出します。
上記で定義した「期待する結果」をよく見ながら、それに沿う形でemit呼び出しを行います。
var m  = function() {
  emit(this.username, {count:1, iine: this.iine});
};
ここでは集計そのものは行いません。

Map関数の中で、必ずemit関数を呼び出すという決まりになっています。
Map関数内での"this"は、1個のドキュメントを表します。この例でいうとつぶやき1件分です。

MapReduce処理が実行されると、emit関数は下記のような形で3回呼び出されることになります。

emit( 'pikachu', { count:1 , iine:10} );
emit( 'pikachu', { count:1 , iine:20} );
emit( 'pikachu', { count:1 , iine:30} );
Reduce関数を作成する

Reduce関数は集計ロジックを担当します。

var r = function (key, values) {

  var result = {count:0, iine:0};  //集計結果を初期化

  values.forEach(function(value){
    result.count += value.count;
    result.iine  += value.iine;
  });

  return result;
}

function(key, values)の部分に注目してください。
keyは、emit関数を呼び出したときの第1引数がきます。つまり'pikachu'という文字列が来ます。

valuesは、emit関数呼び出したときの第2引数がリスト化されたものが来ます。
values = [ {count:1 , iine:10}, {count:1 , iine:10}, {count:1 , iine:10} ];

このkeyとvaluesを使って集計作業を行い、集計結果をオブジェクトにしてreturnしてやるのです。

集計結果オブジェクト(result)の形をよく見ると、Map関数で定義したオブジェクトの形とよく似ています。
これは偶然ではありません。
MapReduceを実行する
実行します。

({ out: {inline:1}}というのは、処理結果を保存せずに画面表示させたいときのおまじないです。)
> db.entries.mapReduce(m,r, { out: {inline:1}});
{
        "results" : [
                {
                        "_id" : "pikachu",
                        "value" : {
                                "count" : 3,
                                "iine" : 60
                        }
                }
        ],
        "timeMillis" : 1,
        "counts" : {
                "input" : 3,
                "emit" : 3,
                "reduce" : 1,
                "output" : 1
        },
        "ok" : 1,
}
お見事!
期待する結果が返ってきましたね。

他のユーザを追加
ピカチューの人気をみて、ミュウツーもつぶやき始めました。
> db.entries.save( {username:'myutu', text:'myu myu' ,     iine:1 } );
> db.entries.save( {username:'myutu', text:'myu myu myu' , iine:2 } );

再度mapReduceを実行します。
> db.entries.mapReduce(m,r, { out: {inline:1}});
{
        "results" : [
                {
                        "_id" : "myutu",
                        "value" : {
                                "count" : 2,
                                "iine" : 3
                        }
                },
                {
                        "_id" : "pikachu",
                        "value" : {
                                "count" : 3,
                                "iine" : 60
                        }
                }
        ],
        "timeMillis" : 0,
        "counts" : {
                "input" : 5,
                "emit" : 5,
                "reduce" : 2,
                "output" : 2
        },
        "ok" : 1,
}
キタ!
ユーザごとに、つぶやき数といいね数が集計されました。


これを応用すれば、他にもいろいろな集計を行うことができます。

まとめ

MongoDBのMapReduceを使って、かんたんな集計をする方法を紹介しました。
参考
カテゴリ: