私が仕事で携わっている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にしなければならないので注意!
indexオプションの引数をbooleanに変更
Elasticsearch1.7の頃は、indexesのindexオプションは、
- analyzed
- not_analyzed
- no
の3種類があったのだけれど、これらについてWaningが出ていた。 indexオプションはbooleanを渡すようになった模様。
ただし、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でアドバイスをしていただけた。
多分、boolとtermをfilterのところに書く感じかなぁと。参考はこの辺ですかねぇ?https://t.co/K2YkiqJTTW
— Jun Ohtani (@johtani) 2017年8月31日
ということで、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で期待の検索結果のテストを書いていたので、安心して修正することができた。できたんだけれど、検索のテストなのでテストデータの登録が大量にあり、かつ検索条件が大量にあるせいでテストケースも多いため、テストにかなり時間がかかって、ちょっとした変更でも確認するのが大変だった。とはいえ、テストコードがなかったらもっと大変である…。テストコードの有り難さを再確認できた。