Rubyで理解するMapReduce

MapReduceの勉強と練習をかねてRubyでそれらしいことを書いてみます。
間違ったことをしてるかもしれないので、詳しい人がツッコミを入れてくれると嬉しいです。

ruby 1.8.7で動作を確認しています。

テーマ

Apacheのログっぽいデータを分析して、それぞれのファイルへのアクセス数を算出します。

入力データはこんな感じ。

# Apacheのログっぽいデータの集合
input_data = [
  '[04/01 00:00:00] "GET index.html HTTP/1.1" 200',
  '[04/01 00:00:00] "GET index.html HTTP/1.1" 200',
  '[04/01 00:00:00] "GET reduce.html HTTP/1.1" 200',
  '[04/01 00:00:00] "GET reduce.html HTTP/1.1" 200',
  '[04/01 00:00:00] "GET index.html HTTP/1.1" 200',
  '[04/01 00:00:00] "GET map.html HTTP/1.1" 200',
]

not MapReduce

まずは何も考えず普通に書いてみます。

r = {}
input_data.each do |d|
  f = d[/GET (.+?) /, 1]
  r[f] ||= 0
  r[f] += 1
end
p r
# => {"map.html"=>1, "index.html"=>3, "reduce.html"=>2}

MapReduce

次にMapReduceで書いてみます。

# MapReduce!!
p input_data.map{|d| d[/GET (.+?) /, 1] }.group_by{|d| d}.reduce({}){|r,kv| r.update({kv[0] => kv[1].size}) }
# => {"map.html"=>1, "index.html"=>3, "reduce.html"=>2}

分解すると、次のことをやっています。

# Mapフェーズ
# ログデータからファイル名を抜き出す
m = input_data.map{|d| d[/GET (.+?) /, 1] }
p m
# => ["index.html", "index.html", "reduce.html", "reduce.html", "index.html", "map.html"]

# Shuffleフェーズ
# 同じ名前のファイルをまとめる
g = m.group_by{|d| d}
p g
# => {"map.html"=>["map.html"], "index.html"=>["index.html", "index.html", "index.html"], "reduce.html"=>["reduce.html", "reduce.html"]}

# Reduceフェーズ
# ファイルの個数をカウントする
g.reduce({}){|r,kv| r.update({kv[0] => kv[1].size}) }
p g
# => {"map.html"=>1, "index.html"=>3, "reduce.html"=>2}

まとめ

not MapReduceの方が効率が良さそうですが、MapReduceには以下の利点があります。
MapはMapで、ReduceはReduceでそれぞれ入力が独立してるので、並列化することができます。これは大規模なデータを解析する場合に生きてきます。
MapとReduceそれぞれがシンプルに書けることも、MapReduceのメリットだと思います。