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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +15 -9
- data/Gemfile +5 -4
- data/README.md +214 -0
- data/elastic_record.gemspec +7 -7
- data/lib/elastic_record.rb +1 -0
- data/lib/elastic_record/callbacks.rb +46 -14
- data/lib/elastic_record/config.rb +1 -21
- data/lib/elastic_record/connection.rb +24 -14
- data/lib/elastic_record/errors.rb +5 -0
- data/lib/elastic_record/index.rb +11 -1
- data/lib/elastic_record/index/deferred.rb +1 -0
- data/lib/elastic_record/index/documents.rb +95 -18
- data/lib/elastic_record/index/manage.rb +0 -8
- data/lib/elastic_record/index/mapping.rb +1 -10
- data/lib/elastic_record/json.rb +29 -0
- data/lib/elastic_record/relation.rb +9 -5
- data/lib/elastic_record/relation/batches.rb +4 -40
- data/lib/elastic_record/relation/none.rb +0 -4
- data/lib/elastic_record/relation/search_methods.rb +48 -38
- data/lib/elastic_record/relation/value_methods.rb +2 -2
- data/lib/elastic_record/tasks/index.rake +2 -2
- data/test/dummy/.env.example +1 -0
- data/test/dummy/.env.test +1 -0
- data/test/dummy/app/models/project.rb +1 -1
- data/test/dummy/app/models/test_model.rb +3 -2
- data/test/dummy/app/models/widget.rb +3 -3
- data/test/dummy/config/initializers/elastic_record.rb +1 -1
- data/test/dummy/db/migrate/20151211225259_create_projects.rb +7 -0
- data/test/dummy/db/schema.rb +8 -1
- data/test/elastic_record/callbacks_test.rb +16 -2
- data/test/elastic_record/config_test.rb +1 -2
- data/test/elastic_record/connection_test.rb +52 -9
- data/test/elastic_record/index/documents_test.rb +55 -21
- data/test/elastic_record/index/mapping_test.rb +0 -10
- data/test/elastic_record/integration/active_record_test.rb +3 -3
- data/test/elastic_record/log_subscriber_test.rb +4 -4
- data/test/elastic_record/relation/batches_test.rb +5 -24
- data/test/elastic_record/relation/delegation_test.rb +4 -3
- data/test/elastic_record/relation/finder_methods_test.rb +1 -0
- data/test/elastic_record/relation/search_methods_test.rb +47 -45
- data/test/elastic_record/relation_test.rb +18 -10
- data/test/helper.rb +4 -3
- metadata +21 -12
- data/README.rdoc +0 -146
- data/test/dummy/config/database.yml +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa1490c6f1419caa30179923ab783f171cb76a40
|
4
|
+
data.tar.gz: 998fa7673a9166e71699850e4ea3ac83655ca9b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bc5edfa1886f1ee4d942d590489c33223613145e8145706168c1a94843f3ec9d0faf165e1285adacfcc67820e3ed0d078e8a22d25fa94828b10491b7f97732f
|
7
|
+
data.tar.gz: cb659e49e466576f1ab4195d481561393083131ec031d3c2c10bb724d1cd0e2e8a1722c49b76d4009b6e85377762e823506b12ba45701d6f9e935666f2b08c9f
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,10 +1,16 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
+
[](http://travis-ci.org/data-axle/elastic_record)
|
3
|
+
[](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.
|
data/elastic_record.gemspec
CHANGED
@@ -2,22 +2,22 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = 'elastic_record'
|
5
|
-
s.version = '
|
6
|
-
s.summary = '
|
7
|
-
s.description = 'Find your records with
|
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.
|
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', '>=
|
22
|
-
s.add_dependency 'activemodel'
|
21
|
+
s.add_dependency 'arelastic', '~> 1.1', '>= 1.1.2'
|
22
|
+
s.add_dependency 'activemodel', '~> 0'
|
23
23
|
end
|
data/lib/elastic_record.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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) ?
|
41
|
-
response =
|
41
|
+
body = json.is_a?(Hash) ? ElasticRecord::JSON.encode(json) : json
|
42
|
+
response = http_request_with_retry(method, path, body)
|
42
43
|
|
43
|
-
json =
|
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
|
50
|
+
def http_request_with_retry(*args)
|
50
51
|
with_retry do
|
51
|
-
|
52
|
-
http = new_http
|
52
|
+
response = http_request(*args)
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
|