searchkick 4.4.0 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +160 -3
- data/LICENSE.txt +1 -1
- data/README.md +567 -421
- data/lib/searchkick/bulk_reindex_job.rb +12 -8
- data/lib/searchkick/controller_runtime.rb +40 -0
- data/lib/searchkick/index.rb +167 -74
- data/lib/searchkick/index_cache.rb +30 -0
- data/lib/searchkick/index_options.rb +465 -404
- data/lib/searchkick/indexer.rb +15 -8
- data/lib/searchkick/log_subscriber.rb +57 -0
- data/lib/searchkick/middleware.rb +9 -2
- data/lib/searchkick/model.rb +50 -51
- data/lib/searchkick/process_batch_job.rb +9 -25
- data/lib/searchkick/process_queue_job.rb +4 -3
- data/lib/searchkick/query.rb +106 -77
- data/lib/searchkick/record_data.rb +1 -1
- data/lib/searchkick/record_indexer.rb +136 -51
- data/lib/searchkick/reindex_queue.rb +51 -9
- data/lib/searchkick/reindex_v2_job.rb +10 -34
- data/lib/searchkick/relation.rb +247 -0
- data/lib/searchkick/relation_indexer.rb +155 -0
- data/lib/searchkick/results.rb +131 -96
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick/where.rb +11 -0
- data/lib/searchkick.rb +202 -96
- data/lib/tasks/searchkick.rake +14 -10
- metadata +18 -85
- data/CONTRIBUTING.md +0 -53
- data/lib/searchkick/bulk_indexer.rb +0 -173
- data/lib/searchkick/logging.rb +0 -246
data/lib/searchkick/indexer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# thread-local (technically fiber-local) indexer
|
2
|
+
# used to aggregate bulk callbacks across models
|
1
3
|
module Searchkick
|
2
4
|
class Indexer
|
3
5
|
attr_reader :queued_items
|
@@ -14,15 +16,20 @@ module Searchkick
|
|
14
16
|
def perform
|
15
17
|
items = @queued_items
|
16
18
|
@queued_items = []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
|
20
|
+
return if items.empty?
|
21
|
+
|
22
|
+
response = Searchkick.client.bulk(body: items)
|
23
|
+
if response["errors"]
|
24
|
+
# note: delete does not set error when item not found
|
25
|
+
first_with_error = response["items"].map do |item|
|
26
|
+
(item["index"] || item["delete"] || item["update"])
|
27
|
+
end.find { |item| item["error"] }
|
28
|
+
raise ImportError, "#{first_with_error["error"]} on item with id '#{first_with_error["_id"]}'"
|
25
29
|
end
|
30
|
+
|
31
|
+
# maybe return response in future
|
32
|
+
nil
|
26
33
|
end
|
27
34
|
end
|
28
35
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# based on https://gist.github.com/mnutt/566725
|
2
|
+
module Searchkick
|
3
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
4
|
+
def self.runtime=(value)
|
5
|
+
Thread.current[:searchkick_runtime] = value
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.runtime
|
9
|
+
Thread.current[:searchkick_runtime] ||= 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.reset_runtime
|
13
|
+
rt = runtime
|
14
|
+
self.runtime = 0
|
15
|
+
rt
|
16
|
+
end
|
17
|
+
|
18
|
+
def search(event)
|
19
|
+
self.class.runtime += event.duration
|
20
|
+
return unless logger.debug?
|
21
|
+
|
22
|
+
payload = event.payload
|
23
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
24
|
+
|
25
|
+
index = payload[:query][:index].is_a?(Array) ? payload[:query][:index].join(",") : payload[:query][:index]
|
26
|
+
type = payload[:query][:type]
|
27
|
+
request_params = payload[:query].except(:index, :type, :body)
|
28
|
+
|
29
|
+
params = []
|
30
|
+
request_params.each do |k, v|
|
31
|
+
params << "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
|
32
|
+
end
|
33
|
+
|
34
|
+
debug " #{color(name, YELLOW, bold: true)} #{index}#{type ? "/#{type.join(',')}" : ''}/_search#{params.any? ? '?' + params.join('&') : nil} #{payload[:query][:body].to_json}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def request(event)
|
38
|
+
self.class.runtime += event.duration
|
39
|
+
return unless logger.debug?
|
40
|
+
|
41
|
+
payload = event.payload
|
42
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
43
|
+
|
44
|
+
debug " #{color(name, YELLOW, bold: true)} #{payload.except(:name).to_json}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def multi_search(event)
|
48
|
+
self.class.runtime += event.duration
|
49
|
+
return unless logger.debug?
|
50
|
+
|
51
|
+
payload = event.payload
|
52
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
53
|
+
|
54
|
+
debug " #{color(name, YELLOW, bold: true)} _msearch #{payload[:body]}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,10 +1,17 @@
|
|
1
|
-
require "faraday
|
1
|
+
require "faraday"
|
2
2
|
|
3
3
|
module Searchkick
|
4
4
|
class Middleware < Faraday::Middleware
|
5
5
|
def call(env)
|
6
|
-
|
6
|
+
path = env[:url].path.to_s
|
7
|
+
if path.end_with?("/_search")
|
7
8
|
env[:request][:timeout] = Searchkick.search_timeout
|
9
|
+
elsif path.end_with?("/_msearch")
|
10
|
+
# assume no concurrent searches for timeout for now
|
11
|
+
searches = env[:request_body].count("\n") / 2
|
12
|
+
# do not allow timeout to exceed Searchkick.timeout
|
13
|
+
timeout = [Searchkick.search_timeout * searches, Searchkick.timeout].min
|
14
|
+
env[:request][:timeout] = timeout
|
8
15
|
end
|
9
16
|
@app.call(env)
|
10
17
|
end
|
data/lib/searchkick/model.rb
CHANGED
@@ -5,9 +5,9 @@ module Searchkick
|
|
5
5
|
|
6
6
|
unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :deep_paging, :default_fields,
|
7
7
|
:filterable, :geo_shape, :highlight, :ignore_above, :index_name, :index_prefix, :inheritance, :language,
|
8
|
-
:locations, :mappings, :match, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
|
9
|
-
:special_characters, :stem, :stem_conversions, :suggest, :synonyms, :text_end,
|
10
|
-
:text_middle, :text_start, :
|
8
|
+
:locations, :mappings, :match, :max_result_window, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
|
9
|
+
:special_characters, :stem, :stemmer, :stem_conversions, :stem_exclusion, :stemmer_override, :suggest, :synonyms, :text_end,
|
10
|
+
:text_middle, :text_start, :unscope, :word, :word_end, :word_middle, :word_start]
|
11
11
|
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
12
12
|
|
13
13
|
raise "Only call searchkick once per model" if respond_to?(:searchkick_index)
|
@@ -22,52 +22,78 @@ module Searchkick
|
|
22
22
|
raise ArgumentError, "Invalid value for callbacks"
|
23
23
|
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
25
|
+
base = self
|
26
|
+
|
27
|
+
mod = Module.new
|
28
|
+
include(mod)
|
29
|
+
mod.module_eval do
|
30
|
+
def reindex(method_name = nil, mode: nil, refresh: false)
|
31
|
+
self.class.searchkick_index.reindex([self], method_name: method_name, mode: mode, refresh: refresh, single: true)
|
32
|
+
end unless base.method_defined?(:reindex)
|
33
|
+
|
34
|
+
def similar(**options)
|
35
|
+
self.class.searchkick_index.similar_record(self, **options)
|
36
|
+
end unless base.method_defined?(:similar)
|
37
|
+
|
38
|
+
def search_data
|
39
|
+
data = respond_to?(:to_hash) ? to_hash : serializable_hash
|
40
|
+
data.delete("id")
|
41
|
+
data.delete("_id")
|
42
|
+
data.delete("_type")
|
43
|
+
data
|
44
|
+
end unless base.method_defined?(:search_data)
|
45
|
+
|
46
|
+
def should_index?
|
47
|
+
true
|
48
|
+
end unless base.method_defined?(:should_index?)
|
49
|
+
end
|
33
50
|
|
34
51
|
class_eval do
|
35
|
-
cattr_reader :searchkick_options, :searchkick_klass
|
52
|
+
cattr_reader :searchkick_options, :searchkick_klass, instance_reader: false
|
36
53
|
|
37
54
|
class_variable_set :@@searchkick_options, options.dup
|
38
55
|
class_variable_set :@@searchkick_klass, self
|
39
|
-
class_variable_set :@@
|
40
|
-
class_variable_set :@@searchkick_index_cache, {}
|
56
|
+
class_variable_set :@@searchkick_index_cache, Searchkick::IndexCache.new
|
41
57
|
|
42
58
|
class << self
|
43
59
|
def searchkick_search(term = "*", **options, &block)
|
44
|
-
|
45
|
-
|
60
|
+
if Searchkick.relation?(self)
|
61
|
+
raise Searchkick::Error, "search must be called on model, not relation"
|
62
|
+
end
|
46
63
|
|
47
64
|
Searchkick.search(term, model: self, **options, &block)
|
48
65
|
end
|
49
66
|
alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
|
50
67
|
|
51
68
|
def searchkick_index(name: nil)
|
52
|
-
|
53
|
-
|
69
|
+
index_name = name || searchkick_klass.searchkick_index_name
|
70
|
+
index_name = index_name.call if index_name.respond_to?(:call)
|
54
71
|
index_cache = class_variable_get(:@@searchkick_index_cache)
|
55
|
-
index_cache
|
72
|
+
index_cache.fetch(index_name) { Searchkick::Index.new(index_name, searchkick_options) }
|
56
73
|
end
|
57
74
|
alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
|
58
75
|
|
59
76
|
def searchkick_reindex(method_name = nil, **options)
|
60
|
-
|
61
|
-
relation = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
|
62
|
-
(respond_to?(:queryable) && queryable != unscoped.with_default_scope)
|
63
|
-
|
64
|
-
searchkick_index.reindex(searchkick_klass, method_name, scoped: relation, **options)
|
77
|
+
searchkick_index.reindex(self, method_name: method_name, **options)
|
65
78
|
end
|
66
79
|
alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
|
67
80
|
|
68
81
|
def searchkick_index_options
|
69
82
|
searchkick_index.index_options
|
70
83
|
end
|
84
|
+
|
85
|
+
def searchkick_index_name
|
86
|
+
@searchkick_index_name ||= begin
|
87
|
+
options = class_variable_get(:@@searchkick_options)
|
88
|
+
if options[:index_name]
|
89
|
+
options[:index_name]
|
90
|
+
elsif options[:index_prefix].respond_to?(:call)
|
91
|
+
-> { [options[:index_prefix].call, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_") }
|
92
|
+
else
|
93
|
+
[options.key?(:index_prefix) ? options[:index_prefix] : Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
71
97
|
end
|
72
98
|
|
73
99
|
# always add callbacks, even when callbacks is false
|
@@ -78,33 +104,6 @@ module Searchkick
|
|
78
104
|
after_save :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
|
79
105
|
after_destroy :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
|
80
106
|
end
|
81
|
-
|
82
|
-
def reindex(method_name = nil, **options)
|
83
|
-
RecordIndexer.new(self).reindex(method_name, **options)
|
84
|
-
end unless method_defined?(:reindex)
|
85
|
-
|
86
|
-
# TODO switch to keyword arguments
|
87
|
-
def similar(options = {})
|
88
|
-
self.class.searchkick_index.similar_record(self, **options)
|
89
|
-
end unless method_defined?(:similar)
|
90
|
-
|
91
|
-
def search_data
|
92
|
-
data = respond_to?(:to_hash) ? to_hash : serializable_hash
|
93
|
-
data.delete("id")
|
94
|
-
data.delete("_id")
|
95
|
-
data.delete("_type")
|
96
|
-
data
|
97
|
-
end unless method_defined?(:search_data)
|
98
|
-
|
99
|
-
def should_index?
|
100
|
-
true
|
101
|
-
end unless method_defined?(:should_index?)
|
102
|
-
|
103
|
-
if defined?(Cequel) && self < Cequel::Record && !method_defined?(:destroyed?)
|
104
|
-
def destroyed?
|
105
|
-
transient?
|
106
|
-
end
|
107
|
-
end
|
108
107
|
end
|
109
108
|
end
|
110
109
|
end
|
@@ -3,34 +3,18 @@ module Searchkick
|
|
3
3
|
queue_as { Searchkick.queue_name }
|
4
4
|
|
5
5
|
def perform(class_name:, record_ids:, index_name: nil)
|
6
|
-
|
7
|
-
|
8
|
-
record_ids = routing.keys
|
6
|
+
model = Searchkick.load_model(class_name)
|
7
|
+
index = model.searchkick_index(name: index_name)
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
# determine which records to delete
|
16
|
-
delete_ids = record_ids - records.map { |r| r.id.to_s }
|
17
|
-
delete_records = delete_ids.map do |id|
|
18
|
-
m = klass.new
|
19
|
-
m.id = id
|
20
|
-
if routing[id]
|
21
|
-
m.define_singleton_method(:search_routing) do
|
22
|
-
routing[id]
|
23
|
-
end
|
9
|
+
items =
|
10
|
+
record_ids.map do |r|
|
11
|
+
parts = r.split(/(?<!\|)\|(?!\|)/, 2)
|
12
|
+
.map { |v| v.gsub("||", "|") }
|
13
|
+
{id: parts[0], routing: parts[1]}
|
24
14
|
end
|
25
|
-
m
|
26
|
-
end
|
27
15
|
|
28
|
-
|
29
|
-
index
|
30
|
-
Searchkick.callbacks(:bulk) do
|
31
|
-
index.bulk_index(records) if records.any?
|
32
|
-
index.bulk_delete(delete_records) if delete_records.any?
|
33
|
-
end
|
16
|
+
relation = Searchkick.scope(model)
|
17
|
+
RecordIndexer.new(index).reindex_items(relation, items, method_name: nil)
|
34
18
|
end
|
35
19
|
end
|
36
20
|
end
|
@@ -3,15 +3,16 @@ module Searchkick
|
|
3
3
|
queue_as { Searchkick.queue_name }
|
4
4
|
|
5
5
|
def perform(class_name:, index_name: nil, inline: false)
|
6
|
-
model = class_name
|
6
|
+
model = Searchkick.load_model(class_name)
|
7
|
+
index = model.searchkick_index(name: index_name)
|
7
8
|
limit = model.searchkick_options[:batch_size] || 1000
|
8
9
|
|
9
10
|
loop do
|
10
|
-
record_ids =
|
11
|
+
record_ids = index.reindex_queue.reserve(limit: limit)
|
11
12
|
if record_ids.any?
|
12
13
|
batch_options = {
|
13
14
|
class_name: class_name,
|
14
|
-
record_ids: record_ids,
|
15
|
+
record_ids: record_ids.uniq,
|
15
16
|
index_name: index_name
|
16
17
|
}
|
17
18
|
|