searchkickのコードリーディング
普段、ElasticsearchとDBの関連付け兼Elasticsearchのクライアントとして searchkickをよく使っている。
searchkickは便利で、
Model.search('term', options).results
のようにメソッドを呼ぶだけで、Elasticsearchに検索をかけて、取得したドキュメントの_id
フィールドを元にDBに検索をかける。その結果をModel オブジェクトの配列として受け取ることができる。
searchkickはoptionに並び並び順を指定することができる。 例えば
Model.search('term', order: { hoge: :desc }).results
といった感じに指定すれば、最終的に得られるModelオブジェクトの配列は hoge の降順に整列されている。
そこで少し疑問に思ったことがあった。searchkickが発行するsqlにはorder by
などは指定されていなかった。
Elasticsearchに検索でパフォーマンスを出すために非正規化されたデータが入っているので order by column
はできないにせよ、
order by field(id, ids)
のようなsqlが発行されるのだろうと思っていた。
少し気になったのでどうやってソートしているのかsearchkickのコードを読んでみた。
searchkickがelasticsearchへの検索結果を元にDBにクエリを投げるのは results
メソッドなのでそのあたり読んだ。
results
メソッドが定義されているのはこの辺。
まず
hits.group_by { |hit, _| hit["_type"] }.each do |type, grouped_hits| results[type] = results_query(type.camelize.constantize, grouped_hits).to_a.index_by { |r| r.id.to_s } end
でDBに問い合わせを行い、その結果をresults
変数に以下のような形式で格納している
{ type: { id1: model_object1, id2: model_object2 } }
(id*
は対になっているmodel_objectが保持しているrecordのidです。)
ここではまだソートは行っていない。コードを追っていくとModel.where(id: ids)
を呼んでいるだけだ。
その下の方にご丁寧に sort
とと書かれたコメントとともに以下のような処理が書かれている
hits.map do |hit| result = results[hit["_type"]][hit["_id"].to_s] if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable]) unless result.respond_to?(:search_hit) result.define_singleton_method(:search_hit) do hit end end if hit["highlight"] && !result.respond_to?(:search_highlights) highlights = Hash[hit["highlight"].map { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, v.first] }] result.define_singleton_method(:search_highlights) do highlights end end end result end.compact
なんか長々と書かれているが、大半はoptionによってmethodを定義するみたいなやつなので純粋にソートに関する部分を切り出したら以下のようになる
hits.map do |hit| results[hit["_type"]][hit["_id"].to_s] end.compact
めちゃくちゃシンプルで、Elasticsearchへの検索の結果からどの順番でソートすれば良いかは分かっているので、DBへの問い合わせ結果をその順序で抜き出して配列にしているだけ。
下手にorder by field(id, ids)
みたいなクエリでもindexでソートを解決できずにfilesortが発生することもあるが、この方法だとO(n)のコストでソートできるので、不特定多数の人に使われるgemと考えると、DBでソートせずrubyでソートするのが正しいのだろうと思った。
また、order by field(id, ids)
をしているクエリでfilesortが発生したらsearchkickのようにruby側でソートするのも手だと思った。
gem のコードたまにしか読まないけど、読むと毎回何かしらの学びがある。