DockerでElasticSearchサーバーを立てる その2
前回に引き続きやっていきます。ElasticSearchを実際に使ってサンプルアプリを作ってみることにしました。
作ったサンプルアプリはこちら
https://github.com/yss44/searchkick_test
Pluginの確認を行なう。
そういえば、Pluginがしっかり起動しているか確認していなかったので確認。
$ curl -XGET localhost:9200/_nodes/plugin?pretty { "ok" : true, "cluster_name" : "elasticsearch", "nodes" : { "e54cayVVTAS4JdxIXy5DpA" : { "name" : "Sludge", "transport_address" : "inet[/10.0.2.15:9300]", "hostname" : "vagrant-ubuntu-saucy-64", "version" : "0.90.7", "http_address" : "inet[/10.0.2.15:9200]", "plugins" : [ { "name" : "analysis-kuromoji", "description" : "Kuromoji analysis support", "jvm" : true, "site" : false }, { "name" : "HQ", "description" : "No description found for HQ.", "url" : "/_plugin/HQ/", "jvm" : false, "site" : true } ] } } }
しっかり入っているようです。安心しました。
Searchkickのアプリ作成
*baseアプリの作成
rails g scaffold article title:string content:text
rails g scaffold comment article:references text:text
vim models/article.rb
article.rb
+ has_many :comments
rake db:migrate
を行なってシンプルなcrudアプリを生成。これでテストを行っていきます。
- searchkickの設定
Defaultでelasticsearchのurlは
localhost:9200
を使う。今回はDocker上で動いているので、環境変数でDocker上のelasticsearchのurlを指定する必要がある。
今回はアプリの起動時に環境変数読み込む形にしておく。
config/settings.yml
development: ELASTICSEARCH_URL: http://192.168.33.10:9200 test: ELASTICSEARCH_URL: http://192.168.33.10:9200 production: ELASTICSEARCH_URL:http://192.168.33.10:9200
config/application.rb
# you've limited to :test, :development, or :production. Bundler.require(:default, Rails.env) ENV.update YAML.load_file('config/settings.yml')[Rails.env] rescue {} module SearchkickTest class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded.
これで準備OK,searchkickを導入して行く
article.rb
class Article < ActiveRecord::Base has_many :comments searchkick end
あとは、適当にテキストを集めて来てデータベースに入れておく(seeds.rb)
で、searchkickのインストール時に入るrakeでreindex掛けてみる
rake searchkick:reindex CLASS=Article
アプリケーション起動時に環境変数指定しているので、rakeの時に環境変数が反映されずエラーがでました。開発環境汚したくないので次のような形で無理矢理指定して行きます。
rake searchkick:reindex CLASS=Article ELASTICSEARCH_URL=http://192.168.33.10:9200
で無事にインデックス完了。ちゃんとインデックスされてるか確認する。
http://192.168.33.10:9200/_plugin/HQ/
statusがyellowになってる。。。
RED - Damnit. Some or all of (primary) shards are not ready.
YELLOW - Elasticsearch has allocated all of the primary shards, but some/all of the replicas have not been allocated.
GREEN - Great. Your cluster is fully operational. Elasticsearch is able to allocate all shards and replicas to machines within the cluster.
レプリケーションされていないかららしい。気にしないことにしておく。
Elasticsearch "Yellow" cluster status explained
コントローラーに検索用のアクション作成
articles_controller.rb
def search @articles = Article.search params[:q] return render json:@articles end
routes.rb
get "search" => "articles#search"
articles#indexのviewに検索ボックスを追加
<div> <%= form_tag('/search',method: :get) do %> <input type="text" name="q"> <button tye="submit">Search</button> <% end %> </div>
とかしてあげると、しっかり検索して、jsonで返ってくることを確認。
Indexされるデータを指定する
models/articles.rb
class Article < ActiveRecord::Base has_many :comments searchkick def search_data { title:title, content:content, score:score, comments:comments.map(&:text) } end end
そして、reindexする。
rake searchkick:reindex CLASS=Article ELASTICSEARCH_URL=http://192.168.33.10:9200
すると、コメントの中身からも検索をかけられるようになっています。
Eager load associations
検索時に、has_manyやbelongs_toで繋がっているモデルも一度にネストした状態で取得できます。ネストした子モデルを取得するために何度もDBに取得しに行かないようにします。
controllers/articles_controller.rb
def search @articles = Article.search params[:q],include:[:comments] return render json:@articles end
autocomplete
検索キーワードの自動補完を行なう設定をしてみます。どうやらtypeahead.jsかjquery UIを使用して実装するのが良いみたいです。今回はtypeahead.jsを使ってやってみます。
まずはモデルの変更。タイトルを自動補完するように設定します。
article.rb
class Article < ActiveRecord::Base has_many :comments - searchkick + searchkick autocomplete:["title"]
次にコントローラーでautocompleteを受けるアクションを作成
articles_controller.rb
+ def autocomplete + render json: Article.search(params[:term], autocomplete: true, limit: 10).map(&:title) + end +
ルーティングを通します。
routes.rb
get "search" => "articles#search" + get "autocomplete" => "articles#autocomplete"
vendor/assets以下にtypeahead.jsを落として来て配置。application.jsで読み込みます。とりあえずautocompleteの処理を書くために、custom.js.coffeを追加し、application.jsでこちらも読み込んでおきます。
application.js
//= require typeahead //= requrie custom
続いてViewの設定をします。 まずは、Viewのサーチボックスにidを振っておきます。
index.html.erb
<div> <%= form_tag('/search',method: :get) do %> - <input type="text" name="q"> + <input type="text" name="q" id="query"> <button tye="submit">Search</button> <% end %> </div>
最後に、typeahead.jsを使って、autocompleteするように記述。
custom.js
+$(-> + $("#query").typeahead( + remote: "/autocomplete?term=%QUERY" + ) + +)
これで準備は整いました。あとは
rake searchkick:reindex CLASS=Article ELASTICSEARCH_URL=http://192.168.33.10:9200
を行なって検索ボックスに入力を始めると、自動補完されていろいろと出て来たかと思います。CSSが乗っていないのが原因でひどくズレていますが、今回は動作を確認することが目的なので省略します。
条件指定して検索
ここからは、解説のみになります。 SQLライクに検索出来るみたいですね。
Product.search "2% Milk", where: {in_stock: true}, limit: 10, offset: 50
特定のフィールドだけを検索する
fields: [:name, :brand]
whereの条件指定方法
where: { expires_at: {gt: Time.now}, # lt, gte, lte also available orders_count: 1..10, # equivalent to {gte: 1, lte: 10} aisle_id: [25, 30], # in store_id: {not: 2}, # not aisle_id: {not: [25, 30]}, # not in or: [ [{in_stock: true}, {backordered: true}] ] }
limit,offset
limit: 20, offset: 40
boost
boost: "orders_count" # give popular documents a little boost
Personalized Results
特定のユーザーに最適化された結果を返すことも出来るようです。parsonalize:でなにをkeyとして結果を絞るかを指定。user_idsに製品を買った人のIDを指定する。検索時にユーザーIDを指定して検索掛けると、user_idが含まれているProductをboostして表示する。というような形でしょうか。
class Product < ActiveRecord::Base searchkick personalize: "user_ids" def search_data { name: name, user_ids: orders.pluck(:user_id) # boost this product for these users # [4, 8, 15, 16, 23, 42] } end end
Keep Getting Better
検索された結果を元に、検索エンジンを学習させて行く機能をつける方法もあるようです。公式のドキュメントだけだとよくわからんって感じですが、ここみたら解決しました。検索された際に、コントローラーでメトリクス収集すること、それと毎日cronなんかでreindexし直すことが必要があるみたいですね。
class Search < ActiveRecord::Base belongs_to :product # fields: id, query, searched_at, converted_at, product_id end
class Product < ActiveRecord::Base has_many :searches searchkick conversions: "conversions" # name of field def search_data { name: name, conversions: searches.group("query").count # {"ice cream" => 234, "chocolate" => 67, "cream" => 2} } end end
RailsでApache Solrを使って全文検索機能を実装しようとしていましたが、ElasticSearchの方がシンプルで分かりやすいような気がします。Apache Solrを使って開発していた際に、本番環境への移行で非常に苦労しました。。。Sunspot_solrからの切り替えやらなんやら。今度は、ElasticSearchを利用して実装してみようかなと思います。