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 数のランキング チャート Visualization: Bar Chart - Google Charts — Google Developers 順当に前田敦子 さんがトップですね。コメント数はそもそも500という上限があるみたいなので、アクティビティ数に比例してますね。
MongoDB はシェルから JavaScript ライクに扱えるので、 MapReduce も非常に手軽に試せますね。
tilfin
freelance software engineer