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

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RailsでElasticsearch: ソート機能を実装

elasticsearch 検索

f:id:nipe880324:20151017170217p:plain:w420

前々回は「RailsでElasticsearchを使って簡単な全文検索」、
前回は、「ページネーション・1ページあたりの表示件数」を実装しました。
今回は、「ソート機能」を実装します。Elasticsearchのソート機能があるのでかなり簡単に作成できます。


動作確認

  • Mac OS X 10.11 El Capitan
  • elasticsearch 1.7.2
  • Rails 4.2.3
  • elasticsearch-dsl 0.1.2
  • elasticsearch-model 0.1.8
  • elasticsearch-rails 0.1.8

1. ソートのElasticsearchのクエリ

sortパラメータを指定することで、ソートすることができます。
ソートキーとorderにソート順の「asc(昇順)」か「desc(降順)」を指定します。

日付でソート

デフォルトのソート順はasc(昇順)です。
Marvel Senseを開き下記クエリを実行します。
ドキュメントをcreated_atの降順(desc)に並べます。つまり、「新しい順」に並べます。

GET restaurant_development/_search
{
  "query": { "match_all": {} },
  "sort": { "created_at": { "order": "desc" } }
}

これを使えば、商品などで価格フィールドがある場合は、「安い順」「高い順」なども簡単にできます。

ソートするフィールドに値がない場合に、最後に表示するか(_last)、最初に表示するか(_first)をmissingで指定することもできます。
デフォルトでは最後に表示するようになっているので特段指定しなくてもいいと思います。
下記は、created_atに値がないドキュメントを最初に表示します。

GET restaurant_development/_search
{
  "query": { "match_all": {} },
  "sort": { "created_at": { "order": "desc", "missing": "_first" } }
}


関連度でソート

関連度でソートをする場合、デフォルトのソート順はdesc(関連度が高い順)です。
_scoreを指定することで関連度順にソートできます。

GET restaurant_development/_search
{
  "query": { "match_all": {} },
  "sort": "_score"
}

例えば、titleとcommentがあるときに、titleにマッチしたほうがスコアを高くなるように設定するなどして、関連度のスコアを変えます。スコアの設定は省略します。




2. ソート機能を実装

「出店の新しい順」「あいうえお順」などのソート順を指定できるリンクを追加します。
f:id:nipe880324:20151018052901p:plain:w420


ソート順のリンクを表示

ソート順のリンクを画面に表示するsort_linksヘルパーメソッドでリンクを作成します。
また、created_atを出店日とし、画面に表示させます。
(※created_atはあくまでレコードが作成された日付なので、管理上はopend_atなどカラムを新たに追加し、その値を出店日としたほうがよいです)

<!-- app/views/top/index.html.erb -->

<!-- 検索結果の表示 -->
<div class="col-xs-9">
  <div>検索結果: <%= current_page(params: params) %> / 約<%= @restaurants.results.total %></div>
  <div>表示件数: <%= per_page_links per_pages: [40, 80, 120], query_string: query_string(params) %></div>
  <!-- ソート順のリンクを追加 -->
  <div>ソート順: <%= sort_links %></div>

  <div id="results">
    <% @restaurants.each do |r| %>
      <hr />
      <div class="result">
        <h4><%= r.name %><%= r.name_kana %></h4>
        <p class="text-muted">
          <small>都道府県: <%= r.pref.name %></small>
          <small>カテゴリ: <%= r.category.name %></small>
          <!-- 出店日を追加 -->
          <small>出店日: <%= r.created_at %></small>
        </p>
      </div>
    <% end %>
  </div>


sort_linksヘルパーメソッドを作成

ソート順のリンクを返すsort_linksヘルパーメソッドを作成します。
また、query_stringメソッド:sortキーを追加します。

# app/helpers/application_helper.rb
module ApplicationHelper
  ...
  # ソートのリンクを返す
  def sort_links
    # 現在の値の設定。例:{ sort: 'created_at+asc' }
    current = { sort: query_string.fetch(:sort, ::Restaurant::SORTS.first[:sort]) }

    # aタグの作成
    ::Restaurant::SORTS.map do |sort|
      if current == sort.except(:name)
        sort[:name] # 現在設定されているソート順をテキストで表示
      else
        link_to(sort[:name], "?#{query_string.merge(sort: sort[:sort]).to_query}")
      end
    end.join(' | ').html_safe
  end

  # クエリストリングを作成
  def query_string(params)
    params.slice(:q, :closed, :per, :sort)
  end
end

ソート順のデータをRestaurantモデルにSORTSという定数で定義しておきます。

# app/models/restaurant.rb
class Restaurant < ActiveRecord::Base
  belongs_to :category
  belongs_to :pref

  # ソートの組み合わせ
  # name: 画面に表示する文字列。sort: <ソートするキー名>+<ソート順序(asc or desc)>
  SORTS = [
    { name: '出店の新しい順', sort: 'created_at+desc' },
    { name: '出店の古い順',   sort: 'created_at+asc' },
    { name: 'あいうえお順',   sort: 'name_kana+asc' }
  ]
  ...


ソートクエリの作成

searchメソッドを修正して、ソートをできるようにします。
まずは、paramsからソートキーとソートオーダーを取得します。
また、lasticsearch-ruby/elasticsearch-dsl/lib/elasticsearch/dsl/search/sort.rbを参考にし、search_definitionsortを定義します。

#app/models/restaurant.rb

def self.search(params = {})
  # 検索パラメータを取得
  ...
  # sort_by: ソートのキー('created_at'など)、order: ソートの順序('asc'か'desc')
  sort_by, order = (params[:sort] || SORTS.first[:sort]).split('+')

  # 検索クエリを作成(Elasticsearch::DSLを利用)
  # 参考: https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-dsl
  search_definition = Elasticsearch::DSL::Search.search {
    query {
      ...
    }

    # ソート
    # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html
    sort {
      by sort_by, order: order
    }
  }

  __elasticsearch__.search(search_definition)
end


ソートの動作確認

http://localhost:3000/にアクセスし、ソート順が指定できます。
f:id:nipe880324:20151018052901p:plain:w420

このときElasticsearchへのクエリは次のようになり、sortでソートするフィールド名が、orderで順序が指定されていることがわかると思います。

Started GET "/?sort=created_at%2Basc" for ::1 at 2015-10-18 05:24:09 +0900
Processing by TopController#index as HTML
  Parameters: {"sort"=>"created_at+asc"}
  Restaurant Search (18.5ms) {index: "restaurant_development", type: "restaurant", body: {query: {filtered: {query: {match_all: {}}, filter: {term: {closed: "false"}}}}, sort: [{"created_at"=>{order: "asc"}}]}, size: 40, from: 0}
  Rendered top/index.html.erb within layouts/application (222.3ms)
Completed 200 OK in 472ms (Views: 441.0ms | ActiveRecord: 0.0ms | Elasticsearch: 18.5ms)

以上です。

まとめ

今回は、Elasticsearchのsortキーを追加することで簡単にソートに実装しました。
次回は、ファセット(アグリゲーション)と Post Filterを実装します。

参考文献