アーカイブ

‘Rails’ カテゴリーのアーカイブ

RailsでPGroongaを使う。その2 このエントリーを含むはてなブックマーク はてなブックマーク - RailsでPGroongaを使う。その2

2016 年 11 月 1 日 Comments off

RailsでPGroongaを使う。 » サイキョウライン

全文検索そのものと、キーワードのハイライトについてはよかったのですが、pgroonga.score関数でとれるスコアを使ったソートで問題が・・・(つづく)

前回の続き、この問題について。端的に言うと、せっかく作ったインデックスを使ってくれずスコアが取れないため、結果として正しくソートが出来ていません。

pgroonga.score関数の仕様はこの様になっています。

pgroonga.score関数 | PGroonga

pgroonga.score関数はインデックスを使わずに全文検索した場合は常に0.0を返します。言い換えると、pgroonga.score関数はシーケンシャルスキャンで全文検索を実行した場合は常に0.0を返します。

そして、チュートリアルでは作ったインデックスを使わせる対策として、enable_seqscanをoffにしていました。

チュートリアル | PGroonga

確実にpgroongaインデックスを使うためにシーケンシャルスキャンを無効にします。

直にクエリを発行するなら、SELECTの前後でenable_seqscanをoff→DEFAULTと一時的に切り替えてあげればよいと思うのですが、ActiveRecordを使っているとクエリが発行されるのは実際に結果が必要になったタイミングなので、この様に書いても意味がありません。

> Article.connection.execute 'SET enable_seqscan = off'
> articles = Article.full_text_search('桃太郎')
> Article.connection.execute 'SET enable_seqscan = DEFAULT'

SELECTの前後にコールバックがあればいいのですが、探してみても用意されてはいないみたい。
after_initialize and after_findは期待している動きとはちょっと違う。)

かと言って、postgresql.confに書いてしまうのはやり過ぎ、というか全体でシーケンシャルスキャンをoffにしてしまうのは問題だと思います。
このような場合、どう実装するのがいいのでしょうか。

[15:30 追記]
ActiveRecordが生成するSQLをpsqlで流すとインデックスが使われることは確認できているので、enable_seqscanをどこで切り替えるか、に悩んでいます。

> puts Article.full_text_search('桃太郎').to_sql
SELECT "articles".*, pgroonga.score("articles") AS pgroonga_score FROM "articles"  WHERE (title %% '桃太郎' OR body %% '桃太郎')  ORDER BY pgroonga_score DESC
=> EXPLAIN SELECT "articles".*, pgroonga.score("articles") AS pgroonga_score FROM "articles"  WHERE (title %% '桃太郎' OR body %% '桃 太郎')  ORDER BY pgroonga_score DESC;
                               QUERY PLAN
-------------------------------------------------------------------------
 Sort  (cost=1.14..1.15 rows=4 width=242)
   Sort Key: (score(articles.*)) DESC
   ->  Seq Scan on articles  (cost=0.00..1.10 rows=4 width=242)
         Filter: ((title %% '桃太郎'::text) OR (body %% '桃太郎'::text))
(4 rows)

=> SET enable_seqscan = off;
SET
=> EXPLAIN SELECT "articles".*, pgroonga.score("articles") AS pgroonga_score FROM "articles"  WHERE (title %% '桃太郎' OR body %% '桃太郎')  ORDER BY pgroonga_score DESC;
                                                    QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Sort  (cost=4.08..4.09 rows=4 width=242)
   Sort Key: (score(articles.*)) DESC
   ->  Bitmap Heap Scan on articles  (cost=0.00..4.04 rows=4 width=242)
         Recheck Cond: ((title %% '桃太郎'::text) OR (body %% '桃太郎'::text))
         ->  BitmapOr  (cost=0.00..0.00 rows=2 width=0)
               ->  Bitmap Index Scan on index_articles_on_id_and_title_and_body  (cost=0.00..0.00 rows=2 width=0)
                     Index Cond: (title %% '桃太郎'::text)
               ->  Bitmap Index Scan on index_articles_on_id_and_title_and_body  (cost=0.00..0.00 rows=1 width=0)
                     Index Cond: (body %% '桃太郎'::text)
(9 rows)

=> SET enable_seqscan = DEFAULT;
SET
=> EXPLAIN SELECT "articles".*, pgroonga.score("articles") AS pgroonga_score FROM "articles"  WHERE (title %% '桃太郎' OR body %% '桃太郎')  ORDER BY pgroonga_score DESC;
                               QUERY PLAN
-------------------------------------------------------------------------
 Sort  (cost=1.14..1.15 rows=4 width=242)
   Sort Key: (score(articles.*)) DESC
   ->  Seq Scan on articles  (cost=0.00..1.10 rows=4 width=242)
         Filter: ((title %% '桃太郎'::text) OR (body %% '桃太郎'::text))
(4 rows)
カテゴリー: PostgreSQL, Rails タグ:

RailsでPGroongaを使う。 このエントリーを含むはてなブックマーク はてなブックマーク - RailsでPGroongaを使う。

2016 年 10 月 27 日 Comments off

とあるRailsアプリで全文検索が必要になったため、PGroongaを試してみました。

環境はそれぞれこんな感じ。(表示は一部省略)

% cat /etc/lsb-release
DISTRIB_DESCRIPTION="Ubuntu 16.04.1 LTS"

% sudo aptitude show postgresql
パッケージ: postgresql
バージョン: 9.5+173

% sudo aptitude show postgresql-9.5-pgroonga
パッケージ: postgresql-9.5-pgroonga
バージョン: 1.1.5-2~xenial1

まず、参考にしたのはこのページです。

Ruby on RailsでPostgreSQLとPGroongaを使って日本語全文検索を実現する方法 – ククログ(2015-11-09)

この記事ではRuby on Railsで作ったアプリケーションからPGroongaを使って日本語全文検索
機能を実現する方法を説明します。

そして、使ったそれぞれの関数のリファレンスはこちら。

実際のコードはこんな感じです。
PostgreSQLにエクステンションを追加するマイグレーション

PGroongaを使わせるためのインデックスを追加するマイグレーション
カラムがこうなっている理由はこちらです。

pgroonga.score関数 | PGroonga

pgroonga.score関数を使うには、pgroongaインデックスにプライマリーキーに指定したカラムを追加する必要があります。もし、プライマリーキーに指定したカラムをpgroongaインデ>ックスに追加していない場合は、pgroonga.score関数は常に0.0を返します。

全文検索をしているクラス
titleとbodyを検索対象にしています。

実際に使用するとこうなります。

> article = Article.full_text_search('桃太郎', true).first
> article.highlighted_body
=> むかしむかしあるところで<span class="keyword">桃太郎</span>という少年が鬼ヶ島に行って鬼を退治しました。

全文検索そのものと、キーワードのハイライトについてはよかったのですが、pgroonga.score関数でとれるスコアを使ったソートで問題が・・・(つづく)

ヒント: インデックス

カテゴリー: PostgreSQL, Rails タグ:

Rails4→5で目にしたdeprecationたち。 このエントリーを含むはてなブックマーク はてなブックマーク - Rails4→5で目にしたdeprecationたち。

2016 年 8 月 24 日 Comments off

なにかの役に立つかも知れないので、淡淡と書いておきます。

DEPRECATION WARNING: Method map is deprecated and will be removed in Rails 5.1, as `ActionController::Parameters` no longer inherits from hash. Using this deprecated behavior exposes potential security problems. If you continue to use this method you may be creating a security vulnerability in your app that can be exploited. Instead, consider using one of these documented methods which are not deprecated: http://api.rubyonrails.org/v5.0.0.1/classes/ActionController/Parameters.html

DEPRECATION WARNING: uniq is deprecated and will be removed from Rails 5.1 (use distinct instead)

DEPRECATION WARNING: `redirect_to :back` is deprecated and will be removed from Rails 5.1. Please use `redirect_back(fallback_location: fallback_location)` where `fallback_location` represents the location to use if the request has no HTTP referer information.

DEPRECATION WARNING: `render :text` is deprecated because it does not actually render a `text/plain` response. Switch to `render plain: 'plain text'` to render as `text/plain`, `render html: '<strong>HTML</strong>'` to render as `text/html`, or `render body: 'raw'` to match the deprecated behavior and render with the default Content-Type, which is `text/plain`.

DEPRECATION WARNING: xhr and xml_http_request methods are deprecated in favor of `get :index, xhr: true` and `post :create, xhr: true`

DEPRECATION WARNING: ActionController::TestCase HTTP request methods will accept only keyword arguments in future Rails versions.
Examples:
get :show, params: { id: 1 }, session: { user_id: 1 }
process :update, method: :post, params: { id: 1 }

カテゴリー: Rails タグ:

belongs_to_required_by_defaultが効かない。 このエントリーを含むはてなブックマーク はてなブックマーク - belongs_to_required_by_defaultが効かない。

2016 年 8 月 17 日 Comments off

Rails 5.0.0からbelongs_toがデフォルトで必須になったはずなのだけれど、そうなっていなかったので調べた。
結論から言うと「config/initializers/new_framework_defaults.rb」に置いたままではダメで、「config/application.rb」に書かないといけない。
理由は読まれるタイミングと使われるタイミング。(またこれか)

最初に、belongs_toの必須が定義されるのはここで、「model.belongs_to_required_by_default」が元になっている。
activerecord-5.0.0.1/lib/active_record/associations/builder/belongs_to.rb

128       if reflection.options[:optional].nil?
129         required = model.belongs_to_required_by_default
130       else
131         required = !reflection.options[:optional]
132       end

で、上の「model.belongs_to_required_by_default」がどこでセットされるかというと、ここ。追加したコメントはあとで出てきます。
activesupport-5.0.0.1/lib/active_support/core_ext/module/attribute_accessors.rb

123       class_eval(<<-EOS, __FILE__, __LINE__ + 1)
124         @@#{sym} = nil unless defined? @@#{sym}
125
126         def self.#{sym}=(obj)
127 #{if sym == :belongs_to_required_by_default
128 'puts "#{name}.#{__method__} # ここで決まる!"'
129 end}
130           @@#{sym} = obj
131         end
132       EOS

って書いても分かりにくいので、コメントを仕込んでタイミングを表示してみた。こんな感じ。

config/application.rb

20 puts "#{__FILE__.sub("#{Rails.root}/", '')}:#{__LINE__} Rails.application.config.active_record.belongs_to_required_by_default #=> #{Rails.application.config.active_record.belongs_to_required_by_default}"
 21   config.active_record.belongs_to_required_by_default = true # ここに書かないとダメ!
 22 puts "#{__FILE__.sub("#{Rails.root}/", '')}:#{__LINE__} Rails.application.config.active_record.belongs_to_required_by_default #=> #{Rails.application.config.active_record.belongs_to_required_by_default}"

config/initializers/new_framework_defaults.rb

17 # Require `belongs_to` associations by default. Previous versions had false.
 18 puts "#{__FILE__.sub("#{Rails.root}/", '')}:#{__LINE__} Rails.application.config.active_record.belongs_to_required_by_default #=> #{Rails.application.config.active_record.belongs_to_required_by_default}"
 19 Rails.application.config.active_record.belongs_to_required_by_default = true
 20 puts "#{__FILE__.sub("#{Rails.root}/", '')}:#{__LINE__} Rails.application.config.active_record.belongs_to_required_by_default #=> #{Rails.application.config.active_record.belongs_to_required_by_default}"

まずは「config/application.rb」に書かなかった場合

% bundle exec rails c
config/application.rb:20 Rails.application.config.active_record.belongs_to_required_by_default #=>
config/application.rb:22 Rails.application.config.active_record.belongs_to_required_by_default #=>
config/initializers/new_framework_defaults.rb:18 Rails.application.config.active_record.belongs_to_required_by_default #=>
config/initializers/new_framework_defaults.rb:20 Rails.application.config.active_record.belongs_to_required_by_default #=> true
Loading development environment (Rails 5.0.0.1)
irb(main):001:0> ActiveRecord::Base.belongs_to_required_by_default
=> nil

つぎに「config/application.rb」に書いた場合

% bundle exec rails c
config/application.rb:20 Rails.application.config.active_record.belongs_to_required_by_default #=>
config/application.rb:22 Rails.application.config.active_record.belongs_to_required_by_default #=> true
ActiveRecord::Base.belongs_to_required_by_default= # ここで決まる!
config/initializers/new_framework_defaults.rb:18 Rails.application.config.active_record.belongs_to_required_by_default #=> true
config/initializers/new_framework_defaults.rb:20 Rails.application.config.active_record.belongs_to_required_by_default #=> true
Loading development environment (Rails 5.0.0.1)
irb(main):001:0> ActiveRecord::Base.belongs_to_required_by_default
=> true

config/application.rb」に書かないとnilのままなんだね・・・。

って、すぐ遭遇しそうなことなので、自分が基本的なことを見落としている気がしてきたぞ。

カテゴリー: Rails タグ:

time_zoneはconfig/initializersに移しちゃ(まだ?)ダメっぽい。 このエントリーを含むはてなブックマーク はてなブックマーク - time_zoneはconfig/initializersに移しちゃ(まだ?)ダメっぽい。

2016 年 8 月 11 日 Comments off
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
# config.time_zone = 'Central Time (US & Canada)'

config/application.rbにあったこのコメントがRails 5.0.0から消えたので、config/initializersに移動すべきと解釈してファイルを分けたら効かなくなってしまった。
理由は、config/initializersが読まれる前にconfig.time_zoneが使われてしまうから。

なので、まだconfig.time_zoneはconfig/application.rbに書いておかないといけない。
気になってconfig.i18n.default_localeも調べたらこっちは大丈夫だった。

それぞれの読まれる/使われる順番はこんな感じ。

% bundle exec rails c
config/application.rb:21
vendor/bundle/gems/activesupport-5.0.0/lib/active_support/railtie.rb:32 Time.zone #=>
vendor/bundle/gems/activesupport-5.0.0/lib/active_support/railtie.rb:33 Time.zone #=> (GMT+00:00) UTC
config/initializers/i18n.rb:1
config/initializers/time_zone.rb:1
vendor/bundle/gems/activesupport-5.0.0/lib/active_support/i18n_railtie.rb:49 I18n.default_locale #=> en
vendor/bundle/gems/activesupport-5.0.0/lib/active_support/i18n_railtie.rb:51 I18n.default_locale #=> ja
Loading development environment (Rails 5.0.0)
irb(main):001:0> Time.zone
=> #<ActiveSupport::TimeZone:0x007f97bc001968 @name="UTC", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>>
irb(main):002:0> I18n.default_locale
=> :ja
カテゴリー: Rails タグ:

per_form_csrf_tokensはどう影響するのか。 このエントリーを含むはてなブックマーク はてなブックマーク - per_form_csrf_tokensはどう影響するのか。

2016 年 8 月 10 日 Comments off

Configuring Rails Applications — Ruby on Rails Guides

config.action_controller.per_form_csrf_tokens configures whether CSRF tokens are only valid for the method/action they were generated for.

5.0から加わったこれ、tokenがmethodとactionに関連付くようになるオプションということで、自分でform_authenticity_tokenを直に呼んでいるところでは影響を受けるのかが気になってコードを読んでみた。
結果から言うと、自分でform_authenticity_tokenを呼んでいる場合には影響を受けない。

rails/request_forgery_protection.rb at master · rails/rails

# Sets the token value for the current session.
def form_authenticity_token(form_options: {})
  masked_authenticity_token(session, form_options: form_options)
end

form_authenticity_tokenはform_optionsを引数で受け取っていて、デフォルトは空になってる。

rails/request_forgery_protection.rb at master · rails/rails

# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
def masked_authenticity_token(session, form_options: {})
  action, method = form_options.values_at(:action, :method)
  raw_token = if per_form_csrf_tokens && action && method
    action_path = normalize_action_path(action)
    per_form_csrf_token(session, action_path, method)
  else
    real_csrf_token(session)
  end

form_authenticity_tokenで受け取った引数はこんな感じで使われる。
ここのper_form_csrf_tokensは、config/initializers/new_framework_defaults.rbにあるやつそのもの。

ではform_optionsはどこから来るのかっていうと、ここ。

rails/url_helper.rb at master · rails/rails

def token_tag(token=nil, form_options: {})
  if token != false && protect_against_forgery?
    token ||= form_authenticity_token(form_options: form_options)
    tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
  else
    "".freeze
  end
end

url_helperの中だ。

ひとまず影響がないことが分かったけれど変わりやすそうな場所ではあるので、注意が必要かな。

カテゴリー: Rails タグ:

カラムの順番について。 このエントリーを含むはてなブックマーク はてなブックマーク - カラムの順番について。

2016 年 2 月 8 日 Comments off

細かいことが気になるシリーズ。
MySQLのADD COLUMNにはAFTERがあるがPostgreSQLにはないため、add_columnに:afterを付けても無視される。
そのためカラムは追加した順に並ぶので、順番を意識したい場合にどうするかという話。たとえばテーブル定義書を作るとき。
Active Recordを通して使うのでいつもは気にする必要がないのだけれど、定義書の順番とdb/schema.rbの順番が異なっているのは気持ち悪い。

PostgreSQLでも出来なくはないけれど別にテーブルつくって云々て、MySQLならオプション1個で済むことをそこまで手間かけてやるの?て感じはしてる。

さて、どうしましょ。

MySQL :: MySQL 5.7 Reference Manual :: 13.1.8 ALTER TABLE Syntax

13.1.8 ALTER TABLE Syntax

PostgreSQL: Documentation: 9.5: ALTER TABLE

ALTER TABLE — change the definition of a table

ActiveRecord::ConnectionAdapters::SchemaStatements

Adds a new column to the named table.

カテゴリー: MySQL, PostgreSQL, Rails タグ:

tryとtry!と。 このエントリーを含むはてなブックマーク はてなブックマーク - tryとtry!と。

2015 年 11 月 5 日 Comments off

koicさんのスライド(Safe navigation operator in Ruby)を、Rubyにもtryができるのかーとありがたく読んでいたのですが、この9枚目、まさにまだtryを使って(!!)いました。。。今日からtry!に切り替えようと思います。

Object

Same as try, but will raise a NoMethodError exception if the receiver is not nil and does not implement the tried method.

Ruby 2.3.0以降でRailsを使うときは「.try!」を使うか「.?」を使うかが気になりましたが、これは「RailsではなくRubyの」メソッドである「.?」に切り替えていくべきですよね。たぶ、ん。

ところでSlideShareって各スライドを開く直リンクがあった気がするんですが、気のせいかな。

って、http://www.slideshare.net/koic/safe-navigation-operator-in-ruby/#9←これで行けるんですね!(教えていただきました。)

[2015-11-06 追記] 「&.」に変わったそうです。

change DOTQ · ruby/ruby@837babd

from “.?” to “&.”. [ruby-core:71363] [Feature #11537]

カテゴリー: Rails, Ruby タグ:

react-railsとbrowserify-railsを併用することにした。 このエントリーを含むはてなブックマーク はてなブックマーク - react-railsとbrowserify-railsを併用することにした。

2015 年 10 月 13 日 Comments off

サイキョウライン

現時点ではreact-railsのやり方に寄せてモジュールのソースを取り込むことにしました。

やっぱりnodeモジュールの管理はnpmに任せることにしました。

reactjs/react-rails

place the following in your application.js:

//= require react
//= require react_ujs
//= require components

react-railsのインストール時に追記するこれをそのまま使って、埋まっているreactはこんなファイルを置いて置き換えています。(その後のreact_ujsでエラーが出るのを防ぐため)
app/assets/javascripts/react.js

if (typeof window !== 'undefined' && window !== null) {
  window.React = require('react');
}

windowの存在判定はCoffeeScriptの「window?」のコンパイル結果をパクりました。
副作用がでちゃったりイカしたやり方を見つけたらバリバリ変えていく所存です。

カテゴリー: JavaScript, Rails タグ:

react-railsを使ったアプリでnodeモジュールを使う。 このエントリーを含むはてなブックマーク はてなブックマーク - react-railsを使ったアプリでnodeモジュールを使う。

2015 年 10 月 7 日 Comments off

react-railsでは実行環境に合わせて、

react-rails/asset_variant.rb at master · reactjs/react-rails

@react_directory = GEM_ROOT.join(‘lib/assets/react-source/’).join(@react_build).to_s

react-rails/railtie.rb at master · reactjs/react-rails

app.config.assets.paths << asset_variant.react_directory

こんな感じでgemに含んでいるreact.jsを使っています。ということは、npmの管理下にありません。
そうなると困るのが、他のnodeモジュールを使いたい場合にどうするかで、だいぶググってあちこちでソースを読んで悩んだのですが、現時点ではreact-railsのやり方に寄せてモジュールのソースを取り込むことにしました。
それぞれこんな感じです。(コードは抜粋)

app/assets/javascripts/application.js

//= require modules

app/assets/javascripts/modules.js

//= require_tree ./modules

js/gulpfile.babel.js

let paths = {
  module_sources: [
    './node_modules/react-redux/dist/react-redux.min.js',
    './node_modules/redux/dist/redux.min.js'
  ],
  modules_dir: '../app/assets/javascripts/modules/'
};
gulp.task('copy_modules', () => {
  gulp.src(paths.module_sources)
    .pipe(gulp.dest(paths.modules_dir));
});

js/package.json

{
  "devDependencies": {
    "react-redux": "^3.1.0",
    "redux": "^3.0.2"
  }
}

※jsディレクトリについてはこちら。

react-railsを使ったアプリでコンポーネントのテストを走らせてみた。 » サイキョウライン

Rails.rootの下に「js」というディレクトリを掘ってjs部屋

nodeモジュールはnpmで一括管理するのが相応しいと思っているのですが、肝心のreactがreact-railsの中にいるので、これだけ例外になるのが気持ち悪く感じています。
実際にreact-railsを使ってアプリを書いている方はどの様な方法でこの問題を解決しているでしょうか。
そもそもreact-railsを使わずにbrowserify-railsを使って汎用的(かな?)な方法で管理するべきなんですかね。

カテゴリー: JavaScript, Rails タグ: