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,128 @@
1
+ module Searchkick
2
+ class RecordData
3
+ TYPE_KEYS = ["type", :type]
4
+
5
+ attr_reader :index, :record
6
+
7
+ def initialize(index, record)
8
+ @index = index
9
+ @record = record
10
+ end
11
+
12
+ def index_data
13
+ data = record_data
14
+ data[:data] = search_data
15
+ {index: data}
16
+ end
17
+
18
+ def update_data(method_name)
19
+ data = record_data
20
+ data[:data] = {doc: search_data(method_name)}
21
+ {update: data}
22
+ end
23
+
24
+ def delete_data
25
+ {delete: record_data}
26
+ end
27
+
28
+ def search_id
29
+ id = record.respond_to?(:search_document_id) ? record.search_document_id : record.id
30
+ id.is_a?(Numeric) ? id : id.to_s
31
+ end
32
+
33
+ def document_type(ignore_type = false)
34
+ index.klass_document_type(record.class, ignore_type)
35
+ end
36
+
37
+ private
38
+
39
+ def record_data
40
+ data = {
41
+ _index: index.name,
42
+ _id: search_id,
43
+ _type: document_type
44
+ }
45
+ data[:_routing] = record.search_routing if record.respond_to?(:search_routing)
46
+ data
47
+ end
48
+
49
+ def search_data(method_name = nil)
50
+ partial_reindex = !method_name.nil?
51
+
52
+ source = record.send(method_name || :search_data)
53
+
54
+ # conversions
55
+ index.conversions_fields.each do |conversions_field|
56
+ if source[conversions_field]
57
+ source[conversions_field] = source[conversions_field].map { |k, v| {query: k, count: v} }
58
+ end
59
+ end
60
+
61
+ # hack to prevent generator field doesn't exist error
62
+ if !partial_reindex
63
+ index.suggest_fields.each do |field|
64
+ if !source[field] && !source[field.to_sym]
65
+ source[field] = nil
66
+ end
67
+ end
68
+ end
69
+
70
+ # locations
71
+ index.locations_fields.each do |field|
72
+ if source[field]
73
+ if !source[field].is_a?(Hash) && (source[field].first.is_a?(Array) || source[field].first.is_a?(Hash))
74
+ # multiple locations
75
+ source[field] = source[field].map { |a| location_value(a) }
76
+ else
77
+ source[field] = location_value(source[field])
78
+ end
79
+ end
80
+ end
81
+
82
+ if index.options[:inheritance]
83
+ if !TYPE_KEYS.any? { |tk| source.key?(tk) }
84
+ source[:type] = document_type(true)
85
+ end
86
+ end
87
+
88
+ cast_big_decimal(source)
89
+
90
+ source
91
+ end
92
+
93
+ def location_value(value)
94
+ if value.is_a?(Array)
95
+ value.map(&:to_f).reverse
96
+ elsif value.is_a?(Hash)
97
+ {lat: value[:lat].to_f, lon: value[:lon].to_f}
98
+ else
99
+ value
100
+ end
101
+ end
102
+
103
+ # change all BigDecimal values to floats due to
104
+ # https://github.com/rails/rails/issues/6033
105
+ # possible loss of precision :/
106
+ def cast_big_decimal(obj)
107
+ case obj
108
+ when BigDecimal
109
+ obj.to_f
110
+ when Hash
111
+ obj.each do |k, v|
112
+ # performance
113
+ if v.is_a?(BigDecimal)
114
+ obj[k] = v.to_f
115
+ elsif v.is_a?(Enumerable) ||
116
+ obj[k] = cast_big_decimal(v)
117
+ end
118
+ end
119
+ when Enumerable
120
+ obj.map do |v|
121
+ cast_big_decimal(v)
122
+ end
123
+ else
124
+ obj
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,59 @@
1
+ module Searchkick
2
+ class RecordIndexer
3
+ attr_reader :record, :index
4
+
5
+ def initialize(record)
6
+ @record = record
7
+ @index = record.class.searchkick_index
8
+ end
9
+
10
+ def reindex(method_name = nil, refresh: false, mode: nil)
11
+ unless [true, nil, :async, :queue].include?(mode)
12
+ raise ArgumentError, "Invalid value for mode"
13
+ end
14
+
15
+ mode ||= Searchkick.callbacks_value || index.options[:callbacks] || true
16
+
17
+ case mode
18
+ when :queue
19
+ if method_name
20
+ raise Searchkick::Error, "Partial reindex not supported with queue option"
21
+ end
22
+
23
+ index.reindex_queue.push(record.id.to_s)
24
+ when :async
25
+ unless defined?(ActiveJob)
26
+ raise Searchkick::Error, "Active Job not found"
27
+ end
28
+
29
+ Searchkick::ReindexV2Job.perform_later(
30
+ record.class.name,
31
+ record.id.to_s,
32
+ method_name ? method_name.to_s : nil
33
+ )
34
+ else # bulk, true
35
+ reindex_record(method_name)
36
+
37
+ index.refresh if refresh
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def reindex_record(method_name)
44
+ if record.destroyed? || !record.persisted? || !record.should_index?
45
+ begin
46
+ index.remove(record)
47
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
48
+ # do nothing
49
+ end
50
+ else
51
+ if method_name
52
+ index.update_record(record, method_name)
53
+ else
54
+ index.store(record)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,38 @@
1
+ module Searchkick
2
+ class ReindexQueue
3
+ attr_reader :name
4
+
5
+ def initialize(name)
6
+ @name = name
7
+
8
+ raise Searchkick::Error, "Searchkick.redis not set" unless Searchkick.redis
9
+ end
10
+
11
+ def push(record_id)
12
+ Searchkick.with_redis { |r| r.lpush(redis_key, record_id) }
13
+ end
14
+
15
+ # TODO use reliable queuing
16
+ def reserve(limit: 1000)
17
+ record_ids = Set.new
18
+ while record_ids.size < limit && (record_id = Searchkick.with_redis { |r| r.rpop(redis_key) })
19
+ record_ids << record_id
20
+ end
21
+ record_ids.to_a
22
+ end
23
+
24
+ def clear
25
+ Searchkick.with_redis { |r| r.del(redis_key) }
26
+ end
27
+
28
+ def length
29
+ Searchkick.with_redis { |r| r.llen(redis_key) }
30
+ end
31
+
32
+ private
33
+
34
+ def redis_key
35
+ "searchkick:reindex_queue:#{name}"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ module Searchkick
2
+ class ReindexV2Job < ActiveJob::Base
3
+ RECORD_NOT_FOUND_CLASSES = [
4
+ "ActiveRecord::RecordNotFound",
5
+ "Mongoid::Errors::DocumentNotFound",
6
+ "NoBrainer::Error::DocumentNotFound",
7
+ "Cequel::Record::RecordNotFound"
8
+ ]
9
+
10
+ queue_as { Searchkick.queue_name }
11
+
12
+ def perform(klass, id, method_name = nil)
13
+ model = klass.constantize
14
+ record =
15
+ begin
16
+ if model.respond_to?(:unscoped)
17
+ model.unscoped.find(id)
18
+ else
19
+ model.find(id)
20
+ end
21
+ rescue => e
22
+ # check by name rather than rescue directly so we don't need
23
+ # to determine which classes are defined
24
+ raise e unless RECORD_NOT_FOUND_CLASSES.include?(e.class.name)
25
+ nil
26
+ end
27
+
28
+ unless record
29
+ record = model.new
30
+ record.id = id
31
+ end
32
+
33
+ RecordIndexer.new(record).reindex(method_name, mode: true)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,235 @@
1
+ require "forwardable"
2
+
3
+ module Searchkick
4
+ class Results
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ attr_reader :klass, :response, :options
9
+
10
+ def_delegators :results, :each, :any?, :empty?, :size, :length, :slice, :[], :to_ary
11
+
12
+ def initialize(klass, response, options = {})
13
+ @klass = klass
14
+ @response = response
15
+ @options = options
16
+ end
17
+
18
+ def results
19
+ @results ||= begin
20
+ if options[:load]
21
+ # results can have different types
22
+ results = {}
23
+
24
+ hits.group_by { |hit, _| hit["_type"] }.each do |type, grouped_hits|
25
+ klass = (!options[:index_name] && @klass) || type.camelize.constantize
26
+ results[type] = results_query(klass, grouped_hits).to_a.index_by { |r| r.id.to_s }
27
+ end
28
+
29
+ # sort
30
+ hits.map do |hit|
31
+ result = results[hit["_type"]][hit["_id"].to_s]
32
+ if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
33
+ if hit["highlight"] && !result.respond_to?(:search_highlights)
34
+ highlights = Hash[hit["highlight"].map { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, v.first] }]
35
+ result.define_singleton_method(:search_highlights) do
36
+ highlights
37
+ end
38
+ end
39
+ end
40
+ result
41
+ end.compact
42
+ else
43
+ hits.map do |hit|
44
+ result =
45
+ if hit["_source"]
46
+ hit.except("_source").merge(hit["_source"])
47
+ elsif hit["fields"]
48
+ hit.except("fields").merge(hit["fields"])
49
+ else
50
+ hit
51
+ end
52
+
53
+ if hit["highlight"]
54
+ highlight = Hash[hit["highlight"].map { |k, v| [base_field(k), v.first] }]
55
+ options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
56
+ result["highlighted_#{k}"] ||= (highlight[k] || result[k])
57
+ end
58
+ end
59
+
60
+ result["id"] ||= result["_id"] # needed for legacy reasons
61
+ HashWrapper.new(result)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def suggestions
68
+ if response["suggest"]
69
+ response["suggest"].values.flat_map { |v| v.first["options"] }.sort_by { |o| -o["score"] }.map { |o| o["text"] }.uniq
70
+ elsif options[:term] == "*"
71
+ []
72
+ else
73
+ raise "Pass `suggest: true` to the search method for suggestions"
74
+ end
75
+ end
76
+
77
+ def aggregations
78
+ response["aggregations"]
79
+ end
80
+
81
+ def aggs
82
+ @aggs ||= begin
83
+ if aggregations
84
+ aggregations.dup.each do |field, filtered_agg|
85
+ buckets = filtered_agg[field]
86
+ # move the buckets one level above into the field hash
87
+ if buckets
88
+ filtered_agg.delete(field)
89
+ filtered_agg.merge!(buckets)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def took
97
+ response["took"]
98
+ end
99
+
100
+ def error
101
+ response["error"]
102
+ end
103
+
104
+ def model_name
105
+ klass.model_name
106
+ end
107
+
108
+ def entry_name(options = {})
109
+ if options.empty?
110
+ # backward compatibility
111
+ model_name.human.downcase
112
+ else
113
+ default = options[:count] == 1 ? model_name.human : model_name.human.pluralize
114
+ model_name.human(options.reverse_merge(default: default))
115
+ end
116
+ end
117
+
118
+ def total_count
119
+ response["hits"]["total"]
120
+ end
121
+ alias_method :total_entries, :total_count
122
+
123
+ def current_page
124
+ options[:page]
125
+ end
126
+
127
+ def per_page
128
+ options[:per_page]
129
+ end
130
+ alias_method :limit_value, :per_page
131
+
132
+ def padding
133
+ options[:padding]
134
+ end
135
+
136
+ def total_pages
137
+ (total_count / per_page.to_f).ceil
138
+ end
139
+ alias_method :num_pages, :total_pages
140
+
141
+ def offset_value
142
+ (current_page - 1) * per_page + padding
143
+ end
144
+ alias_method :offset, :offset_value
145
+
146
+ def previous_page
147
+ current_page > 1 ? (current_page - 1) : nil
148
+ end
149
+ alias_method :prev_page, :previous_page
150
+
151
+ def next_page
152
+ current_page < total_pages ? (current_page + 1) : nil
153
+ end
154
+
155
+ def first_page?
156
+ previous_page.nil?
157
+ end
158
+
159
+ def last_page?
160
+ next_page.nil?
161
+ end
162
+
163
+ def out_of_range?
164
+ current_page > total_pages
165
+ end
166
+
167
+ def hits
168
+ if error
169
+ raise Searchkick::Error, "Query error - use the error method to view it"
170
+ else
171
+ @response["hits"]["hits"]
172
+ end
173
+ end
174
+
175
+ def with_hit
176
+ results.zip(hits)
177
+ end
178
+
179
+ def highlights(multiple: false)
180
+ hits.map do |hit|
181
+ Hash[hit["highlight"].map { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, multiple ? v : v.first] }]
182
+ end
183
+ end
184
+
185
+ def with_highlights(multiple: false)
186
+ results.zip(highlights(multiple: multiple))
187
+ end
188
+
189
+ def misspellings?
190
+ @options[:misspellings]
191
+ end
192
+
193
+ private
194
+
195
+ def results_query(records, hits)
196
+ ids = hits.map { |hit| hit["_id"] }
197
+ if options[:includes] || options[:model_includes]
198
+ included_relations = []
199
+ combine_includes(included_relations, options[:includes])
200
+ combine_includes(included_relations, options[:model_includes][records]) if options[:model_includes]
201
+
202
+ records =
203
+ if defined?(NoBrainer::Document) && records < NoBrainer::Document
204
+ if Gem.loaded_specs["nobrainer"].version >= Gem::Version.new("0.21")
205
+ records.eager_load(included_relations)
206
+ else
207
+ records.preload(included_relations)
208
+ end
209
+ else
210
+ records.includes(included_relations)
211
+ end
212
+ end
213
+
214
+ if options[:scope_results]
215
+ records = options[:scope_results].call(records)
216
+ end
217
+
218
+ Searchkick.load_records(records, ids)
219
+ end
220
+
221
+ def combine_includes(result, inc)
222
+ if inc
223
+ if inc.is_a?(Array)
224
+ result.concat(inc)
225
+ else
226
+ result << inc
227
+ end
228
+ end
229
+ end
230
+
231
+ def base_field(k)
232
+ k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
233
+ end
234
+ end
235
+ end