searchkick-hooopo 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.travis.yml +35 -0
  4. data/CHANGELOG.md +491 -0
  5. data/Gemfile +12 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +1908 -0
  8. data/Rakefile +20 -0
  9. data/benchmark/Gemfile +23 -0
  10. data/benchmark/benchmark.rb +97 -0
  11. data/lib/searchkick/bulk_reindex_job.rb +17 -0
  12. data/lib/searchkick/index.rb +500 -0
  13. data/lib/searchkick/index_options.rb +333 -0
  14. data/lib/searchkick/indexer.rb +28 -0
  15. data/lib/searchkick/logging.rb +242 -0
  16. data/lib/searchkick/middleware.rb +12 -0
  17. data/lib/searchkick/model.rb +156 -0
  18. data/lib/searchkick/process_batch_job.rb +23 -0
  19. data/lib/searchkick/process_queue_job.rb +23 -0
  20. data/lib/searchkick/query.rb +901 -0
  21. data/lib/searchkick/reindex_queue.rb +38 -0
  22. data/lib/searchkick/reindex_v2_job.rb +39 -0
  23. data/lib/searchkick/results.rb +216 -0
  24. data/lib/searchkick/tasks.rb +33 -0
  25. data/lib/searchkick/version.rb +3 -0
  26. data/lib/searchkick.rb +215 -0
  27. data/searchkick.gemspec +28 -0
  28. data/test/aggs_test.rb +197 -0
  29. data/test/autocomplete_test.rb +75 -0
  30. data/test/boost_test.rb +175 -0
  31. data/test/callbacks_test.rb +59 -0
  32. data/test/ci/before_install.sh +17 -0
  33. data/test/errors_test.rb +19 -0
  34. data/test/gemfiles/activerecord31.gemfile +7 -0
  35. data/test/gemfiles/activerecord32.gemfile +7 -0
  36. data/test/gemfiles/activerecord40.gemfile +8 -0
  37. data/test/gemfiles/activerecord41.gemfile +8 -0
  38. data/test/gemfiles/activerecord42.gemfile +7 -0
  39. data/test/gemfiles/activerecord50.gemfile +7 -0
  40. data/test/gemfiles/apartment.gemfile +8 -0
  41. data/test/gemfiles/cequel.gemfile +8 -0
  42. data/test/gemfiles/mongoid2.gemfile +7 -0
  43. data/test/gemfiles/mongoid3.gemfile +6 -0
  44. data/test/gemfiles/mongoid4.gemfile +7 -0
  45. data/test/gemfiles/mongoid5.gemfile +7 -0
  46. data/test/gemfiles/mongoid6.gemfile +8 -0
  47. data/test/gemfiles/nobrainer.gemfile +8 -0
  48. data/test/gemfiles/parallel_tests.gemfile +8 -0
  49. data/test/geo_shape_test.rb +172 -0
  50. data/test/highlight_test.rb +78 -0
  51. data/test/index_test.rb +153 -0
  52. data/test/inheritance_test.rb +83 -0
  53. data/test/marshal_test.rb +8 -0
  54. data/test/match_test.rb +276 -0
  55. data/test/misspellings_test.rb +56 -0
  56. data/test/model_test.rb +42 -0
  57. data/test/multi_search_test.rb +22 -0
  58. data/test/multi_tenancy_test.rb +22 -0
  59. data/test/order_test.rb +46 -0
  60. data/test/pagination_test.rb +53 -0
  61. data/test/partial_reindex_test.rb +58 -0
  62. data/test/query_test.rb +35 -0
  63. data/test/records_test.rb +10 -0
  64. data/test/reindex_test.rb +52 -0
  65. data/test/reindex_v2_job_test.rb +32 -0
  66. data/test/routing_test.rb +23 -0
  67. data/test/should_index_test.rb +32 -0
  68. data/test/similar_test.rb +28 -0
  69. data/test/sql_test.rb +198 -0
  70. data/test/suggest_test.rb +85 -0
  71. data/test/synonyms_test.rb +67 -0
  72. data/test/test_helper.rb +527 -0
  73. data/test/where_test.rb +223 -0
  74. metadata +250 -0
@@ -0,0 +1,333 @@
1
+ module Searchkick
2
+ module IndexOptions
3
+ def index_options
4
+ options = @options
5
+ language = options[:language]
6
+ language = language.call if language.respond_to?(:call)
7
+
8
+ if options[:mappings] && !options[:merge_mappings]
9
+ settings = options[:settings] || {}
10
+ mappings = options[:mappings]
11
+ else
12
+ below22 = Searchkick.server_below?("2.2.0")
13
+ below50 = Searchkick.server_below?("5.0.0-alpha1")
14
+ default_type = below50 ? "string" : "text"
15
+ default_analyzer = :searchkick_index
16
+ keyword_mapping =
17
+ if below50
18
+ {
19
+ type: default_type,
20
+ index: "not_analyzed"
21
+ }
22
+ else
23
+ {
24
+ type: "keyword"
25
+ }
26
+ end
27
+
28
+ keyword_mapping[:ignore_above] = (options[:ignore_above] || 30000) unless below22
29
+
30
+ settings = {
31
+ analysis: {
32
+ analyzer: {
33
+ searchkick_keyword: {
34
+ type: "custom",
35
+ tokenizer: "keyword",
36
+ filter: ["lowercase"] + (options[:stem_conversions] == false ? [] : ["searchkick_stemmer"])
37
+ },
38
+ default_analyzer => {
39
+ type: "custom",
40
+ # character filters -> tokenizer -> token filters
41
+ # https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html
42
+ char_filter: ["ampersand"],
43
+ tokenizer: "standard",
44
+ # synonym should come last, after stemming and shingle
45
+ # shingle must come before searchkick_stemmer
46
+ filter: ["standard", "lowercase", "asciifolding", "searchkick_index_shingle", "searchkick_stemmer"]
47
+ },
48
+ searchkick_search: {
49
+ type: "custom",
50
+ char_filter: ["ampersand"],
51
+ tokenizer: "standard",
52
+ filter: ["standard", "lowercase", "asciifolding", "searchkick_search_shingle", "searchkick_stemmer"]
53
+ },
54
+ searchkick_search2: {
55
+ type: "custom",
56
+ char_filter: ["ampersand"],
57
+ tokenizer: "standard",
58
+ filter: ["standard", "lowercase", "asciifolding", "searchkick_stemmer"]
59
+ },
60
+ # https://github.com/leschenko/elasticsearch_autocomplete/blob/master/lib/elasticsearch_autocomplete/analyzers.rb
61
+ searchkick_autocomplete_search: {
62
+ type: "custom",
63
+ tokenizer: "keyword",
64
+ filter: ["lowercase", "asciifolding"]
65
+ },
66
+ searchkick_word_search: {
67
+ type: "custom",
68
+ tokenizer: "standard",
69
+ filter: ["lowercase", "asciifolding"]
70
+ },
71
+ searchkick_suggest_index: {
72
+ type: "custom",
73
+ tokenizer: "standard",
74
+ filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
75
+ },
76
+ searchkick_text_start_index: {
77
+ type: "custom",
78
+ tokenizer: "keyword",
79
+ filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
80
+ },
81
+ searchkick_text_middle_index: {
82
+ type: "custom",
83
+ tokenizer: "keyword",
84
+ filter: ["lowercase", "asciifolding", "searchkick_ngram"]
85
+ },
86
+ searchkick_text_end_index: {
87
+ type: "custom",
88
+ tokenizer: "keyword",
89
+ filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
90
+ },
91
+ searchkick_word_start_index: {
92
+ type: "custom",
93
+ tokenizer: "standard",
94
+ filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
95
+ },
96
+ searchkick_word_middle_index: {
97
+ type: "custom",
98
+ tokenizer: "standard",
99
+ filter: ["lowercase", "asciifolding", "searchkick_ngram"]
100
+ },
101
+ searchkick_word_end_index: {
102
+ type: "custom",
103
+ tokenizer: "standard",
104
+ filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
105
+ }
106
+ },
107
+ filter: {
108
+ searchkick_index_shingle: {
109
+ type: "shingle",
110
+ token_separator: ""
111
+ },
112
+ # lucky find http://web.archiveorange.com/archive/v/AAfXfQ17f57FcRINsof7
113
+ searchkick_search_shingle: {
114
+ type: "shingle",
115
+ token_separator: "",
116
+ output_unigrams: false,
117
+ output_unigrams_if_no_shingles: true
118
+ },
119
+ searchkick_suggest_shingle: {
120
+ type: "shingle",
121
+ max_shingle_size: 5
122
+ },
123
+ searchkick_edge_ngram: {
124
+ type: "edgeNGram",
125
+ min_gram: 1,
126
+ max_gram: 50
127
+ },
128
+ searchkick_ngram: {
129
+ type: "nGram",
130
+ min_gram: 1,
131
+ max_gram: 50
132
+ },
133
+ searchkick_stemmer: {
134
+ # use stemmer if language is lowercase, snowball otherwise
135
+ # TODO deprecate language option in favor of stemmer
136
+ type: language == language.to_s.downcase ? "stemmer" : "snowball",
137
+ language: language || "English"
138
+ }
139
+ },
140
+ char_filter: {
141
+ # https://www.elastic.co/guide/en/elasticsearch/guide/current/custom-analyzers.html
142
+ # &_to_and
143
+ ampersand: {
144
+ type: "mapping",
145
+ mappings: ["&=> and "]
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ if Searchkick.env == "test"
152
+ settings[:number_of_shards] = 1
153
+ settings[:number_of_replicas] = 0
154
+ end
155
+
156
+ if options[:similarity]
157
+ settings[:similarity] = {default: {type: options[:similarity]}}
158
+ end
159
+
160
+ settings.deep_merge!(options[:settings] || {})
161
+
162
+ # synonyms
163
+ synonyms = options[:synonyms] || []
164
+
165
+ synonyms = synonyms.call if synonyms.respond_to?(:call)
166
+
167
+ if synonyms.any?
168
+ settings[:analysis][:filter][:searchkick_synonym] = {
169
+ type: "synonym",
170
+ synonyms: synonyms.select { |s| s.size > 1 }.map { |s| s.is_a?(Array) ? s.join(",") : s }.map(&:downcase)
171
+ }
172
+ # choosing a place for the synonym filter when stemming is not easy
173
+ # https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
174
+ # TODO use a snowball stemmer on synonyms when creating the token filter
175
+
176
+ # http://elasticsearch-users.115913.n3.nabble.com/synonym-multi-words-search-td4030811.html
177
+ # I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
178
+ # - Only apply the synonym expansion at index time
179
+ # - Don't have the synonym filter applied search
180
+ # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
181
+ settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_synonym")
182
+ settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_synonym"
183
+
184
+ %w(word_start word_middle word_end).each do |type|
185
+ settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym")
186
+ end
187
+ end
188
+
189
+ if options[:wordnet]
190
+ settings[:analysis][:filter][:searchkick_wordnet] = {
191
+ type: "synonym",
192
+ format: "wordnet",
193
+ synonyms_path: Searchkick.wordnet_path
194
+ }
195
+
196
+ settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_wordnet")
197
+ settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_wordnet"
198
+
199
+ %w(word_start word_middle word_end).each do |type|
200
+ settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_wordnet")
201
+ end
202
+ end
203
+
204
+ if options[:special_characters] == false
205
+ settings[:analysis][:analyzer].each do |_, analyzer_settings|
206
+ analyzer_settings[:filter].reject! { |f| f == "asciifolding" }
207
+ end
208
+ end
209
+
210
+ mapping = {}
211
+
212
+ # conversions
213
+ Array(options[:conversions]).each do |conversions_field|
214
+ mapping[conversions_field] = {
215
+ type: "nested",
216
+ properties: {
217
+ query: {type: default_type, analyzer: "searchkick_keyword"},
218
+ count: {type: "integer"}
219
+ }
220
+ }
221
+ end
222
+
223
+ mapping_options = Hash[
224
+ [:suggest, :word, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight, :searchable, :filterable]
225
+ .map { |type| [type, (options[type] || []).map(&:to_s)] }
226
+ ]
227
+
228
+ word = options[:word] != false && (!options[:match] || options[:match] == :word)
229
+
230
+ mapping_options[:searchable].delete("_all")
231
+
232
+ analyzed_field_options = {type: default_type, index: "analyzed", analyzer: default_analyzer}
233
+
234
+ mapping_options.values.flatten.uniq.each do |field|
235
+ fields = {}
236
+
237
+ if options.key?(:filterable) && !mapping_options[:filterable].include?(field)
238
+ fields[field] = {type: default_type, index: "no"}
239
+ else
240
+ fields[field] = keyword_mapping
241
+ end
242
+
243
+ if !options[:searchable] || mapping_options[:searchable].include?(field)
244
+ if word
245
+ fields["analyzed"] = analyzed_field_options
246
+
247
+ if mapping_options[:highlight].include?(field)
248
+ fields["analyzed"][:term_vector] = "with_positions_offsets"
249
+ end
250
+ end
251
+
252
+ mapping_options.except(:highlight, :searchable, :filterable, :word).each do |type, f|
253
+ if options[:match] == type || f.include?(field)
254
+ fields[type] = {type: default_type, index: "analyzed", analyzer: "searchkick_#{type}_index"}
255
+ end
256
+ end
257
+ end
258
+
259
+ mapping[field] = fields[field].merge(fields: fields.except(field))
260
+ end
261
+
262
+ (options[:locations] || []).map(&:to_s).each do |field|
263
+ mapping[field] = {
264
+ type: "geo_point"
265
+ }
266
+ end
267
+
268
+ options[:geo_shape] = options[:geo_shape].product([{}]).to_h if options[:geo_shape].is_a?(Array)
269
+ (options[:geo_shape] || {}).each do |field, shape_options|
270
+ mapping[field] = shape_options.merge(type: "geo_shape")
271
+ end
272
+
273
+ routing = {}
274
+ if options[:routing]
275
+ routing = {required: true}
276
+ unless options[:routing] == true
277
+ routing[:path] = options[:routing].to_s
278
+ end
279
+ end
280
+
281
+ dynamic_fields = {
282
+ # analyzed field must be the default field for include_in_all
283
+ # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
284
+ # however, we can include the not_analyzed field in _all
285
+ # and the _all index analyzer will take care of it
286
+ "{name}" => keyword_mapping.merge(include_in_all: !options[:searchable])
287
+ }
288
+
289
+ if options.key?(:filterable)
290
+ dynamic_fields["{name}"] = {type: default_type, index: "no"}
291
+ end
292
+
293
+ unless options[:searchable]
294
+ if options[:match] && options[:match] != :word
295
+ dynamic_fields[options[:match]] = {type: default_type, index: "analyzed", analyzer: "searchkick_#{options[:match]}_index"}
296
+ end
297
+
298
+ if word
299
+ dynamic_fields["analyzed"] = analyzed_field_options
300
+ end
301
+ end
302
+
303
+ # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
304
+ multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
305
+
306
+ all_enabled = !options[:searchable] || options[:searchable].to_a.map(&:to_s).include?("_all")
307
+
308
+ mappings = {
309
+ _default_: {
310
+ _all: all_enabled ? analyzed_field_options : {enabled: false},
311
+ properties: mapping,
312
+ _routing: routing,
313
+ # https://gist.github.com/kimchy/2898285
314
+ dynamic_templates: [
315
+ {
316
+ string_template: {
317
+ match: "*",
318
+ match_mapping_type: "string",
319
+ mapping: multi_field
320
+ }
321
+ }
322
+ ]
323
+ }
324
+ }.deep_merge(options[:mappings] || {})
325
+ end
326
+
327
+ {
328
+ settings: settings,
329
+ mappings: mappings
330
+ }
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,28 @@
1
+ module Searchkick
2
+ class Indexer
3
+ attr_reader :queued_items
4
+
5
+ def initialize
6
+ @queued_items = []
7
+ end
8
+
9
+ def queue(items)
10
+ @queued_items.concat(items)
11
+ perform unless Searchkick.callbacks_value == :bulk
12
+ end
13
+
14
+ def perform
15
+ items = @queued_items
16
+ @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
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,242 @@
1
+ # based on https://gist.github.com/mnutt/566725
2
+ require "active_support/core_ext/module/attr_internal"
3
+
4
+ module Searchkick
5
+ module QueryWithInstrumentation
6
+ def execute_search
7
+ name = searchkick_klass ? "#{searchkick_klass.name} Search" : "Search"
8
+ event = {
9
+ name: name,
10
+ query: params
11
+ }
12
+ ActiveSupport::Notifications.instrument("search.searchkick", event) do
13
+ super
14
+ end
15
+ end
16
+ end
17
+
18
+ module IndexWithInstrumentation
19
+ def store(record)
20
+ event = {
21
+ name: "#{record.searchkick_klass.name} Store",
22
+ id: search_id(record)
23
+ }
24
+ if Searchkick.callbacks_value == :bulk
25
+ super
26
+ else
27
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
28
+ super
29
+ end
30
+ end
31
+ end
32
+
33
+ def remove(record)
34
+ name = record && record.searchkick_klass ? "#{record.searchkick_klass.name} Remove" : "Remove"
35
+ event = {
36
+ name: name,
37
+ id: search_id(record)
38
+ }
39
+ if Searchkick.callbacks_value == :bulk
40
+ super
41
+ else
42
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
43
+ super
44
+ end
45
+ end
46
+ end
47
+
48
+ def update_record(record, method_name)
49
+ event = {
50
+ name: "#{record.searchkick_klass.name} Update",
51
+ id: search_id(record)
52
+ }
53
+ if Searchkick.callbacks_value == :bulk
54
+ super
55
+ else
56
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
57
+ super
58
+ end
59
+ end
60
+ end
61
+
62
+ def bulk_index(records)
63
+ if records.any?
64
+ event = {
65
+ name: "#{records.first.searchkick_klass.name} Import",
66
+ count: records.size
67
+ }
68
+ event[:id] = search_id(records.first) if records.size == 1
69
+ if Searchkick.callbacks_value == :bulk
70
+ super
71
+ else
72
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
73
+ super
74
+ end
75
+ end
76
+ end
77
+ end
78
+ alias_method :import, :bulk_index
79
+
80
+ def bulk_update(records, *args)
81
+ if records.any?
82
+ event = {
83
+ name: "#{records.first.searchkick_klass.name} Update",
84
+ count: records.size
85
+ }
86
+ event[:id] = search_id(records.first) if records.size == 1
87
+ if Searchkick.callbacks_value == :bulk
88
+ super
89
+ else
90
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
91
+ super
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def bulk_delete(records)
98
+ if records.any?
99
+ event = {
100
+ name: "#{records.first.searchkick_klass.name} Delete",
101
+ count: records.size
102
+ }
103
+ event[:id] = search_id(records.first) if records.size == 1
104
+ if Searchkick.callbacks_value == :bulk
105
+ super
106
+ else
107
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
108
+ super
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ module IndexerWithInstrumentation
116
+ def perform
117
+ if Searchkick.callbacks_value == :bulk
118
+ event = {
119
+ name: "Bulk",
120
+ count: queued_items.size
121
+ }
122
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
123
+ super
124
+ end
125
+ else
126
+ super
127
+ end
128
+ end
129
+ end
130
+
131
+ module SearchkickWithInstrumentation
132
+ def multi_search(searches)
133
+ event = {
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
136
+ }
137
+ ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
138
+ super
139
+ end
140
+ end
141
+ end
142
+
143
+ # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/log_subscriber.rb
144
+ class LogSubscriber < ActiveSupport::LogSubscriber
145
+ def self.runtime=(value)
146
+ Thread.current[:searchkick_runtime] = value
147
+ end
148
+
149
+ def self.runtime
150
+ Thread.current[:searchkick_runtime] ||= 0
151
+ end
152
+
153
+ def self.reset_runtime
154
+ rt = runtime
155
+ self.runtime = 0
156
+ rt
157
+ end
158
+
159
+ def search(event)
160
+ self.class.runtime += event.duration
161
+ return unless logger.debug?
162
+
163
+ payload = event.payload
164
+ name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
165
+ type = payload[:query][:type]
166
+ index = payload[:query][:index].is_a?(Array) ? payload[:query][:index].join(",") : payload[:query][:index]
167
+
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}'"
171
+ end
172
+
173
+ def request(event)
174
+ self.class.runtime += event.duration
175
+ return unless logger.debug?
176
+
177
+ payload = event.payload
178
+ name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
179
+
180
+ debug " #{color(name, YELLOW, true)} #{payload.except(:name).to_json}"
181
+ end
182
+
183
+ def multi_search(event)
184
+ self.class.runtime += event.duration
185
+ return unless logger.debug?
186
+
187
+ payload = event.payload
188
+ name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
189
+
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]}'"
193
+ end
194
+ end
195
+
196
+ # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/railties/controller_runtime.rb
197
+ module ControllerRuntime
198
+ extend ActiveSupport::Concern
199
+
200
+ protected
201
+
202
+ attr_internal :searchkick_runtime
203
+
204
+ def process_action(action, *args)
205
+ # We also need to reset the runtime before each action
206
+ # because of queries in middleware or in cases we are streaming
207
+ # and it won't be cleaned up by the method below.
208
+ Searchkick::LogSubscriber.reset_runtime
209
+ super
210
+ end
211
+
212
+ def cleanup_view_runtime
213
+ searchkick_rt_before_render = Searchkick::LogSubscriber.reset_runtime
214
+ runtime = super
215
+ searchkick_rt_after_render = Searchkick::LogSubscriber.reset_runtime
216
+ self.searchkick_runtime = searchkick_rt_before_render + searchkick_rt_after_render
217
+ runtime - searchkick_rt_after_render
218
+ end
219
+
220
+ def append_info_to_payload(payload)
221
+ super
222
+ payload[:searchkick_runtime] = (searchkick_runtime || 0) + Searchkick::LogSubscriber.reset_runtime
223
+ end
224
+
225
+ module ClassMethods
226
+ def log_process_action(payload)
227
+ messages = super
228
+ runtime = payload[:searchkick_runtime]
229
+ messages << ("Searchkick: %.1fms" % runtime.to_f) if runtime.to_f > 0
230
+ messages
231
+ end
232
+ end
233
+ end
234
+ 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)
239
+ Searchkick::LogSubscriber.attach_to :searchkick
240
+ ActiveSupport.on_load(:action_controller) do
241
+ include Searchkick::ControllerRuntime
242
+ end
@@ -0,0 +1,12 @@
1
+ require "faraday/middleware"
2
+
3
+ module Searchkick
4
+ class Middleware < Faraday::Middleware
5
+ def call(env)
6
+ if env[:method] == :get && env[:url].path.to_s.end_with?("/_search")
7
+ env[:request][:timeout] = Searchkick.search_timeout
8
+ end
9
+ @app.call(env)
10
+ end
11
+ end
12
+ end