searchkickを読む
仕事で使ってるけど割とふいんき()って感じでよくわかっていないことが多いので読む
基本的にREADMEが非常に充実しているので、ぶっちゃけコード読まなくても結構なんとかなる。
が、挙動をある程度知ってないとElasticsearchインフラをごにょごにょするときに困る。
なお読むのはv4.3.1
まずはルートのlib/searchkick.rb
から。
Searchkick.xxx
諸々のattr_accessorやclient, envなどが定義されている。clientがオプション付きのElasticsearch gemのソレであることがわかる。
def self.client
@client ||= begin
require "typhoeus/adapters/faraday" if defined?(Typhoeus) && Gem::Version.new(Faraday::VERSION) < Gem::Version.new("0.14.0")
Elasticsearch::Client.new({
url: ENV["ELASTICSEARCH_URL"],
transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
retry_on_failure: 2
}.deep_merge(client_options)) do |f|
f.use Searchkick::Middleware
f.request signer_middleware_key, signer_middleware_aws_params if aws_credentials
end
end
end
ちょっと下にいくと検索本体であろうSearchkick.search
が定義されているが、これはまた後ほど。
一番下までいくと
# TODO find better ActiveModel hook
require "active_model/callbacks"
ActiveModel::Callbacks.include(Searchkick::Model)
ActiveSupport.on_load(:active_record) do
extend Searchkick::Model
end
詳しくは不勉強にて知らないが、雰囲気的にはActiveRecordなクラスでSearchkick::Model
内のコードが使えるようにしているのであろう。
on_loadについてはドキュメントがあった: https://railsguides.jp/engines.html#active-supportのon-loadフック
lib/searchkick/model.rb
を見ると、Searchkick::Model
にdef searchkick
が定義されている。定義でいうとこれだけ。
要するに
class Product < ApplicationRecord
searchkick
end
のsearchkick
部分があるだけで、それ以外はこのメソッドが定義しているようだ。
メソッド内部は大きく分けて、オプションsanitize&読み込み・index名決定・class_evalになっている。
オプションのsanitizeは
unknown_keywords = options.keys - [:_all, :_type, :batch_size, ...]
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
となっていて、keysからvalidなkeywordsを引いて残りがあればunknownとしてる。なるほどなー。
index名決定は、index_name
やindex_prefix
など指定できるが、デフォルトであれば
[Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
で、まぁSearchkick.index_prefix
やSearchkick.index_suffix
なども無指定であれば
"#{model_name.plural}_#{Searchkick.env}"
と等価になる。要するにproducts_development
とかそんな感じ。
なおSearchkick.env
の定義はlib/searchkick.rb
にあった。名前の通りのもの。
class_evalの中には、search_index
やreindex
、またcallbackなどが定義されている。
モデルにはSearchkick::Index
のインスタンスがくっついており、おそらくESのindexへのアクセスなどをしているのだろう。
またsearchkick_reindex
にてsearchkick_index.reindex
という呼び出しが見え、indexするなどの操作もしていそう。
それぞれのメソッドやalias定義で
alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
や
def reindex(method_name = nil, **options)
RecordIndexer.new(self).reindex(method_name, **options)
end unless method_defined?(:reindex)
という感じにunless method_defined?
しており、ユーザによる自前定義を上書きしないようにしている。
てか、defの末尾にもunlessつけて分岐できるのか。いや、冷静に考えれば特に不思議は無いし、なんならなるほど感ある。
次はlib/searchkick/index.rb
にあるSearchkick::Index
を見てみる。
パッと見で
def create(body = {})
client.indices.create index: name, body: body
end
def delete
if alias_exists?
# can't call delete directly on aliases in ES 6
indices = client.indices.get_alias(name: name).keys
client.indices.delete index: indices
else
client.indices.delete index: name
end
end
def exists?
client.indices.exists index: name
end
def refresh
client.indices.refresh index: name
end
def alias_exists?
client.indices.exists_alias name: name
end
def mapping
client.indices.get_mapping index: name
end
def settings
client.indices.get_settings index: name
end
と、いかにもES本体の操作のwrapperですよって感じのコードが並んでいる。
ちなみにclient
はSearchkick.client
なので要するにElasticsearch::Client
のインスタンス。
もうちょっと見るとdef promote
がありindexのaliasを使った切り替えとかやるのかなーとか、def reindex
やdef reindex_scope
などreindexの実体っぽいもの、def create_index
にタイムスタンプ付きindexを作っている部分があったり、色々実装の詳細に踏み込んでいけそうな感じ。
とはいえファイルごとに網羅的に見ていくと上下レイヤがわからず微妙なので、ユースケースごとに縦に掘っていくのがよさそうか。
SomeModel.reindex
と呼び出した場合を考える。lib/searchkick/model.rb
のclass_evalの中にある定義を見ると
def searchkick_reindex(method_name = nil, **options)
# TODO relation = Searchkick.relation?(self)
relation = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
(respond_to?(:queryable) && queryable != unscoped.with_default_scope)
searchkick_index.reindex(searchkick_klass, method_name, scoped: relation, **options)
end
alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
relation
が長いが、respond_to?
は後段のための前提チェックみたいな感じなので、理解のために除去していみると
relation = (current_scope && current_scope.to_sql != default_scoped.to_sql) ||
(queryable != unscoped.with_default_scope)
となる。読めそう。
current_scope
はActiveRecord::Relationでwhereとかくっつけたときの現在の条件全体的な感じであろう。きっと。
そしてdefault_scoped
とsql比較をして不一致を確認しているので、前半は「モデルのdefault_scope以外に何かクエリが付与されているか」って感じか。
※後半のqueryable
はよくわからなかった。ググるとmongoidの話とかが出てくるのでそっち系かも。ここでは無視。
relationという変数名ではあるがsearchkick_index.reindex
にscoped
というoption名で渡されていることや、上にあるTODOコメントの雰囲気からも、この意図で合ってそう。
冒頭に戻ってSomeModel.reindex
の呼び出しであれば
searchkick_index.reindex(SomeModel, nil, scoped: false)
で呼び出されると言えそう。
lib/searchkick/index.rb
に戻ってreindex
の定義を見る。
def reindex(relation, method_name, scoped:, full: false, scope: nil, **options)
refresh = options.fetch(:refresh, !scoped)
options.delete(:refresh)
if method_name
# TODO throw ArgumentError
Searchkick.warn("unsupported keywords: #{options.keys.map(&:inspect).join(", ")}") if options.any?
# update
import_scope(relation, method_name: method_name, scope: scope)
self.refresh if refresh
true
elsif scoped && !full
# TODO throw ArgumentError
Searchkick.warn("unsupported keywords: #{options.keys.map(&:inspect).join(", ")}") if options.any?
# reindex association
import_scope(relation, scope: scope)
self.refresh if refresh
true
else
# full reindex
reindex_scope(relation, scope: scope, **options)
end
end
オプションによって3方向に分岐するようだが、今回の前提ではmethod_name
はnil
、scoped
とfull
はfalse
である。
とすると実行されるコードは
# full reindex
reindex_scope(relation, scope: scope, **options)
であり、もうちょっと具体値を入れるとreindex_scope(SomeModel, scope: nil)
となる。
# full reindex
らしい。まぁそうだな。