仙台 Ruby Vim JavaScript社長

片平堂のブログ

VPSからAmazon S3とGoogle Apps for Businessに移行しました

さくらのVPSで運用していたkatahirado.jp(VALUE DOMAINで取得)を、WebサイトはAmazon S3に、メールはGoogle Apps for Businessにそれぞれ移行しました。
Webサイト、メールサーバの他に、redmine、git、Railsのデモアプリが動いていましたが、 git,redminegithubやPivotal Tracker等を使用することにして、既存のデータはとりあえずローカル(MBP)に。
Railsアプリのデモは動かすならHerokuかなと思っていますが、一旦保留としました。
S3とRoute 53,Google Appsでの作業をメモ。

Amazon S3

katahirado.jpはルートドメインでの静的なWebサイトなので、Amazon Route 53を使ってホストする必要がありました。
Amazon S3でのWebサイトですが、基本こちらの通りで。

www.katahirado.jpと、karahido.jpのバケットを作成しました。
なのですが、バケットポリシーを追加する時に、AWS Sample Bucket PolicyのWebページが丁度なくなっていたので、AWS Policy Generatorを利用してバケットポリシーを作成しました。*1

f:id:yuichi_katahira:20140418102545p:plain

Sample Bucket Policiesをクリックするとページがなくなってました。

f:id:yuichi_katahira:20140418102738p:plain

AWS Policy Generatorをクリック。

f:id:yuichi_katahira:20140418102545p:plain

Select Type of PolicyをS3 Bucket Policyにして、Principalを*に。

f:id:yuichi_katahira:20140418103603p:plain

ActionsでGetObjectにチェック。

f:id:yuichi_katahira:20140418103644p:plain

Amazon Resource Name (ARN)のところはご丁寧に formatが乗っているので、それに合わせて入力しました。

arn:aws:s3:::<bucket_name>/<key_name>

arn:aws:s3:::www.katahirado.jp/*

f:id:yuichi_katahira:20140418103750p:plain

Add StatementしてGenerate Policyして表示されたものをコピーして貼り付ければOK

f:id:yuichi_katahira:20140418103848p:plain

f:id:yuichi_katahira:20140418103902p:plain

Amazon Route 53

Amazon Route 53でDNSの設定をします。ここいらを参考に。

Create Hosted Zoneして出来たNS,SOA以外にkatahirado.jpのA,www.katahirado.jpのCNAMEを追加。
MXレコードはGoogle Apps for Businessがまだ処理し終わっていないので、まだ登録しません。
というか出来ません。
VALUE DOMAINの管理コンソールにいってNSのValueをネームサーバとして登録。

なお、メール等のダウンタイムを極力なくすという意味ではAmazon Route 53→Google Apps for Business→S3とかの流れの方が良かった気がします。
ちなみにGoogle Apps for Businessでの処理で若干手間取って、30分ぐらいメールサーバー的にアレになってたかと。

Google Apps for Business

次、Google Apps for Businessを独自ドメインで設定。
以下が大変丁寧。

Google Apps for Businessのドメイン所有権の確認、MXレコード登録の流れで、Google AppsAmazonを行ったり来たりして若干混乱したので流れを整理。

Google Apps for Businessの無料トライアル申し込み

ドメイン所有権の確認(Google Apps for Business)

Google Appsの管理コンソールからTXTレコードに設定する値を取得する

Amazon Route53のコンソールで取得したTXTレコードを登録

ドメインの所有権確認終了(Google Apps for Business)

Google Appsの管理コンソールからMXレコードに設定する値を取得する
http://angelndxp.wordpress.com/2012/05/27/amazon-route53%E3%82%92google-apps%E3%81%AEdns%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%A8%E3%81%97%E3%81%A6%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B-%EF%BD%9E%E7%8B%AC%E8%87%AA%E3%83%89%E3%83%A1%E3%82%A4/

Amazon Route53のコンソールでMXレコードの設定を行う
http://angelndxp.wordpress.com/2012/05/28/amazon-route53%E3%82%92google-apps%E3%81%AEdns%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%A8%E3%81%97%E3%81%A6%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B-%EF%BD%9E%E3%83%A1%E3%83%BC%E3%83%AB%E8%BB%A2%E9%80%81/

MXレコードの設定反映完了(Google Apps for Business)

感想

若干手間取りましたが、がっちりサーバを構築するのに比べるとだいぶお手軽。
クラウドもずいぶん環境整ったなとしみじみ。
サーバ運用を自宅サーバVPSで長いこと続けてきましたが、もうクラウドでいいですね。
追記: 料金はさくらのVPSが年間一括払いだったので、月924円(4/18現在)。
Google Apps for Businessが年払いで一人利用なので、月500円、AWSは運用してみないとわかりませんが、Route 53は1ゾーンで、S3も大したアクセスでもないので両方で多分200円以内、合わせてもVPSより安くなるかなと想定しています。

*1:一時的な問題だったようで作業翌日には復活していました

nginxとunicornをOS Xで自動起動にする

このエントリーで書いたレシピ食材検索アプリですが、自宅ネットワーク内のMac miniで、nginx+unicornで稼働させています。
permissionや所有者ではまると面倒なので、nginxもunicornもユーザ権限で動かしてます。nginxはport 8080、unicornはport 5000にしてます。妻にはURLのhttp://x.x.x.x:8080/をブックマークしてもらいました。

で、mac miniの再起動時に手動でnginxとunicornを起動させるのが面倒で、自動起動するようにしたので、やり方を記録しておきます。nginxとunicornの設定ファイル等もついでに記録。

#app_path/config/unicorn.rb

worker_processes 2

listen File.expand_path('tmp/sockets/unicorn.sock', ENV['RAILS_ROOT'])

stderr_path File.expand_path('log/unicorn.log', ENV['RAILS_ROOT'])
stdout_path File.expand_path('log/unicorn.log', ENV['RAILS_ROOT'])

preload_app true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!

  old_pid = "#{ server.config[:pid] }.oldbin"
  unless old_pid == server.pid
    begin
      Process.kill :QUIT, File.read(old_pid).to_i
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

#/usr/local/etc/nginx/nginx.conf

user user_account;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    upstream unicorn.rails_app{
        server unix:/path/to/kkfoodstuff/tmp/sockets/unicorn.sock fail_timeout=0;
    }

    server {
        listen       8080;
        server_name  localhost;

        location / {
            root /path/to/kkfoodstuff/public;
            index  index.html index.htm;

            try_files $uri/index.html $uri.html $uri @unicorn_rails_app;
        }

        location @unicorn_rails_app {
    if (-f $request_filename) { break; }
            proxy_redirect off;
            proxy_set_header Host               $http_host;
            proxy_set_header X-Real-IP          $remote_addr;
            proxy_set_header X-Forwarded-Host   $http_host;
            proxy_set_header X-Forwarded-Server $http_host;
            proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
            proxy_pass http://unicorn.rails_app;
        }
    }
}

nginxの自動起動は、brewでnginxをインストールすると、やり方を表示してくれるのでその通りにするだけで自動起動できます。

Docroot is: /usr/local/var/www

The default port has been set in /usr/local/etc/nginx/nginx.conf to 8080 so that
nginx can run without sudo.

To have launchd start nginx at login:
    ln -sfv /usr/local/opt/nginx/*.plist ~/Library/LaunchAgents
Then to load nginx now:
    launchctl load ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist
Or, if you don't want/need launchctl, you can just run:
    nginx

unicornはlocal.unicorn_rails.plistというファイル名でlaunchd.plistを用意しました。
#~Library/LaunchAgents/local.unicorn_rails.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>local.unicorn_rails</string>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>WorkingDirectory</key>
  <string>/path/to/kkfoodstuff</string>
  <key>ProgramArguments</key>
  <array>
        <string>/Users/user_account/.rbenv/shims/bundle</string>
        <string>exec</string>
        <string>unicorn_rails</string>
        <string>-c</string>
        <string>/path/to/kkfoodstuff/config/unicorn.rb</string>
        <string>-E</string>
        <string>production</string>
        <string>-p</string>
        <string>5000</string>
  </array>
</dict>
</plist>

launchctrl コマンドでシステムにloadしておく。

$ launchctl load -w ~/Library/LaunchAgents/local.unicorn_rails.plist

参考

RailsでMySQL InnoDBの全文検索機能とMeCabによる形態素解析を使って出来るだけ検索結果をヒットさせる

前のエントリーの通り、妻と自分用にレシピ検索アプリを作りました。
ソースコードこちら

出来るだけ検索結果で、食材をいい感じでヒットさせるために色々やったので、記録しておきます。
あと、検索結果を出来るだけヒットさせることに、全文検索機能は実はあんまり関係ないので、タイトルは釣りです。
いい感じでヒットするというのは、例えば白菜、はくさい、ハクサイでそれぞれ検索しても白菜、はくさい、ハクサイで登録されたデータが全てヒットするという状態です。これにプラスして、レシピタイトルのあいまい検索が可能になる、というのがゴールです。
要点としては、表記にゆらぎがあっても検索で出来るだけヒットさせるために、データ登録時にMeCab形態素解析して読みがなも登録する、検索時もMeCab使う、collate(照合順序)にutf8_unicode_ciを使用する、です。



妻の要望、実際の使い方は以下の様な感じ。
食材メインでの検索。献立的アプローチ。


何日間かの献立を作成するため、今ある食材を使うレシピを探す。

レシピを作るのに足りない使用食材と、料理本の掲載ページ、レシピ名を買い物メモ兼献立メモに書き足す。

メモを持って足りない食材を買いに行く


一方、自分の要望、使い方
レシピ名で検索するのがメイン。索引代わり


今ある食材を確認して、その食材を使うレシピをいくつか思い出す。

あのレシピどの料理本にのってたかな?レシピ名で曖昧検索、料理本の掲載ページを確認

料理本を見て、食材がたりなかったら適当にあるもので作る、アレンジする


妻と私では、検索ではあるのですが、求めているものが違います。
妻の場合は、食材で検索し、レシピで使う食材が出来るだけヒットして欲しい。
それに対して、私は料理名が曖昧検索出来るといい。


使い勝手的に、検索フォームはシンプルに一つだけのほうが混乱がなくわかりやすいと思ったので、フォームは一つだけにして内部的に妻の使い方でも、自分の使い方でも、ヒットするように実装しました。

レシピのデータ構造は、検索時に食材名を厳密にヒットさせるなら食材をマスタにして正規化する、タグ等にして補完で入力をカバーするなどのアプローチも考えられます。
しかし、それでは入力、マスター管理が面倒になってしまいますし、例えば豚肉、豚バラ薄切り、豚コマ切れ肉、など等、表記の揺れがどうしてもでてきてしまいます。
今回は、時間のあるときにお互いに少しずつレシピを手入力していくことにしたので、妻が出来るだけ簡単に入力できるように、タイトル、レシピ本の情報をstringで、その他食材等の情報をtextと全て自由記述で保存し、検索で頑張ることにしました。

以下の様なデータで、実際にそれぞれの要望をどう解決していったのか見ていきます。


料理名:豚肉と白菜のうま煮
料理本・掲載ページ:料理本A 20ページ
食材:

 白菜
 豚肉
 人参
 しいたけ

料理名:なんとか鍋
料理本・掲載ページ:料理本A 42ページ
食材:

 はくさい
 ・・・・

料理名:味噌汁
料理本・掲載ページ:料理本B 30ページ
食材:

 ハクサイ
 ・・・・


このデータに対して、妻の使い方ですと、白菜、はくさい、ハクサイ等の検索ワードでも、表記にゆらぎがある食材に検索でいい感じにヒットすればいいわけです。自分の場合はいわゆるLike検索ができればOKです。


全てヒットするために一つずつ解決していきました。

はくさい、ハクサイの検索でひらがな、カタカナ両方にヒットするように

これは、collate(照合順序)をutf8_unicode_ciにすればヒットするようになります。データベース全体がutf8_unicode_ciになるのは嫌だったので、検索用にutf8_unicode_ciの別テーブルを作成、レシピ登録時にレコードを生成するようにしました。
https://github.com/katahirado/kkfoodstuff/blob/master/config/database.yml.sample
https://github.com/katahirado/kkfoodstuff/blob/master/db/migrate/20140318065327_create_search_contents.rb
ちなみにテストのDBは、デフォルトだと、schema.rbから生成されて、migrationのoptions: 'COLLATE=utf8_unicode_ci'が無視されて、ひらがなでカタカナにヒットさせるといった検索のテストが失敗してしまうので、SQLダンプファイルをrake db:structure:dumpで作成の上、config/application.rb で、db/schema.rb ではなく db/structure.sql を吐くよう指定しました。

config.active_record.schema_format = :sql

漢字で登録された食材の白菜に、ひらがなやカタカナで検索してもヒットさせるように

形態素解析を利用して、読みがなも登録してしまうという方法を取りました。
mecabmecab-ipadicをインストール、RubyMeCabを繋ぐgemはnattoを利用しました。
豚肉と白菜のうま煮の食材部分"白菜\n豚肉\n人参\nしいたけ"を処理して下記のようになるようにしました。

Analyze.parse(recipe.content)
=> "白菜 ハクサイ 豚肉 ブタニク 人参 ニンジン しいたけ シイタケ"

これをレシピ名も同様の処理を施して、料理登録時に、検索用のテーブルに格納しました。
これでひらがな、カタカナで検索しても漢字で登録された食材にヒットするようになりました。
ついでにFULLTEXT INDEXも貼ってます。
https://github.com/katahirado/kkfoodstuff/blob/master/db/migrate/20140320120819_add_fulltext_index_to_search_contents.rb


参考

白菜と漢字で検索しても食材がはくさい、ハクサイとなっているレシピにヒットさせる

上記が出来たので簡単です。検索ワードをMeCabでparseして、読みがなと元の検索ワードをORで検索すればヒットします。
ちなみに検索ワードを形態素解析でばらしたりせず、読みがなを付与するだけにしています。
これは、例えば'はくさい'というキーワードが入力された場合に形態素解析してしまうと、"は くさい"などとなり、'は'が助詞と解釈されるといった、意図に反するような場合が出るためです。
投げられるSQLはこんな感じです。

SELECT `search_contents`.* FROM `search_contents`  WHERE match(search_contents.title,search_contents.content) against('白菜 ハクサイ' in boolean mode)

白菜のうま煮など、レシピ名のあいまい検索でヒットさせる

データ件数的には2000件弱程度なので、like検索でも問題ないのですが、NgramとBoolean Full-Text Searchesを利用しました。
これもレシピ登録時に、検索用のテーブルにngram(bi-gram)変換したデータを登録しています。
検索ワードもngram(bi-gram)変換しています。
Ngramを使った部分だけのSQLではこんな感じです。

SELECT `search_contents`.* FROM `search_contents`  WHERE match(search_contents.title_ngram,search_contents.content_ngram) against ('+(+白菜 +菜の +のう +うま +ま煮)' in boolean mode)

わかち書きでの全文検索も検討したのですが、検索ワードの例で出したように例えば"はくさいの〜"というレシピ名を形態素解析すると、"は くさい の 〜"などと、はが助詞と解釈されたりして、うぐぐとなったので、手っ取り早くNgramにしました。


参考

検索ワードがスペースで区切られていた場合はAND検索

"白菜 豚肉"と食材が入力される場合、"豚肉と うま煮"などのようにレシピ名の絞り込みとして入力される場合があります。
Ngramに対しては、要素ごとに条件を生成してそれをANDでつないで検索します。
食材の場合は検索ワード、読みのORのペアごとに条件を生成し、それをANDでつないで検索します。
投げられるSQLはそれぞれこうなります。

SELECT `search_contents`.* FROM `search_contents`  WHERE ((match(search_contents.title,search_contents.content) against('白菜 ハクサイ' in boolean mode) AND match(search_contents.title,search_contents.content) against('豚肉 ブタニク' in boolean mode)) OR match(search_contents.title_ngram,search_contents.content_ngram) against ('+白菜' in boolean mode) AND match(search_contents.title_ngram,search_contents.content_ngram) against ('+豚肉' in boolean mode))

SELECT `search_contents`.* FROM `search_contents`  WHERE ((match(search_contents.title,search_contents.content) against('豚肉と ブタニクト' in boolean mode) AND match(search_contents.title,search_contents.content) against('うま煮 ウマニ' in boolean mode)) OR match(search_contents.title_ngram,search_contents.content_ngram) against ('+豚肉 +肉と' in boolean mode) AND match(search_contents.title_ngram,search_contents.content_ngram) against ('+うま +ま煮' in boolean mode))

検索結果は読みがな順

レシピ登録時に料理名の読みがなを検索用テーブルに登録するようにして、検索結果の並び順に使用しました。

検索ワードが一文字の場合に対応する

だいぶいい感じにヒットしてくれるようになりましたが、まだ問題が残っていました。
ngramをbi-gramで処理しているので、一文字の検索ワードが来ると、空でSQLに投げてしまい、エラーになってしまいます。
色々方法はあろうかと思いますが、そもそも全文検索を使う意味があるほどのデータ量でもないので、検索ワードに1文字の単語が来た場合のみ、Ngramではなく、LIKE検索で逃げることにしました。

SELECT `search_contents`.* FROM `search_contents`  WHERE (match(search_contents.title,search_contents.content) against('+(豚 ブタ) +(煮 ニ)' in boolean mode) OR (origin_title LIKE '%豚%'  AND origin_title LIKE '%煮%' ) OR (origin_content LIKE '%豚%'  AND origin_content LIKE '%煮%' ))  ORDER BY `search_contents`.`title_yomi` ASC

その他

collate(照合順序)にutf8_unicode_ciを使うと、前述のとおり、ゆらぎを吸収して検索が出来るわけですが、それには相応のデメリットもあります。
例えばウドを検索しようとすると、ゴボ'ウと'ササミのサラダとか、サヤエンド'ウと'豚バラの〜とか、ザワークラ'ウト'などがヒットしたりします。
ただ、これは逆にいうと、例えば粉山椒にサンショウがヒットするということでもあるので、やむを得ない部分かなと思います。
今回色々富豪なアプローチをしていますが、データ件数が2000件弱と少ないですし、一度登録後はめったにデータ更新もされないと思うのでよしとしています。

実際の処理はソースコードを読めばだいたい分かるかと思います。

以上。

妻と自分の為にRailsでレシピ食材検索アプリを作りました

https://github.com/katahirado/kkfoodstuff


レシピの登録と、検索ができるだけのシンプルなWebアプリです。
あまり特別なことはしていないのですが、検索でしっかりヒットさせたかったので、MeCabによる形態素解析を使ってます。
あとMySQL InnoDB全文検索機能を使っています。
構成はRuby2.1.1,Rails4.0.4,MySQL5.6.17,nginx,unicorn
家庭内で使っているので、外部で稼働していません。
技術的なことは別エントリーで書きます。

妻には満足頂いているようです。

IT系で自分の仕事を家族にわかってもらうには、家族が欲しているものを作るといいかもしれませんね

Rails + Adobe AIRでの業務システム

昨年末から4月末まで、 Rails(JSON+RubyAMF) + Adobe AIR(Flexベース,デスクトップアプリ)での業務システムな仕事をがっちりとやっておりました。

Adobe AIRというと、今だとモバイル開発が主戦場で、WindowsMac向けのデスクトップアプリとしては、世間一般ではオワコン扱いなのかもしれません*1

しかし、今回Rails+Adobe AIRの組み合わせでやってみて、個人的にはこういう組み合わせは業務システム用途ではまだまだイケる、アリだよねという感想を持ちました。


以下、いいとおもった事をつらつらと。

デスクトップアプリなので、ブラウザ間の差異とか気にしなくていい。
Adobe AIRCookieの取り扱いはブラウザのものを使うので、DeviseとかRailsに構築した認証の仕組みがそのまま使える。
Railsサーバ側ではAPI提供に徹して、AIR側と疎結合に構築すれば、構成変更等にも柔軟に対応出来る。
PDFでの印刷処理が楽*2
開発言語がActionScriptなので、クライアントサイドをJavaScriptで開発する時よりハマリが少なくていい。クラス定義、パッケージ、型宣言があるので大分いい。


もっとも、これは開発側から見てのいい点なので、発注側から見た場合にはこの限りではないでしょう。
これからだと、技術者の確保が難しくなっていく(る)とか容易に想像出来ますし。
Flex SDKがASFへ委託されたりとか、プラットフォーム自体の将来性がどうなのかとか。



・・とかありますが、責任持って面倒見られる人がいれば、発注側にとっても今でもいい選択肢かと。

*1:TweetDeckのAdobe AIR版終了なんてニュースもありましたし

*2:そのまま安易に処理すると画質が荒くて汚いが・・

つぶのみをTwitter API version 1.1対応しました

https://dev.twitter.com/blog/current-status-api-v1.1
https://dev.twitter.com/blog/changes-coming-to-twitter-api

検索APIが認証必須に変わっているので、対応しないと検索などが何れできなくなってしまいます。

で、重い腰を上げて対応しました。
と、言ってもtwitter4Jをバージョンアップしただけです。
検索の結果もユーザータイムラインやホームタイムラインの結果と同じ内容のツイートのデータを返すようになった為、若干の作業が必要になりましたが、大した苦労なく対応できました。

twitter4J本当便利。ありがたいことです。

つぶのみはこちら