searchkick 4.0.0 → 5.0.0

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, 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
@@ -1,4 +1,4 @@
1
- require "faraday/middleware"
1
+ require "faraday"
2
2
 
3
3
  module Searchkick
4
4
  class Middleware < Faraday::Middleware
@@ -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, :word, :wordnet, :word_end, :word_middle, :word_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
- index_name =
25
- if options[:index_name]
26
- options[:index_name]
27
- elsif options[:index_prefix].respond_to?(:call)
28
- -> { [options[:index_prefix].call, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_") }
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 :@@searchkick_index, index_name
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.search(term, {model: self}.merge(options), &block)
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 = class_variable_get(:@@searchkick_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[index] ||= Searchkick::Index.new(index, searchkick_options)
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
- scoped = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
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
- # separate routing from id
7
- routing = Hash[record_ids.map { |r| r.split(/(?<!\|)\|(?!\|)/, 2).map { |v| v.gsub("||", "|") } }]
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
- 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
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.constantize
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
- limit = model.searchkick_index.options[:batch_size] || 1000
9
- record_ids = model.searchkick_index.reindex_queue.reserve(limit: limit)
10
- if record_ids.any?
11
- Searchkick::ProcessBatchJob.perform_later(
12
- class_name: model.name,
13
- record_ids: record_ids
14
- )
15
- # TODO when moving to reliable queuing, mark as complete
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
- if record_ids.size == limit
18
- Searchkick::ProcessQueueJob.perform_later(class_name: class_name)
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