searchkick_evichat 0.0.2

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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE.md +7 -0
  3. data/.gitignore +22 -0
  4. data/.travis.yml +33 -0
  5. data/CHANGELOG.md +571 -0
  6. data/CONTRIBUTING.md +51 -0
  7. data/Gemfile +16 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +1895 -0
  10. data/Rakefile +16 -0
  11. data/benchmark/Gemfile +24 -0
  12. data/benchmark/index.rb +99 -0
  13. data/benchmark/search.rb +48 -0
  14. data/docs/Searchkick-3-Upgrade.md +57 -0
  15. data/lib/searchkick.rb +255 -0
  16. data/lib/searchkick/bulk_indexer.rb +171 -0
  17. data/lib/searchkick/bulk_reindex_job.rb +27 -0
  18. data/lib/searchkick/hash_wrapper.rb +12 -0
  19. data/lib/searchkick/index.rb +331 -0
  20. data/lib/searchkick/index_options.rb +414 -0
  21. data/lib/searchkick/indexer.rb +28 -0
  22. data/lib/searchkick/logging.rb +243 -0
  23. data/lib/searchkick/middleware.rb +12 -0
  24. data/lib/searchkick/model.rb +105 -0
  25. data/lib/searchkick/multi_search.rb +41 -0
  26. data/lib/searchkick/process_batch_job.rb +23 -0
  27. data/lib/searchkick/process_queue_job.rb +23 -0
  28. data/lib/searchkick/query.rb +978 -0
  29. data/lib/searchkick/record_data.rb +128 -0
  30. data/lib/searchkick/record_indexer.rb +59 -0
  31. data/lib/searchkick/reindex_queue.rb +38 -0
  32. data/lib/searchkick/reindex_v2_job.rb +36 -0
  33. data/lib/searchkick/results.rb +235 -0
  34. data/lib/searchkick/tasks.rb +29 -0
  35. data/lib/searchkick/version.rb +3 -0
  36. data/searchkick.gemspec +30 -0
  37. data/test/aggs_test.rb +217 -0
  38. data/test/autocomplete_test.rb +81 -0
  39. data/test/boost_test.rb +207 -0
  40. data/test/callbacks_test.rb +59 -0
  41. data/test/ci/before_install.sh +17 -0
  42. data/test/errors_test.rb +19 -0
  43. data/test/gemfiles/activerecord42.gemfile +7 -0
  44. data/test/gemfiles/activerecord50.gemfile +7 -0
  45. data/test/gemfiles/activerecord51.gemfile +7 -0
  46. data/test/gemfiles/apartment.gemfile +8 -0
  47. data/test/gemfiles/cequel.gemfile +8 -0
  48. data/test/gemfiles/mongoid5.gemfile +7 -0
  49. data/test/gemfiles/mongoid6.gemfile +12 -0
  50. data/test/gemfiles/nobrainer.gemfile +8 -0
  51. data/test/gemfiles/parallel_tests.gemfile +8 -0
  52. data/test/geo_shape_test.rb +171 -0
  53. data/test/highlight_test.rb +94 -0
  54. data/test/index_test.rb +165 -0
  55. data/test/inheritance_test.rb +82 -0
  56. data/test/language_test.rb +79 -0
  57. data/test/marshal_test.rb +13 -0
  58. data/test/match_test.rb +293 -0
  59. data/test/misspellings_test.rb +56 -0
  60. data/test/model_test.rb +40 -0
  61. data/test/multi_search_test.rb +37 -0
  62. data/test/multi_tenancy_test.rb +22 -0
  63. data/test/order_test.rb +40 -0
  64. data/test/pagination_test.rb +96 -0
  65. data/test/partial_reindex_test.rb +65 -0
  66. data/test/query_test.rb +43 -0
  67. data/test/reindex_test.rb +87 -0
  68. data/test/reindex_v2_job_test.rb +27 -0
  69. data/test/routing_test.rb +23 -0
  70. data/test/should_index_test.rb +32 -0
  71. data/test/similar_test.rb +28 -0
  72. data/test/sql_test.rb +190 -0
  73. data/test/suggest_test.rb +100 -0
  74. data/test/support/kaminari.yml +21 -0
  75. data/test/synonyms_test.rb +69 -0
  76. data/test/test_helper.rb +593 -0
  77. data/test/where_test.rb +249 -0
  78. metadata +250 -0
@@ -0,0 +1,414 @@
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
+ index_type = options[:_type]
8
+ index_type = index_type.call if index_type.respond_to?(:call)
9
+
10
+ if options[:mappings] && !options[:merge_mappings]
11
+ settings = options[:settings] || {}
12
+ mappings = options[:mappings]
13
+ else
14
+ below60 = Searchkick.server_below?("6.0.0")
15
+ below62 = Searchkick.server_below?("6.2.0")
16
+
17
+ default_type = "text"
18
+ default_analyzer = :searchkick_index
19
+ keyword_mapping = {type: "keyword"}
20
+
21
+ all = options.key?(:_all) ? options[:_all] : false
22
+ index_true_value = true
23
+ index_false_value = false
24
+
25
+ keyword_mapping[:ignore_above] = options[:ignore_above] || 30000
26
+
27
+ settings = {
28
+ analysis: {
29
+ analyzer: {
30
+ searchkick_keyword: {
31
+ type: "custom",
32
+ tokenizer: "keyword",
33
+ filter: ["lowercase"] + (options[:stem_conversions] ? ["searchkick_stemmer"] : [])
34
+ },
35
+ default_analyzer => {
36
+ type: "custom",
37
+ # character filters -> tokenizer -> token filters
38
+ # https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html
39
+ char_filter: ["ampersand"],
40
+ tokenizer: "standard",
41
+ # synonym should come last, after stemming and shingle
42
+ # shingle must come before searchkick_stemmer
43
+ filter: ["standard", "lowercase", "asciifolding", "searchkick_index_shingle", "searchkick_stemmer"]
44
+ },
45
+ searchkick_search: {
46
+ type: "custom",
47
+ char_filter: ["ampersand"],
48
+ tokenizer: "standard",
49
+ filter: ["standard", "lowercase", "asciifolding", "searchkick_search_shingle", "searchkick_stemmer"]
50
+ },
51
+ searchkick_search2: {
52
+ type: "custom",
53
+ char_filter: ["ampersand"],
54
+ tokenizer: "standard",
55
+ filter: ["standard", "lowercase", "asciifolding", "searchkick_stemmer"]
56
+ },
57
+ # https://github.com/leschenko/elasticsearch_autocomplete/blob/master/lib/elasticsearch_autocomplete/analyzers.rb
58
+ searchkick_autocomplete_search: {
59
+ type: "custom",
60
+ tokenizer: "keyword",
61
+ filter: ["lowercase", "asciifolding"]
62
+ },
63
+ searchkick_word_search: {
64
+ type: "custom",
65
+ tokenizer: "standard",
66
+ filter: ["lowercase", "asciifolding"]
67
+ },
68
+ searchkick_suggest_index: {
69
+ type: "custom",
70
+ tokenizer: "standard",
71
+ filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
72
+ },
73
+ searchkick_text_start_index: {
74
+ type: "custom",
75
+ tokenizer: "keyword",
76
+ filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
77
+ },
78
+ searchkick_text_middle_index: {
79
+ type: "custom",
80
+ tokenizer: "keyword",
81
+ filter: ["lowercase", "asciifolding", "searchkick_ngram"]
82
+ },
83
+ searchkick_text_end_index: {
84
+ type: "custom",
85
+ tokenizer: "keyword",
86
+ filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
87
+ },
88
+ searchkick_word_start_index: {
89
+ type: "custom",
90
+ tokenizer: "standard",
91
+ filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
92
+ },
93
+ searchkick_word_middle_index: {
94
+ type: "custom",
95
+ tokenizer: "standard",
96
+ filter: ["lowercase", "asciifolding", "searchkick_ngram"]
97
+ },
98
+ searchkick_word_end_index: {
99
+ type: "custom",
100
+ tokenizer: "standard",
101
+ filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
102
+ }
103
+ },
104
+ filter: {
105
+ searchkick_index_shingle: {
106
+ type: "shingle",
107
+ token_separator: ""
108
+ },
109
+ # lucky find http://web.archiveorange.com/archive/v/AAfXfQ17f57FcRINsof7
110
+ searchkick_search_shingle: {
111
+ type: "shingle",
112
+ token_separator: "",
113
+ output_unigrams: false,
114
+ output_unigrams_if_no_shingles: true
115
+ },
116
+ searchkick_suggest_shingle: {
117
+ type: "shingle",
118
+ max_shingle_size: 5
119
+ },
120
+ searchkick_edge_ngram: {
121
+ type: "edgeNGram",
122
+ min_gram: 1,
123
+ max_gram: 50
124
+ },
125
+ searchkick_ngram: {
126
+ type: "nGram",
127
+ min_gram: 1,
128
+ max_gram: 50
129
+ },
130
+ searchkick_stemmer: {
131
+ # use stemmer if language is lowercase, snowball otherwise
132
+ type: language == language.to_s.downcase ? "stemmer" : "snowball",
133
+ language: language || "English"
134
+ }
135
+ },
136
+ char_filter: {
137
+ # https://www.elastic.co/guide/en/elasticsearch/guide/current/custom-analyzers.html
138
+ # &_to_and
139
+ ampersand: {
140
+ type: "mapping",
141
+ mappings: ["&=> and "]
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ case language
148
+ when "chinese"
149
+ settings[:analysis][:analyzer].merge!(
150
+ default_analyzer => {
151
+ type: "ik_smart"
152
+ },
153
+ searchkick_search: {
154
+ type: "ik_smart"
155
+ },
156
+ searchkick_search2: {
157
+ type: "ik_max_word"
158
+ }
159
+ )
160
+
161
+ settings[:analysis][:filter].delete(:searchkick_stemmer)
162
+ when "japanese"
163
+ settings[:analysis][:analyzer].merge!(
164
+ default_analyzer => {
165
+ type: "kuromoji"
166
+ },
167
+ searchkick_search: {
168
+ type: "kuromoji"
169
+ },
170
+ searchkick_search2: {
171
+ type: "kuromoji"
172
+ }
173
+ )
174
+ when "korean"
175
+ settings[:analysis][:analyzer].merge!(
176
+ default_analyzer => {
177
+ type: "openkoreantext-analyzer"
178
+ },
179
+ searchkick_search: {
180
+ type: "openkoreantext-analyzer"
181
+ },
182
+ searchkick_search2: {
183
+ type: "openkoreantext-analyzer"
184
+ }
185
+ )
186
+ when "vietnamese"
187
+ settings[:analysis][:analyzer].merge!(
188
+ default_analyzer => {
189
+ type: "vi_analyzer"
190
+ },
191
+ searchkick_search: {
192
+ type: "vi_analyzer"
193
+ },
194
+ searchkick_search2: {
195
+ type: "vi_analyzer"
196
+ }
197
+ )
198
+ when "polish", "ukrainian", "smartcn"
199
+ settings[:analysis][:analyzer].merge!(
200
+ default_analyzer => {
201
+ type: language
202
+ },
203
+ searchkick_search: {
204
+ type: language
205
+ },
206
+ searchkick_search2: {
207
+ type: language
208
+ }
209
+ )
210
+ end
211
+
212
+ if Searchkick.env == "test"
213
+ settings[:number_of_shards] = 1
214
+ settings[:number_of_replicas] = 0
215
+ end
216
+
217
+ if options[:similarity]
218
+ settings[:similarity] = {default: {type: options[:similarity]}}
219
+ end
220
+
221
+ unless below62
222
+ settings[:index] = {
223
+ max_ngram_diff: 49,
224
+ max_shingle_diff: 4
225
+ }
226
+ end
227
+
228
+ settings.deep_merge!(options[:settings] || {})
229
+
230
+ # synonyms
231
+ synonyms = options[:synonyms] || []
232
+
233
+ synonyms = synonyms.call if synonyms.respond_to?(:call)
234
+
235
+ if synonyms.any?
236
+ settings[:analysis][:filter][:searchkick_synonym] = {
237
+ type: "synonym",
238
+ # only remove a single space from synonyms so three-word synonyms will fail noisily instead of silently
239
+ synonyms: synonyms.select { |s| s.size > 1 }.map { |s| s.is_a?(Array) ? s.map { |s2| s2.sub(/\s+/, "") }.join(",") : s }.map(&:downcase)
240
+ }
241
+ # choosing a place for the synonym filter when stemming is not easy
242
+ # https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
243
+ # TODO use a snowball stemmer on synonyms when creating the token filter
244
+
245
+ # http://elasticsearch-users.115913.n3.nabble.com/synonym-multi-words-search-td4030811.html
246
+ # I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
247
+ # - Only apply the synonym expansion at index time
248
+ # - Don't have the synonym filter applied search
249
+ # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
250
+ settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_synonym") if below60
251
+ settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_synonym"
252
+
253
+ %w(word_start word_middle word_end).each do |type|
254
+ settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym")
255
+ end
256
+ end
257
+
258
+ if options[:wordnet]
259
+ settings[:analysis][:filter][:searchkick_wordnet] = {
260
+ type: "synonym",
261
+ format: "wordnet",
262
+ synonyms_path: Searchkick.wordnet_path
263
+ }
264
+
265
+ settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_wordnet")
266
+ settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_wordnet"
267
+
268
+ %w(word_start word_middle word_end).each do |type|
269
+ settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_wordnet")
270
+ end
271
+ end
272
+
273
+ if options[:special_characters] == false
274
+ settings[:analysis][:analyzer].each_value do |analyzer_settings|
275
+ analyzer_settings[:filter].reject! { |f| f == "asciifolding" }
276
+ end
277
+ end
278
+
279
+ mapping = {}
280
+
281
+ # conversions
282
+ Array(options[:conversions]).each do |conversions_field|
283
+ mapping[conversions_field] = {
284
+ type: "nested",
285
+ properties: {
286
+ query: {type: default_type, analyzer: "searchkick_keyword"},
287
+ count: {type: "integer"}
288
+ }
289
+ }
290
+ end
291
+
292
+ mapping_options = Hash[
293
+ [:suggest, :word, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight, :searchable, :filterable]
294
+ .map { |type| [type, (options[type] || []).map(&:to_s)] }
295
+ ]
296
+
297
+ word = options[:word] != false && (!options[:match] || options[:match] == :word)
298
+
299
+ mapping_options[:searchable].delete("_all")
300
+
301
+ analyzed_field_options = {type: default_type, index: index_true_value, analyzer: default_analyzer}
302
+
303
+ mapping_options.values.flatten.uniq.each do |field|
304
+ fields = {}
305
+
306
+ if options.key?(:filterable) && !mapping_options[:filterable].include?(field)
307
+ fields[field] = {type: default_type, index: index_false_value}
308
+ else
309
+ fields[field] = keyword_mapping
310
+ end
311
+
312
+ if !options[:searchable] || mapping_options[:searchable].include?(field)
313
+ if word
314
+ fields["analyzed"] = analyzed_field_options
315
+
316
+ if mapping_options[:highlight].include?(field)
317
+ fields["analyzed"][:term_vector] = "with_positions_offsets"
318
+ end
319
+ end
320
+
321
+ mapping_options.except(:highlight, :searchable, :filterable, :word).each do |type, f|
322
+ if options[:match] == type || f.include?(field)
323
+ fields[type] = {type: default_type, index: index_true_value, analyzer: "searchkick_#{type}_index"}
324
+ end
325
+ end
326
+ end
327
+
328
+ mapping[field] = fields[field].merge(fields: fields.except(field))
329
+ end
330
+
331
+ (options[:locations] || []).map(&:to_s).each do |field|
332
+ mapping[field] = {
333
+ type: "geo_point"
334
+ }
335
+ end
336
+
337
+ options[:geo_shape] = options[:geo_shape].product([{}]).to_h if options[:geo_shape].is_a?(Array)
338
+ (options[:geo_shape] || {}).each do |field, shape_options|
339
+ mapping[field] = shape_options.merge(type: "geo_shape")
340
+ end
341
+
342
+ if options[:inheritance]
343
+ mapping[:type] = keyword_mapping
344
+ end
345
+
346
+ routing = {}
347
+ if options[:routing]
348
+ routing = {required: true}
349
+ unless options[:routing] == true
350
+ routing[:path] = options[:routing].to_s
351
+ end
352
+ end
353
+
354
+ dynamic_fields = {
355
+ # analyzed field must be the default field for include_in_all
356
+ # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
357
+ # however, we can include the not_analyzed field in _all
358
+ # and the _all index analyzer will take care of it
359
+ "{name}" => keyword_mapping
360
+ }
361
+
362
+ if below60 && all
363
+ dynamic_fields["{name}"][:include_in_all] = !options[:searchable]
364
+ end
365
+
366
+ if options.key?(:filterable)
367
+ dynamic_fields["{name}"] = {type: default_type, index: index_false_value}
368
+ end
369
+
370
+ unless options[:searchable]
371
+ if options[:match] && options[:match] != :word
372
+ dynamic_fields[options[:match]] = {type: default_type, index: index_true_value, analyzer: "searchkick_#{options[:match]}_index"}
373
+ end
374
+
375
+ if word
376
+ dynamic_fields["analyzed"] = analyzed_field_options
377
+ end
378
+ end
379
+
380
+ # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
381
+ multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
382
+
383
+ mappings = {
384
+ index_type => {
385
+ properties: mapping,
386
+ _routing: routing,
387
+ # https://gist.github.com/kimchy/2898285
388
+ dynamic_templates: [
389
+ {
390
+ string_template: {
391
+ match: "*",
392
+ match_mapping_type: "string",
393
+ mapping: multi_field
394
+ }
395
+ }
396
+ ]
397
+ }
398
+ }
399
+
400
+ if below60
401
+ all_enabled = all && (!options[:searchable] || options[:searchable].to_a.map(&:to_s).include?("_all"))
402
+ mappings[index_type][:_all] = all_enabled ? analyzed_field_options : {enabled: false}
403
+ end
404
+
405
+ mappings = mappings.deep_merge(options[:mappings] || {})
406
+ end
407
+
408
+ {
409
+ settings: settings,
410
+ mappings: mappings
411
+ }
412
+ end
413
+ end
414
+ 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,243 @@
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 -H 'Content-Type: application/json' -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 -H 'Content-Type: application/json' -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
+
236
+ Searchkick::Query.prepend(Searchkick::QueryWithInstrumentation)
237
+ Searchkick::Index.prepend(Searchkick::IndexWithInstrumentation)
238
+ Searchkick::Indexer.prepend(Searchkick::IndexerWithInstrumentation)
239
+ Searchkick.singleton_class.prepend(Searchkick::SearchkickWithInstrumentation)
240
+ Searchkick::LogSubscriber.attach_to :searchkick
241
+ ActiveSupport.on_load(:action_controller) do
242
+ include Searchkick::ControllerRuntime
243
+ end