Android Studioを入れてFacebookSDKのLogin機能を使うまでの作業記録
Android Studioの導入以降
柄にも無くAndroid Appliの開発に手を染め始めた@yutakikuchi_です。
Android Appliの開発をする為にはEclipseかAndroid Studioを導入すると良いようです。ぐぐってみると断然Eclipseのドキュメントが多いようですが、EclipseはGradleというAndroid Appliをビルドするツールが導入しづらいとの事で、僕はAndroid Studioを選びました。Android Studioの導入はdotinstallに詳しく載っているので、僕と同じ初学者の方は一度参考にする事をお勧めします。Androidアプリ開発入門 (全12回) - プログラミングならドットインストール またschooでも同じような講義が公開されていますがGradleに対する説明がある冒頭だけ見れば良いと思います。その他はdotinstallでOK。Android StudioではじめるAndroidアプリケーション実践入門 - 無料動画学習|schoo(スクー)
dotinstallで無料会員で視聴できるのはMyActivity.Java、activity_my.xmlを編集してボタンを押下した時に画面に表示する文言を変更するという所までです。アプリの細かい開発の動画もあるようなんですが有料会員でないと利用できません... Oh...。Javaにビジネスロジック書いて取得したデータをViewに反映したいという欲求を満たす為に今日は自分で試してみた事を記録しておきます。
類似度計算と転置Indexとb-Bit Minwise Hashing
Recommend Engineでの類似度計算
RecommendEngineを作る時の話。アイテム間の相関を計算する為にユーザーの購買データからJaccard係数やCos類似度を求める手法が一般的です(アイテム×ユーザーTableと、アイテム×アイテム相関Tableが必要)。しかしアイテムの個数(N)×ユーザー数(M)の行列を作り、Nの中から2つのアイテムを取り出してそれぞれの係数や類似度を求め、それを個数分繰り返していたら行列が大きくなる程計算が大変になります。特にアイテムの購買という行為がほとんど発生しないので、購買のベクトルがほとんど0となる疎ベクトルが作られて効率が悪く感じられます。一時期はこれを回避する為にベクトル数を減らす(購買データが多いユーザーに超超限定する)事で回避していたんですが、ユーザーが偏るしデータも少なくなってしまう事を問題として認識していました。そこでデータ数を減らすよりもっと色んな方法あるっしょって事で調べてみました。
レコメンドにおける類似度計算その傾向と対策 #DSIRNLP 第4回 2013.9.1 // Speaker Deck
転置Indexを使う手法。特定のアイテムAを買ったUser一覧をIndexから引き、User一覧が買った商品一覧を引いて来てアイテムA以外の共起回数を計算する。この方法では共起回数の計算はそこまで大変ではなく、アイテム数とユーザー数の両方が増えても処理時間への影響が小さい(らしい)です。
b-Bit Minwise Hashing
b-bit miniwise Hashingという手法。ハッシュ関数(MurmurHash3等)を使って2つのアイテムの全ベクトル要素に対して適用し、それぞれの最小の値が一致する確率はJaccard係数と等しいという理論から導きだされます。ハッシュ関数だけ共有すれば分散処理も行ける優れもの。b-bitというのは保存するbit数の事でMurmurHash3の下位1bitで良いようです。ただしハッシュ値の衝突が生じるので衝突確率を補正した値をJaccard係数とするようです。
自分が詳しく把握していなかったのは上の2つなんですが、他に調べていて手法が見つかったらここに纏めて行こうと思います。
ブラウザ識別用Cookieを生成する「mod_oreore(仮)」を作ったった
mod_oreore(仮)
ネーミングセンスが糞すぎる@yutakikuchi_です。
アクセス履歴をLogに落として行動履歴を追いたい時はCookieに識別子を設定するのが一般的かと思います。一般的にあるCookie識別子の設定のタイミングはFWやアプリケーションのでやるというように様々パターンを見かけますが、今回はApacheのレイヤーで自動的に付与してくれるModuleを作ってみました。因に同じようなApacheModuleは幾つか存在しますが、完全なる一意性が保証されていないことやApacheのVersionで使えなかったり等、ちょっとイケテナイ感じがしたので自作してみました。※mod_oreoreとはユーザー視点で「俺だよ!俺!」っというLogに自ら足跡を残す意味で、決してオレオレ詐欺とは関係ありません。
mod_usertrack ※ 一意性に問題あり
mod_session_cookie ※ apache2.3以降で利用可能
github : mod_oreore(仮)
識別子の値にはRequestを受け付けたサーバーのIPアドレス、リクエスト時刻(タイムスタンプ:マイクロ秒)、ApacheのプロセスID、コネクションIDを重ね合わせ、最終的な出力はbase64のURLSafeな形でencodeしています。base64する前に生成した識別子を暗号化しようと思ったのですが、処理が冗長的な気がして辞めました(ソースには暗号化をそのまま残しております)。またDOS攻撃を防ぐ処理は入れていません。
設定と確認
$ sudo yum install httpd httpd-devel openssl openssl-devel $ git clone https://github.com/yutakikuchi/apache_module.git $ cd apache_module/mod_oreore $ sudo apxs -i -a -c -I/usr/include/openssl -L/usr/lib64 -lcrypto mod_oreore.c $ sudo cp conf/oreore.conf /etc/httpd/conf.d/ $ sudo vim /etc/httpd/conf.d/oreore.conf <IfModule mod_oreore.c> #NameパラメータでCookie名を指定する(ここは自由に書き換える) Name OREORE #Domainパラメータの先頭は.で始める(ここは自由に書き換える) Domain .cookie-test.com #Expiresパラメータで有効期間を指定する(指定可能なのはyears,months,weeks,days,hours,minutes,seconds) Expires "2 years" </IfModule> $ sudo vim /etc/httpd/conf/httpd.conf #以下をcombinedログに指定 LogFormat "domain:%V\thost:%h\tserver:%A\tident:%l\tuser:%u\ttime:%{%d/%b/%Y:%H:%M:%S %z}t\tmethod:%m\tpath:%U%q\tprotocol:%H\tstatus:%>s\tsize:%b\treferer:%{Referer}i\tagent:%{User-Agent}i\tresponse_time:%D\toreore_cookie:%{OREORE}C" combined $ sudo /etc/init.d/httpd restart $ #Webブラウザで2回アクセスしてみる $ tail -f /var/log/httpd/access_log domain:cookie-test.com host:XXXXXXX server:YYYYYYY ident:- user:- time:07/Aug/2014:16:37:09 +090method:GET path:/index.html protocol:HTTP/1.1 status:304 size:- referer:- agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36 response_time:5588 oreore_cookie:id=<ここに設定された識別子が入る>&v=1 $ sudo vim /etc/httpd/conf/httpd.conf #消したい場合は以下を削除orコメントアウト LoadModule oreore_module /usr/lib64/httpd/modules/mod_oreore.soApache側で全て自動的に処理をしてくれるので楽ですね。(^o^)/
後は識別子毎にLog行をsortしたりするとブラウザ毎にどんな行動があったかが纏められて把握できますね。(^o^)/(^o^)/
常駐型受託開発の経験から
常駐型受託開発
久しぶりにブログを更新します。「常駐型受託開発」という言葉が正確かどうかも分からないけど、とりあえず取引先のシステムを作ってました@yutakikuchi_です。とある会社様(クライント)の新規事業立ち上げが目的で、そのクライアント様のOfficeにお邪魔しながら約1年程携わらせて頂きました。結論から言うとこの経験が凄く身になってとても良かったと感じました。(※優秀なスタッフさんが沢山いらっしゃる環境で、とても親切にして頂いた事は厚くお礼を申し上げます。)
受託やらないほうが良いぜ!論を当然否定することはしませんし、逆に強く薦めもしませんが、新規事業立ち上げに必要な事を凄く近いところで経験できたのは今後自分が事業を立ち上げる事にもプラスの材料になったと思います。今日はこの開発現場で感じた受託開発とマネジメントについて記録するだけなので、内容的には糞つまらないことかもしれませんが、どなたかのお役に立てればと思います。因に僕の会社には社員がいないので、一人で出向いていました。
常駐型受託で良かった点、残念だった点
良かった点
- 自分と同じような受託社長さん達と知り合う事が出来た(ここ凄く大事)。また受託開発現場というのを肌で感じれた。
- 自分の技術力の立ち位置を把握できた。また技術力よりも人間力/信頼力が仕事を得る材料である事も理解できた。
- 常駐する事で社員さんとのコミュニケーションが密にできたし、認識の大きな間違いも少なかった。
- 事業立ち上げフェーズから入れてもらえた事で社員さんと同等のStartUPの進め方のノウハウを身につける事ができた。
- 0からのスタートなので、幅広く必要なシステムの根幹を作り込んだ。またその仕組みをいち早く作れるように考える頭も身につけた。
- 注文請書、請求書等のやり取り作業を全部自分で経験できた。契約面で質問したい内容があれば直ぐに現場でも解消する事が出来た。
残念だった点
- 常駐型+時間契約だったので労働時間の対価を得るような内容になってしまった事。またそれによって時間的な拘束が多く発生してしまった事。これにより他社からの開発を受けられなくなってしまった。
- 受託に重きを置きすぎて自社開発に手がつけられなくなってしまった事。(僕はここの比率を途中で変更してしまったのだが、最初に決めた信念を貫く必要があると感じた。)
- 決められた方針に反論しづらい、また自分が思った改善策や良いアイディアを率先して現場に浸透させづらい。どうしても所詮受託の意見という考えが浮かんでしまう。
- イベントや勉強会で直接的な成果を発表しづらい。発表する場合はコアな話は出来なく、概要レベルもしくは一般論に置き換えて話す必要がある。
- 頑張った事に対する評価についてあれこれ考えてしまう。
受託エンジニアの種別
受託エンジニアと言っても様々な背景を持った人と知り合う事ができたのでそれもプラスの材料になりました。今回の現場では受託エンジニアご本人が別の会社に所属しているか否(独立している)か、採用に紹介会社がいるのか否かという2×2の4パターンありました。凄く失礼な話かもしれませんが、この区分けによりなんとなーくのエンジニア特性が見れて取れたように思います。ここでも書けるような内容としては個人を見た場合、会社所属の人たちは要件の抽出とそれを具体化する力、独立している人達は何かやったるぜー!という野心と個性が強く見れた気がします。マネジメントをする人はご自身で特性を見つけ出して知っておくと良いかもしれません。ここはもっと詳しく書きたいんですがこの辺まで(笑)
受託の人数
現場で社員を雇えない状況で今直ぐ対応する人が欲しい場合は受託の人数を増やすのが手っ取り早いですが、その人数が増えすぎるとマネジメントが確実に行き届かなくなります。これはどんなに優秀な社員マネージャーがいたとしても受託に対する指示を一人ずつに細かく伝えるのは不可能であり、結局のところ一番コミュニケーションが発生するのは社員マネージャと受託では無く、機能担当の社員エンジニアと受託の直接になります。この社員さんのコミュニケーションコストが半端無い。仕様の伝達、ソースコードレビュー、改修依頼等、ただでさえ忙しい社員エンジニアさんもその辺のサポートに相当な工数が掛かってしまいます。今回の経験での一つの指標として、どんなに苦しい状況でも社員エンジニアの数より受託の数を増やすことはNGであり、更に言うと理想的には受託人数が社員数の半分より小さくなるように採用すべきかと思いました。もしそれでも人数対工数の折り合いが付かないのであれば、常駐型だけではなくマネジメントを含めて外部委託会社に依頼する、そもそものリリーススケジュールを見直すか機能の削ぎ落としや簡略化を図るのが得策と思います。大事なので2度言いますが、単純に常駐型受託の人数を増やすのはマネジメントのリスクが大きくなります。あと、受託に重要な機能を任せると後々いなくなってしまった時に大変な事になるので、機能の重要度で委託するかどうかを考える事が必要だと思います。
紹介業者の戦略
僕も最初は受託を1人採用するのは紹介会社1社を経由するケースしか無いのかと思ったのですが、実際には間に紹介会社を2,3社挟むケースもあってその分受託サラリーのマージンが引かれているようです。このケースで美味しい思いをしているのは間にいる紹介業者だけであり、発注元は金額が高くなるし現場で頑張っているエンジニアの労は報われません。人材だけでなく仲介業全般での当然の話なんでしょうけど、このシステムってなんとかならないんでしょうか。受託の士気を上げる為にも常識的なマージンであって欲しいと強く希望します。
紹介会社は既に送り出している受託の人から都度現場状況をヒアリングして、更に人数を増やせるか、そしてどれぐらいのレベルのエンジニアなら長期採用されるのかというのを把握しています。紹介会社としても当然長く現場にいて欲しいという希望があるので、今求められているスキルと新たに送り出せる人のマッチング具合が気になるのだと思います。ある程度の採用単価が保証される事も当然前提の一つですが、ちゃんとした技術基盤があり高い目標を常に持ち続けられるいい環境という情報がうまく紹介会社側に伝われば優秀なエンジニアが来てくれる可能性は高まります。逆に炎上状態やスキル面での低レベル感が伝わると当然紹介会社もハイエンドな人材を送り出したいとは思わなくなってしまう。後者に嵌ってしまうと現場としては完全なる悪循環ですよね。
男なら潔くC言語書けよと言われた話。〜mod_db,mod_dbdの実装〜
恩師に言われた言葉
Geek女優の池澤あやかさんに会いたいと思っている@yutakikuchi_です。
池澤さんはRubyが出来てSFCで女優さんなんて羨ましいですね〜。僕なんてRubyは得意じゃないし東京とは言えないような都心から離れた場所の地味な国立大だし、何よりお金も無いパンピーだしね〜。
僕の学生時代にもRubyはあったんですけどRailsはまだ出始めでそんなに流行っている雰囲気は無かったし、Webを書くには面倒くさいJSP/ServletかPerlかって感じでした。ApacheのModuleでWebを書ける事も学生ながら知っていたんですが、ポインタ、メモリの動的確保/解放の間違いが頻発して開発効率が落ちるから極力Javaで、どうしてもCを書かなければ行けない時はC++で逃げてました。
でも学生時代に恩師に言われたんですよね、JavaやPerlを書く奴はチャラいやつと。男なら潔くC言語書けよと。 (因に恩師はJavaとJavascriptの違いも良く理解していなかったと思いますけどww) おそらく恩師が言いたいのは学生の時から高級言語に頼る事無く、まずはプログラミングの仕組みを理解するためにC言語で苦労してエキスパートを目指せっていう意図だったと解釈しています。
C言語をやると他の言語と違って問題の発生率が高い。 要は言語側であまり頑張ってくれないから自分で工夫する必要があります。特に他のコンポーネントに接続する時には接続数の制御/ポートの使い切り、ConnectionPoolの実装、使用メモリのオーバー、良く分からないSegmentation Fault等必ず経験します。こういった問題解決の経験値をつける事でエンジニアの実力が養われるのだと思います。
恩師の言葉を信じてCの勉強を重ねた結果、会社の開発チームに配属された時は凄く重宝されましたし、極力PHP以外を書く案件を回して貰えました。だからCを勉強して来て良かったと思ってます。しかし今後C言語はどうなるんでしょうか... 身の回りではゲーム系、広告配信エンジニア以外は触って無さそうです。その他の言語が強力で簡単WebアプリぐらいだとCで頑張って書くとか全く無いんですよね。学生の勉強の仕組み理解と違って会社は効率化や作業時間短縮を掲げてくるのでC推しも難しいですね。
前置きが長くなりましたが、それでも僕はCを触る事が多いのでこのエントリーではC言語からのDB接続について書きたいと思います。
DB接続
mysqlclientを使った自作ApacheModule mod_db.c
mysqlclient,prepared stmtを使ったApache Moduleのmod_dbは以下になります。完全なる自作なのであまり自信が無いですが、並列テストをしてもSegmentation Faultは発生しませんでした。下のソースではbindしたいparameterとresultをmemsetで設定するところが分かりづらいのと、Pointerの設定を間違えてmemory errorを起こし易いので注意が必要です。このプログラムの駄目な点はmysql_real_connectで都度Connectionしているので、接続のOverHeadが大きいところです。実際にabでbenchmarkを取ってみるとCommand Connectが多く発生していますし、netstatでも大量のTIME_WAITが出てます。都度接続の方式で頑張る場合は、TIME_WAITが残り続けると実行Port不足に陥って処理が進まなくなる可能性があるので、/etc/sysctlのnet.ipv4.tcp_tw_recycle=1に設定してリサイクルを速く回す設定を入れるか、net.ipv4.ip_local_port_range = 1024 65535のように使用可能なPortを増やすかのどちらかが有効になりそうです。ただしsysctlの設定をチューニングする場合は十分にテストすると良いと思います。
$ sudo yum install mysql-devel -y $ sudo vim /etc/httpd/conf/httpd.conf <IfModule mod_db.c> DBHost localhost DBPort 3306 DBUser root DBPass root DBName helloworld DBTableName hello </IfModule> $ sudo apxs -i -a -c -I /usr/include/mysql -L /usr/lib64/mysql -lmysqlclient mod_db.c $ sudo /etc/init.d/httpd restart $ ab -n 10000 -c 30 "http://localhost/" Server Software: Apache/2.2.15 Server Hostname: localhost Server Port: 80 Document Path: / Document Length: 68 bytes Concurrency Level: 30 Time taken for tests: 25.316 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 2352350 bytes HTML transferred: 680680 bytes Requests per second: 395.01 [#/sec] (mean) Time per request: 75.948 [ms] (mean) Time per request: 2.532 [ms] (mean, across all concurrent requests) Transfer rate: 90.74 [Kbytes/sec] received $ mysql> SHOW PROCESSLIST; +------+----------------------+-----------------+------+---------+------+-------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +------+----------------------+-----------------+------+---------+------+-------+------------------+ | 443 | root | localhost | NULL | Query | 0 | NULL | SHOW PROCESSLIST | | 9824 | unauthenticated user | connecting host | NULL | Connect | NULL | login | NULL | | 9825 | unauthenticated user | connecting host | NULL | Connect | NULL | login | NULL | | 9826 | unauthenticated user | connecting host | NULL | Connect | NULL | login | NULL | | 9827 | unauthenticated user | connecting host | NULL | Connect | NULL | login | NULL | +------+----------------------+-----------------+------+---------+------+-------+------------------+ $ sudo netstat -antp tcp 0 0 ::1:80 ::1:50593 TIME_WAIT - tcp 0 0 ::1:80 ::1:52086 TIME_WAIT - tcp 0 0 ::1:80 ::1:52864 TIME_WAIT - tcp 0 0 ::1:80 ::1:51305 TIME_WAIT - tcp 0 0 ::1:80 ::1:52557 TIME_WAIT - tcp 0 0 ::1:80 ::1:53047 TIME_WAIT - tcp 0 0 ::1:80 ::1:49646 TIME_WAIT - tcp 0 1 ::1:80 ::1:53489 FIN_WAIT1 - #設定を変える場合は十分にテストが必要 $ sudo vim /etc/sysctl.conf net.ipv4.tcp_tw_recycle = 1 net.ipv4.ip_local_port_range = 1024 65535 $ sudo sysctl -p#include "httpd.h" #include "http_config.h" #include "http_protocol.h" #include "apr_hash.h" #include "apr_tables.h" #include "apr_strings.h" #include "ap_config.h" #include "util_script.h" #include "http_log.h" #include "mysql/mysql.h" #include "stdio.h" #include "stdlib.h" module AP_MODULE_DECLARE_DATA db_module; typedef struct { int port; MYSQL *conn; MYSQL_STMT *stmt; char *host; char *user; char *pass; char *name; char *table_name; } db_env; /* The db handler */ static int db_handler(request_rec *r) { // connect db_env *db = ap_get_module_config(r->per_dir_config, &db_module); db->conn = mysql_init(NULL); if (!mysql_real_connect(db->conn, db->host, db->user, db->pass, db->name, db->port, NULL, 0)) { mysql_close(db->conn); return HTTP_INTERNAL_SERVER_ERROR; } char *query = (char *)apr_psprintf(r->pool,"SELECT id,title,created_at FROM %s.%s WHERE id = ?", db->name, db->table_name); db->stmt = mysql_stmt_init(db->conn); if (mysql_stmt_prepare(db->stmt, query, strlen(query)) != 0) { goto STMTError; } // bind MYSQL_BIND bind[1]; int id = 1; memset(bind, 0, sizeof(id)); bind[0].buffer = &id; bind[0].buffer_type = MYSQL_TYPE_LONG; bind[0].buffer_length = sizeof(id); bind[0].is_null = 0; if (mysql_stmt_bind_param(db->stmt,bind) != 0 || mysql_stmt_execute(db->stmt) != 0) { goto STMTError; } // bind_result MYSQL_BIND result[3]; memset(result, 0, sizeof(result)); int rid; result[0].buffer = &rid; result[0].buffer_type = MYSQL_TYPE_LONG; result[0].buffer_length = sizeof(rid); result[0].is_null = 0; char title[100]; result[1].buffer = &title; result[1].buffer_type = MYSQL_TYPE_STRING; result[1].buffer_length = sizeof(title); result[1].is_null = 0; MYSQL_TIME ts; result[2].buffer = &ts; result[2].buffer_type = MYSQL_TYPE_DATETIME; result[2].buffer_length = sizeof(ts); result[2].is_null = 0; // bind_result if (mysql_stmt_bind_result(db->stmt,result) != 0 || mysql_stmt_store_result(db->stmt) != 0) { goto STMTError; } // stmt_fetch while (!mysql_stmt_fetch(db->stmt)) { char *time_str = (char *)apr_psprintf(r->pool, "%04d-%02d-%02d %02d:%02d:%02d", ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second); ap_rprintf(r, "id: %d, title: %s, created_at: %s\n", rid, title, time_str); } // close mysql_stmt_close(db->stmt); mysql_close(db->conn); return OK; STMTError: mysql_stmt_close(db->stmt); mysql_close(db->conn); return HTTP_INTERNAL_SERVER_ERROR; } /* make db dir */ static void *make_db_dir(apr_pool_t *p, char *d){ db_env *db; db = (db_env *) apr_pcalloc(p, sizeof(db_env)); return db; } /* * Set the value for the 'DBHost' attribute. */ static const char *set_db_host(cmd_parms *cmd, void *mconfig, const char *name){ db_env *db; db = (db_env *) mconfig; db->host = ap_getword_conf(cmd->pool, &name); return NULL; } /* * Set the value for the 'DBUser' attribute. */ static const char *set_db_user(cmd_parms *cmd, void *mconfig, const char *user){ db_env *db; db = (db_env *) mconfig; db->user = ap_getword_conf(cmd->pool, &user); return NULL; } /* * Set the value for the 'DBPass' attribute. */ static const char *set_db_pass(cmd_parms *cmd, void *mconfig, const char *pass){ db_env *db; db = (db_env *) mconfig; db->pass = ap_getword_conf(cmd->pool, &pass); return NULL; } /* * Set the value for the 'DBPort' attribute. */ static const char *set_db_port(cmd_parms *cmd, void *mconfig, const char *port){ db_env *db; db = (db_env *) mconfig; db->port = *(int *)ap_getword_conf(cmd->pool, &port); return NULL; } /* * Set the value for the 'DBName' attribute. */ static const char *set_db_name(cmd_parms *cmd, void *mconfig, const char *name){ db_env *db; db = (db_env *) mconfig; db->name = ap_getword_conf(cmd->pool, &name); return NULL; } /* * Set the value for the 'DBTable' attribute. */ static const char *set_db_table(cmd_parms *cmd, void *mconfig, const char *table){ db_env *db; db = (db_env *) mconfig; db->table_name = ap_getword_conf(cmd->pool, &table); return NULL; } static const command_rec db_conf_cmds[] = { AP_INIT_TAKE1("DBHost", set_db_host, NULL, OR_FILEINFO, "db hostname"), AP_INIT_TAKE1("DBPort", set_db_port, NULL, OR_FILEINFO, "db port"), AP_INIT_TAKE1("DBUser", set_db_user, NULL, OR_FILEINFO, "db username"), AP_INIT_TAKE1("DBPass", set_db_pass, NULL, OR_FILEINFO, "db password"), AP_INIT_TAKE1("DBName", set_db_name, NULL, OR_FILEINFO, "db name"), AP_INIT_TAKE1("DBTableName", set_db_table, NULL, OR_FILEINFO, "db tablename"), {NULL} }; static void db_register_hooks(apr_pool_t *p){ ap_hook_handler(db_handler, NULL, NULL, APR_HOOK_MIDDLE); } /* Dispatch list for API hooks */ module AP_MODULE_DECLARE_DATA db_module = { STANDARD20_MODULE_STUFF, make_db_dir, /* create per-dir config structures */ NULL, /* merge per-dir config structures */ NULL, /* create per-server config structures */ NULL, /* merge per-server config structures */ db_conf_cmds, /* table of config file commands */ db_register_hooks /* register hooks */ };mod_dbdを使った自作ApacheModule mod_dbdsample.c
mod_dbの問題を回避する為にmod_dbdを利用します。利用する際はapr-util-mysqlというパッケージが必要なのでyumで取得します。mod_dbdはConnectionPoolを作成し接続を永続化しますし、PreparedStatementの機能も持ち合わせています。mod_dbdはぐぐってもさほど有益な情報が得られないので、注意してください。凄く分かりづらい箇所としてはPrepareする際のBind Parameterの指定が"?"ではなく"%s"となる事です。更にはStatementの結果となるresと取得行のrowを初期化としてNULLしておかないと子プロセスでSegmentation Faultを引き起こします。上のmod_dbに比べてPerformanceが若干改善していますが、期待していた以上の働きをしていないため何とも言えない状況でした。MysqlのCommandはSleepになって待機状態なのが分かります。またnetstatでも少ないPortがESTABLISHEDとなっています。
$ sudo yum install apr-util-mysql -y $ sudo vim /etc/httpd/conf/httpd.conf #有効化 LoadModule dbd_module modules/mod_dbd.so #追記 <IfModule mod_dbdsample.c> DBDriver mysql DBDParams "host=localhost,user=root,pass=root,dbname=helloworld" DBDPersist ON DBDKeep 5 DBDMax 10 DBDMin 3 DBDExptime 300 </IfModule> $ sudo apxs -i -a -c mod_dbdsample.c $ sudo /etc/init.d/httpd restart $ ab -n 10000 -c 30 "http://localhost/" Server Software: Apache/2.2.15 Server Hostname: localhost Server Port: 80 Document Path: / Document Length: 68 bytes Concurrency Level: 30 Time taken for tests: 19.113 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 2352820 bytes HTML transferred: 680816 bytes Requests per second: 523.20 [#/sec] (mean) Time per request: 57.340 [ms] (mean) Time per request: 1.911 [ms] (mean, across all concurrent requests) Transfer rate: 120.21 [Kbytes/sec] received mysql> SHOW PROCESSLIST; +-------+------+-----------+------------+---------+------+-------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +-------+------+-----------+------------+---------+------+-------+------------------+ | 443 | root | localhost | NULL | Query | 0 | NULL | SHOW PROCESSLIST | | 24215 | root | localhost | helloworld | Sleep | 294 | | NULL | | 24216 | root | localhost | helloworld | Sleep | 294 | | NULL | | 24217 | root | localhost | helloworld | Sleep | 271 | | NULL | | 24218 | root | localhost | helloworld | Sleep | 294 | | NULL | | 24220 | root | localhost | helloworld | Sleep | 294 | | NULL | | 24221 | root | localhost | helloworld | Sleep | 271 | | NULL | $ sudo netstat -antp tcp 0 0 192.168.56.10:22 192.168.56.1:52664 ESTABLISHED 2161/sshd tcp 0 0 192.168.56.10:22 192.168.56.1:52626 ESTABLISHED 2100/sshd tcp 0 0 192.168.56.10:22 192.168.56.1:52333 ESTABLISHED 1511/sshd tcp 0 0 192.168.56.10:22 192.168.56.1:52332 ESTABLISHED 1494/sshd#include "httpd.h" #include "http_config.h" #include "http_protocol.h" #include "http_log.h" #include "ap_config.h" #include "apr_dbd.h" #include "mod_dbd.h" #include "apr_strings.h" typedef struct{ apr_dbd_results_t *res; apr_dbd_row_t *row; apr_dbd_prepared_t *stmt; ap_dbd_t *dbd; } struct_dbd; static int dbdsample_handler(request_rec *r) { struct_dbd *db = (struct_dbd *)apr_palloc(r->pool, sizeof(struct_dbd)); db->res = NULL; db->row = NULL; db->stmt = NULL; if((db->dbd = ap_dbd_acquire(r)) == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } const char* query = "SELECT id,title,created_at FROM helloworld.hello WHERE id = %s"; if(apr_dbd_prepare(db->dbd->driver, r->pool, db->dbd->handle, query, NULL, &db->stmt) != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error %s.", apr_dbd_error(db->dbd->driver, db->dbd->handle, errno)); return HTTP_INTERNAL_SERVER_ERROR; } const char **params = (const char**)apr_palloc(r->pool,(1*sizeof(char *))); *params = "1"; if(apr_dbd_pselect(db->dbd->driver, r->pool, db->dbd->handle, &db->res, db->stmt, 0, 1, params) != 0) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Error %s.", apr_dbd_error(db->dbd->driver, db->dbd->handle, errno)); return HTTP_INTERNAL_SERVER_ERROR; } while(!apr_dbd_get_row(db->dbd->driver, r->pool, db->res, &db->row, -1)) { const char* id = apr_dbd_get_entry(db->dbd->driver, db->row, 0); const char* title = apr_dbd_get_entry(db->dbd->driver, db->row, 1); const char* created_at = apr_dbd_get_entry(db->dbd->driver, db->row, 2); ap_rprintf(r, "id: %s, title: %s, created_at: %s\n", id, title, created_at); } return OK; } static void dbdsample_register_hooks(apr_pool_t *p) { ap_hook_handler(dbdsample_handler, NULL, NULL, APR_HOOK_MIDDLE); } /* Dispatch list for API hooks */ module AP_MODULE_DECLARE_DATA dbdsample_module = { STANDARD20_MODULE_STUFF, NULL, /* create per-dir config structures */ NULL, /* merge per-dir config structures */ NULL, /* create per-server config structures */ NULL, /* merge per-server config structures */ NULL, /* table of config file commands */ dbdsample_register_hooks /* register hooks */ };
速いよ Java Play Framework
言語とFrameworkの選定
phpにはあまり魅力を感じていない@yutakikuchi_です。本題とは関係ありませんが4.25(金)@ヒカリエのイベントに登壇します。ネタは同窓会GrowthHackとログ集計/解析の2本立てです。興味のある方はどうぞ。【ヒカ☆ラボ】同窓会GrowthHack!×データログ集計、解析!をテーマに事例をまじえお話します! 16年ぶりの再会でも参加率6割の同窓会を開くには?Yahoo出身のエンジニアが語る、アクセスログ可視化、 ユーザ属性解析を行うためのシステム設計のコツとは?
Round 8 results - TechEmpower Framework Benchmarks
さて、本題に入ります。僕がphpを書き始めたのも前職のmain言語として指定されていたことがあり、あまり書いていて楽しく無いとは思っていながらも泣く泣く仕事としてやってい感じです。過去に何回かphpのエントリー書いてますけどそれも仕事で利用する為です。前職は本当にphperの集まりでWebだけじゃなくてバッチ処理やスクリプト処理も全てphpで書こうとする姿勢を初めて見たときは驚愕しました。まぁ速くコードを書くならそれでもいいんでしょうけど。
言語とFrameworkの選定にはドキュメント量、言語のCommitterや精通者がいる、必要なライブラリや機能がある、書き易い、チームの多数決等が基準となり決定されるケースが多いと感じます。僕なら「処理速度が速い」を正義とし、それに掛け合わせる形で「書き易さ」で選ぶと思います。時間コストを考える場合、もし新しい言語とFrameworkの導入で2人月掛かったとしても(2人月掛かることが許される場合)、Daily100万PVのサイトで0.2secリクエストが速くなったとしたら1日の処理削減コストは100万*0.2/(3600*24)の2.3人日。ということは一か月で導入コストの2人月は巻き返せる事になります。更にはユーザーがアプリを使った時の満足感も上がるはずですし。
僕は今Frameworkを使わずにCを書いていてphpの3倍以上の速さが出ているので満足しているんですが、書き易さの点からは本当に最悪な状態。メモリの動的確保/解放や配列処理を本当に間違える...まぁそんな事で処理が速いとされ、C言語より書き易いJava,ScalaやGoのFrameworkを少しずつ勉強かつ紹介していけたらなと思い、今日はPlay Frameworkについて書きます。Play Frameworkはソース更新後の最初のアクセスで自動的にJavaをrebuildしてくれるようなのでコンパイルの手間が省けてとても便利です。
Play Framework
環境の確認と設定
CentOSは6.4、Javaはjava-1.7.0-openjdk、playは2.2.2を使っています。
$ cat /etc/system-release CentOS release 6.4 (Final) $ yum list installed | grep java java-1.5.0-gcj.x86_64 1.5.0.0-29.1.el6 java-1.7.0-openjdk.x86_64 java-1.7.0-openjdk-devel.x86_64 java_cup.x86_64 1:0.10k-5.el6 @base tzdata-java.noarch 2014a-1.el6 @updates $ java -version java version "1.7.0_51" OpenJDK Runtime Environment (rhel-2.4.4.1.el6_5-x86_64 u51-b02) OpenJDK 64-Bit Server VM (build 24.45-b08, mixed mode) $ alternatives --display java /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java - 優先項目 170051 スレーブ keytool: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/keytool スレーブ orbd: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/orbd スレーブ pack200: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/pack200 スレーブ rmid: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/rmid スレーブ rmiregistry: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/rmiregistry スレーブ servertool: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/servertool スレーブ tnameserv: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/tnameserv スレーブ unpack200: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/unpack200 スレーブ jre_exports: /usr/lib/jvm-exports/jre-1.7.0-openjdk.x86_64 スレーブ jre: /usr/lib/jvm/jre-1.7.0-openjdk.x86_64 スレーブ java.1.gz: /usr/share/man/man1/java-java-1.7.0-openjdk.1.gz スレーブ keytool.1.gz: /usr/share/man/man1/keytool-java-1.7.0-openjdk.1.gz スレーブ orbd.1.gz: /usr/share/man/man1/orbd-java-1.7.0-openjdk.1.gz スレーブ pack200.1.gz: /usr/share/man/man1/pack200-java-1.7.0-openjdk.1.gz スレーブ rmid.1.gz: /usr/share/man/man1/rmid-java-1.7.0-openjdk.1.gz スレーブ rmiregistry.1.gz: /usr/share/man/man1/rmiregistry-java-1.7.0-openjdk.1.gz スレーブ servertool.1.gz: /usr/share/man/man1/servertool-java-1.7.0-openjdk.1.gz スレーブ tnameserv.1.gz: /usr/share/man/man1/tnameserv-java-1.7.0-openjdk.1.gz スレーブ unpack200.1.gz: /usr/share/man/man1/unpack200-java-1.7.0-openjdk.1.gz 現在の「最適」バージョンは /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java です。 $ vi ~/.zshrc #2行追記 JAVA_HOME=/usr/lib/jvm/java-1.7.0/ export JAVA_HOME PATH=${JAVA_HOME}/bin:$PATH:/home/yuta/work/src/play/play-2.2.2/:/home/yuta/work/src/sbt/sbt/bin/ export CLASSPATH=$JAVA_HOME/jre/lib/ext:$JAVA_HOME/lib/tools.jar $ type javac javac is /usr/bin/javacinstallと実行
$ wget "http://downloads.typesafe.com/play/2.2.2/play-2.2.2.zip" $ unzip play-2.2.2.zip $ cd play-2.2.2 # helloworldプロジェクトを作成 $./play new helloworld _ _ __ | | __ _ _ _ | '_ \| |/ _' | || | | __/|_|\____|\__ / |_| |__/ play 2.2.2 built with Scala 2.10.3 (running Java 1.7.0_51), http://www.playframework.com The new application will be created in /home/yuta/work/src/play/play-2.2.2/helloworld What is the application name? [helloworld] > helloworld Which template do you want to use for this new application? 1 - Create a simple Scala application 2 - Create a simple Java application # ここではJavaを選択 > 2 OK, application helloworld is created. Have fun! $ cd helloworld $ ../play run ../play run Getting org.scala-sbt sbt 0.13.0 ... :: retrieving :: org.scala-sbt#boot-app confs: [default] 43 artifacts copied, 0 already retrieved (12440kB/579ms) [info] Loading project definition from /home/yuta/work/src/play/play-2.2.2/helloworld/project [info] Set current project to helloworld (in build file:/home/yuta/work/src/play/play-2.2.2/helloworld/) [info] Updating {file:/home/yuta/work/src/play/play-2.2.2/helloworld/}helloworld... [info] Resolving org.fusesource.jansi#jansi;1.4 ... [info] Done updating. --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Ctrl+D to stop and go back to the console...) [info] Compiling 4 Scala sources and 2 Java sources to /home/yuta/work/src/play/play-2.2.2/helloworld/target/scala-2.10/classes... [info] Error occurred during initialization of VM [info] Could not reserve enough space for object heap [error] Error: Could not create the Java Virtual Machine. [error] Error: A fatal exception has occurred. Program will exit.初回実行時はJavaのheapエラーで怒られてしまいました。play-2.2.2/framework/buildファイルのheap領域指定のオプションを修正します。僕は自分の環境に合せて-Xmx1536Mから-Xmxを768Mに修正しました。ぐぐると同じディレクトリにあるbuild.batを修正する内容も見かけるので、そちらの-Xmxも合せて修正しておくと良いと思います。修正後にplay runで問題なく起動します。9000ポートで見れるように解放も忘れないようにしましょう。
$ cd framework $ vi build # Xmxを768Mに修正 "$JAVA" ${DEBUG_PARAM} -Xms512M -Xmx768M -Xss1M -XX:ReservedCodeCacheSize=192m -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=512M ${JAVA_OPTS} -Dfile.encoding=UTF-8 -Dplay.version="${PLAY_VERSION}" -Dplay.home=`dirname $0` -Dsbt.boot.properties=`dirname $0`/sbt/sbt.boot.properties -Dsbt.scala.version=${SBT_SCALA_VERSION} ${PLAY_OPTS} -jar `dirname $0`/sbt/sbt-launch.jar "$@" # playの再起動 $ cd helloworld $ play run --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Ctrl+D to stop and go back to the console...) [info] play - Application started (Dev) $ sudo vim /etc/sysconfig/iptables # 追加 -A INPUT -m state --state NEW -m tcp -p tcp --dport 9000 -j ACCEPT $ sudo service iptables restart
Practice
JavaHome(日本語)
JavaHome(English)目的
以下ではDBからデータを引っ張ってViewに表示するまでの一連の処理をJava Play Frameworkで記載します。
File layout
appの下にMVCのロジックを書くようですが、本家のドキュメントにあるようなmodelsディレクトリが生成されません。ちなみにですが、日本語のplay framewokのドキュメントは2.1.5までのversionしかないので、新しいドキュメントを参照したい場合は本家を見た方が良いです。modelsディレクトリは自分でapp以下に付け足して以下のlayoutにします。また後で追加するjar系のlibraryを配置するlibも無いので追加します。
$ pwd /home/yuta/work/src/play/play-2.2.2/helloworld $ tree -L 2 ├── README ├── app │ ├── controllers │ ├── models │ └── views ├── build.sbt ├── conf │ ├── application.conf │ └── routes ├── lib ├── logs │ └── application.log ├── project │ ├── Build.scala │ ├── build.properties │ ├── plugins.sbt │ ├── project │ └── target ├── public │ ├── images │ ├── javascripts │ └── stylesheets ├── target │ ├── native_libraries │ ├── resolution-cache │ ├── scala-2.10 │ └── streams └── test ├── ApplicationTest.java └── IntegrationTest.javamysqlの設定
お試しなので簡単にid,titile,created_at,updated_atのカラムを持つテーブルを定義し、2つデータをINSERTします。
DROP TABLE helloworld.hello; CREATE TABLE helloworld.hello( id INT(11) NOT NULL AUTO_INCREMENT, title VARCHAR(64) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id) ); INSERT INTO helloworld.hello(title,created_at,updated_at) VALUES('This is Hello World!', NOW(), NOW()); INSERT INTO helloworld.hello(title,created_at,updated_at) VALUES('World is not Enough!', NOW(), NOW());Modelの用意
上のTableと接続するDBModelをmodels/Hello.javaという名前で保存します。一般的なDBModelでpropertyにカラム名、メソッドにデータやり取りの関数を記載します。またここでは紹介しませんがJavaのコードで良く見かけるConnection connection = DB.getConnection();connection.createStatement().execute();のように手続き型でもDBからデータを取得する事は可能なようです。
package models; import java.util.*; import play.db.ebean.*; import play.data.validation.Constraints.*; import javax.persistence.*; @Entity public class Hello extends Model { @Id public Integer id; @Column public String title; @Column public Date created_at; @Column public Date updated_at; public static Finder<Long,Hello> find = new Finder(Long.class, Hello.class); public static List<Hello> all() { return find.all(); } }mysql driver
Play FrameworkのdefaultでのDBEngineはh2になっているのでmysqlに変更します。conf/application.confの設定を以下のように変更します。またproject/Build.scalaというファイルを作成しmysql-connector-javaを依存として記述するとplayの再起動時にdownloadしてくれます。ただしdownload先がProjectの一つ上のディレクトリのrepositoryに配置されるので、lib以下にコピーします。準備が整ったらplay runにて再起動をしてアクセスをしてみます。Database 'default' needs evolution!というerror画面が出てもApply This Script Now!というボタンを押下し、更に先の画面でMart it resolvedというボタンを押下すれば処理が先に進みます。
$ vi conf/application.conf db.default.driver=com.mysql.jdbc.Driver db.default.url="jdbc:mysql://localhost/helloworld?characterEncoding=UTF8" db.default.user=root db.default.password="" ebean.default="models.*" $ vi project/Build.scala import sbt._ import Keys._ import play.Project._ object ApplicationBuild extends Build { val appName = "helloworld" val appVersion = "1.0-SNAPSHOT" val appDependencies = Seq( // Add your project dependencies here, javaCore, javaJdbc, javaEbean, "mysql" % "mysql-connector-java" % "5.1.20" ) val subProject = Project("subProject",file("subProject-dir")) val main = play.Project(appName, appVersion, appDependencies, path = file("playProject")) .dependsOn(subProject) } $ cp ../repository/cache/mysql/mysql-connector-java/jars/mysql-connector-java-5.1.20.jar lib/ $ play run --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Ctrl+D to stop and go back to the console...) [info] Updating {file:/home/yuta/work/src/play/play-2.2.2/helloworld/}subProject... [info] Resolving org.fusesource.jansi#jansi;1.4 ... [info] Done updating. [info] Updating {file:/home/yuta/work/src/play/play-2.2.2/helloworld/}helloworld... [info] Resolving org.fusesource.jansi#jansi;1.4 ... [info] downloading http://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.20/mysql-connector-java-5.1.20.jar ... [info] [SUCCESSFUL ] mysql#mysql-connector-java;5.1.20!mysql-connector-java.jar (3416ms) [info] Done updating. [error] application - ! @6hlo1ai71 - Internal server error, for (GET) [/] -> play.api.db.evolutions.InvalidDatabaseRevision: Database 'default' needs evolution![An SQL script need to be run on your database.] (略) ! @6hlo29f7h - Internal server error, for (GET) [/@evolutions/apply/default?redirect=http%3A%2F%2Flocalhost%3A9000%2F] -> play.api.db.evolutions.InconsistentDatabase: Database 'default' is in an inconsistent state![An evolution has not been applied properly. Please check the problem and resolve it manually before marking it as resolved.]Controller
Controllerのメソッド呼び出しはconf/routesで決定されます。GET / controllers.Application.index()という記述が/にアクセスした時にcontrollersのApplicationクラスのindexメソッドを呼び出すという定義です。Frameworkによくあるお決まりのやつですね。上で定義したHelloModelのallメソッドを呼び出してDBに格納されているデータの一覧を取得してViewに渡します。return ok();の記述でStatus:200OKを返すようです。okの中のhelloworld.render(hello)でhelloworldのviewに渡すデータを書きます。ここではList型を渡します。
package controllers; import java.util.*; import play.*; import views.html.*; import models.*; public class Application extends Controller { public static Result index() { List<Hello> hello = Hello.all(); return ok(helloworld.render(hello)); } }View
Play FrameworkはMVCのMCはJavaで書けてもViewだけはScala文法なのでそこだけは新しく勉強が必要ですが、記述がとても簡単なので難なくこなせると思います。htmlタグの中への記述もとてもSimpleに書けて楽しいです。下のファイルをviews/helloworld.scala.htmlという名前で保存します。これでDBに入れたデータが表示できるので一通りModel=>Controller=>Viewへのデータのやり取りを記載する事ができました。
@(data: List[Hello]) <!DOCTYPE html> <html> <head></head> <body> @for(node <- data) { <p>id: @node.id</p> <p>title:@node.title</p> <p>created_at:@node.created_at</p> <p>updated_at:@node.updated_at</p> } </body> </html>
Performance
php
Frameworkを使わずにmysqlに接続してデータを取得する処理をScriptとして記載したものをabスクリプトで実行した結果です。rpsは176.39となりました。
<!DOCTYPE html> <html> <head></head> <body> <?php $link = mysqli_connect('localhost', 'root', '', 'helloworld'); $query = 'SELECT id,title,created_at,updated_at FROM hello'; $result = $link->query($query); while($row = mysqli_fetch_array($result)) { echo '<p>' . $row['id'] . '</p><br/>'; echo '<p>' . $row['title'] . '</p><br/>'; echo '<p>' . $row['created_at'] . '</p><br/>'; echo '<p>' . $row['updated_at'] . '</p><br/>'; } mysqli_close($link); ?> </body> </html>$ ab -n 50000 -c 100 "http://localhost/helloworld.php" Server Software: Apache/2.2.15 Server Hostname: localhost Server Port: 80 Document Path: /helloworld.php Document Length: 274 bytes Concurrency Level: 100 Time taken for tests: 283.461 seconds Complete requests: 50000 Failed requests: 0 Write errors: 0 Total transferred: 23414976 bytes HTML transferred: 13708768 bytes Requests per second: 176.39 [#/sec] (mean) Time per request: 566.922 [ms] (mean) Time per request: 5.669 [ms] (mean, across all concurrent requests) Transfer rate: 80.67 [Kbytes/sec] receivedJava Play Framework
上で設定したJava Play FrameworkのDB接続をそのまま利用してPerfomanceを測定。rpsは432となり、Java Play Frameworkの勝利です。2.5倍ほどの実力差がでましたね。(※Play Frameworkの設定を良く理解していないのでもしかしたら有利な条件になっているかもしれないです。)
$ ab -n 50000 -c 100 "http://localhost:9000/" Server Software: Server Hostname: localhost Server Port: 9000 Document Path: / Document Length: 382 bytes Concurrency Level: 100 Time taken for tests: 115.527 seconds Complete requests: 50000 Failed requests: 0 Write errors: 0 Total transferred: 23100000 bytes HTML transferred: 19100000 bytes Requests per second: 432.80 [#/sec] (mean) Time per request: 231.055 [ms] (mean) Time per request: 2.311 [ms] (mean, across all concurrent requests) Transfer rate: 195.27 [Kbytes/sec] received
SolrのSpatial Searchを試してみた
前書き
10代の頃は(ゴースト)ライターという職業に憧れていた時期もありました@yutakikuchi_です。
Geospatial Indexes and Queries ― MongoDB Manual 2.4.9
MySQL :: MySQL 4.1 リファレンスマニュアル :: 10.6.1 空間インデックスの作成
位置情報IndexをMongoDBで管理する手法については前に調査済みで、mysqlにもSpatialindexはあまり普及していない印象、ということで...今日は検索SolrのSpatial Searchについて調べてみます。最終的にはFessやNutchでWebPageをCrawlingして得た住所データをGeocodingでLat/Lngデータに変換して自前のServerにIndexingしていく事を考えており、その前段階の作業です。Solrを選ぶ理由ですがSpatial Search以外にもTermVectorでの類似度を算出してくれるMoreLikeThisという機能があり、Lat/Lngデータの掛け合わせでコンテンツを面白くSuggestすることを考えています。MoreLikeThisについても調査したら書きますね。
Solr設定
java, tomcat6, Solr
javaとtomcat、Solr本体が必要なので以下の手順でInstallです。Solrは2014.3.15現在で最新のV4.7.0を取ってきます。僕が3年程前にSolrを使っていた時はV1.*とかだったので、もう過去の記憶や記録は役立たなさそうですね...
$ sudo yum install java-1.7.0-openjdk tomcat6 --enablerepo=remi $ wget "ftp://ftp.riken.jp/net/apache/lucene/solr/4.7.0/solr-4.7.0.tgz" $ tar xf solr-4.7.0.tgzPortfowarding
Solrのadminツールに接続する為の設定です。僕の場合はMacでVirtualBoxを立ち上げ、HostOSからGuestOS(CentOS)に接続してSolrを使っているのでVirtualBox内のPortfowardingとGuestOS側のFireWallの設定をします。VirtualBoxでは設定=>ネットワーク=>ポートフォワーディングで以下の画面に辿れます。Solrのdefaultportである8983を指定しておきます。※GuestOS側のIPアドレスをifconfigで調べて設定してください。
Firewall
下はHostOS側のFirewall設定です。
$ sudo vi /etc/sysconfig/iptables -A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT -A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT #追加 -A INPUT -m state --state NEW -m tcp -p tcp --dport 8983 -j ACCEPT $ sudo service iptables restartSolr-exampleの起動
Solrのexampleにある管理画面を表示してみます。tarで展開したディレクトリ以下にstart.jarファイルがあるのでそれを実行します。下のコマンドを実行してhttp://localhost:8983/solrにアクセスします。start.jarの実行時もそうですが、色々とエラーがでていますが取りあえずは画面を見る事ができます。
$ cd solr-4.7.0/example $ java -jar start.jar 0 [main] INFO org.eclipse.jetty.server.Server – jetty-8.1.10.v20130312 41 [main] INFO org.eclipse.jetty.deploy.providers.ScanningAppProvider – Deployment monitor /home/yuta/work/src/solr/solr-4.7.0/example/contexts at interval 0 50 [main] INFO org.eclipse.jetty.deploy.DeploymentManager – Deployable added: /home/yuta/work/src/solr/solr-4.7.0/example/contexts/solr-jetty-context.xml 1872 [main] INFO org.eclipse.jetty.webapp.StandardDescriptorProcessor – NO JSP Support for /solr, did not find org.apache.jasper.servlet.JspServlet (略)exampledocsの追加
展開したSolrのディレクトリにexampledocsがあるのでSolrに追加してみます。xmlの中身を見てみるとPCパーツや電化製品の情報みたいです。post.shを実行するとcollection1という新しいコレクションが生成され、32個のドキュメントが追加されます。indexのデータはsolr-4.7.0/example/solr/collection1/data/indexに生成されます。
$ cd solr-4.7.0/example/exampledocs $ cat vidcard.xml <add> <doc> <field name="id">EN7800GTX/2DHTV/256M</field> <field name="name">ASUS Extreme N7800GTX/2DHTV (256 MB)</field> <!-- Denormalized --> <field name="manu">ASUS Computer Inc.</field> <!-- Join --> <field name="manu_id_s">asus</field> <field name="cat">electronics</field> <field name="cat">graphics card</field> <field name="features">NVIDIA GeForce 7800 GTX GPU/VPU clocked at 486MHz</field> <field name="features">256MB GDDR3 Memory clocked at 1.35GHz</field> <field name="features">PCI Express x16</field> <field name="features">Dual DVI connectors, HDTV out, video input</field> <field name="features">OpenGL 2.0, DirectX 9.0</field> <field name="weight">16</field> <field name="price">479.95</field> <field name="popularity">7</field> <field name="store">40.7143,-74.006</field> <field name="inStock">false</field> <field name="manufacturedate_dt">2006-02-13T15:26:37Z/DAY</field> </doc> $ ./post.sh *.xml Posting file gb18030-example.xml to http://localhost:8983/solr/update <?xml version="1.0" encoding="UTF-8"?> <response> <lst name="responseHeader"><int name="status">0</int><int name="QTime">164</int></lst> </response> (略) $ ls solr-4.7.0/example/solr/collection1/data/index -rw-rw-r--. 1 yuta yuta 4928 3月 16 00:55 2014 _0.fdt -rw-rw-r--. 1 yuta yuta 45 3月 16 00:55 2014 _0.fdx -rw-rw-r--. 1 yuta yuta 2300 3月 16 00:55 2014 _0.fnm -rw-rw-r--. 1 yuta yuta 250 3月 16 00:55 2014 _0.nvd -rw-rw-r--. 1 yuta yuta 112 3月 16 00:55 2014 _0.nvm -rw-rw-r--. 1 yuta yuta 395 3月 16 00:55 2014 _0.si -rw-rw-r--. 1 yuta yuta 144 3月 16 00:55 2014 _0.tvd -rw-rw-r--. 1 yuta yuta 45 3月 16 00:55 2014 _0.tvx -rw-rw-r--. 1 yuta yuta 1047 3月 16 00:55 2014 _0_Lucene41_0.doc -rw-rw-r--. 1 yuta yuta 34 3月 16 00:55 2014 _0_Lucene41_0.pay -rw-rw-r--. 1 yuta yuta 2204 3月 16 00:55 2014 _0_Lucene41_0.pos -rw-rw-r--. 1 yuta yuta 14134 3月 16 00:55 2014 _0_Lucene41_0.tim -rw-rw-r--. 1 yuta yuta 712 3月 16 00:55 2014 _0_Lucene41_0.tip -rw-rw-r--. 1 yuta yuta 20 3月 16 00:55 2014 segments.gen -rw-rw-r--. 1 yuta yuta 110 3月 16 00:55 2014 segments_2 -rw-rw-r--. 1 yuta yuta 0 3月 16 00:25 2014 write.lockquery実行
SolrはREST形式なのでUPDATEやSELECTも基本的にはcurlにて行います。solr-4.7.0/example/exampledocsにはtest_utf8.shというテストスクリプトがあるのでこれを参考に実行してみます。test_utf8.shの実行ではERROR:と表示されなければ問題ないと判断できます。test_utf8.sh中に記載されているcurlのスクリプトを真似てqueryをASUS、outputをjsonとしてSolrからのresponseを確認します。
$ ./test_utf8.sh Solr server is up. HTTP GET is accepting UTF-8 HTTP POST is accepting UTF-8 HTTP POST defaults to UTF-8 HTTP GET is accepting UTF-8 beyond the basic multilingual plane HTTP POST is accepting UTF-8 beyond the basic multilingual plane HTTP POST + URL params is accepting UTF-8 beyond the basic multilingual plane Response correctly returns UTF-8 beyond the basic multilingual plane $ curl "http://localhost:8983/solr/select?q=ASUS¶ms=explicit&wt=json" | python -mjson.tool { "response": { "docs": [ { "_version_": 1462657512030339072, "cat": [ "electronics", "graphics card" ], "features": [ "NVIDIA GeForce 7800 GTX GPU/VPU clocked at 486MHz", "256MB GDDR3 Memory clocked at 1.35GHz", "PCI Express x16", "Dual DVI connectors, HDTV out, video input", "OpenGL 2.0, DirectX 9.0" ], "id": "EN7800GTX/2DHTV/256M", "inStock": false, "manu": "ASUS Computer Inc.", "manu_id_s": "asus", "manufacturedate_dt": "2006-02-13T00:00:00Z", "name": "ASUS Extreme N7800GTX/2DHTV (256 MB)", "popularity": 7, "price": 479.94999999999999, "price_c": "479.95,USD", "store": "40.7143,-74.006", "weight": 16.0 } ], "numFound": 1, "start": 0 }, "responseHeader": { "QTime": 1, "params": { "params": "explicit", "q": "ASUS", "wt": "json" }, "status": 0 } }設定ファイル
solr-4.7.0/example/solr/collection1/conf/solrconfig.xmlに色々な記述が書かれているのでより深くexampleでのREST仕様を理解したい人は目を通してみると良いかもしれません。solr/selectにアクセスするとsolr.SearchHandlerClassを呼び出すなどの定義が書かれています。その他indexのschemaについてはsolr-4.7.0/example/solr/collection1/conf/schema.xmlで定義されています。bi-gram、tokenizer、filter等の指定がされています。
$ vi solr-4.7.0/example/solr/collection1/conf/solrconfig.xml <requestHandler name="/select" class="solr.SearchHandler"> <!-- default values for query parameters can be specified, these will be overridden by parameters in the request --> <lst name="defaults"> <str name="echoParams">explicit</str> <int name="rows">10</int> <str name="df">text</str> </lst> (略) $ vi solr-4.7.0/example/solr/collection1/conf/schema.xml <!-- CJK bigram (see text_ja for a Japanese configuration using morphological analysis) --> <fieldType name="text_cjk" class="solr.TextField" positionIncrementGap="100"> <analyzer> <tokenizer class="solr.StandardTokenizerFactory"/> <!-- normalize width before bigram, as e.g. half-width dakuten combine --> <filter class="solr.CJKWidthFilterFactory"/> <!-- for any non-CJK --> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.CJKBigramFilterFactory"/> </analyzer> </fieldType>solrpy
solrpy - Python Solr Module - Google Project Hosting
Reference ― solrpy v0.9.2 documentation
SolrがREST形式ならclient言語は別に何だっていいんですが、僕はJavaよりPythonが好きなのでおしゃべりする言語に使用します。solrpyというライブラリがありますが日本語ドキュメントは無いので、英語版を見ましょう。solrpyには2種類のclassがあってclass solr.Solr(url)、class solr.SolrConnection(url)になります。SolrConnectionの方が記述が分かり易いのですが、Solrの方が新しいという事で以下ではclass solr.Solrを使っています。
Solr-exampleのschema.xmlを少し書き換えて配列形式のauthorを登録するような処理を書いてみます。複数のauthorが登録できるようにmultiValued="true"にします。設定しないとsolrpyのスクリプト実行時にsolr.core.SolrException: HTTP code=400, reason=Bad Requestとerrorが出ます。
$ sudo easy_install solrpy (略) Installed /usr/lib/python2.6/site-packages/solrpy-0.9.6-py2.6.egg Processing dependencies for solrpy Finished processing dependencies for solrpy # 下のtest_solrpy.pyを実行 $ python test_solrpy.py (略) solr.core.SolrException: HTTP code=400, reason=Bad Requestとerror # errorを回避する為に修正 $ vi solr-4.7.0/example/solr/collection1/conf/schema.xml <!-- multiValued属性を"true"に設定 --> <field name="author" type="text_general" indexed="true" stored="true" multiValued="true"/> <!-- author_sを追加 --> <field name="author_s" type="text_general" indexed="true" stored="true" multiValued="true"/> # solrのrestart $ java -jar start.jar # 再度実行 $ python test_solrpy.py 1 [u'Lucene in Action'] [u'Erik Hatcher', u'Otis Gospodneti\u0107']#!/usr/bin/env python #coding:utf-8 #solrpyのテスト import solr # add a document to the index con = solr.Solr('http://localhost:8983/solr') doc ={'id':1, 'title':u'Lucene in Action', 'author':[u'Erik Hatcher', u'Otis Gospodneti〓']} con.add(doc,commit=True) # do a search response = con.select('title:lucene') for hit in response.results: print hit['id'],hit['title'],hit['author']
Spatial Search
設定
SpatialSearch - Solr Wiki
SpatialSearchについては上のwikiに詳しく載っています。内容に書かれているschema.xmlのfield name="store"、fieldType name="location"は既にexampleでは定義済みなので追記は不要だと思います。$ vi solr-4.7.0/example/solr/collection1/conf/schema.xml # 以下はexampleのfieldTypeにdefaultで記載済み <field name="store" type="location" indexed="true" stored="true"/> <fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>テスト
post.shにてindex化したデータに対してSpatial Searchを掛けてみると問題なく対象となるデータが抽出出来ている事が確認できます。ちょいとSpatial Searchとして指定するパラメータが複雑ですが纏めると下の表のようになります。
parameter meaning fl 結果として抽出するfield q query (下では*:*を指定) fq queryのfilter(下では{!geofilt}を指定) sfield Spatial Searchの対象field pt 検索位置のlatlng d ptからの検索対象距離を指定 wt 出力形式
$ curl "http://localhost:8983/solr/select?fl=name,store&q=*%3A*&fq=%7B%21geofilt%7D&sfield=store&pt=45.15,-93.85&d=5&wt=json" | python -mjson.tool { "response": { "docs": [ { "name": "Maxtor DiamondMax 11 - hard drive - 500 GB - SATA-300", "store": "45.17614,-93.87341" }, { "name": "Belkin Mobile Power Cord for iPod w/ Dock", "store": "45.18014,-93.87741" }, { "name": "A-DATA V-Series 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - OEM", "store": "45.18414,-93.88141" } ], "numFound": 3, "start": 0 }, "responseHeader": { "QTime": 0, "params": { "d": "5", "fl": "name,store", "fq": "{!geofilt}", "pt": "45.15,-93.85", "q": "*:*", "sfield": "store", "wt": "json" }, "status": 0 } }実用データでテスト
ソニア - 新橋/お好み焼き [食べログ]
広島風お好み焼きがとても美味しい上のお店のデータでテストしてみます。冒頭の写真はお店のお好み焼き(シングルというメニュー)です。新橋/汐留近辺ではとても有名なお店だと思うので近い方は一度行ってみてください。さて、YahooのGeocoderAPIを使ってお店の住所からlatlngを出し、SolrのIndexに格納しSpatialSearchをやってみます。
食べログの案内に汐留駅から372mと書かれており、パラメータd=0.3では抽出不可、d=0.4=抽出可能という点から正確にSpatialSearchが出来たと思います。#!/usr/bin/env python #coding:utf-8 #住所データからSpatial Indexを作成 #latlon.pyとして保存する import urllib,urllib2,json,solr opener = urllib2.build_opener() ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.51.22 (KHTML, like Gecko) Version/5.1.1 Safari/ 534.51.22' referer = 'http://www.yahoo.co.jp/' opener.addheaders = [('User-Agent', ua),('Referer', referer)] appid = 'Yahoo!' # yahooapiのappidをここに定義 address = u'東京都港区新橋5-15-1' try: url = 'http://geo.search.olp.yahooapis.jp/OpenLocalPlatform/V1/geoCoder?appid=%s&query=%s&output=%s' % (appid, urllib.quote_plus(address.encode('utf-8')), 'json') res = json.loads(opener.open(url).read()) if (res['ResultInfo']['Status'] == 200 and res['ResultInfo']['Count'] > 0 ): (lng,lat) = res['Feature'][0]['Geometry']['Coordinates'].split(',') latlng = lat + ',' + lng except urllib2.URLError: print "Error: API" try: con = solr.Solr('http://localhost:8983/solr') doc ={'id':'sonia_latlon', 'name':u'ソニア', 'store':latlng} con.add(doc,commit=True) #汐留駅からの0.5KMの距離で計算 response = con.select('*:*', 'id,name,store', '', 'true', 'id', 'desc', fq='{!geofilt}', sfield='store', d='0.4', pt='35.662800,139.760000') for hit in response.results: print hit['id'], hit['name'], hit['store'] except solr.SolrException: print "Error: Solr"$ python latlon.py sonia_latlon ソニア 35.66223082,139.75596233