読者です 読者をやめる 読者になる 読者になる

AKB48 の Google+ アクティビティデータを MongoDB で MapReduce してみた

MongoDB Ruby

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

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

メンバーの取得とセット

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

$ /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" と指定します。

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

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

{
  "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 用関数
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 用関数
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": <出力コレクション名> }) で実行します。

> db.activities.mapReduce(m, r, { out: { replace: "result" } })
{
        "result" : "result",
        "timeMillis" : 75,
        "counts" : {
                "input" : 889,
                "emit" : 889,
                "reduce" : 62,
                "output" : 76
        },
        "ok" : 1,
}

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

+1の数の降順
> 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
コメント数の降順
> 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
アクティビティ数の降順
> 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 も非常に手軽に試せますね。