patorashのブログ

方向性はまだない

Elasticsearchを1.7系から5系にバージョンアップした

私が仕事で携わっているRailsプロジェクトで使っているElasticsearchのバージョンアップを行った。1.7系から一気に5.5.2 5.1.2*1にアップデートしたため、ハマりどころも多かったので、自分の備忘録のために記録しておく。

Dockerを使ってElasticsearchのバージョン切替

まず、ローカルでのElasticsearchのバージョン切替には、Dockerを使った。Dockerで対象バージョンを切り替えると、もし既存のプログラムに不具合が出た場合でも対象のElasticsearchを以前のものに切り替えるだけでいいので、メンテナンス性に優れている。 以前はHomebrewで入れていたのだが、ミドルウェアのバージョンアップを行うのは大変なので、開発の途中段階でDocker for MacとKitematicを導入し、Elasticsearch、Postgresql、Redisなどのミドルウェアは全てDockerを使って扱うようにしている。

Docker HubからDocker Imageを取得

Macを使っているので、Kitematic経由でElasticsearchのofficialから5.5.2-alpine 5.1.2-alpine*2を取得した。

kuromojiのインストール

デフォルトのイメージだとkuromojiプラグインが入っていないので、shellにログインしてインストールした。

elasticsearch-plugin install analysis-kuromoji

とりあえずこれでローカルの環境はOKとした。(あとでオリジナルのDocker Imageを作った)

Elasticsearchのmappingの修正

Railsでは、elasticsearch-rails、elasticsearch-modelを使っている。とりあえず1.7のときの記述のまま動かして、Elasticsearchのログを見ていると、Warningが大量に出ていた…。もちろん動かない項目もたくさんあった。以降に出てくるコードはRailsの各Modelでのmappingの修正項目なので、Rubyのコードである。

mappingはdynamicをfalseに

Dynamic mappingを有効にしておくと、fieldを定義しなくてもよきに計らってデータ登録ができるみたいであったけれど、fieldを管理したかったのでfalseに設定しておいた。

Before(Elasticsearch 1.7)
mapping do
  # 略
end
After(Elasticsearch 5.1.2)
mapping dynamic: false do
  # 略
end

検索対象項目を全てmappingしなければならない

1.7の頃は、as_indexed_jsonメソッドで渡した要素が全て検索対象になっており、明示的にindexesで設定しなくてもよかった。ところが、5系ではindexesに登録しているfieldでないと、検索項目として無視されてしまった。1.7の頃はanalyzerを指定していたfieldだけindexesを書いて登録し、数字などのfieldはas_indexed_jsonで渡すだけだったのだが、全てindexesで定義し直した。 (ここまで書いて1.7の頃はデフォルトでdynamicがtrueだったってことか?と思った…)

typeがstringのものを、textまたはkeywordに変更

string型が非推奨になっており、textまたはkeywordを指定する必要がある。textはanalyzed, keywordだとnot_analyzedという感じで使い分ける模様。keywordはメールアドレスやタグ等のような文字列に使うそうな。また、ソートに使う項目の場合はtypeはkeywordにしなければならないので注意!

www.elastic.co

indexオプションの引数をbooleanに変更

Elasticsearch1.7の頃は、indexesのindexオプションは、

  • analyzed
  • not_analyzed
  • no

の3種類があったのだけれど、これらについてWaningが出ていた。 indexオプションはbooleanを渡すようになった模様。

www.elastic.co

ただし、index: falseを指定すると、そのフィールドは検索クエリで使えないので、基本的にはtrueにするといいと思う。なお、このオプションは省略しても検索対象になったので、デフォルトでindex: trueの模様。検索対象にしたくない場合のみ、index: falseを指定するのがよさそう。

Before(Elasticsearch 1.7)
mapping dynamic: false do
  indexes :start_date, type: 'date', index: :not_analyzed
end
After(Elasticsearch 5.1.2)
mapping dynamic: false do
  indexes :start_date, type: 'date', index: true
end

関連データの指定方法の変更

リレーションデータも検索対象とする際に、同様にindexesにマッピングしていたのだけれど、オプションを追加しないと動かなかった。 typeにnestedを指定すると、1対多を表現できるようになる模様。省略すると、typeがobjectとなり、1対1になるみたいだった。 また、include_in_rootを指定しないと、検索クエリを生成する際にnested用のクエリを作らないといけなくて、大変なので入れておいたら楽だった。

include_in_rootを指定したほうがいい場合と、しないほうがいい場合のメリット・デメリットがわかっていないのだけれど、私がクエリを書く分には、メリットしかなかった(クエリで"foos.name"と書けるようになった)。 デメリットがある場合は教えてほしい…。

Before(Elasticsearch 1.7)
mapping dynamic: false do
  indexes :foos do
    indexes :name, type: 'string', index: :analyzed, analyzer: :ngram_analyzer
  end
end
After(Elasticsearch 5.1.2)
mapping dynamic: false do
  indexes :foos, type: 'nested', include_in_root: true do
    indexes :name, type: 'string', index: :true, analyzer: :ngram_analyzer
  end
end

Elasticsearchのクエリの修正

クエリに関しても、なくなったオプションや、指定方法の変更などが発生していたので、覚えている限り書いておく。

FilterからQueryに統一

一番大きな変更だったと思うのだが、Filterが使えなくなっていた。Queryの中で処理するということになったので、全てQuery内で処理するようにした。ただし、これだと検索結果のスコアに影響を与えるため(filterはスコアには影響を与えなかった)、場合によってはこれだけではダメかもしれない。

Before(Elasticsearch 1.7)
definition = Elasticsearch::DSL::Search::Search.new
definition.query do
  filtered do
    filter do
      bool do
        must do
          range :start_date do
            gte start_date_gteq
            lte start_date_letq
          end
        end
      end
    end
  end
end
After(Elasticsearch 5.1.2)
definition = Elasticsearch::DSL::Search::Search.new
definition.query do
  bool do
    must do
      range :start_date do
        gte start_date_gteq
        lte start_date_letq
      end
    end
  end
end

termsのexecutionオプションの廃止

termsはデフォルトでor条件になるのですが、execlution: :andと指定するとand条件に変更できた。しかし、これがなくなっていた。これに関しては、どうすればいいんだーとtwitterで呟いていたら、Elastic社の@johtaniさんにtwitterでアドバイスをしていただけた。

ということで、termを複数指定するしかなさそうだった。

Before(Elasticsearch 1.7)
must do
  terms bar_id: bar_ids, execution: :and
end
After(Elasticsearch 5.1.2)
bar_ids.each do |bar_id|
  must do
    term bar_id: bar_id
  end
end

Fieldのmissing指定の廃止

Elasticsearchの場合はfieldの値がない場合はそのfieldが作られないので、nullの場合みたいな条件指定ではなく、そのフィールドがない場合(missing)、という条件指定になっていた。 これが、must_notとexistsを組み合わせる形を使うように変更になっていた。

Before(Elasticsearch 1.7)
must do
  missing field: :end_date
end
After(Elasticsearch 5.1.2)
must_not do
  exists do
    field :end_date
  end
end

やったあとの感想

1.7から5.1.2に一気に上げたので、変更点も多く、思ったよりも時間がかかってしまった。とはいえ、公式ドキュメントやtwitterでのアドバイスを参考にしながらちゃんと更新できたのはよかった。ずっと気になっていたものをやり終えることができたのは素直に嬉しい。

変更が多かったが、RSpecで期待の検索結果のテストを書いていたので、安心して修正することができた。できたんだけれど、検索のテストなのでテストデータの登録が大量にあり、かつ検索条件が大量にあるせいでテストケースも多いため、テストにかなり時間がかかって、ちょっとした変更でも確認するのが大変だった。とはいえ、テストコードがなかったらもっと大変である…。テストコードの有り難さを再確認できた。

参考にしたサイト

*1:HerokuのAddOnのSearchBoxのバージョンが5.1.2だった

*2:こちらもバージョンをSearchBoxと合わせた