elastic_record 4.0.0 → 4.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -6
  3. data/Gemfile +0 -2
  4. data/README.md +25 -9
  5. data/elastic_record.gemspec +1 -1
  6. data/lib/elastic_record/as_document.rb +41 -0
  7. data/lib/elastic_record/callbacks.rb +11 -56
  8. data/lib/elastic_record/connection.rb +1 -1
  9. data/lib/elastic_record/doctype.rb +43 -0
  10. data/lib/elastic_record/index/deferred.rb +13 -15
  11. data/lib/elastic_record/index/documents.rb +23 -13
  12. data/lib/elastic_record/index/manage.rb +4 -6
  13. data/lib/elastic_record/index/mapping.rb +7 -26
  14. data/lib/elastic_record/index/settings.rb +17 -2
  15. data/lib/elastic_record/index.rb +12 -24
  16. data/lib/elastic_record/model.rb +14 -4
  17. data/lib/elastic_record/percolator_model.rb +44 -0
  18. data/lib/elastic_record/relation.rb +1 -2
  19. data/lib/elastic_record/searching.rb +5 -1
  20. data/lib/elastic_record/tasks/index.rake +0 -10
  21. data/lib/elastic_record.rb +3 -0
  22. data/test/dummy/app/models/mock_model.rb +108 -0
  23. data/test/dummy/app/models/project.rb +1 -1
  24. data/test/dummy/app/models/test_model.rb +1 -102
  25. data/test/dummy/app/models/test_percolator_model.rb +8 -0
  26. data/test/dummy/app/models/widget.rb +2 -5
  27. data/test/dummy/app/models/widget_query.rb +14 -0
  28. data/test/dummy/config/environments/test.rb +3 -3
  29. data/test/dummy/config/initializers/elastic_record.rb +1 -1
  30. data/test/elastic_record/as_document_test.rb +65 -0
  31. data/test/elastic_record/callbacks_test.rb +6 -81
  32. data/test/elastic_record/config_test.rb +1 -1
  33. data/test/elastic_record/doctype_test.rb +45 -0
  34. data/test/elastic_record/index/documents_test.rb +1 -1
  35. data/test/elastic_record/index/mapping_test.rb +16 -13
  36. data/test/elastic_record/index/settings_test.rb +34 -1
  37. data/test/elastic_record/index_test.rb +7 -15
  38. data/test/elastic_record/model_test.rb +1 -7
  39. data/test/elastic_record/percolator_model_test.rb +54 -0
  40. data/test/elastic_record/relation/batches_test.rb +0 -1
  41. data/test/elastic_record/relation/delegation_test.rb +0 -1
  42. data/test/elastic_record/relation/finder_methods_test.rb +0 -1
  43. data/test/elastic_record/relation_test.rb +0 -2
  44. data/test/elastic_record/searching_test.rb +7 -0
  45. metadata +11 -10
  46. data/lib/elastic_record/index/configurator.rb +0 -18
  47. data/lib/elastic_record/index/percolator.rb +0 -50
  48. data/lib/elastic_record/index/warmer.rb +0 -24
  49. data/lib/elastic_record/relation/admin.rb +0 -13
  50. data/test/elastic_record/index/configurator_test.rb +0 -18
  51. data/test/elastic_record/index/percolator_test.rb +0 -45
  52. data/test/elastic_record/index/warmer_test.rb +0 -20
  53. data/test/elastic_record/relation/admin_test.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 95195e9bd6f65e35a24b3ed7d8a3d8ee4170a214
4
- data.tar.gz: 65331f5fec26898efe80717794ef0a5ca897895e
3
+ metadata.gz: d074472d548bd216e7bfb7d92c9bebae0c396570
4
+ data.tar.gz: a3ec5801c91b65775d27d59d7eaf57fc13adb131
5
5
  SHA512:
6
- metadata.gz: d151b8500473e3634658de2f8b1869bc25a480c081faf6a48a952312ff6f5db7409c3b26a04424750ddf36611f45fbb659440c32a0e52deeb3147670668ac86c
7
- data.tar.gz: 27171232b4f59a2f290e323c5742b24034926f1231275f6258cb6463e1f8bea9c217e07a3f16530d96791d8d4fbe11812967aa27b2e9fc99bbd137de33af3319
6
+ metadata.gz: b611bc325d5b09e2adac8f34450898c32a23a4eb50050a23673efcee27a44b1ea938c2034fe92d2c29104e9068295df4c2c3abda6d28a813e5e7afe4d11c2365
7
+ data.tar.gz: 5ac3ce49becf9c7d1d13fd12ee5396375c16c37df7ba0339be1f9bae57a88f58f5fc5e2823e2872cc6e185cd7422baacd92c983c439a064e316f093921b003b0
data/.travis.yml CHANGED
@@ -1,14 +1,11 @@
1
- rvm:
2
- - 2.4.1
1
+ rvm: 2.4.1
3
2
  cache: bundler
4
- # Not using Trusty containers because of:
5
- # https://github.com/travis-ci/travis-ci/issues/6842
6
- sudo: required
3
+ sudo: false
7
4
  dist: trusty
8
5
  addons:
9
6
  apt:
10
7
  sources:
11
- - elasticsearch-2.x
8
+ - elasticsearch-5.x
12
9
  packages:
13
10
  - elasticsearch
14
11
  before_script:
data/Gemfile CHANGED
@@ -3,10 +3,8 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  gem 'dotenv-rails'
6
- # gem 'mysql2'
7
6
  gem 'oj'
8
7
  gem 'pg'
9
8
  gem 'rails'
10
9
  gem 'rake', '~> 10.5.0'
11
10
  gem 'webmock', require: false
12
- gem 'arelastic'
data/README.md CHANGED
@@ -153,26 +153,42 @@ end
153
153
  Product.filter(color: 'red').increase_prices
154
154
  ```
155
155
 
156
- ## Index Configuration
156
+ ## Percolators ##
157
+
158
+ ElasticRecord supports representing query documents as a model. Queries are registered and unregistered as query models are created and destroyed.
157
159
 
158
- While elastic search automatically maps fields, you may wish to override the defaults:
160
+ First, include `ElasticRecord::PercolatorModel` into your model. Specify the target model to percolate and how the model should be indexed as an ElasticSearch query.
159
161
 
160
162
  ```ruby
161
- class Product < ActiveRecord::Base
162
- elastic_index.configure do
163
- property :status, type: "string", index: "not_analyzed"
163
+ class ProductQuery
164
+ include ElasticRecord::PercolatorModel
165
+
166
+ self.percolates_model = Product
167
+
168
+ def as_search_document
169
+ Product.filter(status: status).as_elastic
164
170
  end
165
171
  end
166
172
  ```
167
173
 
168
- You can also directly access Product.elastic_index.mapping and Product.elastic_index.settings:
174
+ Use the `percolate` method to find records with queries that match.
175
+
176
+ ```
177
+ product = Product.new(price: 5.99)
178
+ matching_product_queries = ProductQuery.percolate(product)
179
+ ```
180
+
181
+ ## Index Configuration
182
+
183
+ To avoid elasticsearch dynamically mapping fields, you can directly configure Product.doctype.mapping
184
+ and Product.elastic_index.settings:
169
185
 
170
186
  ```ruby
171
187
  class Product
172
- elastic_index.mapping = {
188
+ doctype.mapping = {
173
189
  properties: {
174
- name: {type: "string", index: "analyzed"}
175
- status: {type: "string", index: "not_analyzed"}
190
+ name: {type: "text"},
191
+ status: {type: "keyword"}
176
192
  }
177
193
  }
178
194
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'elastic_record'
5
- s.version = '4.0.0'
5
+ s.version = '4.1.0'
6
6
  s.summary = 'An Elasticsearch querying ORM'
7
7
  s.description = 'Find your records with Elasticsearch'
8
8
 
@@ -0,0 +1,41 @@
1
+ module ElasticRecord
2
+ module AsDocument
3
+ def as_search_document
4
+ doctype.mapping[:properties].each_with_object({}) do |(field, mapping), result|
5
+ value = elastic_search_value field, mapping
6
+
7
+ unless value.nil?
8
+ result[field] = value
9
+ end
10
+ end
11
+ end
12
+
13
+ def as_partial_update_document
14
+ mappings = doctype.mapping[:properties]
15
+
16
+ changed.each_with_object({}) do |field, result|
17
+ if field_mapping = mappings[field]
18
+ result[field] = elastic_search_value field, field_mapping
19
+ end
20
+ end
21
+ end
22
+
23
+ def elastic_search_value(field, mapping)
24
+ value = try field
25
+ return if value.nil?
26
+
27
+ value = case mapping[:type]
28
+ when :object
29
+ value.as_search_document
30
+ when :nested
31
+ value.map(&:as_search_document)
32
+ else
33
+ value
34
+ end
35
+
36
+ if value.present? || value == false
37
+ value
38
+ end
39
+ end
40
+ end
41
+ end
@@ -4,71 +4,26 @@ module ElasticRecord
4
4
  return unless base.respond_to?(:after_save) && base.respond_to?(:after_destroy)
5
5
 
6
6
  base.class_eval do
7
- after_create do
8
- self.class.elastic_index.index_record self
9
- end
10
-
11
- after_update if: :changed? do
12
- method = self.class.elastic_index.partial_updates ? :update_record : :index_record
13
- self.class.elastic_index.send(method, self)
14
- end
15
-
16
- after_destroy do
17
- self.class.elastic_index.delete_document id
18
- end
7
+ after_create :create_index_document
8
+ after_update :update_index_document, if: :changed?
9
+ after_destroy :delete_index_document
19
10
  end
20
11
  end
21
12
 
22
- def as_search
23
- json = {}
13
+ private
24
14
 
25
- elastic_index.mapping[:properties].each do |field, mapping|
26
- value = elastic_search_value field, mapping
27
-
28
- unless value.nil?
29
- json[field] = value
30
- end
15
+ def create_index_document
16
+ self.class.elastic_index.index_record self
31
17
  end
32
18
 
33
- amend_as_search(json) if respond_to?(:amend_as_search)
19
+ def update_index_document
20
+ method = self.class.elastic_index.partial_updates ? :update_record : :index_record
34
21
 
35
- json
36
- end
37
-
38
- def as_partial_update_document
39
- json = {}
40
-
41
- mappings = elastic_index.mapping[:properties]
42
- changed.each do |field|
43
- if field_mapping = mappings[field]
44
- json[field] = elastic_search_value field, field_mapping
45
- end
22
+ self.class.elastic_index.send(method, self)
46
23
  end
47
24
 
48
- amend_partial_update_document(json) if respond_to?(:amend_partial_update_document)
49
-
50
- json
51
- end
52
-
53
- def elastic_search_value(field, mapping)
54
- value = try field
55
- return if value.nil?
56
-
57
- value = case mapping[:type]
58
- when :object
59
- value.as_search
60
- when :nested
61
- value.map(&:as_search)
62
- else
63
- value
64
- end
65
-
66
- if value.present? || value == false
67
- value
25
+ def delete_index_document
26
+ self.class.elastic_index.delete_document id
68
27
  end
69
-
70
- rescue
71
- raise "Field not found for #{field.inspect}"
72
- end
73
28
  end
74
29
  end
@@ -76,7 +76,7 @@ module ElasticRecord
76
76
  delete: Net::HTTP::Delete
77
77
  }
78
78
  def new_request(method, path, body)
79
- request = METHODS[method].new(path)
79
+ request = METHODS[method].new(path.starts_with?('/') ? path : "/#{path}")
80
80
  request.basic_auth(options[:username], options[:password]) if options[:username].present?
81
81
  request.body = body
82
82
  request.content_type = 'application/json'
@@ -0,0 +1,43 @@
1
+ module ElasticRecord
2
+ class Doctype
3
+ attr_accessor :name, :mapping, :analysis
4
+
5
+ DEFAULT_MAPPING = {
6
+ properties: {
7
+ },
8
+ _all: {
9
+ enabled: false
10
+ }
11
+ }
12
+
13
+ PERCOLATOR_MAPPING = {
14
+ "properties" => {
15
+ "query" => {
16
+ "type" => "percolator"
17
+ }
18
+ }
19
+ }
20
+
21
+ def self.percolator_doctype
22
+ new('queries', PERCOLATOR_MAPPING)
23
+ end
24
+
25
+ def initialize(name, mapping = DEFAULT_MAPPING.deep_dup)
26
+ @name = name
27
+ @mapping = mapping
28
+ @analysis = {}
29
+ end
30
+
31
+ def mapping=(custom_mapping)
32
+ mapping.deep_merge!(custom_mapping)
33
+ end
34
+
35
+ def analysis=(custom_analysis)
36
+ analysis.deep_merge!(custom_analysis)
37
+ end
38
+
39
+ def ==(other)
40
+ name == other.name && mapping == other.mapping && analysis == other.analysis
41
+ end
42
+ end
43
+ end
@@ -14,9 +14,8 @@ module ElasticRecord
14
14
  attr_accessor :bulk_stack
15
15
 
16
16
  def initialize(index)
17
- self.index = index
18
- self.bulk_stack = []
19
- @deferring_enabled = false
17
+ @index = index
18
+ @bulk_stack = []
20
19
  reset!
21
20
  end
22
21
 
@@ -34,30 +33,29 @@ module ElasticRecord
34
33
  self.writes_made = false
35
34
  end
36
35
 
37
- def flush!
38
- deferred_actions.each do |queued_action|
39
- self.writes_made = true
40
- queued_action.run(index.real_connection)
41
- end
42
- deferred_actions.clear
43
- end
44
-
45
36
  private
46
37
  READ_METHODS = [:json_get, :head]
47
38
  def method_missing(method, *args, &block)
48
39
  super unless index.real_connection.respond_to?(method)
49
40
 
50
41
  if READ_METHODS.include?(method)
51
- flush!
52
- index.real_connection.json_post("/#{index.alias_name}/_refresh") if requires_refresh?(method, *args)
42
+ flush_deferred_actions!
43
+ if method == :json_get && args.first =~ /^\/(.*)\/_search/
44
+ index.real_connection.json_post("/#{$1.partition('/').first}/_refresh")
45
+ end
46
+
53
47
  index.real_connection.send(method, *args, &block)
54
48
  else
55
49
  deferred_actions << DeferredAction.new(method, args, block)
56
50
  end
57
51
  end
58
52
 
59
- def requires_refresh?(method, *args)
60
- method == :json_get && args.first =~ /_search/
53
+ def flush_deferred_actions!
54
+ deferred_actions.each do |queued_action|
55
+ self.writes_made = true
56
+ queued_action.run(index.real_connection)
57
+ end
58
+ deferred_actions.clear
61
59
  end
62
60
  end
63
61
 
@@ -53,58 +53,68 @@ module ElasticRecord
53
53
  module Documents
54
54
  def index_record(record, index_name: alias_name)
55
55
  unless disabled
56
- index_document(record.send(record.class.primary_key), record.as_search, index_name: index_name)
56
+ index_document(
57
+ record.send(record.class.primary_key),
58
+ record.as_search_document,
59
+ doctype: record.doctype,
60
+ index_name: index_name
61
+ )
57
62
  end
58
63
  end
59
64
 
60
65
  def update_record(record, index_name: alias_name)
61
66
  unless disabled
62
- update_document(record.send(record.class.primary_key), record.as_partial_update_document, index_name: index_name)
67
+ update_document(
68
+ record.send(record.class.primary_key),
69
+ record.as_partial_update_document,
70
+ doctype: record.doctype,
71
+ index_name: index_name
72
+ )
63
73
  end
64
74
  end
65
75
 
66
- def index_document(id, document, parent: nil, index_name: alias_name)
76
+ def index_document(id, document, doctype: model.doctype, parent: nil, index_name: alias_name)
67
77
  if batch = current_bulk_batch
68
- instructions = { _index: index_name, _type: type, _id: id }
78
+ instructions = { _index: index_name, _type: doctype.name, _id: id }
69
79
  instructions[:parent] = parent if parent
70
80
 
71
81
  batch << { index: instructions }
72
82
  batch << document
73
83
  else
74
- path = "/#{index_name}/#{type}/#{id}"
84
+ path = "/#{index_name}/#{doctype.name}/#{id}"
75
85
  path << "?parent=#{parent}" if parent
76
86
 
77
87
  connection.json_put path, document
78
88
  end
79
89
  end
80
90
 
81
- def update_document(id, document, parent: nil, index_name: alias_name)
91
+ def update_document(id, document, doctype: model.doctype, parent: nil, index_name: alias_name)
82
92
  params = {doc: document, doc_as_upsert: true}
83
93
 
84
94
  if batch = current_bulk_batch
85
- instructions = { _index: index_name, _type: type, _id: id, _retry_on_conflict: 3 }
95
+ instructions = { _index: index_name, _type: doctype.name, _id: id, _retry_on_conflict: 3 }
86
96
  instructions[:parent] = parent if parent
87
97
 
88
98
  batch << { update: instructions }
89
99
  batch << params
90
100
  else
91
- path = "/#{index_name}/#{type}/#{id}/_update?retry_on_conflict=3"
101
+ path = "/#{index_name}/#{doctype.name}/#{id}/_update?retry_on_conflict=3"
92
102
  path << "&parent=#{parent}" if parent
93
103
 
94
104
  connection.json_post path, params
95
105
  end
96
106
  end
97
107
 
98
- def delete_document(id, parent: nil, index_name: alias_name)
108
+ def delete_document(id, doctype: model.doctype, parent: nil, index_name: alias_name)
99
109
  raise "Cannot delete document with empty id" if id.blank?
100
110
  index_name ||= alias_name
101
111
 
102
112
  if batch = current_bulk_batch
103
- instructions = { _index: index_name, _type: type, _id: id, _retry_on_conflict: 3 }
113
+ instructions = { _index: index_name, _type: doctype.name, _id: id, _retry_on_conflict: 3 }
104
114
  instructions[:parent] = parent if parent
105
115
  batch << { delete: instructions }
106
116
  else
107
- path = "/#{index_name}/#{type}/#{id}"
117
+ path = "/#{index_name}/#{doctype.name}/#{id}"
108
118
  path << "&parent=#{parent}" if parent
109
119
 
110
120
  connection.json_delete path
@@ -122,7 +132,7 @@ module ElasticRecord
122
132
  end
123
133
 
124
134
  def record_exists?(id)
125
- get(id)['found']
135
+ get(id, model.doctype)['found']
126
136
  end
127
137
 
128
138
  def search(elastic_query, options = {})
@@ -131,7 +141,7 @@ module ElasticRecord
131
141
  url += "?#{options.to_query}"
132
142
  end
133
143
 
134
- get url, elastic_query
144
+ get url, model.doctype, elastic_query
135
145
  end
136
146
 
137
147
  def explain(id, elastic_query)
@@ -9,9 +9,7 @@ module ElasticRecord
9
9
 
10
10
  def create(index_name = new_index_name)
11
11
  connection.json_put "/#{index_name}", {
12
- "mappings" => {
13
- type => mapping
14
- },
12
+ "mappings" => mapping_body,
15
13
  "settings" => settings
16
14
  }
17
15
  index_name
@@ -31,8 +29,8 @@ module ElasticRecord
31
29
  connection.head("/#{index_name}") == '200'
32
30
  end
33
31
 
34
- def type_exists?(index_name = alias_name)
35
- connection.head("/#{index_name}/#{type}") == '200'
32
+ def type_exists?(index_name = alias_name, type = model.doctype.name)
33
+ connection.head("/#{index_name}/_mapping/#{type}") == '200'
36
34
  end
37
35
 
38
36
  def deploy(index_name)
@@ -78,7 +76,7 @@ module ElasticRecord
78
76
  end
79
77
 
80
78
  def all_names
81
- connection.json_get("/#{alias_name}/_mapping/#{type}/").keys
79
+ connection.json_get("/#{alias_name}/_mapping/#{model.doctype.name}/").keys
82
80
  rescue
83
81
  # TODO: In ES 1.4, this returns empty rather than a 404
84
82
  []