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.
@@ -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