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
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