searchkick 4.0.0 → 5.0.0
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 +234 -96
- data/LICENSE.txt +1 -1
- data/README.md +446 -268
- data/lib/searchkick/bulk_reindex_job.rb +12 -8
- data/lib/searchkick/controller_runtime.rb +40 -0
- data/lib/searchkick/index.rb +174 -56
- data/lib/searchkick/index_cache.rb +30 -0
- data/lib/searchkick/index_options.rb +472 -349
- data/lib/searchkick/indexer.rb +15 -8
- data/lib/searchkick/log_subscriber.rb +57 -0
- data/lib/searchkick/middleware.rb +1 -1
- data/lib/searchkick/model.rb +51 -48
- data/lib/searchkick/process_batch_job.rb +10 -26
- data/lib/searchkick/process_queue_job.rb +21 -12
- data/lib/searchkick/query.rb +183 -51
- data/lib/searchkick/record_data.rb +0 -1
- data/lib/searchkick/record_indexer.rb +135 -50
- data/lib/searchkick/reindex_queue.rb +43 -6
- data/lib/searchkick/reindex_v2_job.rb +10 -34
- data/lib/searchkick/relation.rb +36 -0
- data/lib/searchkick/relation_indexer.rb +150 -0
- data/lib/searchkick/results.rb +162 -80
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick.rb +203 -79
- data/lib/tasks/searchkick.rake +21 -11
- metadata +17 -71
- data/CONTRIBUTING.md +0 -53
- data/lib/searchkick/bulk_indexer.rb +0 -171
- data/lib/searchkick/logging.rb +0 -243
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, 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, 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, true)} _msearch #{payload[:body]}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/searchkick/model.rb
CHANGED
@@ -3,11 +3,11 @@ module Searchkick
|
|
3
3
|
def searchkick(**options)
|
4
4
|
options = Searchkick.model_options.merge(options)
|
5
5
|
|
6
|
-
unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :default_fields,
|
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, :settings, :similarity,
|
9
|
-
:special_characters, :stem, :stem_conversions, :suggest, :synonyms, :text_end,
|
10
|
-
:text_middle, :text_start, :
|
8
|
+
:locations, :mappings, :match, :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)
|
@@ -15,54 +15,83 @@ module Searchkick
|
|
15
15
|
Searchkick.models << self
|
16
16
|
|
17
17
|
options[:_type] ||= -> { searchkick_index.klass_document_type(self, true) }
|
18
|
+
options[:class_name] = model_name.name
|
18
19
|
|
19
20
|
callbacks = options.key?(:callbacks) ? options[:callbacks] : :inline
|
20
21
|
unless [:inline, true, false, :async, :queue].include?(callbacks)
|
21
22
|
raise ArgumentError, "Invalid value for callbacks"
|
22
23
|
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
else
|
30
|
-
[options.key?(:index_prefix) ? options[:index_prefix] : Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
|
25
|
+
mod = Module.new
|
26
|
+
include(mod)
|
27
|
+
mod.module_eval do
|
28
|
+
def reindex(method_name = nil, mode: nil, refresh: false)
|
29
|
+
self.class.searchkick_index.reindex([self], method_name: method_name, mode: mode, refresh: refresh, single: true)
|
31
30
|
end
|
32
31
|
|
32
|
+
def similar(**options)
|
33
|
+
self.class.searchkick_index.similar_record(self, **options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def search_data
|
37
|
+
data = respond_to?(:to_hash) ? to_hash : serializable_hash
|
38
|
+
data.delete("id")
|
39
|
+
data.delete("_id")
|
40
|
+
data.delete("_type")
|
41
|
+
data
|
42
|
+
end
|
43
|
+
|
44
|
+
def should_index?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
33
49
|
class_eval do
|
34
|
-
cattr_reader :searchkick_options, :searchkick_klass
|
50
|
+
cattr_reader :searchkick_options, :searchkick_klass, instance_reader: false
|
35
51
|
|
36
52
|
class_variable_set :@@searchkick_options, options.dup
|
37
53
|
class_variable_set :@@searchkick_klass, self
|
38
|
-
class_variable_set :@@
|
39
|
-
class_variable_set :@@searchkick_index_cache, {}
|
54
|
+
class_variable_set :@@searchkick_index_cache, Searchkick::IndexCache.new
|
40
55
|
|
41
56
|
class << self
|
42
57
|
def searchkick_search(term = "*", **options, &block)
|
43
|
-
Searchkick.
|
58
|
+
if Searchkick.relation?(self)
|
59
|
+
raise Searchkick::Error, "search must be called on model, not relation"
|
60
|
+
end
|
61
|
+
|
62
|
+
Searchkick.search(term, model: self, **options, &block)
|
44
63
|
end
|
45
64
|
alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
|
46
65
|
|
47
|
-
def searchkick_index
|
48
|
-
index =
|
66
|
+
def searchkick_index(name: nil)
|
67
|
+
index = name || searchkick_index_name
|
49
68
|
index = index.call if index.respond_to?(:call)
|
50
69
|
index_cache = class_variable_get(:@@searchkick_index_cache)
|
51
|
-
index_cache
|
70
|
+
index_cache.fetch(index) { Searchkick::Index.new(index, searchkick_options) }
|
52
71
|
end
|
53
72
|
alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
|
54
73
|
|
55
74
|
def searchkick_reindex(method_name = nil, **options)
|
56
|
-
|
57
|
-
(respond_to?(:queryable) && queryable != unscoped.with_default_scope)
|
58
|
-
|
59
|
-
searchkick_index.reindex(searchkick_klass, method_name, scoped: scoped, **options)
|
75
|
+
searchkick_index.reindex(self, method_name: method_name, **options)
|
60
76
|
end
|
61
77
|
alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
|
62
78
|
|
63
79
|
def searchkick_index_options
|
64
80
|
searchkick_index.index_options
|
65
81
|
end
|
82
|
+
|
83
|
+
def searchkick_index_name
|
84
|
+
@searchkick_index_name ||= begin
|
85
|
+
options = class_variable_get(:@@searchkick_options)
|
86
|
+
if options[:index_name]
|
87
|
+
options[:index_name]
|
88
|
+
elsif options[:index_prefix].respond_to?(:call)
|
89
|
+
-> { [options[:index_prefix].call, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_") }
|
90
|
+
else
|
91
|
+
[options.key?(:index_prefix) ? options[:index_prefix] : Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
66
95
|
end
|
67
96
|
|
68
97
|
# always add callbacks, even when callbacks is false
|
@@ -73,32 +102,6 @@ module Searchkick
|
|
73
102
|
after_save :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
|
74
103
|
after_destroy :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
|
75
104
|
end
|
76
|
-
|
77
|
-
def reindex(method_name = nil, **options)
|
78
|
-
RecordIndexer.new(self).reindex(method_name, **options)
|
79
|
-
end unless method_defined?(:reindex)
|
80
|
-
|
81
|
-
def similar(options = {})
|
82
|
-
self.class.searchkick_index.similar_record(self, options)
|
83
|
-
end unless method_defined?(:similar)
|
84
|
-
|
85
|
-
def search_data
|
86
|
-
data = respond_to?(:to_hash) ? to_hash : serializable_hash
|
87
|
-
data.delete("id")
|
88
|
-
data.delete("_id")
|
89
|
-
data.delete("_type")
|
90
|
-
data
|
91
|
-
end unless method_defined?(:search_data)
|
92
|
-
|
93
|
-
def should_index?
|
94
|
-
true
|
95
|
-
end unless method_defined?(:should_index?)
|
96
|
-
|
97
|
-
if defined?(Cequel) && self < Cequel::Record && !method_defined?(:destroyed?)
|
98
|
-
def destroyed?
|
99
|
-
transient?
|
100
|
-
end
|
101
|
-
end
|
102
105
|
end
|
103
106
|
end
|
104
107
|
end
|
@@ -2,35 +2,19 @@ module Searchkick
|
|
2
2
|
class ProcessBatchJob < ActiveJob::Base
|
3
3
|
queue_as { Searchkick.queue_name }
|
4
4
|
|
5
|
-
def perform(class_name:, record_ids:)
|
6
|
-
|
7
|
-
|
8
|
-
record_ids = routing.keys
|
5
|
+
def perform(class_name:, record_ids:, index_name: nil)
|
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
|
@@ -2,21 +2,30 @@ module Searchkick
|
|
2
2
|
class ProcessQueueJob < ActiveJob::Base
|
3
3
|
queue_as { Searchkick.queue_name }
|
4
4
|
|
5
|
-
def perform(class_name:)
|
6
|
-
model = class_name
|
5
|
+
def perform(class_name:, index_name: nil, inline: false)
|
6
|
+
model = Searchkick.load_model(class_name)
|
7
|
+
index = model.searchkick_index(name: index_name)
|
8
|
+
limit = model.searchkick_options[:batch_size] || 1000
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
loop do
|
11
|
+
record_ids = index.reindex_queue.reserve(limit: limit)
|
12
|
+
if record_ids.any?
|
13
|
+
batch_options = {
|
14
|
+
class_name: class_name,
|
15
|
+
record_ids: record_ids.uniq,
|
16
|
+
index_name: index_name
|
17
|
+
}
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
+
if inline
|
20
|
+
# use new.perform to avoid excessive logging
|
21
|
+
Searchkick::ProcessBatchJob.new.perform(**batch_options)
|
22
|
+
else
|
23
|
+
Searchkick::ProcessBatchJob.perform_later(**batch_options)
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO when moving to reliable queuing, mark as complete
|
19
27
|
end
|
28
|
+
break unless record_ids.size == limit
|
20
29
|
end
|
21
30
|
end
|
22
31
|
end
|