chewy 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +13 -5
- data/.gitignore +1 -0
- data/.travis.yml +5 -3
- data/CHANGELOG.md +75 -0
- data/README.md +487 -92
- data/Rakefile +3 -2
- data/chewy.gemspec +2 -2
- data/filters +76 -0
- data/lib/chewy.rb +5 -3
- data/lib/chewy/config.rb +36 -19
- data/lib/chewy/fields/base.rb +5 -1
- data/lib/chewy/index.rb +22 -10
- data/lib/chewy/index/actions.rb +13 -13
- data/lib/chewy/index/search.rb +7 -2
- data/lib/chewy/query.rb +382 -64
- data/lib/chewy/query/context.rb +174 -0
- data/lib/chewy/query/criteria.rb +127 -34
- data/lib/chewy/query/loading.rb +9 -9
- data/lib/chewy/query/nodes/and.rb +25 -0
- data/lib/chewy/query/nodes/base.rb +17 -0
- data/lib/chewy/query/nodes/bool.rb +32 -0
- data/lib/chewy/query/nodes/equal.rb +34 -0
- data/lib/chewy/query/nodes/exists.rb +20 -0
- data/lib/chewy/query/nodes/expr.rb +28 -0
- data/lib/chewy/query/nodes/field.rb +106 -0
- data/lib/chewy/query/nodes/missing.rb +20 -0
- data/lib/chewy/query/nodes/not.rb +25 -0
- data/lib/chewy/query/nodes/or.rb +25 -0
- data/lib/chewy/query/nodes/prefix.rb +18 -0
- data/lib/chewy/query/nodes/query.rb +20 -0
- data/lib/chewy/query/nodes/range.rb +63 -0
- data/lib/chewy/query/nodes/raw.rb +15 -0
- data/lib/chewy/query/nodes/regexp.rb +31 -0
- data/lib/chewy/query/nodes/script.rb +20 -0
- data/lib/chewy/query/pagination.rb +28 -22
- data/lib/chewy/railtie.rb +23 -0
- data/lib/chewy/rspec/update_index.rb +20 -3
- data/lib/chewy/type/adapter/active_record.rb +78 -5
- data/lib/chewy/type/adapter/base.rb +46 -0
- data/lib/chewy/type/adapter/object.rb +40 -8
- data/lib/chewy/type/base.rb +1 -1
- data/lib/chewy/type/import.rb +18 -44
- data/lib/chewy/type/observe.rb +24 -14
- data/lib/chewy/version.rb +1 -1
- data/lib/tasks/chewy.rake +27 -0
- data/spec/chewy/config_spec.rb +30 -12
- data/spec/chewy/fields/base_spec.rb +11 -5
- data/spec/chewy/index/actions_spec.rb +20 -20
- data/spec/chewy/index/search_spec.rb +5 -5
- data/spec/chewy/index_spec.rb +28 -8
- data/spec/chewy/query/context_spec.rb +173 -0
- data/spec/chewy/query/criteria_spec.rb +219 -12
- data/spec/chewy/query/loading_spec.rb +6 -4
- data/spec/chewy/query/nodes/and_spec.rb +16 -0
- data/spec/chewy/query/nodes/bool_spec.rb +22 -0
- data/spec/chewy/query/nodes/equal_spec.rb +32 -0
- data/spec/chewy/query/nodes/exists_spec.rb +18 -0
- data/spec/chewy/query/nodes/missing_spec.rb +15 -0
- data/spec/chewy/query/nodes/not_spec.rb +16 -0
- data/spec/chewy/query/nodes/or_spec.rb +16 -0
- data/spec/chewy/query/nodes/prefix_spec.rb +16 -0
- data/spec/chewy/query/nodes/query_spec.rb +12 -0
- data/spec/chewy/query/nodes/range_spec.rb +32 -0
- data/spec/chewy/query/nodes/raw_spec.rb +11 -0
- data/spec/chewy/query/nodes/regexp_spec.rb +31 -0
- data/spec/chewy/query/nodes/script_spec.rb +15 -0
- data/spec/chewy/query/pagination_spec.rb +3 -2
- data/spec/chewy/query_spec.rb +83 -26
- data/spec/chewy/rspec/update_index_spec.rb +20 -0
- data/spec/chewy/type/adapter/active_record_spec.rb +102 -0
- data/spec/chewy/type/adapter/object_spec.rb +82 -0
- data/spec/chewy/type/import_spec.rb +30 -1
- data/spec/chewy/type/mapping_spec.rb +1 -1
- data/spec/chewy/type/observe_spec.rb +46 -12
- data/spec/spec_helper.rb +7 -6
- data/spec/support/class_helpers.rb +2 -2
- metadata +98 -48
- data/.rvmrc +0 -1
- data/lib/chewy/index/client.rb +0 -13
- data/spec/chewy/index/client_spec.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MzNiZmUxMGUxNzE1ODdkYzQzYTAyMzAwN2VhNDhmZDkzYzUyMDZhNQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MWIwNDA3MzMyNzRlZGM2ODllODA4ZjAxMzI0NzM5OTc3MGU4NjViMA==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDk1NWIzZmYzYmZhNjkzOTI0OTY0MWVkYjYzNDE2N2FiZGUzOGQ1YjI0NzQ4
|
10
|
+
OTgzMTgyNDI2M2RlMjJhMWQzMjk1MzZjOGFkODQ3NzFjZmEwOTQ2M2UzNDMy
|
11
|
+
MDJjY2Y0ZmFiZjU5NjY3MTE5MzQ3NDk0ZDg4MzdjZDJkOTZhNGI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NWEyODY1M2JhZjEwMDI1MTUyNGQ1NzM4ZTU0NTgyYWRkOWM3NWI0ZmQ2ZmY1
|
14
|
+
ODkyMjc5OTQwNThlODA2YjY3MGM5MTQyZDA5NzMzZDBjOWYyYmI2NmMyNGQx
|
15
|
+
OTU3ZGM3OTEyOGJjMWIyMmEwYWY2MjkzOGJmM2Q3MzQ2MzgzMGE=
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -2,6 +2,8 @@ language: ruby
|
|
2
2
|
rvm:
|
3
3
|
- 1.9.3
|
4
4
|
- 2.0.0
|
5
|
-
- rbx
|
6
|
-
|
7
|
-
- elasticsearch
|
5
|
+
# - rbx
|
6
|
+
before_install:
|
7
|
+
- curl -# https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-0.90.9.tar.gz | tar xz -C /tmp
|
8
|
+
before_script:
|
9
|
+
- TEST_CLUSTER_COMMAND="/tmp/elasticsearch-0.90.9/bin/elasticsearch" rake elasticsearch:start
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# master
|
2
|
+
|
3
|
+
# Version 0.1.0
|
4
|
+
|
5
|
+
* Added filters simplified DSL. See [context.rb](lib/chewy/query/context.rb) for more info.
|
6
|
+
|
7
|
+
* Queries and filters join system reworked. See [query.rb](lib/chewy/query.rb) for more info.
|
8
|
+
|
9
|
+
* Added query `merge` method
|
10
|
+
|
11
|
+
* `update_index` matcher now wraps expected block in `Chewy.atomic` by default.
|
12
|
+
This behaviour can be prevented with `atomic: false` option passing
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
expect { user.save! }.to update_index('users#user', atomic: false)
|
16
|
+
```
|
17
|
+
|
18
|
+
* Renamed `Chewy.observing_enabled` to `Chewy.urgent_update` with `false` as default
|
19
|
+
|
20
|
+
* `update_elasticsearch` renamed to `update_index`, added `update_index`
|
21
|
+
`:urgent` option
|
22
|
+
|
23
|
+
* Added import ActiveSupport::Notifications instrumentation
|
24
|
+
`ActiveSupport::Notifications.subscribe('import_objects.chewy') { |*args| }`
|
25
|
+
|
26
|
+
* Added `types!` and `only!` query chain methods, which purges previously
|
27
|
+
chained types and fields
|
28
|
+
|
29
|
+
* `types` chain method now uses types filter
|
30
|
+
|
31
|
+
* Added `types` query chain method
|
32
|
+
|
33
|
+
* Changed types access API:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
UsersIndex::User # => UsersIndex::User
|
37
|
+
UsersIndex::types_hash['user'] # => UsersIndex::User
|
38
|
+
UsersIndex.user # => UsersIndex::User
|
39
|
+
UsersIndex.types # => [UsersIndex::User]
|
40
|
+
UsersIndex.type_names # => ['user']
|
41
|
+
```
|
42
|
+
|
43
|
+
* `update_elasticsearch` method name as the second argument
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
update_elasticsearch('users#user', :self)
|
47
|
+
update_elasticsearch('users#user', :users)
|
48
|
+
```
|
49
|
+
|
50
|
+
* Changed index handle methods, removed `index_` prefix. I.e. was
|
51
|
+
`UsersIndex.index_create`, became `UsersIndex.create`
|
52
|
+
|
53
|
+
* Ability to pass value proc for source object context if arity == 0
|
54
|
+
`field :full_name, value: ->{ first_name + last_name }` instead of
|
55
|
+
`field :full_name, value: ->(u){ u.first_name + u.last_name }`
|
56
|
+
|
57
|
+
* Added `.only` chain to `update_index` matcher
|
58
|
+
|
59
|
+
* Added ability to pass ActiveRecord::Relation as a scope for load
|
60
|
+
`CitiesIndex.all.load(scope: {city: City.include(:country)})`
|
61
|
+
|
62
|
+
* Added method `all` to index for query DSL consistency
|
63
|
+
|
64
|
+
* Implemented isolated adapters to simplify adding new ORMs
|
65
|
+
|
66
|
+
* Query DLS chainable methods delegated to index class
|
67
|
+
(no longer need to call MyIndex.search.query, just MyIndex.query)
|
68
|
+
|
69
|
+
# Version 0.0.1
|
70
|
+
|
71
|
+
* Query dsl
|
72
|
+
|
73
|
+
* Basic index hadling
|
74
|
+
|
75
|
+
* Initial version
|
data/README.md
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/toptal/chewy.png)](https://travis-ci.org/toptal/chewy)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/toptal/chewy.png)](https://codeclimate.com/github/toptal/chewy)
|
3
|
+
|
1
4
|
# Chewy
|
2
5
|
|
3
6
|
Chewy is ODM and wrapper for official elasticsearch client (https://github.com/elasticsearch/elasticsearch-ruby)
|
@@ -23,17 +26,17 @@ Or install it yourself as:
|
|
23
26
|
1. Create `/app/chewy/users_index.rb`
|
24
27
|
|
25
28
|
```ruby
|
26
|
-
|
29
|
+
class UsersIndex < Chewy::Index
|
27
30
|
|
28
|
-
|
31
|
+
end
|
29
32
|
```
|
30
33
|
|
31
34
|
2. Add one or more types mapping
|
32
35
|
|
33
36
|
```ruby
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
+
class UsersIndex < Chewy::Index
|
38
|
+
define_type User.active # or just model instead_of scope: define_type User
|
39
|
+
end
|
37
40
|
```
|
38
41
|
|
39
42
|
Newly-defined index type class is accessible via `UsersIndex.user` or `UsersIndex::User`
|
@@ -41,20 +44,21 @@ Or install it yourself as:
|
|
41
44
|
3. Add some type mappings
|
42
45
|
|
43
46
|
```ruby
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
54
|
-
field :rating, type: 'integer' # custom data type
|
55
|
-
field :created_at, type: 'date', include_in_all: false
|
47
|
+
class UsersIndex < Chewy::Index
|
48
|
+
define_type User.active.includes(:country, :badges, :projects) do
|
49
|
+
field :first_name, :last_name # multiple fields without additional options
|
50
|
+
field :email, analyzer: 'email' # elasticsearch-related options
|
51
|
+
field :country, value: ->(user) { user.country.name } # custom value proc
|
52
|
+
field :badges, value: ->(user) { user.badges.map(&:name) } # passing array values to index
|
53
|
+
field :projects, type: 'object' do # the same syntax for `multi_field`
|
54
|
+
field :title
|
55
|
+
field :description # default data type is `string`
|
56
56
|
end
|
57
|
+
field :rating, type: 'integer' # custom data type
|
58
|
+
field :created, type: 'date', include_in_all: false,
|
59
|
+
value: ->{ created_at } # value proc for source object context
|
57
60
|
end
|
61
|
+
end
|
58
62
|
```
|
59
63
|
|
60
64
|
Mapping definitions - http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping.html
|
@@ -62,32 +66,33 @@ Or install it yourself as:
|
|
62
66
|
4. Add some index- and type-related settings
|
63
67
|
|
64
68
|
```ruby
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
}
|
69
|
+
class UsersIndex < Chewy::Index
|
70
|
+
settings analysis: {
|
71
|
+
analyzer: {
|
72
|
+
email: {
|
73
|
+
tokenizer: 'keyword',
|
74
|
+
filter: ['lowercase']
|
72
75
|
}
|
73
76
|
}
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
field :about_translations, type: 'object'
|
86
|
-
field :rating, type: 'integer' # custom data type
|
87
|
-
field :created_at, type: 'date', include_in_all: false
|
77
|
+
}
|
78
|
+
|
79
|
+
define_type User.active.includes(:country, :badges, :projects) do
|
80
|
+
root _boost: { name: :_boost, null_value: 1.0 } do
|
81
|
+
field :first_name, :last_name
|
82
|
+
field :email, analyzer: 'email'
|
83
|
+
field :country, value: ->(user) { user.country.name }
|
84
|
+
field :badges, value: ->(user) { user.badges.map(&:name) }
|
85
|
+
field :projects, type: 'object' do
|
86
|
+
field :title
|
87
|
+
field :description
|
88
88
|
end
|
89
|
+
field :about_translations, type: 'object'
|
90
|
+
field :rating, type: 'integer'
|
91
|
+
field :created, type: 'date', include_in_all: false,
|
92
|
+
value: ->{ created_at }
|
89
93
|
end
|
90
94
|
end
|
95
|
+
end
|
91
96
|
```
|
92
97
|
|
93
98
|
Index settings - http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html
|
@@ -96,95 +101,486 @@ Or install it yourself as:
|
|
96
101
|
5. Add model observing code
|
97
102
|
|
98
103
|
```ruby
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
104
|
+
class User < ActiveRecord::Base
|
105
|
+
update_index('users#user') { self } # specifying index, type and backreference
|
106
|
+
# for updating after user save or destroy
|
107
|
+
end
|
103
108
|
|
104
|
-
|
105
|
-
|
109
|
+
class Country < ActiveRecord::Base
|
110
|
+
has_many :users
|
106
111
|
|
107
|
-
|
108
|
-
|
112
|
+
update_index('users#user') { users } # return single object or collection
|
113
|
+
end
|
109
114
|
|
110
|
-
|
111
|
-
|
112
|
-
|
115
|
+
class Project < ActiveRecord::Base
|
116
|
+
update_index('users#user') { user if user.active? } # you can return even `nil` from the backreference
|
117
|
+
end
|
113
118
|
|
114
|
-
|
115
|
-
|
119
|
+
class Bage < ActiveRecord::Base
|
120
|
+
has_and_belongs_to_many :users
|
116
121
|
|
117
|
-
|
118
|
-
|
119
|
-
|
122
|
+
update_index('users') { users } # if index has only one type
|
123
|
+
# there is no need to specify updated type
|
124
|
+
end
|
120
125
|
```
|
121
126
|
|
127
|
+
Also, you can use second argument for method name passing:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
update_index('users#user', :self)
|
131
|
+
update_index('users#user', :users)
|
132
|
+
```
|
133
|
+
|
134
|
+
### Types access
|
135
|
+
|
136
|
+
You are able to access index-defined types with the following API:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
UsersIndex::User # => UsersIndex::User
|
140
|
+
UsersIndex.types_hash['user'] # => UsersIndex::User
|
141
|
+
UsersIndex.user # => UsersIndex::User
|
142
|
+
UsersIndex.types # => [UsersIndex::User]
|
143
|
+
UsersIndex.type_names # => ['user']
|
144
|
+
```
|
145
|
+
|
122
146
|
### Index manipulation
|
123
147
|
|
124
148
|
```ruby
|
125
|
-
|
126
|
-
|
127
|
-
UsersIndex.import # import with 0 arguments process all the data specified in type definition
|
128
|
-
# literally, User.active.includes(:country, :bages, :projects).find_in_batches
|
149
|
+
UsersIndex.delete # destroy index if exists
|
150
|
+
UsersIndex.delete!
|
129
151
|
|
130
|
-
|
131
|
-
|
152
|
+
UsersIndex.create
|
153
|
+
UsersIndex.create! # use bang or non-bang methods
|
154
|
+
|
155
|
+
UsersIndex.purge
|
156
|
+
UsersIndex.purge! # deletes then creates index
|
157
|
+
|
158
|
+
UsersIndex::User.import # import with 0 arguments process all the data specified in type definition
|
159
|
+
# literally, User.active.includes(:country, :badges, :projects).find_in_batches
|
160
|
+
UsersIndex::User.import User.where('rating > 100') # or import specified users scope
|
161
|
+
UsersIndex::User.import User.where('rating > 100').to_a # or import specified users array
|
162
|
+
UsersIndex::User.import [1, 2, 42] # pass even ids for import, it will be handled in the most effective way
|
163
|
+
|
164
|
+
UsersIndex.import # import every defined type
|
165
|
+
UsersIndex.reset # purges index and imports default data for all types
|
132
166
|
```
|
133
167
|
|
134
168
|
Also if passed user is #destroyed? or specified id is not existing in the database, import will perform `delete` index for this it
|
135
169
|
|
136
170
|
### Observing strategies
|
137
171
|
|
138
|
-
There are
|
172
|
+
There are 3 strategies for index updating: do not update index at all, update right after save and cummulative update. The first is by default.
|
173
|
+
|
174
|
+
#### Updating index on-demand
|
175
|
+
|
176
|
+
By default Chewy indexes are not updated when the observed model is saved or destroyed.
|
177
|
+
This depends on the `Chewy.urgent_update` (false by default) or on the per-model update config.
|
178
|
+
If you will perform `Chewy.urgent_update = true`, all the models will start to update elasticsearch
|
179
|
+
index right after save. Also
|
139
180
|
|
140
181
|
```ruby
|
141
|
-
|
142
|
-
|
143
|
-
|
182
|
+
class User < ActiveRecord::Base
|
183
|
+
update_index 'users#user', 'self', urgent: true
|
184
|
+
end
|
144
185
|
```
|
145
186
|
|
146
|
-
|
187
|
+
will make the same effect for User model only.
|
188
|
+
Note than urgent update options affects only outside-atomic-block behavour. Inside
|
189
|
+
the `Chewy.atomic { }` block indexes updates as described below.
|
190
|
+
|
191
|
+
#### Using atomic updates
|
192
|
+
|
193
|
+
To perform atomic cummulative updates, use `Chewy.atomic`:
|
147
194
|
|
148
195
|
```ruby
|
149
|
-
|
150
|
-
|
151
|
-
|
196
|
+
Chewy.atomic do
|
197
|
+
user.each { |user| user.update_attributes(name: user.name.strip) }
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
Index update will be performed once per Chewy.atomic block for every affected type.
|
202
|
+
This strategy is highly usable for rails actions:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
class ApplicationController < ActionController::Base
|
206
|
+
around_action { |&block| Chewy.atomic(&block) }
|
207
|
+
end
|
152
208
|
```
|
153
209
|
|
210
|
+
Also atomic blocks might be nested and don't affect each other.
|
211
|
+
|
154
212
|
### Index querying
|
155
213
|
|
156
214
|
```ruby
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
215
|
+
scope = UsersIndex.query(term: {name: 'foo'})
|
216
|
+
.filter(range: {rating: {gte: 100}})
|
217
|
+
.order(created: :desc)
|
218
|
+
.limit(20).offset(100)
|
219
|
+
|
220
|
+
scope.to_a # => will produce array of UserIndex::User or other types instances
|
221
|
+
scope.map { |user| user.email }
|
222
|
+
scope.total_count # => will return total objects count
|
223
|
+
|
224
|
+
scope.per(10).page(3) # supports kaminari pagination
|
225
|
+
scope.explain.map { |user| user._explanation }
|
226
|
+
scope.only(:id, :email) # returns ids and emails only
|
227
|
+
|
228
|
+
scope.merge(other_scope) # queries could be merged
|
169
229
|
```
|
170
230
|
|
171
231
|
Also, queries can be performed on a type individually
|
172
232
|
|
173
233
|
```ruby
|
174
|
-
|
234
|
+
UsersIndex::User.filter(term: {name: 'foo'}) # will return UserIndex::User collection only
|
235
|
+
```
|
236
|
+
|
237
|
+
If you are performing more then one `filter` or `query` in the chain,
|
238
|
+
all the filters and queries will be concatenated in the way specified by
|
239
|
+
`filter_mode` and `query_mode` respectively.
|
240
|
+
|
241
|
+
Default `filter_mode` is `:and` and default `query_mode` is `bool`.
|
242
|
+
|
243
|
+
Available filter modes are: `:and`, `:or`, `:must`, `:should` and
|
244
|
+
any minimum_should_match-acceptable value
|
245
|
+
|
246
|
+
Available query modes are: `:must`, `:should`, `:dis_max`, any
|
247
|
+
minimum_should_match-acceptable value or float value for dis_max
|
248
|
+
query with tie_breaker specified.
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
UsersIndex::User.filter{ name == 'Fred' }.filter{ age < 42 } # will be wrapped with `and` filter
|
252
|
+
UsersIndex::User.filter{ name == 'Fred' }.filter{ age < 42 }.filter_mode(:should) # will be wrapped with bool `should` filter
|
253
|
+
UsersIndex::User.filter{ name == 'Fred' }.filter{ age < 42 }.filter_mode('75%') # will be wrapped with bool `should` filter with `minimum_should_match: '75%'`
|
254
|
+
```
|
255
|
+
|
256
|
+
See [query.rb](lib/chewy/query.rb) for more info.
|
257
|
+
|
258
|
+
### Filters query DSL.
|
259
|
+
|
260
|
+
There is a test version of filters creating DSL:
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
UsersIndex.filter{ name == 'Fred' } # will produce `term` filter.
|
264
|
+
UsersIndex.filter{ age <= 42 } # will produce `range` filter.
|
175
265
|
```
|
176
266
|
|
267
|
+
The basis of the DSL is expression.
|
268
|
+
There are 2 types of expressions:
|
269
|
+
|
270
|
+
* Simple function
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
UsersIndex.filter{ s('doc["num"] > 1') } # script expression
|
274
|
+
UsersIndex.filter{ q(query_string: {query: 'lazy fox'}) } # query expression
|
275
|
+
```
|
276
|
+
|
277
|
+
* Field-dependant composite expression.
|
278
|
+
Consists of the field name (with dot notation or not),
|
279
|
+
value and action operator between them. Field name might take
|
280
|
+
additional options for passing to the result expression.
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
UsersIndex.filter{ name == 'Name' } # simple field term filter
|
284
|
+
UsersIndex.filter{ name(:bool) == ['Name1', 'Name2'] } # terms query with `execution: :bool` option passed
|
285
|
+
UsersIndex.filter{ answers.title =~ /regexp/ } # regexp filter for `answers.title` field
|
286
|
+
```
|
287
|
+
|
288
|
+
You can combine expressions as you wish with combination operators help
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
UsersIndex.filter{ (name == 'Name') & (email == 'Email') } # combination produces `and` filter
|
292
|
+
UsersIndex.filter{
|
293
|
+
must(
|
294
|
+
should(name =~ 'Fr').should_not(name == 'Fred') & (age == 42), email =~ /gmail\.com/
|
295
|
+
) | ((roles.admin == true) & name?)
|
296
|
+
} # many of the combination possibilities
|
297
|
+
```
|
298
|
+
|
299
|
+
Also, there is a special syntax for cache enabling:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
UsersIndex.filter{ ~name == 'Name' } # you can apply tilda to the field name
|
303
|
+
UsersIndex.filter{ ~(name == 'Name') } # or to the whole expression
|
304
|
+
|
305
|
+
# if you are applying cache to the one part of range filter
|
306
|
+
# the whole filter will be cached:
|
307
|
+
UsersIndex.filter{ ~(age > 42) & (age <= 50) }
|
308
|
+
|
309
|
+
# You can pass cache options as a field option also.
|
310
|
+
UsersIndex.filter{ name(cache: true) == 'Name' }
|
311
|
+
UsersIndex.filter{ name(cache: false) == 'Name' }
|
312
|
+
|
313
|
+
# With regexp filter you can pass _cache_key
|
314
|
+
UsersIndex.filter{ name(cache: 'name_regexp') =~ /Name/ }
|
315
|
+
# Or not
|
316
|
+
UsersIndex.filter{ name(cache: true) =~ /Name/ }
|
317
|
+
```
|
318
|
+
|
319
|
+
Compliance cheatsheet for filters and DSL expressions:
|
320
|
+
|
321
|
+
* Term filter
|
322
|
+
|
323
|
+
```json
|
324
|
+
{"term": {"name": "Fred"}}
|
325
|
+
{"not": {"term": {"name": "Johny"}}}
|
326
|
+
```
|
327
|
+
|
328
|
+
```ruby
|
329
|
+
UsersIndex.filter{ name == 'Fred' }
|
330
|
+
UsersIndex.filter{ name != 'Johny' }
|
331
|
+
```
|
332
|
+
|
333
|
+
* Terms filter
|
334
|
+
|
335
|
+
```json
|
336
|
+
{"terms": {"name": ["Fred", "Johny"]}}
|
337
|
+
{"not": {"terms": {"name": ["Fred", "Johny"]}}}
|
338
|
+
|
339
|
+
{"terms": {"name": ["Fred", "Johny"], "execution": "or"}}
|
340
|
+
|
341
|
+
{"terms": {"name": ["Fred", "Johny"], "execution": "and"}}
|
342
|
+
|
343
|
+
{"terms": {"name": ["Fred", "Johny"], "execution": "bool"}}
|
344
|
+
|
345
|
+
{"terms": {"name": ["Fred", "Johny"], "execution": "fielddata"}}
|
346
|
+
```
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
UsersIndex.filter{ name == ['Fred', 'Johny'] }
|
350
|
+
UsersIndex.filter{ name != ['Fred', 'Johny'] }
|
351
|
+
|
352
|
+
UsersIndex.filter{ name(:|) == ['Fred', 'Johny'] }
|
353
|
+
UsersIndex.filter{ name(:or) == ['Fred', 'Johny'] }
|
354
|
+
UsersIndex.filter{ name(execution: :or) == ['Fred', 'Johny'] }
|
355
|
+
|
356
|
+
UsersIndex.filter{ name(:&) == ['Fred', 'Johny'] }
|
357
|
+
UsersIndex.filter{ name(:and) == ['Fred', 'Johny'] }
|
358
|
+
UsersIndex.filter{ name(execution: :and) == ['Fred', 'Johny'] }
|
359
|
+
|
360
|
+
UsersIndex.filter{ name(:b) == ['Fred', 'Johny'] }
|
361
|
+
UsersIndex.filter{ name(:bool) == ['Fred', 'Johny'] }
|
362
|
+
UsersIndex.filter{ name(execution: :bool) == ['Fred', 'Johny'] }
|
363
|
+
|
364
|
+
UsersIndex.filter{ name(:f) == ['Fred', 'Johny'] }
|
365
|
+
UsersIndex.filter{ name(:fielddata) == ['Fred', 'Johny'] }
|
366
|
+
UsersIndex.filter{ name(execution: :fielddata) == ['Fred', 'Johny'] }
|
367
|
+
```
|
368
|
+
|
369
|
+
* Regexp filter (== and =~ are equivalent)
|
370
|
+
|
371
|
+
```json
|
372
|
+
{"regexp": {"name.first": "s.*y"}}
|
373
|
+
|
374
|
+
{"not": {"regexp": {"name.first": "s.*y"}}}
|
375
|
+
|
376
|
+
{"regexp": {"name.first": {"value": "s.*y", "flags": "ANYSTRING|INTERSECTION"}}}
|
377
|
+
```
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
UsersIndex.filter{ name.first == /s.*y/ }
|
381
|
+
UsersIndex.filter{ name.first =~ /s.*y/ }
|
382
|
+
|
383
|
+
UsersIndex.filter{ name.first != /s.*y/ }
|
384
|
+
UsersIndex.filter{ name.first !~ /s.*y/ }
|
385
|
+
|
386
|
+
UsersIndex.filter{ name.first(:anystring, :intersection) == /s.*y/ }
|
387
|
+
UsersIndex.filter{ name.first(flags: [:anystring, :intersection]) == /s.*y/ }
|
388
|
+
```
|
389
|
+
|
390
|
+
* Prefix filter
|
391
|
+
|
392
|
+
```json
|
393
|
+
{"prefix": {"name": "Fre"}}
|
394
|
+
{"not": {"prefix": {"name": "Joh"}}}
|
395
|
+
```
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
UsersIndex.filter{ name =~ re' }
|
399
|
+
UsersIndex.filter{ name !~ 'Joh' }
|
400
|
+
```
|
401
|
+
|
402
|
+
* Exists filter
|
403
|
+
|
404
|
+
```json
|
405
|
+
{"exists": {"field": "name"}}
|
406
|
+
```
|
407
|
+
|
408
|
+
```ruby
|
409
|
+
UsersIndex.filter{ name? }
|
410
|
+
UsersIndex.filter{ !!name }
|
411
|
+
UsersIndex.filter{ !!name? }
|
412
|
+
UsersIndex.filter{ name != nil }
|
413
|
+
UsersIndex.filter{ !(name == nil) }
|
414
|
+
```
|
415
|
+
|
416
|
+
* Missing filter
|
417
|
+
|
418
|
+
```json
|
419
|
+
{"missing": {"field": "name", "existence": true, "null_value": false}}
|
420
|
+
{"missing": {"field": "name", "existence": true, "null_value": true}}
|
421
|
+
{"missing": {"field": "name", "existence": false, "null_value": true}}
|
422
|
+
```
|
423
|
+
|
424
|
+
```ruby
|
425
|
+
UsersIndex.filter{ !name }
|
426
|
+
UsersIndex.filter{ !name? }
|
427
|
+
UsersIndex.filter{ name == nil }
|
428
|
+
```
|
429
|
+
|
430
|
+
* Range
|
431
|
+
|
432
|
+
```json
|
433
|
+
{"range": {"age": {"gt": 42}}}
|
434
|
+
{"range": {"age": {"gte": 42}}}
|
435
|
+
{"range": {"age": {"lt": 42}}}
|
436
|
+
{"range": {"age": {"lte": 42}}}
|
437
|
+
|
438
|
+
{"range": {"age": {"gt": 40, "lt": 50}}}
|
439
|
+
{"range": {"age": {"gte": 40, "lte": 50}}}
|
440
|
+
|
441
|
+
{"range": {"age": {"gt": 40, "lte": 50}}}
|
442
|
+
{"range": {"age": {"gte": 40, "lt": 50}}}
|
443
|
+
```
|
444
|
+
|
445
|
+
```ruby
|
446
|
+
UsersIndex.filter{ age > 42 }
|
447
|
+
UsersIndex.filter{ age >= 42 }
|
448
|
+
UsersIndex.filter{ age < 42 }
|
449
|
+
UsersIndex.filter{ age <= 42 }
|
450
|
+
|
451
|
+
UsersIndex.filter{ age == (40..50) }
|
452
|
+
UsersIndex.filter{ (age > 40) & (age < 50) }
|
453
|
+
UsersIndex.filter{ age == [40..50] }
|
454
|
+
UsersIndex.filter{ (age >= 40) & (age <= 50) }
|
455
|
+
|
456
|
+
UsersIndex.filter{ (age > 40) & (age <= 50) }
|
457
|
+
UsersIndex.filter{ (age >= 40) & (age < 50) }
|
458
|
+
```
|
459
|
+
|
460
|
+
* Bool filter
|
461
|
+
|
462
|
+
```json
|
463
|
+
{"bool": {
|
464
|
+
"must": [{"term": {"name": "Name"}}],
|
465
|
+
"should": [{"term": {"age": 42}}, {"term": {"age": 45}}]
|
466
|
+
}}
|
467
|
+
```
|
468
|
+
|
469
|
+
```ruby
|
470
|
+
UsersIndex.filter{ must(name == 'Name').should(age == 42, age == 45) }
|
471
|
+
```
|
472
|
+
|
473
|
+
* And filter
|
474
|
+
|
475
|
+
```json
|
476
|
+
{"and": [{"term": {"name": "Name"}}, {"range": {"age": {"lt": 42}}}]}
|
477
|
+
```
|
478
|
+
|
479
|
+
```ruby
|
480
|
+
UsersIndex.filter{ (name == 'Name') & (age < 42) }
|
481
|
+
```
|
482
|
+
|
483
|
+
* Or filter
|
484
|
+
|
485
|
+
```json
|
486
|
+
{"or": [{"term": {"name": "Name"}}, {"range": {"age": {"lt": 42}}}]}
|
487
|
+
```
|
488
|
+
|
489
|
+
```ruby
|
490
|
+
UsersIndex.filter{ (name == 'Name') | (age < 42) }
|
491
|
+
```
|
492
|
+
|
493
|
+
```json
|
494
|
+
{"not": {"term": {"name": "Name"}}}
|
495
|
+
{"not": {"range": {"age": {"lt": 42}}}}
|
496
|
+
```
|
497
|
+
|
498
|
+
```ruby
|
499
|
+
UsersIndex.filter{ !(name == 'Name') } # or UsersIndex.filter{ name != 'Name' }
|
500
|
+
UsersIndex.filter{ !(age < 42) }
|
501
|
+
```
|
502
|
+
|
503
|
+
See [context.rb](lib/chewy/query/context.rb) for more info.
|
504
|
+
|
177
505
|
### Objects loading
|
178
506
|
|
179
507
|
It is possible to load source objects from database for every search result:
|
180
508
|
|
181
509
|
```ruby
|
182
|
-
|
510
|
+
scope = UsersIndex.filter(range: {rating: {gte: 100}})
|
511
|
+
|
512
|
+
scope.load # => will return User instances array (not a scope because )
|
513
|
+
scope.load(user: { scope: ->(_) { includes(:country) }}) # you can also pass loading scopes for each
|
514
|
+
# possibly returned type
|
515
|
+
scope.load(user: { scope: User.includes(:country) }) # the second scope passing way
|
516
|
+
scope.only(:id).load # it is optimal to request ids only if you are not planning to use type objects
|
517
|
+
```
|
518
|
+
|
519
|
+
### Rake tasks
|
183
520
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
521
|
+
Inside Rails application some index mantaining rake tasks are defined.
|
522
|
+
|
523
|
+
```bash
|
524
|
+
rake chewy:reset:all # resets all the existing indexes, declared in app/chewy
|
525
|
+
rake chewy:reset[users] # resets UsersIndex
|
526
|
+
rake chewy:update[users] # updates UsersIndex
|
527
|
+
```
|
528
|
+
|
529
|
+
### Rspec integration
|
530
|
+
|
531
|
+
Just add `require 'chewy/rspec'` to your spec_helper.rb and you will get additional features:
|
532
|
+
|
533
|
+
#### `update_index` matcher
|
534
|
+
|
535
|
+
```ruby
|
536
|
+
# just update index expectation. Used type class as argument.
|
537
|
+
specify { expect { user.save! }.to update_index(UsersIndex.user) }
|
538
|
+
# expect do not update target index. Type for `update_index` might be specified via string also
|
539
|
+
specify { expect { user.name = 'Duke' }.not_to update_index('users#user') }
|
540
|
+
# expecting update specified objects
|
541
|
+
specify { expect { user.save! }.to update_index(UsersIndex.user).and_reindex(user) }
|
542
|
+
# you can specify even id
|
543
|
+
specify { expect { user.save! }.to update_index(UsersIndex.user).and_reindex(42) }
|
544
|
+
# expected multiple objects to be reindexed
|
545
|
+
specify { expect { [user1, user2].map(&:save!) }
|
546
|
+
.to update_index(UsersIndex.user).and_reindex(user1, user2) }
|
547
|
+
specify { expect { [user1, user2].map(&:save!) }
|
548
|
+
.to update_index(UsersIndex.user).and_reindex(user1).and_reindex(user2) }
|
549
|
+
# expect object to be reindexed exact twice
|
550
|
+
specify { expect { 2.times { user.save! } }
|
551
|
+
.to update_index(UsersIndex.user).and_reindex(user, times: 2) }
|
552
|
+
# expect object in index to be updated with specified fields
|
553
|
+
specify { expect { user.update_attributes!(name: 'Duke') }
|
554
|
+
.to update_index(UsersIndex.user).and_reindex(user, with: {name: 'Duke'}) }
|
555
|
+
# combination of previous two
|
556
|
+
specify { expect { 2.times { user.update_attributes!(name: 'Duke') } }
|
557
|
+
.to update_index(UsersIndex.user).and_reindex(user, times: 2, with: {name: 'Duke'}) }
|
558
|
+
# for every object
|
559
|
+
specify { expect { 2.times { [user1, user2].map { |u| u.update_attributes!(name: 'Duke') } } }
|
560
|
+
.to update_index(UsersIndex.user).and_reindex(user1, user2, times: 2, with: {name: 'Duke'}) }
|
561
|
+
# for every object splitted
|
562
|
+
specify { expect { 2.times { [user1, user2].map { |u| u.update_attributes!(name: "Duke#{u.id}") } } }
|
563
|
+
.to update_index(UsersIndex.user)
|
564
|
+
.and_reindex(user1, with: {name: 'Duke42'}) }
|
565
|
+
.and_reindex(user2, times: 1, with: {name: 'Duke43'}) }
|
566
|
+
# object deletion same abilities as `and_reindex`, except `:with` option
|
567
|
+
specify { expect { user.destroy! }.to update_index(UsersIndex.user).and_delete(user) }
|
568
|
+
# double deletion, whatever it means
|
569
|
+
specify { expect { 2.times { user.destroy! } }.to update_index(UsersIndex.user).and_delete(user, times: 2) }
|
570
|
+
# alltogether
|
571
|
+
specify { expect { user1.destroy!; user2.save! } }
|
572
|
+
.to update_index(UsersIndex.user).and_reindex(user2).and_delete(user1)
|
573
|
+
```
|
574
|
+
|
575
|
+
```ruby
|
576
|
+
# strictly specifing updated and deleted records
|
577
|
+
specify { expect { [user1, user2].map(&:save!) }
|
578
|
+
.to update_index(UsersIndex.user).and_reindex(user1, user2).only }
|
579
|
+
specify { expect { [user1, user2].map(&:destroy!) }
|
580
|
+
.to update_index(UsersIndex.user).and_delete(user1, user2).only }
|
581
|
+
# this will fail
|
582
|
+
specify { expect { [user1, user2].map(&:save!) }
|
583
|
+
.to update_index(UsersIndex.user).and_reindex(user1).only }
|
188
584
|
```
|
189
585
|
|
190
586
|
## TODO a.k.a coming soon:
|
@@ -192,8 +588,6 @@ It is possible to load source objects from database for every search result:
|
|
192
588
|
* Dynamic templates additional DSL
|
193
589
|
* Typecasting support
|
194
590
|
* Advanced (simplyfied) query DSL: `UsersIndex.query { email == 'my@gmail.com' }` will produce term query
|
195
|
-
* Remove Index.search method, all the query DSL methods should be delegated to the Index
|
196
|
-
* Observing strategies reworking
|
197
591
|
* update_all support
|
198
592
|
* Other than ActiveRecord ORMs support (Mongoid)
|
199
593
|
* Maybe, closer ORM/ODM integration, creating index classes implicitly
|
@@ -201,8 +595,9 @@ It is possible to load source objects from database for every search result:
|
|
201
595
|
|
202
596
|
## Contributing
|
203
597
|
|
204
|
-
1. Fork it ( http://github.com
|
598
|
+
1. Fork it ( http://github.com/toptal/chewy/fork )
|
205
599
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
206
|
-
3.
|
207
|
-
4.
|
208
|
-
5.
|
600
|
+
3. Implement your changes, cover it with specs and make sure old specs are passing
|
601
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
602
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
603
|
+
6. Create new Pull Request
|