node.jsでTwitter stream を CouchDB に放り込みつつ、ブラウザに垂れ流すアプリを書いてみた
以前 node.jsでTwitter ChiarpUserStreamを閲覧するWebアプリを作った - 酒日記 はてな支店 を見て、おもしろいなぁ、と思っていたので、やってみました。
変更点は 単にtwitter streamを垂れ流すのではなく、一度CouchDBに放り込んで、CouchDB から MapReduce した結果をstreamさせるところです。MapReduce のストリーミングは、HadoopでMapReduce に初めて出会ったときからやりたかったので、CouchDB ありがとう。
デモアプリを http://demo.yssk22.info/twitter-couchdb-streaming においておきました。ブラウザを開きながら CouchDB に関するつぶやきをすると、リアルタイムに表示が更新されるかと思います。
以下詳細です。
インフラ構成
実際の構成は下のような形です。
node.js を CouchDB Proxy に使っていますが、Proxy じゃなくて直接接続でも問題ないと思います。Proxy に使っているのは、とある別のアプリで twitter の oauth 認証をかます必要があって、そのアプリでこのstreamingの仕組みを作った結果です。
実際には node.js の手前に varnish がいるので、がつがつキャッシュしてくれています。
node.js で、Streaming を CouchDB に.
完全なライブラリソースはgistに→ http://gist.github.com/543500
特におもしろいことをしてるわけではなく、酒日記さんのスクリプトを参考にしつつ、ストリームデータの塊を受け取ったときにCouchDBに放り込むハンドラをかいただけです。 node.js でそのままブラウザに垂れ流す場合は、酒日記さんのほう参照で。CouchDB の場合は CouchDB 自体が commet サーバーにもなるので、その部分は CouchDB にやらせます。
CouchDB で twitter の ストリームを MapReduce する
完全なソースはgistに→ http://gist.github.com/543508
単なる垂れ流し text から正規表現で link、hashtag、mention を抽出しているぐらいはやっておきました。View の名前は timeline です。
ブラウザから CouchDB の変更されたデータをstreamで受け取る
本題はここです。クライアントでリアルタイムに、、とか面倒そうな気がしますが、そんなことはありません。
$.couch.app(function(){ $("#timeline").evently(this.ddoc.evently.timeline, this); });
ええ、これだけ。 CouchApp にくっついてくる evently という jQuery のプラグインが、サーバーサイドと完全に統合された動きをしてくれます。
this.ddoc.evently.timeline とあるのが、これがサーバーサイドのJavaScriptコードの断片です。
実際には、ディレクトリとして、
+ couchapp_root + evently + _changes - query.json - data.js - mustache.html - render.text
という構成にしておきます。
まず、_changes というディレクトリですが、このディレクトリ名を使うと、CouchApp のほうで、自動的に CouchDB の _changes API に接続して、変更通知を受け取ったときに、このディレクトリ配下にあるイベント定義を動作させるような仕組みになっています。 _changes API は Long Poll で実現される CouchDB のAPIです。
次に、query.json ですが、これはイベント発火時に、どのクエリを発行するかの定義をJSONで書いておきます。
{ "view" : "timeline", "descending" : true, "type" : "newRows" }
こんな感じでやっておくと、timeline View に descending=true のパラメータをつけるようになり、type: "newRows" のおかげで、_changes から受け取ったdoc idをベースに、前回からの変更分のみを取得するようになります。
さらに、data.js は、クエリから受け取った結果をどう使うかを定義しておきます。
function(r){ var source = r.value.source; for(var k in source.user){ source['user.' + k] = source.user[k]; } source.created_at = $.prettyDate(source.created_at).toString(); return source; }
ここでは、Viewの結果の1行(r)をうけとって、そこから、時刻表示の変換をしているだけです。 return はテンプレートエンジンに渡すデータを指定します。テンプレートエンジンの都合から、階層化されたオブジェクトを扱えないので、for文で入れ子になっているuserオブジェクトを展開しています。
そして、mustache.html。これは evently 作者の jchris 氏が押してやまないヒゲテンプレートエンジンで、evently は今のところこのテンプレートエンジンのみに対応しています。
<li class="jta-tweet-list-item"> <div class="jta-tweet-profile-image"> <a target="_blank" href="http://twitter.com/{{user.screen_name}}" class="jta-tweet-profile-image-link"> <img border="0" width="48" height="48" title="{{user.name}}" alt="{{user.name}}" src="{{user.profile_image_url}}"> </a> </div> <div class="jta-tweet-body jta-tweet-body-list-profile-image-present"> <span class="tweet-text">{{{text}}}</span> <span class="jta-tweet-timestamp"> <a href="http://twitter.com/{{user.screen_name}}"> {{created_at}} </a> </span> </div> <div class="jta-clear"> </div> </li>
こんな感じで、data.js から受け取った 1つの行に対応するHTMLテンプレートを書いておきます。 jta ってあるのは単に、jTweetAnywhere の css をそのまま使いたかったから、それだけ。
最後に render.text。 これは、上で query, data, mustache で生成されたHTMLのフラグメントをどうやって、jQuery オブジェクト$('#timeline')に放り込むかの定義を、jQuery のセレクタで書いておきます。新しく到着したtweetデータを挿入したいので、prepend と書いておけばOK。
evently の概要はこんな感じで、詳細は、ドキュメントはありませんが、WebCastがあるのでその辺りを聴いておけば大体わかるかと思いま。。。。。 英語なので大体x大体=ちょっとわかったので、あとはソース読んだ方がいいかも。
http://oreillynet.com/pub/e/1604
それはともかく、HTML と JavaScript だけで、本格的なリアルタイムアプリケーションをデータベース付きで実現できるんだぜ、というのは革命的な気がします。
パフォーマンスどうなのよ?
Long Poll なので、コネクションの数には多少気を遣う必要があるかも。ただし、レスポンスは良好です。
手元のML115(Opeteron 1.8GHz, 8GB RAM)で、LAN内(1Gbps)でテストしたところ