searchkick 2.3.2 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
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