elastic_record 2.0.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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