MapReduce について実際やってみたことがなかったので、MongoDB で試しそうと思っていました。
そんななか、AKB48 の(18歳以上?の)メンバーが Google+ を開始しました。これで「バルス」以上に定時でかつてない負荷が Google+ にかかり始めたと思われます。ということで、扱うにはもってこいなデータなのでこれを使うことにしました。

Google+ API は今のところデータのGETしかできないようですが、それで充分です。
とりあえずメンバーの Googel+ のID(?)と名前をMongoDB のコレクションに突っ込みます。
それを元に定期的に各メンバーのアクティビティ(活動のエントリ)を取得しては、
JSON をそのままほぼそのまま別のコレクションに突っ込みました。

メンバーの取得とセット

AKB48 Now on Google+ のHTMLを適当にパースして突っ込みました。
MongoDB シェルで見ると以下のように入ってます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ /proj/arble/mongodb/bin/mongo
MongoDB shell version: 2.0.0
connecting to: test
\> db.idols.find({}, { "_id":0, "id":1, "name":1 })
{ "id" : "108406705498777962659", "name" : "板野友美" }
{ "id" : "112077362806147944184", "name" : "梅田彩佳" }
{ "id" : "105229500895781124316", "name" : "大島優子" }
{ "id" : "108367535733172853340", "name" : "大家志津香" }
{ "id" : "116324240483798147615", "name" : "大矢真那" }
{ "id" : "107135851528812577523", "name" : "小木曽汐莉" }
{ "id" : "111145641865855965824", "name" : "小野晴香" }
{ "id" : "110230842586402039931", "name" : "河西智美" }
{ "id" : "109547251260290757268", "name" : "柏木由紀" }
{ "id" : "108485060451296256117", "name" : "片山陽加" }
has more

Google+ からデータの取り込み

Activities: list - Google+ Platform — Google Developers を使うと、ユーザのアクティビティがリストで取得できます。userId に上記の idを、collection に “public” と指定します。

Ruby で mongodb/mongo-ruby-driver · GitHub を使ってさくっとMongoDBにデータを取得して入れます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'open-uri'
require 'rubygems'
require 'json'
require 'mongo'
conn = Mongo::Connection.new
db = conn.db('plusdb')
act_coll = db.collection("activities")
db.collection("idols").find.each do |idol|
id = idol\["id"\]
url = "https://www.googleapis.com/plus/v1/people/#{id}/activities/public?key=#{API_KEY}"
data = nil
open(url) do |f|
data = JSON.parse(f.read)
data\["items"\].each do |item|
act_coll.insert(item)
end
end
end

JSONのルートから items という配列フィールドの中に各アクティビティが入っているのでそれを1ドキュメント(レコード)として MongoDB に入れていきます。

アクティビティのJSON

Activities - Google+ Platform — Google Developers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"kind":"plus#activity",
"title":"6周年イベント終わりましたんこぶ`・ω・´",
"published":"2011-12-08T14:40:08.000Z",
"updated":"2011-12-08T14:40:08.293Z",
"id":"z12nzhsrlzryw5vyq04cgvfxumqly1w4uu40k",
"url":"https://plus.google.com/101026469701528255144/posts/cwXC5bk8b98",
・・・
"object": {
"objectType":"note",
"content":"6周年イベント終わりましたんこぶ`・ω・´",
"originalContent":"",
"url":"https://plus.google.com/101026469701528255144/posts/cwXC5bk8b98",
"replies": {
"totalItems":41,
"selfLink":"https://www.googleapis.com/plus/v1/activities/z12nzhsrlzr・・・"
},
"plusoners": {
"totalItems":223,
"selfLink":"https://www.googleapis.com/plus/v1/activities/z12nzhsrlzr・・・"
},
"resharers": {
"totalItems":1,
"selfLink":"https://www.googleapis.com/plus/v1/activities/z12nzhsrlzr・・・"
}
},
"actor":{
"displayName":"石田晴香",
"url":"https://plus.google.com/101026469701528255144",
"image":{"url":"https://lh3.googleusercontent.com/-・・・"
}
}

上記の抜粋で、実際集計に使う情報は、actor.displayName の名前と replies.totalItems のコメント回数とplusoners.totalItemsの +1 回数です。

MapReduce を行う。

準備が整ったので、MapReduceを試します。各メンバーの総 +1 数と総コメント数を集計してみます。

MongoDB シェルで MapReduce のそれぞれの関数をあらかじめ定義します。

Map 用関数
1
2
3
4
5
6
7
8
m = function() {
var o = this.object;
emit(this.actor.displayName, {
activity: 1,
reply: o.replies.totalItems,
plus : o.plusoners.totalItems
});
}

Map 関数の this は対象コレクションの各ドキュメント(レコード)になります。emitは Reduceの対象となるデータレコードを追加するもので Map 関数内で1回以上呼び出します。
emitには集計のマップとなるキーと値、emit(key, value) でコールするため、valueにハッシュマップにして複数フィールド値を入れます。

Reduce 用関数
1
2
3
4
5
6
7
8
9
10
11
r = function(key, values) {
var result = {
activity: 0, plus: 0, reply: 0
};
values.forEach(function(v) {
result.activity += v.activity;
result.plus += v.plus;
result.reply += v.reply;
});
return result;
}

Map で emit したエントリを集計します。各フィールドを足し合わせます。

MapReduce を実行

MongoDB シェルで mapReduce メソッドをコレクションに対して呼び出します。mapReduce(<Map関数>, <Reduce関数>, { out: { “replace”: <出力コレクション名> }) で実行します。

1
2
3
4
5
6
7
8
9
10
11
12
> db.activities.mapReduce(m, r, { out: { replace: "result" } })
{
"result" : "result",
"timeMillis" : 75,
"counts" : {
"input" : 889,
"emit" : 889,
"reduce" : 62,
"output" : 76
},
"ok" : 1,
}

続いて結果をそれぞれソートして確認してみる。

+1の数の降順
1
2
3
4
5
6
7
8
9
10
11
12
> db.result.find().sort({"value.plus":-1})
{ "_id" : "前田敦子", "value" : { "activity" : 58, "plus" : 52009, "reply" : 13944 } }
{ "_id" : "小嶋陽菜", "value" : { "activity" : 51, "plus" : 31438, "reply" : 9108 } }
{ "_id" : "高橋みなみ", "value" : { "activity" : 37, "plus" : 25088, "reply" : 10250 } }
{ "_id" : "篠田麻里子", "value" : { "activity" : 36, "plus" : 22010, "reply" : 7644 } }
{ "_id" : "指原莉乃", "value" : { "activity" : 26, "plus" : 16743, "reply" : 5171 } }
{ "_id" : "大島優子", "value" : { "activity" : 14, "plus" : 12693, "reply" : 4270 } }
{ "_id" : "柏木由紀", "value" : { "activity" : 11, "plus" : 12018, "reply" : 3575 } }
{ "_id" : "宮澤佐江", "value" : { "activity" : 20, "plus" : 10839, "reply" : 4824 } }
{ "_id" : "高城亜樹", "value" : { "activity" : 19, "plus" : 10539, "reply" : 3209 } }
{ "_id" : "峯岸みなみ", "value" : { "activity" : 12, "plus" : 10196, "reply" : 930 } }
has more
コメント数の降順
1
2
3
4
5
6
7
8
9
10
11
12
> db.result.find().sort({"value.reply":-1})
{ "_id" : "前田敦子", "value" : { "activity" : 58, "plus" : 52009, "reply" : 13944 } }
{ "_id" : "高橋みなみ", "value" : { "activity" : 37, "plus" : 25088, "reply" : 10250 } }
{ "_id" : "小嶋陽菜", "value" : { "activity" : 51, "plus" : 31438, "reply" : 9108 } }
{ "_id" : "篠田麻里子", "value" : { "activity" : 36, "plus" : 22010, "reply" : 7644 } }
{ "_id" : "指原莉乃", "value" : { "activity" : 26, "plus" : 16743, "reply" : 5171 } }
{ "_id" : "板野友美", "value" : { "activity" : 16, "plus" : 8488, "reply" : 4857 } }
{ "_id" : "宮澤佐江", "value" : { "activity" : 20, "plus" : 10839, "reply" : 4824 } }
{ "_id" : "大島優子", "value" : { "activity" : 14, "plus" : 12693, "reply" : 4270 } }
{ "_id" : "渡辺美優紀", "value" : { "activity" : 14, "plus" : 6195, "reply" : 3796 } }
{ "_id" : "柏木由紀", "value" : { "activity" : 11, "plus" : 12018, "reply" : 3575 } }
has more
アクティビティ数の降順
1
2
3
4
5
6
7
8
9
10
11
12
> db.result.find().sort({"value.activity":-1})
{ "_id" : "前田敦子", "value" : { "activity" : 58, "plus" : 52009, "reply" : 13944 } }
{ "_id" : "小嶋陽菜", "value" : { "activity" : 51, "plus" : 31438, "reply" : 9108 } }
{ "_id" : "高橋みなみ", "value" : { "activity" : 37, "plus" : 25088, "reply" : 10250 } }
{ "_id" : "篠田麻里子", "value" : { "activity" : 36, "plus" : 22010, "reply" : 7644 } }
{ "_id" : "鈴木まりや", "value" : { "activity" : 30, "plus" : 4010, "reply" : 1851 } }
{ "_id" : "仁藤萌乃", "value" : { "activity" : 29, "plus" : 5865, "reply" : 1868 } }
{ "_id" : "米沢瑠美", "value" : { "activity" : 29, "plus" : 3277, "reply" : 1041 } }
{ "_id" : "指原莉乃", "value" : { "activity" : 26, "plus" : 16743, "reply" : 5171 } }
{ "_id" : "松井咲子", "value" : { "activity" : 26, "plus" : 8138, "reply" : 2911 } }
{ "_id" : "佐藤夏希", "value" : { "activity" : 25, "plus" : 4013, "reply" : 2651 } }
has more
+1 数のランキング チャート

f:id:tilfin:20111211172319p:image
Visualization: Bar Chart - Google Charts — Google Developers
順当に前田敦子さんがトップですね。コメント数はそもそも500という上限があるみたいなので、アクティビティ数に比例してますね。

MongoDB はシェルから JavaScript ライクに扱えるので、 MapReduce も非常に手軽に試せますね。