CouchDBでRelaxアプリケーション開発 :: 10分で作る FeedReader -> それ"データベース"でできるよ編の準備

タイトルは釣りですが、まじめにこれは釣りたいww 10分でライブデモができるようになるには事前の準備が必要です。

いろいろCouchDBで遊べそうなのはわかった(何がどうわかったのかは、あとでちゃんとした文書で公開します....そのうち)ので、アプリケーション開発にチャレンジです。

目下のところ、WebJourneyくんはBlog Widgets + HTML Widgets + Feed Widgets という用途でしか使っていないので、とりあえずFeedReaderをCouchDBだけで普通に作るには、というのをチャレンジしてみようと思います。

CouchApp は CouchDBだけでアプリケーションを開発する場合のユーティリティライブラリ(テンプレートエンジン等)が含まれます。JavaScriptライブラリと、一部のコマンドラインタスクを実行するためのPythonライブラリです*1

PythonMac OS X に標準でついている 2.5(/System/Library/Frameworks/Python.framework/Versions/2.5/Resources/Python.app/Contents/MacOS/Python) を使います。easy_install をアップデートしてから、couchappをインストールします。

$sudo easy_install -U setuptools
$sudo easy_install couchapp

以上です。このエントリ起票時点で0.2が入ります。ちなみに、OS X だと http://mac.softpedia.com/progDownload/CouchApp-Download-49777.html とかあったりするんですがね。普通に easy_install のほうがイイと思います。

さて、でプロジェクトの作成。プロジェクトの名前は、relax_readerとでもしておきましょうか。

$ couchapp generate relax_reader
(snip)
  [Errno 13] Permission denied: '/Users/yssk22/.python-eggs/simplejson-2.0.9-py2.5-macosx-10.5-i386.egg-tmp'
(snip)

というわけでいきなりやると、こけます。sudo easy_install で使ったegg cacheが残っているようなので、削除しておきましょう。

$ sudo rm -fr ~/.python-eggs
$ couchapp generate relax_reader
Generating a new CouchApp in /Users/yssk22/project/relax_reader

OKです。ちなみに、なんかどっかでみたようなRailsにインスパイアされてない?ってな具合に、ディレクトリができあがります。

$ cd relax_reader
$ ls -al
total 8
drwxr-xr-x  10 yssk22  staff  340  5 12 05:29 .
drwxr-xr-x  20 yssk22  staff  680  5 12 05:29 ..
-rw-r--r--   1 yssk22  staff    2  5 12 05:29 .couchapprc
drwxr-xr-x   5 yssk22  staff  170  5 12 05:07 _attachments
drwxr-xr-x   3 yssk22  staff  102  5 12 05:07 foo
drwxr-xr-x   4 yssk22  staff  136  5 12 05:07 lib
drwxr-xr-x   3 yssk22  staff  102  5 12 05:07 lists
drwxr-xr-x   3 yssk22  staff  102  5 12 05:07 shows
drwxr-xr-x   3 yssk22  staff  102  5 12 05:07 vendor
drwxr-xr-x   3 yssk22  staff  102  5 12 05:07 views

CouchDBを深く知っていると、

  • views に MapReduceのjs
  • listsには一覧表示用のjs
  • shows には 1ドキュメント表示用のjsを置く

ということは想像つきますね。ほかのディレクトリはおいおい。

で、どうやら views などをみるとexampleというのができているので、このままでもHello World相当のことはやってくれるんじゃね?的な期待をもって、アプリケーションをCouchDBにアップロードします。CouchDBは単なるデータベースではありません。アプリケーションランタイムを備えたデータストアです(これ重要)。ストアドプロシージャーとは全然違います。アプリケーションでありプロシージャーではないです。

で、アップロードどうやるの?っていう話ですが、couchapp のサブコマンド push を使います。

$ couchapp push http://localhost:5984/relax_reader
Pushing CouchApp in /Users/yssk22/project/relax_reader to design doc:
http://localhost:5984/relax_reader/_design/relax_reader
Visit your CouchApp here:
http://localhost:5984/relax_reader/_design/relax_reader/index.html

これで、データストアが作成され、アプリケーションが投入され、UIまで登録されました、と。

_design/relax_reader には、サンプルのアプリケーションが含まれているんですが、lists という一覧を生成するデザインドキュメントに feed というのがあります。。。。

って、つまり何も作らんでもFeedReaderになりますよってことで...

せっかくなのでデザインドキュメントの解説。

function(head, row, req) {
  respondWith(req, {
    html : function() {
      if (head) {
        return '<html><h1>Listing</h1> total rows: '+head.row_count+'<ul/>';
      } else if (row) {
        return '\n<li>Id:' + row.id + '</li>';
      } else {
        return '</ul></html>';
      }
    },
    xml : function() {
      if (head) {
        return {body:'<feed xmlns="http://www.w3.org/2005/Atom">'
          +'<title>Test XML Feed</title>'};
      } else if (row) {
        // Becase Safari can't stand to see that dastardly
        // E4X outside of a string. Outside of tests you
        // can just use E4X literals.
        var entry = new XML('<entry/>');
        entry.id = row.id;
        entry.title = row.key;
        entry.content = row.value;
        return {body:entry};
      } else {
        return {body : "</feed>"};
      }
    }
  })
}

肝心なところは、

        var entry = new XML('<entry/>');
        entry.id = row.id;
        entry.title = row.key;
        entry.content = row.value;
        return {body:entry};

これは、CouchDBMapReduceの結果生成される (ID, Key, Value) ペアのリストをFeedとして返します、っていう話。
なので、Feed の1entryを1つのJSONドキュメントに直して、CouchDB に放り込んであげて、MapReduceを書けてあげれば、以上終了です。

ということで、コマンドラインで動く script/crawl.rb でも作りましょうか。クロールする部分はCouchDBとは全く関係のない外部プロセスなので、どんなスクリプトでもイイです。単に、Rubyのfeed-normalizer的なライブラリのPython/JavaScript 実装を知らないだけです。

#!/usr/bin/env ruby
require 'rubygems'
require 'feed-normalizer'
require 'json'

# クロール対象ターゲット
# もちろん、これを事前にCouchDBに登録できるようにしておくのがいわゆるFeedReaderだけれど
TARGETS = [
           "http://d.hatena.ne.jp/yssk22/rss",
           "http://damienkatz.net/atom.xml"
          ]

docs = []
TARGETS.each do |uri|
  feed = FeedNormalizer::FeedNormalizer.parse(open(uri))
  # Feed は登録
  docs << {
    :_id      => feed.url,
    :doc_type => "Feed",
    :title    => feed.title,
    :description => feed.description,
    :last_updated => feed.last_updated.utc.strftime("%Y/%m/%d %H:%M:%S +0000"),
  }
  # entries も別ドキュメントで
  feed.entries.each do |entry|
    docs << {
      :_id          => entry.url,
      :doc_type     => "Entry",
      :title        => entry.title,
      :description  => entry.description,
      :last_updated => entry.last_updated.utc.strftime("%Y/%m/%d %H:%M:%S +0000")
    }
  end
end

# 送信
Net::HTTP.start("localhost", 5984) do |http|
  res = http.post("/relax_reader/_bulk_docs", { :docs => docs }.to_json)
  puts res.inspect
end

で、

$ ruby script/crawl.rb

とすればOK。

次にMapReduce。Feed 用のFeedと Entry用のFeedと、FeedとEntryをごちゃ混ぜにしたFeedを作ることにします。
views/for_feeds, views/for_entries, views/for_feeds_and_entries とディレクトリを3つ作ってmap.jsをそれぞれ記述。reduceはいらないと思う。

// views/for_feeds/map.js
function(doc){
  if( doc.doc_type === "Feed" ){
     emit(doc.title, doc.description);
  }
}
// views/for_entries/map.js
function(doc){
  if( doc.doc_type === "Entry" ){
     emit(doc.title, doc.description);
  }
}
// views/for_feeds/map.js
function(doc){
  emit(doc.title, doc.description);
}

で、再度couchapp push。ちなみにcouchapp は generate で作った COUCHAPP_ROOT 配下にあるファイルをすべからくCouchDBに放り込みます。なので、、、svnやgitもCouchDBのrevisionシステムの原理原則からいうといらなくなってしまいますww

ソースコードも過去の履歴も、本番環境も、全部データベースにあるから!え、テスト環境?それは、適当にデータベースをレプリカしてどっかでやってくれ!。。うまくいったらこっちに再レプリカして更新してくれよ!

余談ですが、これは本当にLotus Notes アプリケーションみたい(実際のNotesアプリ開発は知らないんですけれど、Notesユーザーとしてはそう見えてしまいます)。10年後に起こる問題が予想できてしまう...

という話はおいといて*2

$ couchapp push http://localhost:5984/relax_reader
Pushing CouchApp in /Users/yssk22/project/relax_reader to design doc:
http://localhost:5984/relax_reader/_design/relax_reader
Visit your CouchApp here:
http://localhost:5984/relax_reader/_design/relax_reader/index.html

で、あとは Accept: application/xml にしたGETリクエストをCouchDBにおくるのです。が、明示的に指定しなくてもCouchDB が Content Negotiation をしてくれます。

http://localhost:5984/relax_reader/_design/relax_reader/_list/feed/for_feeds に対して Firefox でリクエストを送ると、HTMLがかえってきます。Firebug でみると、

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

な具合なので、text/htmlが優先されたようです。一方、コマンドラインからcurlを使って上記のURIにアクセスするとXMLがかえってきます。

$ curl -X GET -H "Accept: application/xml" http://localhost:5984/relax_reader/_design/relax_reader/_list/feed/for_feeds 
<feed xmlns="http://www.w3.org/2005/Atom"><title>Test XML Feed</title><entry>
  <id>http://damienkatz.net/</id>
  <title>Damien Katz</title>
  <content>Everybody keeps on talking about it
Nobody's getting it done</content>
</entry><entry>
  <id>http://d.hatena.ne.jp/yssk22/</id>
  <title>Web屋かもしれない人の日記 || WebJourney 開発ログ</title>
  <content>Web屋かもしれない人の日記 || WebJourney 開発ログ</content>
</entry></feed>imac:util yssk22$ 

当然、Acceptをtext/htmlに切り替えると、Firefoxと同じ結果を取得できます。

$ curl -X GET -H "Accept: text/html" http://localhost:5984/relax_reader/_design/relax_reader/_list/feed/for_feeds 
<html><h1>Listing</h1> total rows: undefined<ul/>
<li>Id:http://damienkatz.net/</li>

これは、すばらしい!!

*1:以前はRuby版のタスクもあったのですが、直近のgithubではbyebyeされてました

*2:まじめに考えれば、今のCouchDBにはWebそしてOpenSourceという大きな味方がいるという点