読者です 読者をやめる 読者になる 読者になる

そういうことだったんですね

いろいろ調べたり学んだりしたことを忘れないように書き連ねています

Rails - sunspot で全文検索をする(1)

rails に sunspot という全文検索のプラグインがあります。 solrという検索エンジンを使っており、モデルに数行コードを追加するだけで、インデックスを動的に作成されます(再構築も可)

https://github.com/sunspot/sunspot

準備

Gemfile に以下を追加

gem "sunspot-rails"
gem "sunspot-solr"

bundle コマンドで実行

$ bundle

設定ファイル(config/sunspot.yml)を作成

$ rails generate sunspot_rails:install

自動生成された config/sunspot.yml は次の通り。

production:
  solr:
    hostname: localhost
    port: 8983
    log_level: WARNING
    # read_timeout: 2
    # open_timeout: 0.5

development:
  solr:
    hostname: localhost
    port: 8982
    log_level: INFO

test:
  solr:
    hostname: localhost
    port: 8981
    log_level: WARNING

solr の起動する・終了する

so起動する

$ bundle exec sunspot-solr start -p 8982

8982 はRAILS_ENV=developmentの場合のポート番号です。config/sunspot.yml の port にあわせます。

終了する

$ bundle exec sunspot-solr stop

モデルを編集

はじめに

scaffold で次のモデルを作ったと想定します。

$ rails g scaffold post title:string body:text blog_id:integer author_id:integer
$ rails g scaffold blog title:string
$ rails g scaffold author name:string email:string

モデルに追加

検索対象は Post モデルです。 タイトルと本文にインデックスを作成します。 searchable メソッドでインデックスの対象を記述します。 text で全文検索対象の属性を指定します。 integer, double, time の属性も指定できます。こちらは範囲指定検索の対象となるようです。

class Post < ActiveRecord::Base
    searchable do
        text :title
        text :body, :stored => true
    end
end

body属性に stored というオプションがついていますが、これはインデックスに値を保存するもののようです(スニペット中の検索ワードを強調表示するために使うhighlight機能で必須)。

テストデータを作成する

scaffold により作成された画面またはデータベースにデータを設定します あいうえお、なんかよりは、適当なニュース記事のタイトルと本文にするとおもしろいです

レコード挿入時に Errno::ECONNREFUSEDというエラーが出た場合は、solrが起動していないかポート番号が間違ってます

検索を実行する

感覚を掴むため rails console で遊んでみます。

Postモデルの'Google'というワードで全文検索をした例です

$ rails console
Loading development environment (Rails 4.0.0)
2.0.0p247 :001 > search = Post.search do
2.0.0p247 :002 >     fulltext 'Google'
2.0.0p247 :003?>   end

次のような値が返されればOKです。

D, [2013-08-27T21:59:32.515315 #23093] DEBUG -- :   SOLR Request (29.5ms)  [ path=# parameters={data: fq=type%3APost&q=Google&fl=%2A+score&qf=title_text+body_texts&defType=dismax&start=0&rows=30, method: post, params: {:wt=>:ruby}, query: wt=ruby, headers: {"Content-Type"=>"application/x-www-form-urlencoded; charset=UTF-8"}, path: select, uri: http://localhost:8982/solr/select?wt=ruby, open_timeout: , read_timeout: , retry_503: , retry_after_limit: } ]
 => <Sunspot::Search:{:fq=>["type:Post"], :q=>"Google", :fl=>"* score", :qf=>"title_text body_texts", :defType=>"dismax", :start=>0, :rows=>30}> 

これだけだと検索結果がわかりません。結果を取得するには2種類のメソッドがあります。

hits

検索ワードに一致した結果のインデックス情報(Sunspot::Search::Hit)のみを返します。 データベースを検索しないので、インデックスに含まれていない属性値にはアクセスできません。

2.0.0p247 :004 > search.hits
 => [#<Sunspot::Search::Hit:Post 3>, #<Sunspot::Search::Hit:Post 2>] 

2件ヒットしているのがわかります。Sunspot::Search::Hit:Post # の # は検索したデータの :id となっているようです。

results

検索ワードに一致した結果のインデックス情報をもとにデータベースからロードした値を返します。 モデルが全てロードされた状態ですが、データベースを検索するので遅くメモリを消費します。

2.0.0p247 :009 > search.results
DEPRECATION WARNING: Relation#all is deprecated. If you want to eager-load a relation, you can call #load (e.g. `Post.where(published: true).load`). If you want to get an array of records from a relation, you can call #to_a (e.g. `Post.where(published: true).to_a`). (called from irb_binding at (irb):9)
  Post Load (2.2ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` IN (3, 2)
 => [#<Post id: 3, title: "Googleの新プロトコルQUICを試す", body: "以前、「Googleが仕掛ける新プロトコルQUICとは何か」のブログエントリーを書いたのが2月末の事で...", blog_id: nil, author_id: nil, created_at: "2013-08-27 11:41:42", updated_at: "2013-08-27 11:41:42">, #<Post id: 2, title: "【製品リリース】Google™ から誕生した新しいスマートフォン「Nexus 4」、日本市場に向け8月...", body: "LG エレクトロニクス(本社:韓国, www.lge.com)の日本法人LG エレクトロニクス・ジャパ...", blog_id: nil, author_id: nil, created_at: "2013-08-27 11:40:35", updated_at: "2013-08-27 11:40:35">] 

こちらは Postのオブジェクトが2つ返されています。

なお、マッチしたサンプルデータは、はてなブックマークに掲載されていた次のデータを使いました。

each_hit_with_result

インデックス情報とデータベースからロードしたオブジェクト両方を取得します。 何かと便利です。。

明日コントローラとビューに組み込んでみた結果を報告してみます。

Ruby on RailsによるWebアプリケーション・スーパーサンプル

Ruby on RailsによるWebアプリケーション・スーパーサンプル