chewy 0.0.1 → 0.1.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 +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
|
+
[](https://travis-ci.org/toptal/chewy)
|
2
|
+
[](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
|