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.
- 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
|
+
[![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.
|
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
|
|