2011
05/24
Redis 使ってみました。
会社に転がってた、↓ の本をつらつらと眺めていて
今まで、よくわかっていなかった NoSQL という物に興味が湧いてきました。
上記の本は、そろそろ出揃った感のある NoSQL 系のデータストアエンジンを一通り並列に紹介してくれる本で、見て、軽くわかった気になるには最適です。
ただ、結局のところ、いつも使っている SQL と何が違うのかまでは読んだだけでは充分に理解できないよね、やっぱり試してみなくちゃね、という事で、使ってみました。
NoSQL と言えども、データストア!データを保存しなくては。入れるデータの元ネタに困りましたが、困った時は他人のふんどしを借りようと Twitter から適宜引っ張ってくる事にしました。キャズムを越えたらしいという噂の Twitter から URL を含んだツイートを引っ張りだして集計したら、世間で話題になっているネタが拾えそうかなと。誰かが勝手にデータをたれ流してくれるのは収集に持ってこいですし。この辺を思い付きだけでやってしまったのが後で祟る事になるのですが、そこは後述。
保存用のスクリプトは Ruby で書く事にしたので、Redis へのアクセスに redis の gem 、また、Twitter からデータを取ってくるのにTwitter の gem を使います。
Twitter はクライアントなどが自動的に URL を短縮した状態で投稿する事が多いので、それを元 URL に戻す処理も入れる事にしました。
#!/usr/bin/ruby
require "rubygems"
require "redis"
require 'twitter'
require 'uri'
require 'net/http'
$lang = "ja"
# 短縮 URL を元に戻すための処理
def extract_url(url)
begin
uri = URI.parse(url)
# 引数で与えられた URL に対して HEAD メソッドを実行
Net::HTTP.start(uri.host, uri.port) do |http|
response = http.head(uri.path, 'User-Agent' => "Redirect Tracker")
# 実URLだと判断する条件は、以下のいずれかに合致する事
# ・レスポンスコードが200
# ・レスポンスヘッダにLocationが含まれない
# ・レスポンスヘッダLocationが、元URLと同一
if (response.code == "200") or (response['Location'] == nil) or (url == response['Location']) then
return url
else
moved_to = response['Location'].match(/^https?:\/\//) ? response['Location'] : "#{uri.host}/#{response['Location']}"
# 二重に短縮されている場合などのために再帰処理
extract_url(moved_to)
end
end
rescue => e
puts " === ERROR ( #{url} ) ==== "
puts e
end
end
begin
# ローカルホストの Redis に接続
redis = Redis.new(:host => "localhost")
# 前回取得時の最終 Tweet ID を拾う
last_id = redis.get "meta:last_id:#{$lang}"
result = nil
# 前回取得した以降の、日本語のツイートの中から、http という文字列で検索
Twitter::Search.new.containing("http").since_id(last_id).lang($lang).per_page(100).filter.reverse_each do |result|
# URL らしき文字列の切り出し
result.text.scan(/https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:@&=+$,%#]+/).each do |url|
# 短縮URL を元に戻す
exturl = extract_url(url)
if (exturl != nil) then
uri = URI.parse(exturl)
else
next
end
# 集計系は incr メソッドでアトミックに処理
id = redis.incr "data:count:whole"
host_count = redis.incr "data:count:#{uri.host}"
url_count = redis.incr "data:count:#{uri.host}:#{uri.path}"
# URL情報をリスト型で記録
redis.lpush "list:whole", exturl
redis.lpush "list:#{$lang}", exturl
redis.lpush "list:hosts", uri.host
# 念のため、ツイート情報も取っておく / Marshal.dump でオブジェクトを serialize する
redis.set "data:tweet:#{id}", Marshal.dump(result)
end
end
rescue => e
puts e
ensure
# 最後に処理した Tweet ID を記録しておく
redis.set "meta:last_id:#{$lang}", result.id
end
上記スクリプトを、cron で定期実行して、情報を収集しておきます。
読み返してみたら、検索結果を Reverse_each した上でリストに lpush してるけど、素直に each & rpush したらよかったんじゃないだろうか…。
表示系は Sinatra で間に合わせ。
require 'rubygems'
require 'sinatra'
require 'redis'
require 'uri'
set :haml, {:format => :html5}
before do
@redis = Redis.new(:host => "localhost")
end
get %r{/([0-9]+|all)?} do
# 全データ件数を取得
@total_count = @redis.get "data:count:whole"
@datas = []
# リスト長を取得(アレ?上の全データ件数と同一では?)
length = @redis.llen "list:whole"
# データ表示件数を取得。デフォルトは 100 件で、指定があれば、その件数。all なら全件
limit = 100
if params[:captures] != nil then
if params[:captures].first == "all" then
limit = length
else
limit = params[:captures].first.to_i
end
end
# downto に渡す都合で加工。リスト長が指定件数に満たない場合はリスト長を限界にする
limit = length - (length < limit ? length - 1 : limit - 1)
# 表示用データを配列に突っ込む
length.downto(limit) do |i|
url = @redis.lindex("list:whole", length - i)
begin
uri = URI.parse(url)
count = @redis.get "data:count:#{uri.host}:#{uri.path}"
rescue
count = 0
end
@datas << { :url => url, :count => count }
end
haml :index
end
__END__
@@ layout
%html
%head
%title twitter url counter
%body
= yield
@@ index
%h1 twitter url counter
%a{:href => "/hosts"} host oriented
%div
There are #{@total_count} datas here.
%div
- @datas.each do |item|
%div
#{item[:count]} :
%a{:href => "#{item[:url]}"} #{item[:url]}
所定の件数を Redis から取得して表示するだけです。
しかし、KVS ほぼ初体験な自分には「ほぼハッシュだからそのつもりで使えよ」という概念がどうも理解されていなかったようです。こんなランキング系のコンテンツなのにソートの機能を持たない list 型に突っ込んでいるのが致命的。ダラダラ出すのには使えますが、それ以外の表示方法の可能性がほぼスポイルされてしまいました。
上記の本の中にあったベンチマークのデータでは、”スピードの向こう側”に突き抜けそうなブッチ切りの爆速を示していた、Redis の速度も、集計時に、URL のリダイレクトを辿るケースが頻発する今回のプログラムではネットワークの遅延が問題になってしまって、あまり恩恵受けれず…。結局何のための Redis なのか、というところが全く見えませんでした。使ってみた、というのが唯一の成果。ちゃんとアドバンテージとディスアドバンテージを見極めた上で新技術を導入しましょうね!というお話でした。
次回は今回の反省を活かして、データストアを MongoDB とかにしてみる予定!!
参考文献





