elastic_record 4.1.8 → 5.1.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 (72) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +15 -8
  3. data/Gemfile +1 -1
  4. data/README.md +29 -21
  5. data/elastic_record.gemspec +1 -1
  6. data/lib/elastic_record.rb +25 -13
  7. data/lib/elastic_record/aggregation_response/aggregation.rb +19 -0
  8. data/lib/elastic_record/aggregation_response/bucket.rb +24 -0
  9. data/lib/elastic_record/aggregation_response/builder.rb +55 -0
  10. data/lib/elastic_record/aggregation_response/has_aggregations.rb +9 -0
  11. data/lib/elastic_record/aggregation_response/multi_bucket_aggregation.rb +9 -0
  12. data/lib/elastic_record/aggregation_response/multi_value_aggregation.rb +6 -0
  13. data/lib/elastic_record/aggregation_response/single_bucket_aggregation.rb +8 -0
  14. data/lib/elastic_record/aggregation_response/single_value_aggregation.rb +7 -0
  15. data/lib/elastic_record/as_document.rb +33 -16
  16. data/lib/elastic_record/callbacks.rb +1 -1
  17. data/lib/elastic_record/config.rb +3 -0
  18. data/lib/elastic_record/connection.rb +4 -4
  19. data/lib/elastic_record/errors.rb +7 -1
  20. data/lib/elastic_record/index.rb +6 -8
  21. data/lib/elastic_record/index/analyze.rb +10 -0
  22. data/lib/elastic_record/index/deferred.rb +2 -2
  23. data/lib/elastic_record/index/documents.rb +42 -29
  24. data/lib/elastic_record/index/manage.rb +6 -8
  25. data/lib/elastic_record/index/mapping.rb +16 -8
  26. data/lib/elastic_record/index/mapping_type.rb +16 -0
  27. data/lib/elastic_record/index/mapping_type_test.rb +18 -0
  28. data/lib/elastic_record/index/settings.rb +6 -17
  29. data/lib/elastic_record/model.rb +2 -14
  30. data/lib/elastic_record/percolator_model.rb +11 -19
  31. data/lib/elastic_record/relation.rb +9 -30
  32. data/lib/elastic_record/relation/batches.rb +5 -3
  33. data/lib/elastic_record/relation/delegation.rb +8 -4
  34. data/lib/elastic_record/relation/finder_methods.rb +8 -0
  35. data/lib/elastic_record/relation/hits.rb +34 -0
  36. data/lib/elastic_record/relation/search_methods.rb +17 -22
  37. data/lib/elastic_record/relation/value_methods.rb +1 -1
  38. data/test/dummy/app/models/project.rb +16 -4
  39. data/test/dummy/app/models/warehouse.rb +5 -16
  40. data/test/dummy/app/models/widget.rb +23 -12
  41. data/test/dummy/app/models/widget_query.rb +1 -0
  42. data/test/dummy/db/migrate/20151211225259_create_warehouses.rb +7 -0
  43. data/test/dummy/db/migrate/20180215140125_create_widgets.rb +11 -0
  44. data/test/dummy/db/schema.rb +10 -3
  45. data/test/elastic_record/aggregation_response/bucket_test.rb +8 -0
  46. data/test/elastic_record/aggregation_response/multi_bucket_aggregation_test.rb +33 -0
  47. data/test/elastic_record/aggregation_response/single_bucket_aggregation_test.rb +15 -0
  48. data/test/elastic_record/as_document_test.rb +55 -29
  49. data/test/elastic_record/callbacks_test.rb +7 -11
  50. data/test/elastic_record/connection_test.rb +3 -16
  51. data/test/elastic_record/index/documents_test.rb +56 -11
  52. data/test/elastic_record/index/manage_test.rb +0 -7
  53. data/test/elastic_record/index/mapping_test.rb +18 -5
  54. data/test/elastic_record/index/settings_test.rb +1 -7
  55. data/test/elastic_record/index_test.rb +0 -4
  56. data/test/elastic_record/integration/active_record_test.rb +6 -9
  57. data/test/elastic_record/model_test.rb +5 -2
  58. data/test/elastic_record/percolator_model_test.rb +25 -11
  59. data/test/elastic_record/relation/batches_test.rb +29 -47
  60. data/test/elastic_record/relation/delegation_test.rb +1 -1
  61. data/test/elastic_record/relation/finder_methods_test.rb +17 -31
  62. data/test/elastic_record/relation/hits_test.rb +49 -0
  63. data/test/elastic_record/relation/search_methods_test.rb +20 -16
  64. data/test/elastic_record/relation_test.rb +19 -50
  65. data/test/helper.rb +7 -3
  66. metadata +20 -9
  67. data/lib/elastic_record/doctype.rb +0 -43
  68. data/lib/elastic_record/json.rb +0 -29
  69. data/lib/elastic_record/name_cache.rb +0 -23
  70. data/test/dummy/db/migrate/20151211225259_create_projects.rb +0 -7
  71. data/test/elastic_record/doctype_test.rb +0 -45
  72. data/test/elastic_record/name_cache_test.rb +0 -16
@@ -5,7 +5,7 @@ module ElasticRecord
5
5
  attr_accessor :servers, :options
6
6
  attr_accessor :request_count, :current_server
7
7
  attr_accessor :max_request_count
8
- attr_accessor :bulk_stack
8
+ attr_accessor :bulk_actions
9
9
  def initialize(servers, options = {})
10
10
  self.servers = Array(servers)
11
11
 
@@ -14,7 +14,7 @@ module ElasticRecord
14
14
  self.request_count = 0
15
15
  self.max_request_count = 100
16
16
  self.options = options.symbolize_keys
17
- self.bulk_stack = []
17
+ self.bulk_actions = nil
18
18
  end
19
19
 
20
20
  def head(path)
@@ -38,10 +38,10 @@ module ElasticRecord
38
38
  end
39
39
 
40
40
  def json_request(method, path, json)
41
- body = json.is_a?(Hash) ? ElasticRecord::JSON.encode(json) : json
41
+ body = json.is_a?(Hash) ? JSON.generate(json) : json
42
42
  response = http_request_with_retry(method, path, body)
43
43
 
44
- json = ElasticRecord::JSON.decode(response.body)
44
+ json = JSON.parse(response.body)
45
45
  raise ConnectionError.new(response.code, json['error']) if json['error']
46
46
 
47
47
  json
@@ -12,4 +12,10 @@ module ElasticRecord
12
12
 
13
13
  class BulkError < Error
14
14
  end
15
- end
15
+
16
+ class ExpiredScrollError < Error
17
+ end
18
+
19
+ class InvalidScrollError < Error
20
+ end
21
+ end
@@ -4,6 +4,7 @@ require 'elastic_record/index/documents'
4
4
  require 'elastic_record/index/manage'
5
5
  require 'elastic_record/index/mapping'
6
6
  require 'elastic_record/index/settings'
7
+ require 'elastic_record/index/mapping_type'
7
8
 
8
9
  require 'active_support/core_ext/object/deep_dup'
9
10
 
@@ -31,18 +32,15 @@ module ElasticRecord
31
32
  include Mapping, Settings
32
33
  include Analyze
33
34
  include Deferred
34
-
35
- attr_accessor :doctypes
35
+ include MappingType
36
36
 
37
37
  attr_accessor :disabled
38
38
  attr_accessor :model
39
39
  attr_accessor :partial_updates
40
40
  attr_accessor :load_from_source
41
41
 
42
- def initialize(models)
43
- models = Array.wrap(models)
44
- @model = models.first
45
- @doctypes = models.map(&:doctype)
42
+ def initialize(model)
43
+ @model = model
46
44
  @disabled = false
47
45
  @load_from_source = false
48
46
  end
@@ -78,9 +76,9 @@ module ElasticRecord
78
76
  model.elastic_connection
79
77
  end
80
78
 
81
- def get(end_path, doctype = nil, json = nil)
79
+ def get(end_path, json = nil)
82
80
  path = "/#{alias_name}"
83
- path += "/#{doctype.name}" if doctype
81
+ path += "/#{mapping_type}"
84
82
  path += "/#{end_path}"
85
83
 
86
84
  connection.json_get path, json
@@ -1,6 +1,16 @@
1
1
  module ElasticRecord
2
2
  class Index
3
3
  module Analyze
4
+ attr_accessor :analysis
5
+
6
+ def analysis
7
+ @analysis ||= {}
8
+ end
9
+
10
+ def analysis=(custom_analysis)
11
+ analysis.deep_merge!(custom_analysis)
12
+ end
13
+
4
14
  def analyze(params)
5
15
  json = connection.json_get "/#{alias_name}/_analyze", params
6
16
  json['tokens'].map { |token_hash| token_hash['token'] }
@@ -11,11 +11,11 @@ module ElasticRecord
11
11
  attr_accessor :index
12
12
  attr_accessor :deferred_actions
13
13
  attr_accessor :writes_made
14
- attr_accessor :bulk_stack
14
+ attr_accessor :bulk_actions
15
15
 
16
16
  def initialize(index)
17
17
  @index = index
18
- @bulk_stack = []
18
+ @bulk_actions = nil
19
19
  reset!
20
20
  end
21
21
 
@@ -13,8 +13,8 @@ module ElasticRecord
13
13
  end
14
14
 
15
15
  def each_slice(&block)
16
- while (hit_ids = request_more_ids).any?
17
- hit_ids.each_slice(batch_size, &block)
16
+ while (hits = request_more_hits).any?
17
+ hits.each_slice(batch_size, &block)
18
18
  end
19
19
  end
20
20
 
@@ -45,6 +45,7 @@ module ElasticRecord
45
45
  @initial_search_response ||= begin
46
46
  search_options = {size: batch_size, scroll: keep_alive}
47
47
  elastic_query = @search.reverse_merge('sort' => '_doc')
48
+
48
49
  @elastic_index.search(elastic_query, search_options)
49
50
  end
50
51
  end
@@ -56,7 +57,6 @@ module ElasticRecord
56
57
  index_document(
57
58
  record.try(:id),
58
59
  record.as_search_document,
59
- doctype: record.doctype,
60
60
  index_name: index_name
61
61
  )
62
62
  end
@@ -67,21 +67,20 @@ module ElasticRecord
67
67
  update_document(
68
68
  record.id,
69
69
  record.as_partial_update_document,
70
- doctype: record.doctype,
71
70
  index_name: index_name
72
71
  )
73
72
  end
74
73
  end
75
74
 
76
- def index_document(id, document, doctype: model.doctype, parent: nil, index_name: alias_name)
75
+ def index_document(id, document, parent: nil, index_name: alias_name)
77
76
  if batch = current_bulk_batch
78
- instructions = { _index: index_name, _type: doctype.name, _id: id }
77
+ instructions = { _index: index_name, _type: mapping_type, _id: id }
79
78
  instructions[:parent] = parent if parent
80
79
 
81
80
  batch << { index: instructions }
82
81
  batch << document
83
82
  else
84
- path = "/#{index_name}/#{doctype.name}/#{id}"
83
+ path = "/#{index_name}/#{mapping_type}/#{id}"
85
84
  path << "?parent=#{parent}" if parent
86
85
 
87
86
  if id
@@ -92,34 +91,34 @@ module ElasticRecord
92
91
  end
93
92
  end
94
93
 
95
- def update_document(id, document, doctype: model.doctype, parent: nil, index_name: alias_name)
94
+ def update_document(id, document, parent: nil, index_name: alias_name)
96
95
  raise "Cannot update a document with empty id" if id.blank?
97
96
  params = {doc: document, doc_as_upsert: true}
98
97
 
99
98
  if batch = current_bulk_batch
100
- instructions = { _index: index_name, _type: doctype.name, _id: id, _retry_on_conflict: 3 }
99
+ instructions = { _index: index_name, _type: mapping_type, _id: id, retry_on_conflict: 3 }
101
100
  instructions[:parent] = parent if parent
102
101
 
103
102
  batch << { update: instructions }
104
103
  batch << params
105
104
  else
106
- path = "/#{index_name}/#{doctype.name}/#{id}/_update?retry_on_conflict=3"
105
+ path = "/#{index_name}/#{mapping_type}/#{id}/_update?retry_on_conflict=3"
107
106
  path << "&parent=#{parent}" if parent
108
107
 
109
108
  connection.json_post path, params
110
109
  end
111
110
  end
112
111
 
113
- def delete_document(id, doctype: model.doctype, parent: nil, index_name: alias_name)
112
+ def delete_document(id, parent: nil, index_name: alias_name)
114
113
  raise "Cannot delete document with empty id" if id.blank?
115
114
  index_name ||= alias_name
116
115
 
117
116
  if batch = current_bulk_batch
118
- instructions = { _index: index_name, _type: doctype.name, _id: id, _retry_on_conflict: 3 }
117
+ instructions = { _index: index_name, _type: mapping_type, _id: id, retry_on_conflict: 3 }
119
118
  instructions[:parent] = parent if parent
120
119
  batch << { delete: instructions }
121
120
  else
122
- path = "/#{index_name}/#{doctype.name}/#{id}"
121
+ path = "/#{index_name}/#{mapping_type}/#{id}"
123
122
  path << "&parent=#{parent}" if parent
124
123
 
125
124
  connection.json_delete path
@@ -129,15 +128,15 @@ module ElasticRecord
129
128
  def delete_by_query(query)
130
129
  scroll_enumerator = build_scroll_enumerator search: query
131
130
 
132
- scroll_enumerator.each_slice do |ids|
131
+ scroll_enumerator.each_slice do |hits|
133
132
  bulk do
134
- ids.each { |id| delete_document(id) }
133
+ hits.each { |hit| delete_document hit['_id'] }
135
134
  end
136
135
  end
137
136
  end
138
137
 
139
138
  def record_exists?(id)
140
- get(id, model.doctype)['found']
139
+ get(id)['found']
141
140
  end
142
141
 
143
142
  def search(elastic_query, options = {})
@@ -146,7 +145,7 @@ module ElasticRecord
146
145
  url += "?#{options.to_query}"
147
146
  end
148
147
 
149
- get url, model.doctype, elastic_query
148
+ get url, elastic_query.update('_source' => load_from_source)
150
149
  end
151
150
 
152
151
  def explain(id, elastic_query)
@@ -160,20 +159,20 @@ module ElasticRecord
160
159
  def scroll(scroll_id, scroll_keep_alive)
161
160
  options = {scroll_id: scroll_id, scroll: scroll_keep_alive}
162
161
  connection.json_get("/_search/scroll?#{options.to_query}")
162
+ rescue ElasticRecord::ConnectionError => e
163
+ case e.status_code
164
+ when '400' then raise ElasticRecord::InvalidScrollError, e.message
165
+ when '404' then raise ElasticRecord::ExpiredScrollError, e.message
166
+ else raise e
167
+ end
163
168
  end
164
169
 
165
- def bulk(options = {})
166
- connection.bulk_stack.push []
167
-
168
- yield
169
-
170
- if current_bulk_batch.any?
171
- body = current_bulk_batch.map { |action| "#{ElasticRecord::JSON.encode(action)}\n" }.join
172
- results = connection.json_post("/_bulk?#{options.to_query}", body)
173
- verify_bulk_results(results)
170
+ def bulk(options = {}, &block)
171
+ if current_bulk_batch
172
+ yield
173
+ else
174
+ start_new_bulk_batch(options, &block)
174
175
  end
175
- ensure
176
- connection.bulk_stack.pop
177
176
  end
178
177
 
179
178
  def bulk_add(batch, index_name: alias_name)
@@ -185,11 +184,25 @@ module ElasticRecord
185
184
  end
186
185
 
187
186
  def current_bulk_batch
188
- connection.bulk_stack.last
187
+ connection.bulk_actions
189
188
  end
190
189
 
191
190
  private
192
191
 
192
+ def start_new_bulk_batch(options, &block)
193
+ connection.bulk_actions = []
194
+
195
+ yield
196
+
197
+ if current_bulk_batch.any?
198
+ body = current_bulk_batch.map { |action| "#{JSON.generate(action)}\n" }.join
199
+ results = connection.json_post("/_bulk?#{options.to_query}", body)
200
+ verify_bulk_results(results)
201
+ end
202
+ ensure
203
+ connection.bulk_actions = nil
204
+ end
205
+
193
206
  def verify_bulk_results(results)
194
207
  return unless results.is_a?(Hash)
195
208
 
@@ -7,10 +7,12 @@ module ElasticRecord
7
7
  index_name
8
8
  end
9
9
 
10
- def create(index_name = new_index_name)
10
+ def create(index_name = new_index_name, setting_overrides: {})
11
11
  connection.json_put "/#{index_name}", {
12
- "mappings" => mapping_body,
13
- "settings" => settings
12
+ "mappings" => {
13
+ mapping_type => mapping
14
+ },
15
+ "settings" => setting_overrides.merge(settings)
14
16
  }
15
17
  index_name
16
18
  end
@@ -29,10 +31,6 @@ module ElasticRecord
29
31
  connection.head("/#{index_name}") == '200'
30
32
  end
31
33
 
32
- def type_exists?(index_name = alias_name, type = model.doctype.name)
33
- connection.head("/#{index_name}/_mapping/#{type}") == '200'
34
- end
35
-
36
34
  def deploy(index_name)
37
35
  actions = [
38
36
  {
@@ -76,7 +74,7 @@ module ElasticRecord
76
74
  end
77
75
 
78
76
  def all_names
79
- connection.json_get("/#{alias_name}/_mapping/#{model.doctype.name}/").keys
77
+ connection.json_get("/#{alias_name}/_mapping").keys
80
78
  rescue
81
79
  # TODO: In ES 1.4, this returns empty rather than a 404
82
80
  []
@@ -1,23 +1,31 @@
1
1
  module ElasticRecord
2
2
  class Index
3
3
  module Mapping
4
+ attr_accessor :mapping
5
+
6
+ DEFAULT_MAPPING = {
7
+ properties: {
8
+ }
9
+ }
10
+ def mapping
11
+ @mapping ||= DEFAULT_MAPPING.deep_dup
12
+ end
13
+
14
+ def mapping=(custom_mapping)
15
+ mapping.deep_merge!(custom_mapping.deep_dup)
16
+ end
17
+
4
18
  def update_mapping(index_name = alias_name)
5
- connection.json_put "/#{index_name}/_mapping", mapping_body
19
+ connection.json_put "/#{index_name}/_mapping/#{mapping_type}", mapping
6
20
  end
7
21
 
8
22
  def get_mapping(index_name = alias_name)
9
- json = connection.json_get "/#{index_name}/_mapping"
23
+ json = connection.json_get "/#{index_name}/_mapping/#{mapping_type}"
10
24
 
11
25
  unless json.empty?
12
26
  json.values.first['mappings']
13
27
  end
14
28
  end
15
-
16
- def mapping_body
17
- doctypes.each_with_object({}) do |doctype, result|
18
- result[doctype.name] = doctype.mapping
19
- end
20
- end
21
29
  end
22
30
  end
23
31
  end
@@ -0,0 +1,16 @@
1
+ module ElasticRecord
2
+ class Index
3
+ # This module facilitates the removal of multiple mapping types from ElasticSearch.
4
+ # See https://www.elastic.co/guide/en/elasticsearch/reference/6.x/removal-of-types.html
5
+ #
6
+ # * 6.x - Type defaults to _doc, but any type can be specified
7
+ # * 7.x - Only _doc will be supported, effectively removing the type concept.
8
+ module MappingType
9
+ attr_accessor :mapping_type
10
+
11
+ def mapping_type
12
+ @mapping_type || '_doc'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ require 'helper'
2
+
3
+ class ElasticRecord::Index::MappingTypeTest < MiniTest::Test
4
+ def test_default
5
+ assert_equal '_doc', index.mapping_type
6
+ end
7
+
8
+ def test_writer
9
+ index.mapping_type = 'widget'
10
+ assert_equal 'widget', index.mapping_type
11
+ end
12
+
13
+ private
14
+
15
+ def index
16
+ @index ||= ElasticRecord::Index.new(Widget)
17
+ end
18
+ end
@@ -6,27 +6,16 @@ module ElasticRecord
6
6
  end
7
7
 
8
8
  def settings
9
- @settings ||=
10
- begin
11
- result = {}
12
-
13
- if (analysis = analysis_body).any?
14
- result['analysis'] = analysis
15
- end
16
-
17
- result
18
- end
9
+ @settings ||= begin
10
+ result = ElasticRecord::Config.default_index_settings.deep_dup
11
+ result['analysis'] = analysis if analysis.any?
12
+ result
13
+ end
19
14
  end
20
15
 
21
- def update_settings(index_name = alias_name)
16
+ def update_settings(index_name = alias_name, settings: self.settings)
22
17
  connection.json_put "/#{index_name}/_settings", settings
23
18
  end
24
-
25
- def analysis_body
26
- doctypes.each_with_object({}) do |doctype, result|
27
- result.deep_merge!(doctype.analysis)
28
- end
29
- end
30
19
  end
31
20
  end
32
21
  end
@@ -6,7 +6,6 @@ module ElasticRecord
6
6
  extend ClassMethods
7
7
  include Callbacks
8
8
  include AsDocument
9
- include NameCache
10
9
 
11
10
  class_attribute :elastic_connection
12
11
  self.elastic_connection = ElasticRecord::Connection.new(ElasticRecord::Config.servers, ElasticRecord::Config.connection_options)
@@ -19,7 +18,8 @@ module ElasticRecord
19
18
 
20
19
  if child < child.base_class
21
20
  child.elastic_index = elastic_index.dup
22
- child.doctype = doctype.dup
21
+ child.elastic_index.model = child
22
+ child.elastic_index.mapping_type = elastic_index.mapping_type
23
23
  end
24
24
  end
25
25
 
@@ -27,14 +27,6 @@ module ElasticRecord
27
27
  Arelastic::Builders::Search
28
28
  end
29
29
 
30
- def doctype
31
- @doctype ||= Doctype.new(base_class.name.demodulize.underscore)
32
- end
33
-
34
- def doctype=(new_doctype)
35
- @doctype = new_doctype
36
- end
37
-
38
30
  def elastic_index
39
31
  @elastic_index ||= ElasticRecord::Index.new(self)
40
32
  end
@@ -51,9 +43,5 @@ module ElasticRecord
51
43
  def elastic_index
52
44
  self.class.elastic_index
53
45
  end
54
-
55
- def doctype
56
- self.class.doctype
57
- end
58
46
  end
59
47
  end