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,171 @@
1
+ module Searchkick
2
+ class BulkIndexer
3
+ attr_reader :index
4
+
5
+ def initialize(index)
6
+ @index = index
7
+ end
8
+
9
+ def import_scope(relation, resume: false, method_name: nil, async: false, batch: false, batch_id: nil, full: false, scope: nil)
10
+ if scope
11
+ relation = relation.send(scope)
12
+ elsif relation.respond_to?(:search_import)
13
+ relation = relation.search_import
14
+ end
15
+
16
+ if batch
17
+ import_or_update relation.to_a, method_name, async
18
+ Searchkick.with_redis { |r| r.srem(batches_key, batch_id) } if batch_id
19
+ elsif full && async
20
+ full_reindex_async(relation)
21
+ elsif relation.respond_to?(:find_in_batches)
22
+ if resume
23
+ # use total docs instead of max id since there's not a great way
24
+ # to get the max _id without scripting since it's a string
25
+
26
+ # TODO use primary key and prefix with table name
27
+ relation = relation.where("id > ?", total_docs)
28
+ end
29
+
30
+ relation = relation.select("id").except(:includes, :preload) if async
31
+
32
+ relation.find_in_batches batch_size: batch_size do |items|
33
+ import_or_update items, method_name, async
34
+ end
35
+ else
36
+ each_batch(relation) do |items|
37
+ import_or_update items, method_name, async
38
+ end
39
+ end
40
+ end
41
+
42
+ def bulk_index(records)
43
+ Searchkick.indexer.queue(records.map { |r| RecordData.new(index, r).index_data })
44
+ end
45
+
46
+ def bulk_delete(records)
47
+ Searchkick.indexer.queue(records.reject { |r| r.id.blank? }.map { |r| RecordData.new(index, r).delete_data })
48
+ end
49
+
50
+ def bulk_update(records, method_name)
51
+ Searchkick.indexer.queue(records.map { |r| RecordData.new(index, r).update_data(method_name) })
52
+ end
53
+
54
+ def batches_left
55
+ Searchkick.with_redis { |r| r.scard(batches_key) }
56
+ end
57
+
58
+ private
59
+
60
+ def import_or_update(records, method_name, async)
61
+ if records.any?
62
+ if async
63
+ Searchkick::BulkReindexJob.perform_later(
64
+ class_name: records.first.class.name,
65
+ record_ids: records.map(&:id),
66
+ index_name: index.name,
67
+ method_name: method_name ? method_name.to_s : nil
68
+ )
69
+ else
70
+ records = records.select(&:should_index?)
71
+ if records.any?
72
+ with_retries do
73
+ # call out to index for ActiveSupport notifications
74
+ if method_name
75
+ index.bulk_update(records, method_name)
76
+ else
77
+ index.bulk_index(records)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def full_reindex_async(scope)
86
+ if scope.respond_to?(:primary_key)
87
+ # TODO expire Redis key
88
+ primary_key = scope.primary_key
89
+
90
+ starting_id =
91
+ begin
92
+ scope.minimum(primary_key)
93
+ rescue ActiveRecord::StatementInvalid
94
+ false
95
+ end
96
+
97
+ if starting_id.nil?
98
+ # no records, do nothing
99
+ elsif starting_id.is_a?(Numeric)
100
+ max_id = scope.maximum(primary_key)
101
+ batches_count = ((max_id - starting_id + 1) / batch_size.to_f).ceil
102
+
103
+ batches_count.times do |i|
104
+ batch_id = i + 1
105
+ min_id = starting_id + (i * batch_size)
106
+ bulk_reindex_job scope, batch_id, min_id: min_id, max_id: min_id + batch_size - 1
107
+ end
108
+ else
109
+ scope.find_in_batches(batch_size: batch_size).each_with_index do |batch, i|
110
+ batch_id = i + 1
111
+
112
+ bulk_reindex_job scope, batch_id, record_ids: batch.map { |record| record.id.to_s }
113
+ end
114
+ end
115
+ else
116
+ batch_id = 1
117
+ # TODO remove any eager loading
118
+ scope = scope.only(:_id) if scope.respond_to?(:only)
119
+ each_batch(scope) do |items|
120
+ bulk_reindex_job scope, batch_id, record_ids: items.map { |i| i.id.to_s }
121
+ batch_id += 1
122
+ end
123
+ end
124
+ end
125
+
126
+ def each_batch(scope)
127
+ # https://github.com/karmi/tire/blob/master/lib/tire/model/import.rb
128
+ # use cursor for Mongoid
129
+ items = []
130
+ scope.all.each do |item|
131
+ items << item
132
+ if items.length == batch_size
133
+ yield items
134
+ items = []
135
+ end
136
+ end
137
+ yield items if items.any?
138
+ end
139
+
140
+ def bulk_reindex_job(scope, batch_id, options)
141
+ Searchkick.with_redis { |r| r.sadd(batches_key, batch_id) }
142
+ Searchkick::BulkReindexJob.perform_later({
143
+ class_name: scope.model_name.name,
144
+ index_name: index.name,
145
+ batch_id: batch_id
146
+ }.merge(options))
147
+ end
148
+
149
+ def with_retries
150
+ retries = 0
151
+
152
+ begin
153
+ yield
154
+ rescue Faraday::ClientError => e
155
+ if retries < 1
156
+ retries += 1
157
+ retry
158
+ end
159
+ raise e
160
+ end
161
+ end
162
+
163
+ def batches_key
164
+ "searchkick:reindex:#{index.name}:batches"
165
+ end
166
+
167
+ def batch_size
168
+ @batch_size ||= index.options[:batch_size] || 1000
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,27 @@
1
+ module Searchkick
2
+ class BulkReindexJob < ActiveJob::Base
3
+ queue_as { Searchkick.queue_name }
4
+
5
+ def perform(params)
6
+ klass = params["class_name"].constantize
7
+ index_name = params["index_name"]
8
+ record_ids = params["record_ids"]
9
+ method_name = params["method_name"]
10
+ batch_id = params["batch_id"]
11
+ min_id = params["min_id"]
12
+ max_id = params["max_id"]
13
+ index = index_name ? Searchkick::Index.new(index_name, **klass.searchkick_options) : klass.searchkick_index
14
+ record_ids ||= min_id..max_id
15
+ index.import_scope(
16
+ Searchkick.load_records(klass, record_ids),
17
+ method_name: method_name,
18
+ batch: true,
19
+ batch_id: batch_id
20
+ )
21
+ end
22
+ end
23
+
24
+ def locale
25
+ return "en"
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module Searchkick
2
+ # Subclass of `Hashie::Mash` to wrap Hash-like structures
3
+ # (responses from Elasticsearch)
4
+ #
5
+ # The primary goal of the subclass is to disable the
6
+ # warning being printed by Hashie for re-defined
7
+ # methods, such as `sort`.
8
+ #
9
+ class HashWrapper < ::Hashie::Mash
10
+ disable_warnings if respond_to?(:disable_warnings)
11
+ end
12
+ end
@@ -0,0 +1,331 @@
1
+ require "searchkick/index_options"
2
+
3
+ module Searchkick
4
+ class Index
5
+ include IndexOptions
6
+
7
+ attr_reader :name, :options
8
+
9
+ def initialize(name, options = {})
10
+ @name = name
11
+ @options = options
12
+ @klass_document_type = {} # cache
13
+ end
14
+
15
+ def create(body = {})
16
+ client.indices.create index: name, body: body
17
+ end
18
+
19
+ def delete
20
+ if !Searchkick.server_below?("6.0.0") && alias_exists?
21
+ # can't call delete directly on aliases in ES 6
22
+ indices = client.indices.get_alias(name: name).keys
23
+ client.indices.delete index: indices
24
+ else
25
+ client.indices.delete index: name
26
+ end
27
+ end
28
+
29
+ def exists?
30
+ client.indices.exists index: name
31
+ end
32
+
33
+ def refresh
34
+ client.indices.refresh index: name
35
+ end
36
+
37
+ def alias_exists?
38
+ client.indices.exists_alias name: name
39
+ end
40
+
41
+ def mapping
42
+ client.indices.get_mapping index: name
43
+ end
44
+
45
+ def settings
46
+ client.indices.get_settings index: name
47
+ end
48
+
49
+ def refresh_interval
50
+ settings.values.first["settings"]["index"]["refresh_interval"]
51
+ end
52
+
53
+ def update_settings(settings)
54
+ client.indices.put_settings index: name, body: settings
55
+ end
56
+
57
+ def tokens(text, options = {})
58
+ client.indices.analyze(body: {text: text}.merge(options), index: name)["tokens"].map { |t| t["token"] }
59
+ end
60
+
61
+ def total_docs
62
+ response =
63
+ client.search(
64
+ index: name,
65
+ body: {
66
+ query: {match_all: {}},
67
+ size: 0
68
+ }
69
+ )
70
+
71
+ response["hits"]["total"]
72
+ end
73
+
74
+ def promote(new_name, update_refresh_interval: false)
75
+ if update_refresh_interval
76
+ new_index = Searchkick::Index.new(new_name, @options)
77
+ settings = options[:settings] || {}
78
+ refresh_interval = (settings[:index] && settings[:index][:refresh_interval]) || "1s"
79
+ new_index.update_settings(index: {refresh_interval: refresh_interval})
80
+ end
81
+
82
+ old_indices =
83
+ begin
84
+ client.indices.get_alias(name: name).keys
85
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
86
+ {}
87
+ end
88
+ actions = old_indices.map { |old_name| {remove: {index: old_name, alias: name}} } + [{add: {index: new_name, alias: name}}]
89
+ client.indices.update_aliases body: {actions: actions}
90
+ end
91
+ alias_method :swap, :promote
92
+
93
+ def retrieve(record)
94
+ client.get(
95
+ index: name,
96
+ type: document_type(record),
97
+ id: search_id(record)
98
+ )["_source"]
99
+ end
100
+
101
+ def all_indices(unaliased: false)
102
+ indices =
103
+ begin
104
+ client.indices.get_aliases
105
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
106
+ {}
107
+ end
108
+ indices = indices.select { |_k, v| v.empty? || v["aliases"].empty? } if unaliased
109
+ indices.select { |k, _v| k =~ /\A#{Regexp.escape(name)}_\d{14,17}\z/ }.keys
110
+ end
111
+
112
+ # remove old indices that start w/ index_name
113
+ def clean_indices
114
+ indices = all_indices(unaliased: true)
115
+ indices.each do |index|
116
+ Searchkick::Index.new(index).delete
117
+ end
118
+ indices
119
+ end
120
+
121
+ # record based
122
+ # use helpers for notifications
123
+
124
+ def store(record)
125
+ bulk_indexer.bulk_index([record])
126
+ end
127
+
128
+ def remove(record)
129
+ bulk_indexer.bulk_delete([record])
130
+ end
131
+
132
+ def update_record(record, method_name)
133
+ bulk_indexer.bulk_update([record], method_name)
134
+ end
135
+
136
+ def bulk_delete(records)
137
+ bulk_indexer.bulk_delete(records)
138
+ end
139
+
140
+ def bulk_index(records)
141
+ bulk_indexer.bulk_index(records)
142
+ end
143
+ alias_method :import, :bulk_index
144
+
145
+ def bulk_update(records, method_name)
146
+ bulk_indexer.bulk_update(records, method_name)
147
+ end
148
+
149
+ def search_id(record)
150
+ RecordData.new(self, record).search_id
151
+ end
152
+
153
+ def document_type(record)
154
+ RecordData.new(self, record).document_type
155
+ end
156
+
157
+ def similar_record(record, **options)
158
+ like_text = retrieve(record).to_hash
159
+ .keep_if { |k, _| !options[:fields] || options[:fields].map(&:to_s).include?(k) }
160
+ .values.compact.join(" ")
161
+
162
+ # TODO deep merge method
163
+ options[:where] ||= {}
164
+ options[:where][:_id] ||= {}
165
+ options[:where][:_id][:not] = record.id.to_s
166
+ options[:per_page] ||= 10
167
+ options[:similar] = true
168
+
169
+ # TODO use index class instead of record class
170
+ Searchkick.search(like_text, model: record.class, **options)
171
+ end
172
+
173
+ # queue
174
+
175
+ def reindex_queue
176
+ Searchkick::ReindexQueue.new(name)
177
+ end
178
+
179
+ # reindex
180
+
181
+ def reindex(relation, method_name, scoped:, full: false, scope: nil, **options)
182
+ refresh = options.fetch(:refresh, !scoped)
183
+
184
+ if method_name
185
+ # update
186
+ import_scope(relation, method_name: method_name, scope: scope)
187
+ self.refresh if refresh
188
+ true
189
+ elsif scoped && !full
190
+ # reindex association
191
+ import_scope(relation, scope: scope)
192
+ self.refresh if refresh
193
+ true
194
+ else
195
+ # full reindex
196
+ reindex_scope(relation, scope: scope, **options)
197
+ end
198
+ end
199
+
200
+ def create_index(index_options: nil)
201
+ index_options ||= self.index_options
202
+ index = Searchkick::Index.new("#{name}_#{Time.now.strftime('%Y%m%d%H%M%S%L')}", @options)
203
+ index.create(index_options)
204
+ index
205
+ end
206
+
207
+ def import_scope(relation, **options)
208
+ bulk_indexer.import_scope(relation, **options)
209
+ end
210
+
211
+ def batches_left
212
+ bulk_indexer.batches_left
213
+ end
214
+
215
+ # other
216
+
217
+ def klass_document_type(klass, ignore_type = false)
218
+ @klass_document_type[[klass, ignore_type]] ||= begin
219
+ if !ignore_type && klass.searchkick_klass.searchkick_options[:_type]
220
+ type = klass.searchkick_klass.searchkick_options[:_type]
221
+ type = type.call if type.respond_to?(:call)
222
+ type
223
+ else
224
+ klass.model_name.to_s.underscore
225
+ end
226
+ end
227
+ end
228
+
229
+ # should not be public
230
+ def conversions_fields
231
+ @conversions_fields ||= begin
232
+ conversions = Array(options[:conversions])
233
+ conversions.map(&:to_s) + conversions.map(&:to_sym)
234
+ end
235
+ end
236
+
237
+ def suggest_fields
238
+ @suggest_fields ||= Array(options[:suggest]).map(&:to_s)
239
+ end
240
+
241
+ def locations_fields
242
+ @locations_fields ||= begin
243
+ locations = Array(options[:locations])
244
+ locations.map(&:to_s) + locations.map(&:to_sym)
245
+ end
246
+ end
247
+
248
+ protected
249
+
250
+ def client
251
+ Searchkick.client
252
+ end
253
+
254
+ def bulk_indexer
255
+ @bulk_indexer ||= BulkIndexer.new(self)
256
+ end
257
+
258
+ # https://gist.github.com/jarosan/3124884
259
+ # http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
260
+ def reindex_scope(relation, import: true, resume: false, retain: false, async: false, refresh_interval: nil, scope: nil)
261
+ if resume
262
+ index_name = all_indices.sort.last
263
+ raise Searchkick::Error, "No index to resume" unless index_name
264
+ index = Searchkick::Index.new(index_name, @options)
265
+ else
266
+ clean_indices unless retain
267
+
268
+ index_options = relation.searchkick_index_options
269
+ index_options.deep_merge!(settings: {index: {refresh_interval: refresh_interval}}) if refresh_interval
270
+ index = create_index(index_options: index_options)
271
+ end
272
+
273
+ import_options = {
274
+ resume: resume,
275
+ async: async,
276
+ full: true,
277
+ scope: scope
278
+ }
279
+
280
+ # check if alias exists
281
+ alias_exists = alias_exists?
282
+ if alias_exists
283
+ # import before promotion
284
+ index.import_scope(relation, **import_options) if import
285
+
286
+ # get existing indices to remove
287
+ unless async
288
+ promote(index.name, update_refresh_interval: !refresh_interval.nil?)
289
+ clean_indices unless retain
290
+ end
291
+ else
292
+ delete if exists?
293
+ promote(index.name, update_refresh_interval: !refresh_interval.nil?)
294
+
295
+ # import after promotion
296
+ index.import_scope(relation, **import_options) if import
297
+ end
298
+
299
+ if async
300
+ if async.is_a?(Hash) && async[:wait]
301
+ puts "Created index: #{index.name}"
302
+ puts "Jobs queued. Waiting..."
303
+ loop do
304
+ sleep 3
305
+ status = Searchkick.reindex_status(index.name)
306
+ break if status[:completed]
307
+ puts "Batches left: #{status[:batches_left]}"
308
+ end
309
+ # already promoted if alias didn't exist
310
+ if alias_exists
311
+ puts "Jobs complete. Promoting..."
312
+ promote(index.name, update_refresh_interval: !refresh_interval.nil?)
313
+ end
314
+ clean_indices unless retain
315
+ puts "SUCCESS!"
316
+ end
317
+
318
+ {index_name: index.name}
319
+ else
320
+ index.refresh
321
+ true
322
+ end
323
+ rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
324
+ if e.message.include?("No handler for type [text]")
325
+ raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 5 or greater"
326
+ end
327
+
328
+ raise e
329
+ end
330
+ end
331
+ end