RailsとElasticsearchで検索機能をつくり色々試してみる - Rspec

## はじめに
RspecでElasticsearchを使ったテストを書く方法を紹介していきます。
[elasticsearch-rails](https://github.com/elastic/elasticsearch-rails/tree/master)を使うことが前提の記事になります。

また、本記事で出てくるサンプルや環境は、[RailsとElasticsearchで検索機能をつくり色々試してみる - その１：サンプルアプリケーションの作成](https://qiita.com/yamashun/items/6ecaa6f161b4cf283db3)をもとにしています。

## 環境
- Ruby 2.5.3
- Rails 5.2.2
- Elasticsearch 6.5.4

gem

- rspec-rails 3.8.2
- elasticsearch-model 6.0.0
- elasticsearch-rails 6.0.0

### テストで使用するElasticsearchのクラスターについて
調査する中で`elasticsearch-extensions` gemを使ってテスト用のclusterを立てる記事が多く見つかりましたが、ローカルの環境で開発用のclusterが起動している状態であれば別に起動する必要はないかと思い使用しませんでした。


## テスト対象のコード
タイトルと説明カラムを持つ漫画モデルを検索する処理を以下のようにconcernsに実装していた場合のテストを考えます。

```ruby:app/models/manga.rb
class Manga < ApplicationRecord
  include MangaSearchable
end
```
 
```ruby:app/models/concerns/manga_searchable.rb
module MangaSearchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model

    # index名
    # 環境名を入れることで開発用とは別にrspec用のindexを作成する
    index_name "es_manga_#{Rails.env}"

    # マッピング情報
    settings do
      mappings dynamic: 'false' do
        indexes :id,                   type: 'integer'
        indexes :title,                type: 'text', analyzer: 'kuromoji'
        indexes :description,          type: 'text', analyzer: 'kuromoji'
      end
    end

    def as_indexed_json(*)
      attributes
        .symbolize_keys
        .slice(:id, :title, :description)
    end
  end

  class_methods do
    def create_index!
      client = __elasticsearch__.client
      # すでにindexを作成済みの場合は削除する
      client.indices.delete index: self.index_name rescue nil
      client.indices.create(index: self.index_name,
                            body: {
                                settings: self.settings.to_hash,
                                mappings: self.mappings.to_hash
                            })
    end

    # 今回テストする検索処理
    def es_search(query)
      __elasticsearch__.search({
        query: {
          multi_match: {
            fields: %w(title description),
            type: 'cross_fields',
            query: query,
            operator: 'and'
          }
        }
      })
    end
  end
end

```
## Rspec
### index作成について
それぞれのテストを独立して行うため、ケース毎にindexを作成するようにします。また、elasticsearchに関わるテストでのみindex作成を行えばよいのでmeta情報でindex作成を制御できるようにするのがよいと思います。
以下はその例です。

```ruby:spec/rails_helper.rb
　　RSpec.configure do |config|

  config.before :each do |example|
    if example.metadata[:elasticsearch]
      Manga.create_index!
    end
  end
```

## テストケース


```rb:spec/models/concerns/manga_searchable_spec.rb
require 'rails_helper'

# elasticsearch: true を追加しindexをテストケース毎に再作成する
RSpec.describe MangaSearchable, elasticsearch: true do

  describe '.es_search' do
    describe '検索ワードにマッチする漫画の検索' do
      let!(:manga_1) do
        create(:manga, title: 'キングダム', description: '時は紀元前―。いまだ一度も統一...')
      end
      let!(:manga_2) do
        create(:manga, title: '僕のヒーローアカデミア', description: '多くの人間が“個性という力を持つ...')
      end
      let!(:manga_3) do
        create(:manga, title: 'はたらく細胞', description: '人間1人あたりの細胞の数、およそ60兆個...')
      end

      before :each do
        # 作成したデータをelasticsearchに登録する
        # refresh: true を追加することで登録したデータをすぐに検索できるようにする
        Manga.__elasticsearch__.import(refresh: true)
      end

      def search_manga_ids
        Manga.es_search(query).records.pluck(:id)
      end

      context '検索ワードがタイトルにマッチする場合' do
        let(:query) { 'キングダム' }

        it '検索ワードにマッチする漫画を取得する' do
          expect(search_manga_ids).to eq [manga_1.id]
        end
      end

      context '検索ワードが本文にマッチする場合' do
        let(:query) { '60兆個' }

        it '検索ワードにマッチする漫画を取得する' do
          expect(search_manga_ids).to eq [manga_3.id]
        end
      end

      context '検索ワードが複数ある場合' do
        let(:query) { '人間 個性' }

        it '両方の検索ワードにマッチする漫画を取得する' do
          expect(search_manga_ids).to eq [manga_2.id]
        end
      end
    end
  end
end

```


### refresh: true オプション
ポイントは、import時にrefresh: true オプションを追加する点です。

[ドキュメント](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-concepts.html#_near_realtime_nrt)より
> Elasticsearch is a near-realtime search platform. What this means is there is a slight latency (normally one second) from the time you index a document until the time it becomes searchable.

documentを登録してから、検索ができるようになるまで通常は1秒かかります。（1秒間隔で更新を反映しているようですが、この間隔は変更することもできます。）
そのため、データをimportしてすぐに検索を行うと、更新が反映されていないためテストに失敗してしまいます。`refresh: true`オプションを渡すことで、importするタイミングでリフレッシュされ検索できるようになります。

## 参考
[Rails から Elasticsearch を使っているときのテストの書き方（elasticsearch-rails, RSpec）](https://blog.inouetakuya.info/entry/2014/11/03/191059)
[Elasticsearchを使ったテストを書くときにsleep 1するのはやめましょう](https://blog.bitjourney.com/entry/2015/08/24/200508)
[ElasticSearchのインデクシングを高速化する](http://kakakazuma.hatenablog.com/entry/2015/03/15/153704)