Rails 4.0.4 から 4.1.0 に Update
こんな感じに作業。
↓
https://github.com/katahirado/kkfoodstuff/commit/7aca5b2986e26e88a78d7d58bc041ed97cba5e9e
https://github.com/katahirado/kkfoodstuff/commit/a0c49840145587336b52cb2d56ba71f1dda1e84c
Warning: you should require 'minitest/autorun' instead. Warning: or add 'gem "minitest"' before 'require "minitest/autorun"'
shoulda-matchersを2.5から2.6に
gem "shoulda-matchers", "~> 2.6.0"
参考
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
漢字で登録された食材の白菜に、ひらがなやカタカナで検索してもヒットさせるように
形態素解析を利用して、読みがなも登録してしまうという方法を取りました。
mecabとmecab-ipadicをインストール、RubyとMeCabを繋ぐ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というと、今だとモバイル開発が主戦場で、WindowsやMac向けのデスクトップアプリとしては、世間一般ではオワコン扱いなのかもしれません*1。
しかし、今回Rails+Adobe AIRの組み合わせでやってみて、個人的にはこういう組み合わせは業務システム用途ではまだまだイケる、アリだよねという感想を持ちました。
以下、いいとおもった事をつらつらと。
デスクトップアプリなので、ブラウザ間の差異とか気にしなくていい。
Adobe AIRはCookieの取り扱いはブラウザのものを使うので、DeviseとかRailsに構築した認証の仕組みがそのまま使える。
Railsサーバ側ではAPI提供に徹して、AIR側と疎結合に構築すれば、構成変更等にも柔軟に対応出来る。
PDFでの印刷処理が楽*2。
開発言語がActionScriptなので、クライアントサイドをJavaScriptで開発する時よりハマリが少なくていい。クラス定義、パッケージ、型宣言があるので大分いい。
もっとも、これは開発側から見てのいい点なので、発注側から見た場合にはこの限りではないでしょう。
これからだと、技術者の確保が難しくなっていく(る)とか容易に想像出来ますし。
Flex SDKがASFへ委託されたりとか、プラットフォーム自体の将来性がどうなのかとか。
・・とかありますが、責任持って面倒見られる人がいれば、発注側にとっても今でもいい選択肢かと。
第28回 Rails勉強会@東北に参加
https://www.facebook.com/events/378705102216002/
OzakiさんによるRails4.0ネタでした。
はじめにこちらのスライド。
https://speakerdeck.com/alindeman/rails-4-dot-0-whirlwind-tour
その中から特にStrong ParametersとTurbolinksについては、スライド見終わった後、RailsCastsをネタに皆で手を動かしながらワイワイとやりました。
http://railscasts.com/episodes/371-strong-parameters
http://railscasts.com/episodes/390-turbolinks
Turbolinksはこのままリリースされたらギャッという人がたくさん出そうでだいぶアレ。
後、スライド中で出てきたnoneというメソッドやallがActiveRecord::Relationを返す件について↓
#Relation#none # Chainable null object > Post.none #<ActiveRecord::Relation []> # Doesn't hit database > Post.none.to_sql "" > Post.none.to_a [] #Relation#all returns a relation (chainable) # Rails 3 > Post.all.class Array # Rails 4 > Post.all.class ActiveRecord::Relation > Post.all.to_a.class Array
勉強会中に、jQueryや関数型言語っぽくチェーンして書けて、条件分岐を書くケースが減るという事かなとか言いましたが、実際の使用ケースを見ないとわからないという意見がありました。
で、記事に書いてみようかと思ったら、Active Record Query Interface — Ruby on Rails Guidesここに書いてあったのでまるまる引用。
9 Null Relation
The none method returns a chainable relation with no records. Any subsequent conditions chained to the returned relation will continue generating empty relations. This is useful in scenarios where you need a chainable response to a method or a scope that could return zero results.
Post.none # returns an empty Relation and fires no queries. # The visible_posts method below is expected to return a Relation. @posts = current_user.visible_posts.where(name: params[:name]) def visible_posts case role when 'Country Manager' Post.where(country: country) when 'Reviewer' Post.published when 'Bad User' Post.none # => returning [] or nil breaks the caller code in this case end end
Rails4.0では、noneでActiveRecord::Relationを返してくれるので、上記のようにcurrent_user.visible_postsのところで空配列が来てるかの条件分岐を入れずに、jQueryのようにチェーンして書けると。
ScalaのOptionのNoneとかも彷彿とさせますね。
Rails3.2までの場合だと、例えば[]やnilを返してそれをチェックするとか、visible_posts_user_nameといったvisible_postsとwhere(name: params[:name])を一気に処理するメソッドを作成するといったアプローチになるかと。
Rails4.0の場合は、visible_postsメソッドを他でも利用出来て、再利用性を高めて、さらに重複を排除してコードを書けるようになるというのがメリットかなあと。
allがActiveRecord::Relationを返すようになるというのも同様な流れな気が。で、allがActiveRecord::Relation返せば、scopedはいらない子になるのでDEPRECATIONと。
もっともallの場合は、all呼ばないでwhere呼ぶ事が多い気がするので使用ケースは少ないかと。
個人的にはこんな風に理解しました。
参加の皆様お疲れ様でした。
第27回Rails勉強会@東北
https://www.facebook.com/events/295738007193281/
参加してきました。
参加者5名。
今回もRailsCastsを手を動かして皆でやっていった感じです。
やったのは、Deviseなどの認証系ライブラリを使わず認証をスクラッチで作るやつです。
認証↓
http://railscasts.com/episodes/250-authentication-from-scratch-revised
認証に権限をつける↓
http://railscasts.com/episodes/385-authorization-from-scratch-part-1
参加の皆様お疲れ様でした。