searchkick 2.3.2 → 4.4.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.
Files changed (80) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +251 -84
  3. data/LICENSE.txt +1 -1
  4. data/README.md +552 -432
  5. data/lib/searchkick/bulk_indexer.rb +173 -0
  6. data/lib/searchkick/bulk_reindex_job.rb +2 -2
  7. data/lib/searchkick/hash_wrapper.rb +12 -0
  8. data/lib/searchkick/index.rb +187 -348
  9. data/lib/searchkick/index_options.rb +494 -282
  10. data/lib/searchkick/logging.rb +17 -13
  11. data/lib/searchkick/model.rb +52 -97
  12. data/lib/searchkick/multi_search.rb +9 -10
  13. data/lib/searchkick/process_batch_job.rb +17 -4
  14. data/lib/searchkick/process_queue_job.rb +20 -12
  15. data/lib/searchkick/query.rb +415 -199
  16. data/lib/searchkick/railtie.rb +7 -0
  17. data/lib/searchkick/record_data.rb +128 -0
  18. data/lib/searchkick/record_indexer.rb +79 -0
  19. data/lib/searchkick/reindex_queue.rb +1 -1
  20. data/lib/searchkick/reindex_v2_job.rb +14 -12
  21. data/lib/searchkick/results.rb +135 -41
  22. data/lib/searchkick/version.rb +1 -1
  23. data/lib/searchkick.rb +130 -61
  24. data/lib/tasks/searchkick.rake +34 -0
  25. metadata +18 -162
  26. data/.gitignore +0 -22
  27. data/.travis.yml +0 -39
  28. data/Gemfile +0 -16
  29. data/Rakefile +0 -20
  30. data/benchmark/Gemfile +0 -23
  31. data/benchmark/benchmark.rb +0 -97
  32. data/lib/searchkick/tasks.rb +0 -33
  33. data/searchkick.gemspec +0 -28
  34. data/test/aggs_test.rb +0 -197
  35. data/test/autocomplete_test.rb +0 -75
  36. data/test/boost_test.rb +0 -202
  37. data/test/callbacks_test.rb +0 -59
  38. data/test/ci/before_install.sh +0 -17
  39. data/test/errors_test.rb +0 -19
  40. data/test/gemfiles/activerecord31.gemfile +0 -7
  41. data/test/gemfiles/activerecord32.gemfile +0 -7
  42. data/test/gemfiles/activerecord40.gemfile +0 -8
  43. data/test/gemfiles/activerecord41.gemfile +0 -8
  44. data/test/gemfiles/activerecord42.gemfile +0 -7
  45. data/test/gemfiles/activerecord50.gemfile +0 -7
  46. data/test/gemfiles/apartment.gemfile +0 -8
  47. data/test/gemfiles/cequel.gemfile +0 -8
  48. data/test/gemfiles/mongoid2.gemfile +0 -7
  49. data/test/gemfiles/mongoid3.gemfile +0 -6
  50. data/test/gemfiles/mongoid4.gemfile +0 -7
  51. data/test/gemfiles/mongoid5.gemfile +0 -7
  52. data/test/gemfiles/mongoid6.gemfile +0 -12
  53. data/test/gemfiles/nobrainer.gemfile +0 -8
  54. data/test/gemfiles/parallel_tests.gemfile +0 -8
  55. data/test/geo_shape_test.rb +0 -175
  56. data/test/highlight_test.rb +0 -78
  57. data/test/index_test.rb +0 -166
  58. data/test/inheritance_test.rb +0 -83
  59. data/test/marshal_test.rb +0 -8
  60. data/test/match_test.rb +0 -276
  61. data/test/misspellings_test.rb +0 -56
  62. data/test/model_test.rb +0 -42
  63. data/test/multi_search_test.rb +0 -36
  64. data/test/multi_tenancy_test.rb +0 -22
  65. data/test/order_test.rb +0 -46
  66. data/test/pagination_test.rb +0 -70
  67. data/test/partial_reindex_test.rb +0 -58
  68. data/test/query_test.rb +0 -35
  69. data/test/records_test.rb +0 -10
  70. data/test/reindex_test.rb +0 -64
  71. data/test/reindex_v2_job_test.rb +0 -32
  72. data/test/routing_test.rb +0 -23
  73. data/test/should_index_test.rb +0 -32
  74. data/test/similar_test.rb +0 -28
  75. data/test/sql_test.rb +0 -214
  76. data/test/suggest_test.rb +0 -95
  77. data/test/support/kaminari.yml +0 -21
  78. data/test/synonyms_test.rb +0 -67
  79. data/test/test_helper.rb +0 -567
  80. data/test/where_test.rb +0 -223
@@ -129,10 +129,10 @@ module Searchkick
129
129
  end
130
130
 
131
131
  module SearchkickWithInstrumentation
132
- def multi_search(searches, **options)
132
+ def multi_search(searches)
133
133
  event = {
134
134
  name: "Multi Search",
135
- body: searches.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join
135
+ body: searches.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join,
136
136
  }
137
137
  ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
138
138
  super
@@ -162,12 +162,17 @@ module Searchkick
162
162
 
163
163
  payload = event.payload
164
164
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
165
- type = payload[:query][:type]
165
+
166
166
  index = payload[:query][:index].is_a?(Array) ? payload[:query][:index].join(",") : payload[:query][:index]
167
+ type = payload[:query][:type]
168
+ request_params = payload[:query].except(:index, :type, :body)
167
169
 
168
- # no easy way to tell which host the client will use
169
- host = Searchkick.client.transport.hosts.first
170
- debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?pretty -d '#{payload[:query][:body].to_json}'"
170
+ params = []
171
+ request_params.each do |k, v|
172
+ params << "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
173
+ end
174
+
175
+ debug " #{color(name, YELLOW, true)} #{index}#{type ? "/#{type.join(',')}" : ''}/_search#{params.any? ? '?' + params.join('&') : nil} #{payload[:query][:body].to_json}"
171
176
  end
172
177
 
173
178
  def request(event)
@@ -187,9 +192,7 @@ module Searchkick
187
192
  payload = event.payload
188
193
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
189
194
 
190
- # no easy way to tell which host the client will use
191
- host = Searchkick.client.transport.hosts.first
192
- debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/_msearch?pretty -d '#{payload[:body]}'"
195
+ debug " #{color(name, YELLOW, true)} _msearch #{payload[:body]}"
193
196
  end
194
197
  end
195
198
 
@@ -232,10 +235,11 @@ module Searchkick
232
235
  end
233
236
  end
234
237
  end
235
- Searchkick::Query.send(:prepend, Searchkick::QueryWithInstrumentation)
236
- Searchkick::Index.send(:prepend, Searchkick::IndexWithInstrumentation)
237
- Searchkick::Indexer.send(:prepend, Searchkick::IndexerWithInstrumentation)
238
- Searchkick.singleton_class.send(:prepend, Searchkick::SearchkickWithInstrumentation)
238
+
239
+ Searchkick::Query.prepend(Searchkick::QueryWithInstrumentation)
240
+ Searchkick::Index.prepend(Searchkick::IndexWithInstrumentation)
241
+ Searchkick::Indexer.prepend(Searchkick::IndexerWithInstrumentation)
242
+ Searchkick.singleton_class.prepend(Searchkick::SearchkickWithInstrumentation)
239
243
  Searchkick::LogSubscriber.attach_to :searchkick
240
244
  ActiveSupport.on_load(:action_controller) do
241
245
  include Searchkick::ControllerRuntime
@@ -1,10 +1,12 @@
1
1
  module Searchkick
2
2
  module Model
3
3
  def searchkick(**options)
4
- unknown_keywords = options.keys - [:_all, :batch_size, :callbacks, :conversions, :default_fields,
5
- :filterable, :geo_shape, :highlight, :ignore_above, :index_name, :index_prefix, :language,
6
- :locations, :mappings, :match, :merge_mappings, :routing, :searchable, :settings, :similarity,
7
- :special_characters, :stem_conversions, :suggest, :synonyms, :text_end,
4
+ options = Searchkick.model_options.merge(options)
5
+
6
+ unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :deep_paging, :default_fields,
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, :stem_exclusion, :stemmer_override, :suggest, :synonyms, :text_end,
8
10
  :text_middle, :text_start, :word, :wordnet, :word_end, :word_middle, :word_start]
9
11
  raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
10
12
 
@@ -12,63 +14,54 @@ module Searchkick
12
14
 
13
15
  Searchkick.models << self
14
16
 
17
+ options[:_type] ||= -> { searchkick_index.klass_document_type(self, true) }
18
+ options[:class_name] = model_name.name
19
+
20
+ callbacks = options.key?(:callbacks) ? options[:callbacks] : :inline
21
+ unless [:inline, true, false, :async, :queue].include?(callbacks)
22
+ raise ArgumentError, "Invalid value for callbacks"
23
+ end
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
33
+
15
34
  class_eval do
16
35
  cattr_reader :searchkick_options, :searchkick_klass
17
36
 
18
- callbacks = options.key?(:callbacks) ? options[:callbacks] : true
19
-
20
37
  class_variable_set :@@searchkick_options, options.dup
21
38
  class_variable_set :@@searchkick_klass, self
22
- class_variable_set :@@searchkick_callbacks, callbacks
23
- class_variable_set :@@searchkick_index, options[:index_name] ||
24
- (options[:index_prefix].respond_to?(:call) && proc { [options[:index_prefix].call, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_") }) ||
25
- [options.key?(:index_prefix) ? options[:index_prefix] : Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
39
+ class_variable_set :@@searchkick_index, index_name
40
+ class_variable_set :@@searchkick_index_cache, {}
26
41
 
27
42
  class << self
28
43
  def searchkick_search(term = "*", **options, &block)
29
- Searchkick.search(term, {model: self}.merge(options), &block)
44
+ # TODO throw error in next major version
45
+ Searchkick.warn("calling search on a relation is deprecated") if Searchkick.relation?(self)
46
+
47
+ Searchkick.search(term, model: self, **options, &block)
30
48
  end
31
49
  alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
32
50
 
33
- def searchkick_index
34
- index = class_variable_get :@@searchkick_index
35
- index = index.call if index.respond_to? :call
36
- Searchkick::Index.new(index, searchkick_options)
51
+ def searchkick_index(name: nil)
52
+ index = name || class_variable_get(:@@searchkick_index)
53
+ index = index.call if index.respond_to?(:call)
54
+ index_cache = class_variable_get(:@@searchkick_index_cache)
55
+ index_cache[index] ||= Searchkick::Index.new(index, searchkick_options)
37
56
  end
38
57
  alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
39
58
 
40
- def enable_search_callbacks
41
- class_variable_set :@@searchkick_callbacks, true
42
- end
43
-
44
- def disable_search_callbacks
45
- class_variable_set :@@searchkick_callbacks, false
46
- end
47
-
48
- def search_callbacks?
49
- class_variable_get(:@@searchkick_callbacks) && Searchkick.callbacks?
50
- end
51
-
52
- def searchkick_reindex(method_name = nil, full: false, **options)
53
- scoped = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
59
+ 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) ||
54
62
  (respond_to?(:queryable) && queryable != unscoped.with_default_scope)
55
63
 
56
- refresh = options.fetch(:refresh, !scoped)
57
-
58
- if method_name
59
- # update
60
- searchkick_index.import_scope(searchkick_klass, method_name: method_name)
61
- searchkick_index.refresh if refresh
62
- true
63
- elsif scoped && !full
64
- # reindex association
65
- searchkick_index.import_scope(searchkick_klass)
66
- searchkick_index.refresh if refresh
67
- true
68
- else
69
- # full reindex
70
- searchkick_index.reindex_scope(searchkick_klass, options)
71
- end
64
+ searchkick_index.reindex(searchkick_klass, method_name, scoped: relation, **options)
72
65
  end
73
66
  alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
74
67
 
@@ -77,68 +70,30 @@ module Searchkick
77
70
  end
78
71
  end
79
72
 
80
- callback_name = callbacks == :async ? :reindex_async : :reindex
73
+ # always add callbacks, even when callbacks is false
74
+ # so Model.callbacks block can be used
81
75
  if respond_to?(:after_commit)
82
- after_commit callback_name, if: proc { self.class.search_callbacks? }
76
+ after_commit :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
83
77
  elsif respond_to?(:after_save)
84
- after_save callback_name, if: proc { self.class.search_callbacks? }
85
- after_destroy callback_name, if: proc { self.class.search_callbacks? }
78
+ after_save :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
79
+ after_destroy :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
86
80
  end
87
81
 
88
- def reindex(method_name = nil, refresh: false, async: false, mode: nil)
89
- klass_options = self.class.searchkick_index.options
90
-
91
- if mode.nil?
92
- mode =
93
- if async
94
- :async
95
- elsif Searchkick.callbacks_value
96
- Searchkick.callbacks_value
97
- elsif klass_options.key?(:callbacks) && klass_options[:callbacks] != :async
98
- # TODO remove 2nd condition in next major version
99
- klass_options[:callbacks]
100
- end
101
- end
102
-
103
- case mode
104
- when :queue
105
- if method_name
106
- raise Searchkick::Error, "Partial reindex not supported with queue option"
107
- else
108
- self.class.searchkick_index.reindex_queue.push(id.to_s)
109
- end
110
- when :async
111
- if method_name
112
- # TODO support Mongoid and NoBrainer and non-id primary keys
113
- Searchkick::BulkReindexJob.perform_later(
114
- class_name: self.class.name,
115
- record_ids: [id.to_s],
116
- method_name: method_name ? method_name.to_s : nil
117
- )
118
- else
119
- self.class.searchkick_index.reindex_record_async(self)
120
- end
121
- else
122
- if method_name
123
- self.class.searchkick_index.update_record(self, method_name)
124
- else
125
- self.class.searchkick_index.reindex_record(self)
126
- end
127
- self.class.searchkick_index.refresh if refresh
128
- end
82
+ def reindex(method_name = nil, **options)
83
+ RecordIndexer.new(self).reindex(method_name, **options)
129
84
  end unless method_defined?(:reindex)
130
85
 
131
- # TODO remove this method in next major version
132
- def reindex_async
133
- reindex(async: true)
134
- end unless method_defined?(:reindex_async)
135
-
86
+ # TODO switch to keyword arguments
136
87
  def similar(options = {})
137
- self.class.searchkick_index.similar_record(self, options)
88
+ self.class.searchkick_index.similar_record(self, **options)
138
89
  end unless method_defined?(:similar)
139
90
 
140
91
  def search_data
141
- respond_to?(:to_hash) ? to_hash : serializable_hash
92
+ data = respond_to?(:to_hash) ? to_hash : serializable_hash
93
+ data.delete("id")
94
+ data.delete("_id")
95
+ data.delete("_type")
96
+ data
142
97
  end unless method_defined?(:search_data)
143
98
 
144
99
  def should_index?
@@ -2,25 +2,24 @@ module Searchkick
2
2
  class MultiSearch
3
3
  attr_reader :queries
4
4
 
5
- def initialize(queries, retry_misspellings: false)
5
+ def initialize(queries)
6
6
  @queries = queries
7
- @retry_misspellings = retry_misspellings
8
7
  end
9
8
 
10
9
  def perform
11
10
  if queries.any?
12
- perform_search(queries, retry_misspellings: @retry_misspellings)
11
+ perform_search(queries)
13
12
  end
14
13
  end
15
14
 
16
15
  private
17
16
 
18
- def perform_search(queries, retry_misspellings: true)
19
- responses = client.msearch(body: queries.flat_map { |q| [q.params.except(:body), q.body] })["responses"]
17
+ def perform_search(search_queries, perform_retry: true)
18
+ responses = client.msearch(body: search_queries.flat_map { |q| [q.params.except(:body), q.body] })["responses"]
20
19
 
21
20
  retry_queries = []
22
- queries.each_with_index do |query, i|
23
- if retry_misspellings && query.retry_misspellings?(responses[i])
21
+ search_queries.each_with_index do |query, i|
22
+ if perform_retry && query.retry_misspellings?(responses[i])
24
23
  query.send(:prepare) # okay, since we don't want to expose this method outside Searchkick
25
24
  retry_queries << query
26
25
  else
@@ -28,11 +27,11 @@ module Searchkick
28
27
  end
29
28
  end
30
29
 
31
- if retry_misspellings && retry_queries.any?
32
- perform_search(retry_queries, retry_misspellings: false)
30
+ if retry_queries.any?
31
+ perform_search(retry_queries, perform_retry: false)
33
32
  end
34
33
 
35
- queries
34
+ search_queries
36
35
  end
37
36
 
38
37
  def client
@@ -1,8 +1,12 @@
1
1
  module Searchkick
2
2
  class ProcessBatchJob < ActiveJob::Base
3
- queue_as :searchkick
3
+ queue_as { Searchkick.queue_name }
4
+
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
4
9
 
5
- def perform(class_name:, record_ids:)
6
10
  klass = class_name.constantize
7
11
  scope = Searchkick.load_records(klass, record_ids)
8
12
  scope = scope.search_import if scope.respond_to?(:search_import)
@@ -10,10 +14,19 @@ module Searchkick
10
14
 
11
15
  # determine which records to delete
12
16
  delete_ids = record_ids - records.map { |r| r.id.to_s }
13
- delete_records = delete_ids.map { |id| m = klass.new; m.id = id; m }
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
24
+ end
25
+ m
26
+ end
14
27
 
15
28
  # bulk reindex
16
- index = klass.searchkick_index
29
+ index = klass.searchkick_index(name: index_name)
17
30
  Searchkick.callbacks(:bulk) do
18
31
  index.bulk_index(records) if records.any?
19
32
  index.bulk_delete(delete_records) if delete_records.any?
@@ -1,22 +1,30 @@
1
1
  module Searchkick
2
2
  class ProcessQueueJob < ActiveJob::Base
3
- queue_as :searchkick
3
+ queue_as { Searchkick.queue_name }
4
4
 
5
- def perform(class_name:)
5
+ def perform(class_name:, index_name: nil, inline: false)
6
6
  model = class_name.constantize
7
+ limit = model.searchkick_options[:batch_size] || 1000
7
8
 
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
9
+ loop do
10
+ record_ids = model.searchkick_index(name: index_name).reindex_queue.reserve(limit: limit)
11
+ if record_ids.any?
12
+ batch_options = {
13
+ class_name: class_name,
14
+ record_ids: record_ids,
15
+ index_name: index_name
16
+ }
16
17
 
17
- if record_ids.size == limit
18
- Searchkick::ProcessQueueJob.perform_later(class_name: class_name)
18
+ if inline
19
+ # use new.perform to avoid excessive logging
20
+ Searchkick::ProcessBatchJob.new.perform(**batch_options)
21
+ else
22
+ Searchkick::ProcessBatchJob.perform_later(**batch_options)
23
+ end
24
+
25
+ # TODO when moving to reliable queuing, mark as complete
19
26
  end
27
+ break unless record_ids.size == limit
20
28
  end
21
29
  end
22
30
  end