elastic_record 4.0.0 → 4.1.0

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