elastic_record 2.0.2 → 3.0.0

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +15 -9
  4. data/Gemfile +5 -4
  5. data/README.md +214 -0
  6. data/elastic_record.gemspec +7 -7
  7. data/lib/elastic_record.rb +1 -0
  8. data/lib/elastic_record/callbacks.rb +46 -14
  9. data/lib/elastic_record/config.rb +1 -21
  10. data/lib/elastic_record/connection.rb +24 -14
  11. data/lib/elastic_record/errors.rb +5 -0
  12. data/lib/elastic_record/index.rb +11 -1
  13. data/lib/elastic_record/index/deferred.rb +1 -0
  14. data/lib/elastic_record/index/documents.rb +95 -18
  15. data/lib/elastic_record/index/manage.rb +0 -8
  16. data/lib/elastic_record/index/mapping.rb +1 -10
  17. data/lib/elastic_record/json.rb +29 -0
  18. data/lib/elastic_record/relation.rb +9 -5
  19. data/lib/elastic_record/relation/batches.rb +4 -40
  20. data/lib/elastic_record/relation/none.rb +0 -4
  21. data/lib/elastic_record/relation/search_methods.rb +48 -38
  22. data/lib/elastic_record/relation/value_methods.rb +2 -2
  23. data/lib/elastic_record/tasks/index.rake +2 -2
  24. data/test/dummy/.env.example +1 -0
  25. data/test/dummy/.env.test +1 -0
  26. data/test/dummy/app/models/project.rb +1 -1
  27. data/test/dummy/app/models/test_model.rb +3 -2
  28. data/test/dummy/app/models/widget.rb +3 -3
  29. data/test/dummy/config/initializers/elastic_record.rb +1 -1
  30. data/test/dummy/db/migrate/20151211225259_create_projects.rb +7 -0
  31. data/test/dummy/db/schema.rb +8 -1
  32. data/test/elastic_record/callbacks_test.rb +16 -2
  33. data/test/elastic_record/config_test.rb +1 -2
  34. data/test/elastic_record/connection_test.rb +52 -9
  35. data/test/elastic_record/index/documents_test.rb +55 -21
  36. data/test/elastic_record/index/mapping_test.rb +0 -10
  37. data/test/elastic_record/integration/active_record_test.rb +3 -3
  38. data/test/elastic_record/log_subscriber_test.rb +4 -4
  39. data/test/elastic_record/relation/batches_test.rb +5 -24
  40. data/test/elastic_record/relation/delegation_test.rb +4 -3
  41. data/test/elastic_record/relation/finder_methods_test.rb +1 -0
  42. data/test/elastic_record/relation/search_methods_test.rb +47 -45
  43. data/test/elastic_record/relation_test.rb +18 -10
  44. data/test/helper.rb +4 -3
  45. metadata +21 -12
  46. data/README.rdoc +0 -146
  47. data/test/dummy/config/database.yml +0 -15
@@ -3,6 +3,11 @@ module ElasticRecord
3
3
  end
4
4
 
5
5
  class ConnectionError < Error
6
+ attr_reader :status_code
7
+ def initialize(status_code, message)
8
+ @status_code = status_code
9
+ super(message)
10
+ end
6
11
  end
7
12
 
8
13
  class BulkError < Error
@@ -40,8 +40,10 @@ module ElasticRecord
40
40
  include Deferred
41
41
 
42
42
  attr_accessor :model
43
+
43
44
  attr_accessor :disabled
44
45
  attr_accessor :has_percolator
46
+ attr_accessor :partial_updates
45
47
 
46
48
  def initialize(model)
47
49
  @model = model
@@ -53,10 +55,18 @@ module ElasticRecord
53
55
  @mapping = mapping.deep_dup
54
56
  end
55
57
 
58
+ def alias_name=(name)
59
+ @alias_name = name
60
+ end
61
+
56
62
  def alias_name
57
63
  @alias_name ||= model.base_class.name.demodulize.underscore.pluralize
58
64
  end
59
65
 
66
+ def type=(name)
67
+ @type = name
68
+ end
69
+
60
70
  def type
61
71
  @type ||= model.base_class.name.demodulize.underscore
62
72
  end
@@ -84,7 +94,7 @@ module ElasticRecord
84
94
  private
85
95
 
86
96
  def new_index_name
87
- "#{alias_name}_#{(Time.now.to_f * 100).to_i}"
97
+ "#{alias_name}_#{Time.now.utc.strftime('%Y%m%d_%H%M%S')}"
88
98
  end
89
99
 
90
100
  end
@@ -16,6 +16,7 @@ module ElasticRecord
16
16
  def initialize(index)
17
17
  self.index = index
18
18
  self.bulk_stack = []
19
+ @deferring_enabled = false
19
20
  reset!
20
21
  end
21
22
 
@@ -2,38 +2,105 @@ require 'active_support/core_ext/object/to_query'
2
2
 
3
3
  module ElasticRecord
4
4
  class Index
5
- module Documents
6
- def index_record(record, index_name = nil)
7
- return if disabled
5
+ class ScanSearch
6
+ attr_reader :scroll_id
7
+ attr_accessor :total_hits
8
+
9
+ def initialize(elastic_index, scroll_id, options = {})
10
+ @elastic_index = elastic_index
11
+ @scroll_id = scroll_id
12
+ @options = options
13
+ end
8
14
 
9
- index_document(record.send(record.class.primary_key), record.as_search, index_name)
15
+ def each_slice(&block)
16
+ while (hit_ids = request_more_ids).any?
17
+ hit_ids.each_slice(requested_batch_size, &block)
18
+ end
10
19
  end
11
20
 
12
- def index_document(id, document, index_name = nil)
13
- return if disabled
21
+ def request_more_ids
22
+ json = @elastic_index.scroll(@scroll_id, keep_alive)
23
+ json['hits']['hits'].map { |hit| hit['_id'] }
24
+ end
14
25
 
15
- index_name ||= alias_name
26
+ def keep_alive
27
+ @options[:keep_alive] || (raise "Must provide a :keep_alive option")
28
+ end
16
29
 
30
+ def requested_batch_size
31
+ @options[:batch_size]
32
+ end
33
+ end
34
+
35
+ module Documents
36
+ def index_record(record, index_name: alias_name)
37
+ unless disabled
38
+ index_document(record.send(record.class.primary_key), record.as_search, index_name: index_name)
39
+ end
40
+ end
41
+
42
+ def update_record(record, index_name: alias_name)
43
+ unless disabled
44
+ update_document(record.send(record.class.primary_key), record.as_partial_update_document, index_name: index_name)
45
+ end
46
+ end
47
+
48
+ def index_document(id, document, parent: nil, index_name: alias_name)
17
49
  if batch = current_bulk_batch
18
- batch << { index: { _index: index_name, _type: type, _id: id } }
50
+ instructions = { _index: index_name, _type: type, _id: id }
51
+ instructions[:parent] = parent if parent
52
+
53
+ batch << { index: instructions }
19
54
  batch << document
20
55
  else
21
- connection.json_put "/#{index_name}/#{type}/#{id}", document
56
+ path = "/#{index_name}/#{type}/#{id}"
57
+ path << "?parent=#{parent}" if parent
58
+
59
+ connection.json_put path, document
22
60
  end
23
61
  end
24
62
 
25
- def delete_document(id, index_name = nil)
63
+ def update_document(id, document, parent: nil, index_name: alias_name)
64
+ params = {doc: document, doc_as_upsert: true}
65
+
66
+ if batch = current_bulk_batch
67
+ instructions = { _index: index_name, _type: type, _id: id, _retry_on_conflict: 3 }
68
+ instructions[:parent] = parent if parent
69
+
70
+ batch << { update: instructions }
71
+ batch << params
72
+ else
73
+ path = "/#{index_name}/#{type}/#{id}/_update?retry_on_conflict=3"
74
+ path << "&parent=#{parent}" if parent
75
+
76
+ connection.json_post path, params
77
+ end
78
+ end
79
+
80
+ def delete_document(id, parent: nil, index_name: alias_name)
81
+ raise "Cannot delete document with empty id" if id.blank?
26
82
  index_name ||= alias_name
27
83
 
28
84
  if batch = current_bulk_batch
29
- batch << { delete: { _index: index_name, _type: type, _id: id } }
85
+ instructions = { _index: index_name, _type: type, _id: id, _retry_on_conflict: 3 }
86
+ instructions[:parent] = parent if parent
87
+ batch << { delete: instructions }
30
88
  else
31
- connection.json_delete "/#{index_name}/#{type}/#{id}"
89
+ path = "/#{index_name}/#{type}/#{id}"
90
+ path << "&parent=#{parent}" if parent
91
+
92
+ connection.json_delete path
32
93
  end
33
94
  end
34
95
 
35
96
  def delete_by_query(query)
36
- connection.json_delete "/#{alias_name}/#{type}/_query", query
97
+ scan_search = create_scan_search query
98
+
99
+ scan_search.each_slice do |ids|
100
+ bulk do
101
+ ids.each { |id| delete_document(id) }
102
+ end
103
+ end
37
104
  end
38
105
 
39
106
  def record_exists?(id)
@@ -53,6 +120,18 @@ module ElasticRecord
53
120
  get "_explain", elastic_query
54
121
  end
55
122
 
123
+ def create_scan_search(elastic_query, options = {})
124
+ options[:batch_size] ||= 100
125
+ options[:keep_alive] ||= ElasticRecord::Config.scroll_keep_alive
126
+
127
+ search_options = {search_type: 'scan', size: options[:batch_size], scroll: options[:keep_alive]}
128
+ json = search(elastic_query, search_options)
129
+
130
+ ScanSearch.new(self, json['_scroll_id'], options).tap do |scan_search|
131
+ scan_search.total_hits = json['hits']['total']
132
+ end
133
+ end
134
+
56
135
  def scroll(scroll_id, scroll_keep_alive)
57
136
  options = {scroll_id: scroll_id, scroll: scroll_keep_alive}
58
137
  connection.json_get("/_search/scroll?#{options.to_query}")
@@ -64,7 +143,7 @@ module ElasticRecord
64
143
  yield
65
144
 
66
145
  if current_bulk_batch.any?
67
- body = current_bulk_batch.map { |action| "#{ActiveSupport::JSON.encode(action)}\n" }.join
146
+ body = current_bulk_batch.map { |action| "#{ElasticRecord::JSON.encode(action)}\n" }.join
68
147
  results = connection.json_post("/_bulk?#{options.to_query}", body)
69
148
  verify_bulk_results(results)
70
149
  end
@@ -72,12 +151,10 @@ module ElasticRecord
72
151
  connection.bulk_stack.pop
73
152
  end
74
153
 
75
- def bulk_add(batch, index_name = nil)
76
- index_name ||= alias_name
77
-
154
+ def bulk_add(batch, index_name: alias_name)
78
155
  bulk do
79
156
  batch.each do |record|
80
- index_record(record, index_name)
157
+ index_record(record, index_name: index_name)
81
158
  end
82
159
  end
83
160
  end
@@ -23,14 +23,6 @@ module ElasticRecord
23
23
  end
24
24
  end
25
25
 
26
- def open(index_name)
27
- connection.json_post("/#{index_name}/_open")
28
- end
29
-
30
- def close(index_name)
31
- connection.json_post("/#{index_name}/_close")
32
- end
33
-
34
26
  def exists?(index_name)
35
27
  connection.head("/#{index_name}") == '200'
36
28
  end
@@ -16,22 +16,13 @@ module ElasticRecord
16
16
  end
17
17
  end
18
18
 
19
- def delete_mapping(index_name = alias_name)
20
- connection.json_delete "/#{index_name}/#{type}"
21
- end
22
-
23
19
  def mapping
24
20
  @mapping ||= {
25
- _source: {
26
- enabled: false
21
+ properties: {
27
22
  },
28
23
  _all: {
29
24
  enabled: false
30
25
  },
31
- properties: {
32
- created_at: {type: "date", index: "not_analyzed", format: "dateOptionalTime"},
33
- updated_at: {type: "date", index: "not_analyzed", format: "dateOptionalTime"}
34
- },
35
26
  dynamic_templates: [
36
27
  {
37
28
  no_string_analyzing: {
@@ -0,0 +1,29 @@
1
+ module ElasticRecord
2
+ module JSON
3
+ class << self
4
+ def parser
5
+ @@parser ||= :active_support
6
+ end
7
+
8
+ def parser=(value)
9
+ @@parser = value
10
+ end
11
+
12
+ def decode(json)
13
+ if ElasticRecord::JSON.parser == :oj
14
+ Oj.compat_load(json)
15
+ else
16
+ ActiveSupport::JSON.decode(json)
17
+ end
18
+ end
19
+
20
+ def encode(data)
21
+ if ElasticRecord::JSON.parser == :oj
22
+ Oj.dump(data, mode: :compat)
23
+ else
24
+ ActiveSupport::JSON.encode(data)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -22,10 +22,6 @@ module ElasticRecord
22
22
  search_results['hits']['total']
23
23
  end
24
24
 
25
- def facets
26
- search_results['facets']
27
- end
28
-
29
25
  def aggregations
30
26
  search_results['aggregations']
31
27
  end
@@ -47,6 +43,11 @@ module ElasticRecord
47
43
  search_hits.map { |hit| hit['_id'] }
48
44
  end
49
45
 
46
+ def delete_all
47
+ find_ids_in_batches { |ids| klass.delete(ids) }
48
+ klass.elastic_index.delete_by_query(as_elastic)
49
+ end
50
+
50
51
  def ==(other)
51
52
  to_a == other
52
53
  end
@@ -72,7 +73,10 @@ module ElasticRecord
72
73
  end
73
74
 
74
75
  def search_results
75
- @search_results ||= klass.elastic_index.search(as_elastic)
76
+ @search_results ||= begin
77
+ options = search_type_value ? {search_type: search_type_value} : {}
78
+ klass.elastic_index.search(as_elastic, options.update(fields: ''))
79
+ end
76
80
  end
77
81
 
78
82
  def load_hits(ids)
@@ -1,29 +1,5 @@
1
1
  module ElasticRecord
2
2
  class Relation
3
- class ScanSearch
4
- attr_reader :scroll_id
5
- attr_accessor :total_hits
6
-
7
- def initialize(model, scroll_id, options = {})
8
- @model = model
9
- @scroll_id = scroll_id
10
- @options = options
11
- end
12
-
13
- def request_more_ids
14
- json = @model.elastic_index.scroll(@scroll_id, keep_alive)
15
- json['hits']['hits'].map { |hit| hit['_id'] }
16
- end
17
-
18
- def keep_alive
19
- @options[:keep_alive] || (raise "Must provide a :keep_alive option")
20
- end
21
-
22
- def requested_batch_size
23
- @options[:batch_size]
24
- end
25
- end
26
-
27
3
  module Batches
28
4
  def find_each(options = {})
29
5
  find_in_batches(options) do |records|
@@ -38,11 +14,11 @@ module ElasticRecord
38
14
  end
39
15
 
40
16
  def find_ids_in_batches(options = {}, &block)
41
- scan_search = create_scan_search(options)
17
+ create_scan_search(options).each_slice(&block)
18
+ end
42
19
 
43
- while (hit_ids = scan_search.request_more_ids).any?
44
- hit_ids.each_slice(scan_search.requested_batch_size, &block)
45
- end
20
+ def create_scan_search(options)
21
+ elastic_index.create_scan_search(as_elastic, options)
46
22
  end
47
23
 
48
24
  def reindex
@@ -50,18 +26,6 @@ module ElasticRecord
50
26
  elastic_index.bulk_add(batch)
51
27
  end
52
28
  end
53
-
54
- def create_scan_search(options = {})
55
- options[:batch_size] ||= 100
56
- options[:keep_alive] ||= ElasticRecord::Config.scroll_keep_alive
57
-
58
- search_options = {search_type: 'scan', size: options[:batch_size], scroll: options[:keep_alive]}
59
- json = klass.elastic_index.search(as_elastic, search_options)
60
-
61
- ElasticRecord::Relation::ScanSearch.new(klass, json['_scroll_id'], options).tap do |scan_search|
62
- scan_search.total_hits = json['hits']['total']
63
- end
64
- end
65
29
  end
66
30
  end
67
31
  end
@@ -9,10 +9,6 @@ module ElasticRecord
9
9
  0
10
10
  end
11
11
 
12
- def facets
13
- {}
14
- end
15
-
16
12
  def aggregations
17
13
  {}
18
14
  end
@@ -1,6 +1,33 @@
1
1
  module ElasticRecord
2
2
  class Relation
3
3
  module SearchMethods
4
+ class FilterChain
5
+ def initialize(scope)
6
+ @scope = scope
7
+ end
8
+
9
+ def not(*filters)
10
+ add_filter_nodes_to_scope(filters) do |filter_node|
11
+ Arelastic::Filters::Not.new(filter_node)
12
+ end
13
+ end
14
+
15
+ def nested(path, *filters)
16
+ add_filter_nodes_to_scope(filters) do |filter_node|
17
+ Arelastic::Filters::Nested.new(path, filter_node)
18
+ end
19
+ end
20
+
21
+ def add_filter_nodes_to_scope(filters)
22
+ filter_value = @scope.send(:build_filter_nodes, filters).map do |filter_node|
23
+ yield filter_node
24
+ end
25
+
26
+ @scope.filter_values += filter_value
27
+ @scope
28
+ end
29
+ end
30
+
4
31
  Relation::MULTI_VALUE_METHODS.each do |name|
5
32
  define_method "#{name}_values" do
6
33
  @values[name] || []
@@ -35,8 +62,12 @@ module ElasticRecord
35
62
  self
36
63
  end
37
64
 
38
- def filter(*args)
39
- clone.filter!(*args)
65
+ def filter(opts = :chain, *rest)
66
+ if opts == :chain
67
+ FilterChain.new(clone)
68
+ else
69
+ clone.filter!(opts, *rest)
70
+ end
40
71
  end
41
72
 
42
73
  def find_by(*args)
@@ -78,18 +109,13 @@ module ElasticRecord
78
109
  end
79
110
  end
80
111
 
81
- def facet!(name_or_facet, options = {})
82
- if name_or_facet.is_a?(String)
83
- self.facet_values += [arelastic.facet[name_or_facet].terms(name_or_facet, options)]
84
- else
85
- self.facet_values += [name_or_facet]
86
- end
87
-
112
+ def search_type!(type)
113
+ self.search_type_value = type
88
114
  self
89
115
  end
90
116
 
91
- def facet(facet_or_name, options = {})
92
- clone.facet! facet_or_name, options
117
+ def search_type(type)
118
+ clone.search_type! type
93
119
  end
94
120
 
95
121
  def aggregate!(aggregation)
@@ -149,7 +175,6 @@ module ElasticRecord
149
175
  build_query_and_filter(query_value, filter_values),
150
176
  build_limit(limit_value),
151
177
  build_offset(offset_value),
152
- build_facets(facet_values),
153
178
  build_aggregations(aggregation_values),
154
179
  build_orders(order_values)
155
180
  ].compact
@@ -178,9 +203,17 @@ module ElasticRecord
178
203
  end
179
204
 
180
205
  def build_filter(filters)
181
- nodes = []
206
+ nodes = build_filter_nodes(filters)
182
207
 
183
- filters.map do |filter|
208
+ if nodes.size == 1
209
+ nodes.first
210
+ elsif nodes.size > 1
211
+ Arelastic::Filters::And.new(nodes)
212
+ end
213
+ end
214
+
215
+ def build_filter_nodes(filters)
216
+ filters.each_with_object([]) do |filter, nodes|
184
217
  if filter.is_a?(Arelastic::Filters::Filter)
185
218
  nodes << filter
186
219
  elsif filter.is_a?(ElasticRecord::Relation)
@@ -200,27 +233,8 @@ module ElasticRecord
200
233
  end
201
234
  end
202
235
  end
203
-
204
- if nodes.size == 1
205
- nodes.first
206
- elsif nodes.size > 1
207
- Arelastic::Filters::And.new(nodes)
208
- end
209
236
  end
210
237
 
211
- # def normalize_value(value)
212
- # case value
213
- # when true
214
- # "T"
215
- # when false
216
- # "F"
217
- # when Time
218
- # value.iso8601
219
- # else
220
- # value
221
- # end
222
- # end
223
-
224
238
  def build_limit(limit)
225
239
  if limit
226
240
  Arelastic::Searches::Size.new(limit)
@@ -237,10 +251,6 @@ module ElasticRecord
237
251
  Arelastic::Searches::Aggregations.new(aggregations) unless aggregations.empty?
238
252
  end
239
253
 
240
- def build_facets(facets)
241
- Arelastic::Searches::Facets.new(facets) unless facets.empty?
242
- end
243
-
244
254
  def build_orders(orders)
245
255
  return if orders.empty?
246
256
 
@@ -248,7 +258,7 @@ module ElasticRecord
248
258
  if order.is_a?(Arelastic::Sorts::Sort)
249
259
  order
250
260
  else
251
- Arelastic::Sorts::Sort.new(order)
261
+ Arelastic::Sorts::Field.new(order)
252
262
  end
253
263
  end
254
264