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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f659725ca3b959336aaf4a3b8e5964ad5bb9a645
4
- data.tar.gz: ecd8c9c725a0e20d9d36f9273fb8cb211f173f1d
3
+ metadata.gz: fa1490c6f1419caa30179923ab783f171cb76a40
4
+ data.tar.gz: 998fa7673a9166e71699850e4ea3ac83655ca9b8
5
5
  SHA512:
6
- metadata.gz: 6185d059786865130f6f51128dd99523d01762fd46752874c1b253d0c1794b90ecf77f06ac1a1394f29588b259db107e2b1da33cc1c67849c8d3edc391584986
7
- data.tar.gz: b4ca5642f7db62e9a6cdf619c6808802f5adddbf079f1900f7387de0753a459b4737d7447080c1cffd4c6c4915460be74be690ec95afbe4d3d608c2d35db65ae
6
+ metadata.gz: 1bc5edfa1886f1ee4d942d590489c33223613145e8145706168c1a94843f3ec9d0faf165e1285adacfcc67820e3ed0d078e8a22d25fa94828b10491b7f97732f
7
+ data.tar.gz: cb659e49e466576f1ab4195d481561393083131ec031d3c2c10bb724d1cd0e2e8a1722c49b76d4009b6e85377762e823506b12ba45701d6f9e935666f2b08c9f
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  Gemfile.lock
2
2
  test/dummy/log/*.log
3
+ test/dummy/.env
data/.travis.yml CHANGED
@@ -1,10 +1,16 @@
1
- services:
2
- - elasticsearch
1
+ rvm: 2.2.3
2
+ cache: bundler
3
+ sudo: required
4
+ dist: trusty
5
+ before_install:
6
+ - wget https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/distribution/deb/elasticsearch/2.2.2/elasticsearch-2.2.2.deb
7
+ - sudo dpkg -i elasticsearch-2.2.2.deb
8
+ - sudo service elasticsearch start
9
+ - sudo apt-get install --yes mysql-server
3
10
  before_script:
4
- - bundle exec rake app:db:create
5
- - bundle exec rake app:index:reset
6
- rvm:
7
- - 2.1.2
8
- env:
9
- - "GEM=ar:mysql2"
10
- - "GEM=ar:postgresql"
11
+ - cp test/dummy/.env.example test/dummy/.env
12
+ - curl localhost:9200
13
+ - bundle exec rake app:db:setup
14
+ - bundle exec rake app:index:reset
15
+ services:
16
+ - postgresql
data/Gemfile CHANGED
@@ -1,10 +1,11 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
- gem 'rails', '~> 4.2.0'
5
- gem 'fakeweb'
6
- gem 'rake'
7
- gem 'arelastic', path: '~/code/arelastic'
8
4
 
5
+ gem 'dotenv-rails'
6
+ gem 'oj'
9
7
  gem 'mysql2'
10
8
  gem 'pg'
9
+ gem 'rails', '~> 4.2.0'
10
+ gem 'rake', '~> 10.5.0'
11
+ gem 'webmock', require: false
data/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # ElasticRecord #
2
+ [![Build Status](https://secure.travis-ci.org/data-axle/elastic_record.png?rvm=2.0.0)](http://travis-ci.org/data-axle/elastic_record)
3
+ [![Code Climate](https://codeclimate.com/github/data-axle/elastic_record.png)](https://codeclimate.com/github/data-axle/elastic_record)
4
+
5
+ ElasticRecord is an Elasticsearch 2.x ORM.
6
+
7
+ ## Setup ##
8
+
9
+ Include ElasticRecord into your model:
10
+
11
+ ```ruby
12
+ class Product < ActiveRecord::Base
13
+ include ElasticRecord::Model
14
+ end
15
+ ```
16
+
17
+ ### Connection ###
18
+
19
+ There are two ways to set up which server to connect to:
20
+
21
+ ```ruby
22
+ # config/initializers/elastic_search.rb
23
+ ElasticRecord.configure do |config|
24
+ config.servers = "es1.example.com:9200"
25
+ end
26
+ ```
27
+
28
+ ```yaml
29
+ # config/elasticsearch.yml:
30
+ development:
31
+ servers: es1.example.com:9200
32
+ timeout: 10
33
+ retries: 2
34
+ ```
35
+
36
+ ## Search API ##
37
+
38
+ ElasticRecord adds the method 'elastic_search' to your models. It works similar to active_record scoping:
39
+ ```ruby
40
+ search = Product.elastic_search
41
+ ```
42
+
43
+ ### Filtering ###
44
+
45
+ If a simple hash is passed into filter, a term or terms query is created:
46
+
47
+ ```ruby
48
+ search.filter(color: 'red') # Creates a 'term' filter
49
+ search.filter(color: %w(red blue)) # Creates a 'terms' filter
50
+ search.filter(color: nil) # Creates a 'missing' filter
51
+ ```
52
+
53
+ If a hash containing hashes is passed into filter, it is used directly as a filter DSL expression:
54
+
55
+ ```ruby
56
+ search.filter(prefix: { name: "Sca" }) # Creates a prefix filter
57
+ ```
58
+
59
+ An Arelastic object can also be passed in, working similarily to Arel:
60
+
61
+ ```ruby
62
+ # Name starts with 'Sca'
63
+ search.filter(Product.arelastic[:name].prefix("Sca"))
64
+
65
+ # Name does not start with 'Sca'
66
+ search.filter(Product.arelastic[:name].prefix("Sca").negate)
67
+
68
+ # Size is greater than 5
69
+ search.filter(Product.arelastic[:size].gt(5))
70
+
71
+ # Name is 'hola' or name is missing
72
+ search.filter(Product.arelastic[:name].eq("hola").or(Product.arelastic[:name].missing))
73
+ ```
74
+
75
+ Helpful Arel builders can be found at https://github.com/matthuhiggins/arelastic/blob/master/lib/arelastic/builders/filter.rb.
76
+
77
+ ### Querying ###
78
+
79
+ To create a query string, pass a string to search.query:
80
+
81
+ ```ruby
82
+ search.query("red AND fun*") # Creates {query_string: {"red AND fun*"}}
83
+ ```
84
+
85
+ Complex queries are done using either a hash or an arelastic object:
86
+
87
+ ```ruby
88
+ search.query(match: {description: "amazing"})
89
+ ```
90
+
91
+ ### Ordering ###
92
+
93
+ ```ruby
94
+ search.order(:price) # sort by price
95
+ search.order(:color, :price) # sort by color, then price
96
+ search.order(price: :desc) # sort by price in descending order
97
+ ```
98
+
99
+ ### Offsets and Limits ###
100
+
101
+ To change the 'size' and 'from' values of a query, use offset and limit:
102
+
103
+ ```ruby
104
+ search.limit(40).offset(80) # Creates a query with {size: 40, from: 80}
105
+ ```
106
+
107
+ ### Aggregations ###
108
+
109
+ Aggregations are added with the aggregate method:
110
+
111
+ ```ruby
112
+ search.aggregate('popular_colors' => {'terms' => {'field' => 'color'}})
113
+ ```
114
+
115
+ It is important to note that adding aggregations to a query is different than retrieving the results of the query:
116
+
117
+ ```ruby
118
+ search = search.aggregate('popular_colors' => {'terms' => {'field' => 'color'}})
119
+ search.aggregations
120
+ #=> {"popular_colors" => {"buckets" => ...}}
121
+ ```
122
+
123
+ ### Getting Results ###
124
+
125
+ A search object behaves similar to an active_record scope, implementing a few methods of its own and delegating the rest to Array, and your class.
126
+
127
+ ```ruby
128
+ search.count # Return the number of search results
129
+ search.first # Limit results to 1 and return the first result or nil
130
+ search.find(id) # Add an ids filter to the existing query
131
+ search.as_elastic # Return the json hash that will be sent to elastic search.
132
+ ```
133
+
134
+ The search object behaves like an array when necessary:
135
+
136
+ ```ruby
137
+ search.each do |product|
138
+ ...
139
+ end
140
+ ```
141
+
142
+ Class methods can be executed within scopes:
143
+
144
+ ```ruby
145
+ class Product
146
+ def self.increase_prices
147
+ all.each do { |product| product.increment(:price, 10) }
148
+ end
149
+ end
150
+
151
+ # Increase the price of all red products by $10.
152
+ Product.filter(color: 'red').increase_prices
153
+ ```
154
+
155
+ ## Index Configuration
156
+
157
+ While elastic search automatically maps fields, you may wish to override the defaults:
158
+
159
+ ```ruby
160
+ class Product < ActiveRecord::Base
161
+ elastic_index.configure do
162
+ property :status, type: "string", index: "not_analyzed"
163
+ end
164
+ end
165
+ ```
166
+
167
+ You can also directly access Product.elastic_index.mapping and Product.elastic_index.settings:
168
+
169
+ ```ruby
170
+ class Product
171
+ elastic_index.mapping = {
172
+ properties: {
173
+ name: {type: "string", index: "analyzed"}
174
+ status: {type: "string", index: "not_analyzed"}
175
+ }
176
+ }
177
+ end
178
+ ```
179
+
180
+ ### Index Management ###
181
+
182
+ If you need to manage multiple indexes via the rake tasks, you will need to declare them explicitly:
183
+
184
+ ```ruby
185
+ ElasticRecord.configure do |config|
186
+ config.model_names = %w(Product Order Location)
187
+ end
188
+ ```
189
+
190
+ Create the index:
191
+
192
+ ```ruby
193
+ rake index:create CLASS=Product
194
+ ```
195
+
196
+ ### Index Admin Functions ###
197
+
198
+ Core and Index APIs can be accessed with Product.elastic_index. Some examples include:
199
+
200
+ ```ruby
201
+ Product.elastic_index.create_and_deploy # Create a new index
202
+ Product.elastic_index.reset # Delete related indexes and deploy a new one
203
+ Product.elastic_index.refresh # Call the refresh API
204
+ Product.elastic_index.get_mapping # Get the index mapping defined by elastic search
205
+ ```
206
+
207
+ ## JSON Adapter ##
208
+
209
+ By default, ElasticRecord uses ActiveSupport::JSON to serialize Elasticsearch payloads. There
210
+ is optional support for using the Oj gem. To use Oj, ensure that oj is required and set:
211
+ ```ruby
212
+ ElasticRecord::JSON.parser = :oj
213
+ ```
214
+ To return to the default parser, set the variable to :active_support.
@@ -2,22 +2,22 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'elastic_record'
5
- s.version = '2.0.2'
6
- s.summary = 'Use Elastic Search with your objects'
7
- s.description = 'Find your records with elastic search'
5
+ s.version = '3.0.0'
6
+ s.summary = 'An Elasticsearch querying ORM'
7
+ s.description = 'Find your records with Elasticsearch'
8
8
 
9
9
  s.required_ruby_version = '>= 1.9.3'
10
10
  s.required_rubygems_version = ">= 1.8.11"
11
- s.license = 'MIT'
12
11
 
12
+ s.license = 'MIT'
13
13
  s.authors = ['Infogroup', 'Matthew Higgins']
14
14
  s.email = 'developer@matthewhiggins.com'
15
15
  s.homepage = 'http://github.com/data-axle/elastic_record'
16
16
 
17
- s.extra_rdoc_files = ['README.rdoc']
17
+ s.extra_rdoc_files = ['README.md']
18
18
  s.files = `git ls-files`.split("\n")
19
19
  s.test_files = `git ls-files -- {test}/*`.split("\n")
20
20
 
21
- s.add_dependency 'arelastic', '>= 0.7.0'
22
- s.add_dependency 'activemodel'
21
+ s.add_dependency 'arelastic', '~> 1.1', '>= 1.1.2'
22
+ s.add_dependency 'activemodel', '~> 0'
23
23
  end
@@ -8,6 +8,7 @@ module ElasticRecord
8
8
  autoload :Config, 'elastic_record/config'
9
9
  autoload :Connection, 'elastic_record/connection'
10
10
  autoload :Index, 'elastic_record/index'
11
+ autoload :JSON, 'elastic_record/json'
11
12
  autoload :Lucene, 'elastic_record/lucene'
12
13
  autoload :Model, 'elastic_record/model'
13
14
  autoload :Relation, 'elastic_record/relation'
@@ -4,33 +4,29 @@ 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_save if: :changed? do
7
+ after_create do
8
8
  self.class.elastic_index.index_record self
9
9
  end
10
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
+
11
16
  after_destroy do
12
17
  self.class.elastic_index.delete_document id
13
18
  end
14
19
  end
15
20
  end
16
21
 
17
-
18
22
  def as_search
19
23
  json = {}
20
24
 
21
25
  elastic_index.mapping[:properties].each do |field, mapping|
22
- next if !respond_to?(field)
23
- value = send(field)
24
-
25
- if value.present? || value == false
26
- json[field] = case mapping[:type]
27
- when :object
28
- value.as_search
29
- when :nested
30
- value.map(&:as_search)
31
- else
32
- value
33
- end
26
+ value = elastic_search_value field, mapping
27
+
28
+ unless value.nil?
29
+ json[field] = value
34
30
  end
35
31
  end
36
32
 
@@ -38,5 +34,41 @@ module ElasticRecord
38
34
 
39
35
  json
40
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
46
+ end
47
+
48
+ amend_partial_update_document(json) if respond_to?(: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
68
+ end
69
+
70
+ rescue
71
+ raise "Field not found for #{field.inspect}"
72
+ end
41
73
  end
42
74
  end
@@ -29,27 +29,7 @@ module ElasticRecord
29
29
 
30
30
  def settings=(settings)
31
31
  self.servers = settings['servers']
32
-
33
- if settings['options']
34
- warn("**************************************",
35
- "elasticsearch.yml/:options is deprecated. For example, the following:",
36
- "development:",
37
- " servers: 127.0.0.1:9200",
38
- " options:",
39
- " timeout: 10",
40
- " retries: 2",
41
- "",
42
- "becomes:",
43
- "",
44
- "development:",
45
- " servers: 127.0.0.1:9200",
46
- " timeout: 10",
47
- " retries: 2",
48
- "**************************************")
49
- self.connection_options = settings['options']
50
- else
51
- self.connection_options = settings
52
- end
32
+ self.connection_options = settings
53
33
 
54
34
  if scroll_keep_alive = settings['scroll_keep_alive'].presence
55
35
  self.scroll_keep_alive = scroll_keep_alive
@@ -9,15 +9,16 @@ module ElasticRecord
9
9
  def initialize(servers, options = {})
10
10
  self.servers = Array(servers)
11
11
 
12
+ @shuffled_servers = nil
12
13
  self.current_server = next_server
13
14
  self.request_count = 0
14
15
  self.max_request_count = 100
15
16
  self.options = options.symbolize_keys
16
- self.bulk_stack = []
17
+ self.bulk_stack = []
17
18
  end
18
19
 
19
20
  def head(path)
20
- http_request(:head, path).code
21
+ http_request_with_retry(:head, path).code
21
22
  end
22
23
 
23
24
  def json_get(path, json = nil)
@@ -37,25 +38,33 @@ module ElasticRecord
37
38
  end
38
39
 
39
40
  def json_request(method, path, json)
40
- body = json.is_a?(Hash) ? ActiveSupport::JSON.encode(json) : json
41
- response = http_request(method, path, body)
41
+ body = json.is_a?(Hash) ? ElasticRecord::JSON.encode(json) : json
42
+ response = http_request_with_retry(method, path, body)
42
43
 
43
- json = ActiveSupport::JSON.decode response.body
44
- raise ConnectionError.new(json['error']) if json['error']
44
+ json = ElasticRecord::JSON.decode(response.body)
45
+ raise ConnectionError.new(response.code, json['error']) if json['error']
45
46
 
46
47
  json
47
48
  end
48
49
 
49
- def http_request(method, path, body = nil)
50
+ def http_request_with_retry(*args)
50
51
  with_retry do
51
- request = new_request(method, path, body)
52
- http = new_http
52
+ response = http_request(*args)
53
53
 
54
- ActiveSupport::Notifications.instrument("request.elastic_record") do |payload|
55
- payload[:http] = http
56
- payload[:request] = request
57
- payload[:response] = http.request(request)
58
- end
54
+ raise ConnectionError.new(response.code, response.body) if response.code.to_i >= 500
55
+
56
+ response
57
+ end
58
+ end
59
+
60
+ def http_request(method, path, body = nil)
61
+ request = new_request(method, path, body)
62
+ http = new_http
63
+
64
+ ActiveSupport::Notifications.instrument("request.elastic_record") do |payload|
65
+ payload[:http] = http
66
+ payload[:request] = request
67
+ payload[:response] = http.request(request)
59
68
  end
60
69
  end
61
70
 
@@ -70,6 +79,7 @@ module ElasticRecord
70
79
  request = METHODS[method].new(path)
71
80
  request.basic_auth(options[:username], options[:password]) if options[:username].present?
72
81
  request.body = body
82
+ request.content_type = 'application/json'
73
83
  request
74
84
  end
75
85