2015年1月31日土曜日

CouchDB JavaScriptからの読み込みについて

CouchDBのUIを作ってみたくなり、htmlを書いてみます。せっかくなのでjQueryを使って、Ajaxを試して見たいと思ったのですが、ここからが苦難の始まりでした。

簡単なサンプルを書いてみたんですが、どうしてもNo 'Access-Control-Allow-Origin' header is present on the requested resource.というエラーがコンソールに出てきて失敗します。(ChromeのJavaScriptコンソールで確認しています)色々、調べてみたんですが、エラーの意味はXSSを防ぐため、ドメインが異なるJavaScriptを実行したり、読み込もうとするとでるエラーのようです。(数年前、これおを悪用したサイトの改竄が流行った記憶があります)しかし自分はローカル環境、あるいは家庭内の別PCで動かしているcouchdbから読もうとしているんですが、このエラーから逃げれません。
そこで、jQueryの問題なのかcouchdbの問題なのか区別するため、素に戻って単純にJavaScriptでローカルPCに置いたjsonデータを読むプログラムを書いてみました。

   function loadJson(id) {
    $('#msg').append("これから読みます:");

    var url = "http://localhost/~hogehoge/test1.json";
    var xmlHttp;

    xmlHttp = new XMLHttpRequest();
    xmlHttp.open("GET", url, false);
    xmlHttp.send(null);

    $('#msg').append(xmlHttp.responseText);

    $('#msg').append("うまくいった?");

   }

(これをやるために前回「ウェブ共有」の設定を調べました)
ところがMac上のchromeからでは相変わらず同じエラーが出ます。幾ら何でもこれはおかしいだろうと、調べてみるとchromeではセキュリティを厳しくしていて、ローカルPC上のファイルをhttp経由で読もうが、同じチェックをしていてエラーを出すそうです。ちなみに同じものをSafariで実行してみたら、あっさり動きました。(ちなみに同様のスクリプトをjQueryで書くと、Safariでもエラーがでます。どうもjQuery内部で同様のチェックをしているようで、簡単に自分のとこだけで試してみようと思っているのに難儀なことです)

さてCouchDBのUIを作ろうと思ったらなんとかcouchdbの設定で'Access-Control-Allow-Origin' のヘッダをつけるようにしないといけません。
現状、使用しているcouchdbは1.6ですが、1.4のドキュメントには以下の設定があるのがわかりました。

local.iniに以下の設定を追加。

[httpd]
enable_cors = true

[cors]
origins = *

特に[cors]の項目はデフォルトの設定ファイルには存在しておらず大丈夫かと思いましたが、うまくいきました。(ただこの設定は「全て許可する」という意味なんで、あまり推奨できませんが…本当は単にローカルPC上だけで動かしたいだけなら、origins=http://localhostとかするのがいいと思います。でもこれだと、http://127.0.0.1でアクセスすると拒否されてしまい、難しいところです。)
さて実際にcouchDBからデータを読み込んでみます。以下のプログラムで試してみました。(htmlにはボタンをつけて、以下のスクリプトを実行するようにしています)

   function readCouch(id) {
    $('#msg').append("これから読みます:");
    var url;
    url = 'http://localhost:5984/kakeibo';
    $.ajax({
     type: "GET",
     url: url,
     dataType:'json',
    }).done(function(data) {
     alert('読み込み');
     var str = parseJson(data);
     $('#msg').append(str);
    }).fail(function(xhr, textStatus, errorThrown){
     alert('error!!');
     console.log("NG:" + xhr.status);
     console.log("NG:" + textStatus.status);
     $('#msg').append("失敗しました");
    });

    $('#msg').append("うまくいった?");
   }

   function parseJson(json) {
    var ret = 'db_name : ' + json.db_name + '';
    ret = ret + 'doc_count : ' + json.doc_count;
    return ret;
   }

chromeで実行すると以下のようになります。(html上に、<p id=msg>が付けてあり、そこに追記するようにしています)


うまく読めましたね。

2015年1月23日金曜日

Mac Yosemiteでウェブ公開をする(apache2を動かす)

Lionから「設定」に「ウェブ共有」がなくなったそうです。まあ、昔からあまりに簡単にhttpdを起動できるので危ないな〜とは思ってましたが、いざなくなると困ります。Yosemiteではapacheを手動スタートしないといけません。(Lionsは試してなくて、Mavericksは使いましたが、そのときは試してませんでした。いきなりYosemiteに来たんで、気づかなかった)

$ sudo apachectl start

(passwdが聞かれます)

これで、http://localhost/をブラウザで開けるようになります。
この時のhomeは、/Library/WebServer/Documents/になります。(システムルートです)ただこれでは作業上面倒くさいので、従来の自分のhomeのSitesをホームにします。(この後設定ファイルを色々アンコメントしますが、デフォルトでそうなのか、あるいは古いバージョンからYosemiteにUpしたせいなのか正確なところはわかりません。自分はUpしてますので、設定しようと思ったファイルがすでにあるものの内容に問題があり、以下の通りに修正したので所々これでいいのか悩みました)

まず/etc/apache2/httpd.confでmod_userdirを有効にします。

$ sudo vi /etc/apache2/httpd.conf

mod_userdir.soをロードする行がコメントアウトされているので、アンコメントする。
(166行目付近の以下をアンコメントする。)

LoadModule userdir_module libexec/apache2/mod_userdir.so

# User home directories
Include /private/etc/apache2/extra/httpd-userdir.conf

(他のHPみると、phpのモジュールとかもコメントアウトされてしまうとか。そのあたりは必要に応じてということになりますが、基本安全側にコメントされてしまうようです。)

次にユーザーディレクトリを作成します。(これは昔のバージョンのものが自分は残っていました)

$ mkdir ~/Sites

ルート権限で、/etc/apache2/users/ユーザ名.confというファイルを作る。(書式がYosemiteのapache2.4になってから変わってます。以前は2.2だったそうで、サイトによって内容が若干変わってたりします。ただ少なくとも昔のままではダメなことは確かです。)

<Directory “/Users/ユーザ名/Sites/”>
DirectoryIndex index.html index.php
AllowOverride All
Options all
Require all granted
</Directory>

以下のファイルを修正し、アンコメントする。
$ sudo vi /etc/apache2/extra/httpd-userdir.conf

Include /private/etc/apache2/users/*.conf

これでapache2を再スタートすれば、http://localhost/~ユーザ名 で「ウェブ共有」できるようになります。(いくつかのHPでは、ユーザ名の頭の「~」が記されなくてうまくいかず、Yosemiteから変わったんだろうかと悩みましたが結局ここは昔のままのURLです。)
一度起動すれば、以降はマシンを再起動しても自動的に起動してくれます。


なんで急に「ウェブ公開」なんてやろうと思ったかというと、couchDBのUIをhtmlで作ってみたくなり、jQueryとかいじり始めたらどうもうまくいかず、基本のurlでのアクセスから試してみないとわからない、ということになったんです。ただ、ここからまた別の苦闘が始まるのですが、それはまた今度に。

2015年1月12日月曜日

CouchDB Replicatorについて

もう少しCouchDBの基本的な動作を確認します。今度はReplicator(複写)についてです。 作成したDBのBackUpや、あるいは複製を取る機能としてFutonにはReplicatorの機能があります。localにBackUpを保存してもいいんですが、折角なので別のPCにもCouchDBを設定して試してみます。(ちょっと別に興味があったので、MacBookAirにHomebrew入れてCouchDBをインストールしてみました) 

Replicatorの画面は以下の様な感じです。ここで簡単にkakeiboをMacBookAirに新規インストールしたCouchDBにコピーを作ります。(なお、先にMacBookAir側で内容は空でいいんですがコピー先のDBを作成しておく必要があります。どうもいきなり作成はしてくれないようです。PS:remoteから取ってくる場合は新規に作成するかと聞かれて、OKすれば自動的に作成してくれるようです。)


うまくいくとEvent欄にsessionの動作ログが表示されます。

さてここで実験です。localとMacBookAir側でわざとDBを異なる状態にしてReplicatorを動かしたらどうなるかを見てみます。まず、それぞれを以下の状態にします。(赤字がそれぞれ追加したドキュメントです)

local側

date    category      item       price
5/10     果物           バナナ       300
5/10       魚               鯛         1000
5/11     果物           みかん       300
5/11     果物           バナナ       300
5/12       魚               鯛           900
5/14       魚            まぐろ       1000


MacBookAir側

date    category      item       price
5/10     果物           バナナ       300
5/10       魚               鯛         1000
5/11     果物           みかん       300
5/11     果物           バナナ       300
5/12       魚               鯛           900
5/13     果物           みかん       600

相互にReplicateしてみると、それぞれ追加したドキュメントが相手に入ってくれます。


Replicate後

date    category      item       price
5/10     果物           バナナ       300
5/10       魚               鯛         1000
5/11     果物           みかん       300
5/11     果物           バナナ       300
5/12       魚               鯛           900
5/13     果物           みかん       600
5/14       魚            まぐろ       1000

その後、local側で5/13のドキュメントを削除した後、最後MacBookAir側にReplicateしてもMacBookAir側の5/13のドキュメントはすぐには削除されません。単に複写をしているだけではないようです。(しばらく時間がたってからReplicateすると相手のDBにも最新の状況が反映されます)

このあたりの動きとして関連して、先のReplicateするときボタンの横にCotinuousトグルがあるので、それをOnにしてReplicateすると連続して同期してくれます。Statusで動作していることが確認できます。

(ただこれは、local→MacBookAirの方向のReplicateなので、localの変更は自動的にMacBookAir側に反映されますが、MacBookAirの変更はlocalに自動的には反映してくれません。)
なお、この設定はマシンを再起動すると消えてしまいます。恒常的にReplicateさせるには別途設定が必要なようです。(当たり前か)






2015年1月2日金曜日

CouchDBのMapReduceをアプリ(python)で動かす

前回、Futon上でMapReduceを定義、動かしてみました。サンプル的にDBの集計をしたい場合はそれでいいんですが、やはりアプリ(python)からMapReduce処理を指示し、集計処理をしてみたいです。

とりあえず前回、日付毎の集計をするMapReduceを”eachdate”に作成しましたので、これをpythonからCallしてみます。(Keyを指定せずに、全部を対象にしています)

# coding: utf-8
import couchdb

server = couchdb.Server('http://localhost:5984')
db = server['kakeibo']

result = db.view("application/eachdate")

for r in result:
    print r

ところがこれを実行すると以下の結果になってしまいました。

&lt Row key="None," value="2850">

なんかkeys(日付)毎に集計されるのを期待したのですが、keys無視で集計されています。色々、調べまくってみると、どうもデフォルトではreduce関数の入力はMapの結果だけではなく、Reduceの結果も再度受け入れてしまうようです。(そのため、全部集計してしまう:注)これを期待どおり、keys毎に集計したい場合は以下のOptionを入れるそうです。

# coding: utf-8
import couchdb

server = couchdb.Server('http://localhost:5984')
db = server['kakeibo']

result = db.view("application/eachdate", group=True)

for r in result:
    print r

こうすると、期待どおり日付毎の集計をしてくれます。

&lt Row key=u'5/10', value=1300>
&lt Row key=u'5/11', value=650>
&lt Row key=u'5/12', value=900>


(ちなみに、ここにはReduceを行わないoptionとして'reduce=False'というのもあります)

注:Reduce関数の出力が再度Reduce関数の入力に入ってしまう、というのは説明が足りてませんでしたね。(元が英語だったので、意図を解釈しきれてなかった)これは、Mapの結果が大きいと、Map関数が入力データをsplitして複数のMap結果が生成されるそうです。(具体的にどのくらい大きいとsplitされるのかはまだ調べてません。)そうすると、各Map結果に対してReduce関数が動き、最後にそれらのReduce関数の結果をまとめるため、もう一度Reduce関数がCallされるんだそうです。同じReduce関数がCallされるので、そのままだと再集計をしてしまう、ということが起きるようです。それを避けるため、rereduceというflagがあり、Reduce関数内ではそのflagでsplitされたMap関数を処理するReduce関数なのか、最後に集計するReduce関数なのかをif文で識別して動作する、という方法を使うそうです。ここで述べたgroupとsplit、rereduceの関係はまた調べて整理しないとダメですね。

さて、MapReduceのコードをFutonで入力するのもいいですが、python上でコーディングしてしまう方法もあります。

# coding: utf-8
import couchdb

server = couchdb.Server('http://localhost:5984')
db = server['kakeibo']

map_fun = '''function(doc) {
 emit(doc.date, doc.price);
}'''

reduce_fun = '''function(keys, values) {
 return sum(values);
}'''

result = db.query(map_fun, reduce_fun, group=True)

for r in result:
    print r

何のことはありません、関数定義を文字列として渡しているだけですね。
後、これらの結果を見るとreduce定義の引数のところで(keys, values)としましたが、特に(key, value)でもいいようです。まあわかれば当たり前の話ですが、関数定義の時の引数名なんで、順番が大事なだけですね。


PS:
最後に、ログのとり方についてだけ追記しておきます。特に今回悩んだような症状のとき、Reduce関数には何が引数に入っているか知りたくなります。そんな時、ログ関数があります。

reduce_fun = '''function(keys, values, rereduce) {
 log(rereduce);
 log(keys);
 log(values);
 if (rereduce)
  return sum(values);
 else
  return values;
}''

こんなふうにlog()を入れるだけで、/var/log/couchdb/couch.logに記録されます。これを見ると、group=Trueがある時とない時の違いがひと目でわかります。
groupの指定をしていないと以下のログが記録されます。

[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: false
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [["5/10","102d4440a25efcd79349859b67000302"],["5/10","102d4440a25efcd79349859b67000a91"],["5/11","102d4440a25efcd79349859b67000eee"],["5/11","102d4440a25efcd79349859b67001c9b"],["5/12","102d4440a25efcd79349859b67002b7f"]]
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [300,1000,400,250,900]


id情報まで表示されるので見難いのですが、Map関数を通しても、すべての日付のリストがkeysに入っており、対応するpriceもvaluesにリストで5個集められてしまっていることがわかります。(これでは、日付毎の集計なんてされないはずだ)
一方、group=Trueの指定をすると以下のログになりました。

[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: false
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [["5/10","102d4440a25efcd79349859b67000a91"],["5/10","102d4440a25efcd79349859b67000302"]]
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [1000,300]
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.109.0="">] 127.0.0.1 - - POST /kakeibo/_temp_view?group=true 200
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: false
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [["5/11","102d4440a25efcd79349859b67001c9b"],["5/11","102d4440a25efcd79349859b67000eee"]]
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [250,400]
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: false
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [["5/12","102d4440a25efcd79349859b67002b7f"]]
[Sat, 03 Jan 2015 07:31:45 GMT] [info] [<0 data-blogger-escaped-.290.0="">] OS Process #Port<0 data-blogger-escaped-.2666=""> Log :: [900]


今度は、一回のreduce関数がcallされる時にはkeysには日付ごとのリストが集められていることがわかります。(つまり3回reduce関数はcallされています)これではっきりと、想定した動作をなぜしなかったのかがわかります。