searchkick 4.4.0 → 5.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- if items.any?
18
- response = Searchkick.client.bulk(body: items)
19
- if response["errors"]
20
- first_with_error = response["items"].map do |item|
21
- (item["index"] || item["delete"] || item["update"])
22
- end.find { |item| item["error"] }
23
- raise Searchkick::ImportError, "#{first_with_error["error"]} on item with id '#{first_with_error["_id"]}'"
24
- end
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/middleware"
1
+ require "faraday"
2
2
 
3
3
  module Searchkick
4
4
  class Middleware < Faraday::Middleware
5
5
  def call(env)
6
- if env[:method] == :get && env[:url].path.to_s.end_with?("/_search")
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
@@ -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, :word, :wordnet, :word_end, :word_middle, :word_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
- index_name =
26
- if options[:index_name]
27
- options[:index_name]
28
- elsif options[:index_prefix].respond_to?(:call)
29
- -> { [options[:index_prefix].call, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_") }
30
- else
31
- [options.key?(:index_prefix) ? options[:index_prefix] : Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
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 :@@searchkick_index, index_name
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
- # TODO throw error in next major version
45
- Searchkick.warn("calling search on a relation is deprecated") if Searchkick.relation?(self)
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
- index = name || class_variable_get(:@@searchkick_index)
53
- index = index.call if index.respond_to?(:call)
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[index] ||= Searchkick::Index.new(index, searchkick_options)
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
- # TODO relation = Searchkick.relation?(self)
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
- # separate routing from id
7
- routing = Hash[record_ids.map { |r| r.split(/(?<!\|)\|(?!\|)/, 2).map { |v| v.gsub("||", "|") } }]
8
- record_ids = routing.keys
6
+ model = Searchkick.load_model(class_name)
7
+ index = model.searchkick_index(name: index_name)
9
8
 
10
- klass = class_name.constantize
11
- scope = Searchkick.load_records(klass, record_ids)
12
- scope = scope.search_import if scope.respond_to?(:search_import)
13
- records = scope.select(&:should_index?)
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
- # bulk reindex
29
- index = klass.searchkick_index(name: index_name)
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.constantize
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 = model.searchkick_index(name: index_name).reindex_queue.reserve(limit: limit)
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