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.
Files changed (80) hide show
  1. checksums.yaml +13 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -3
  4. data/CHANGELOG.md +75 -0
  5. data/README.md +487 -92
  6. data/Rakefile +3 -2
  7. data/chewy.gemspec +2 -2
  8. data/filters +76 -0
  9. data/lib/chewy.rb +5 -3
  10. data/lib/chewy/config.rb +36 -19
  11. data/lib/chewy/fields/base.rb +5 -1
  12. data/lib/chewy/index.rb +22 -10
  13. data/lib/chewy/index/actions.rb +13 -13
  14. data/lib/chewy/index/search.rb +7 -2
  15. data/lib/chewy/query.rb +382 -64
  16. data/lib/chewy/query/context.rb +174 -0
  17. data/lib/chewy/query/criteria.rb +127 -34
  18. data/lib/chewy/query/loading.rb +9 -9
  19. data/lib/chewy/query/nodes/and.rb +25 -0
  20. data/lib/chewy/query/nodes/base.rb +17 -0
  21. data/lib/chewy/query/nodes/bool.rb +32 -0
  22. data/lib/chewy/query/nodes/equal.rb +34 -0
  23. data/lib/chewy/query/nodes/exists.rb +20 -0
  24. data/lib/chewy/query/nodes/expr.rb +28 -0
  25. data/lib/chewy/query/nodes/field.rb +106 -0
  26. data/lib/chewy/query/nodes/missing.rb +20 -0
  27. data/lib/chewy/query/nodes/not.rb +25 -0
  28. data/lib/chewy/query/nodes/or.rb +25 -0
  29. data/lib/chewy/query/nodes/prefix.rb +18 -0
  30. data/lib/chewy/query/nodes/query.rb +20 -0
  31. data/lib/chewy/query/nodes/range.rb +63 -0
  32. data/lib/chewy/query/nodes/raw.rb +15 -0
  33. data/lib/chewy/query/nodes/regexp.rb +31 -0
  34. data/lib/chewy/query/nodes/script.rb +20 -0
  35. data/lib/chewy/query/pagination.rb +28 -22
  36. data/lib/chewy/railtie.rb +23 -0
  37. data/lib/chewy/rspec/update_index.rb +20 -3
  38. data/lib/chewy/type/adapter/active_record.rb +78 -5
  39. data/lib/chewy/type/adapter/base.rb +46 -0
  40. data/lib/chewy/type/adapter/object.rb +40 -8
  41. data/lib/chewy/type/base.rb +1 -1
  42. data/lib/chewy/type/import.rb +18 -44
  43. data/lib/chewy/type/observe.rb +24 -14
  44. data/lib/chewy/version.rb +1 -1
  45. data/lib/tasks/chewy.rake +27 -0
  46. data/spec/chewy/config_spec.rb +30 -12
  47. data/spec/chewy/fields/base_spec.rb +11 -5
  48. data/spec/chewy/index/actions_spec.rb +20 -20
  49. data/spec/chewy/index/search_spec.rb +5 -5
  50. data/spec/chewy/index_spec.rb +28 -8
  51. data/spec/chewy/query/context_spec.rb +173 -0
  52. data/spec/chewy/query/criteria_spec.rb +219 -12
  53. data/spec/chewy/query/loading_spec.rb +6 -4
  54. data/spec/chewy/query/nodes/and_spec.rb +16 -0
  55. data/spec/chewy/query/nodes/bool_spec.rb +22 -0
  56. data/spec/chewy/query/nodes/equal_spec.rb +32 -0
  57. data/spec/chewy/query/nodes/exists_spec.rb +18 -0
  58. data/spec/chewy/query/nodes/missing_spec.rb +15 -0
  59. data/spec/chewy/query/nodes/not_spec.rb +16 -0
  60. data/spec/chewy/query/nodes/or_spec.rb +16 -0
  61. data/spec/chewy/query/nodes/prefix_spec.rb +16 -0
  62. data/spec/chewy/query/nodes/query_spec.rb +12 -0
  63. data/spec/chewy/query/nodes/range_spec.rb +32 -0
  64. data/spec/chewy/query/nodes/raw_spec.rb +11 -0
  65. data/spec/chewy/query/nodes/regexp_spec.rb +31 -0
  66. data/spec/chewy/query/nodes/script_spec.rb +15 -0
  67. data/spec/chewy/query/pagination_spec.rb +3 -2
  68. data/spec/chewy/query_spec.rb +83 -26
  69. data/spec/chewy/rspec/update_index_spec.rb +20 -0
  70. data/spec/chewy/type/adapter/active_record_spec.rb +102 -0
  71. data/spec/chewy/type/adapter/object_spec.rb +82 -0
  72. data/spec/chewy/type/import_spec.rb +30 -1
  73. data/spec/chewy/type/mapping_spec.rb +1 -1
  74. data/spec/chewy/type/observe_spec.rb +46 -12
  75. data/spec/spec_helper.rb +7 -6
  76. data/spec/support/class_helpers.rb +2 -2
  77. metadata +98 -48
  78. data/.rvmrc +0 -1
  79. data/lib/chewy/index/client.rb +0 -13
  80. data/spec/chewy/index/client_spec.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1731bb408069ab82feb6979f9f70008a0977cb48
4
- data.tar.gz: b676bfdc501aece147b1af8a915f99defd0f2eaa
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MzNiZmUxMGUxNzE1ODdkYzQzYTAyMzAwN2VhNDhmZDkzYzUyMDZhNQ==
5
+ data.tar.gz: !binary |-
6
+ MWIwNDA3MzMyNzRlZGM2ODllODA4ZjAxMzI0NzM5OTc3MGU4NjViMA==
5
7
  SHA512:
6
- metadata.gz: 7ce0646699822d696680438f2be44d12b4a9829470362e841e357e059759474c4a54fdbed4c4b5ec6946b2d50800fd7e8be3a3f31d33e7ac987705fb5622266c
7
- data.tar.gz: 5ede42795877994577c4aac44c5898dd7abacc7d922db0a1563b0e5e1564d8c02fec987eb4f01014f7fc46c4c20811aab5cc4f17280d734be6f7f5ba73fa8c0f
8
+ metadata.gz: !binary |-
9
+ MDk1NWIzZmYzYmZhNjkzOTI0OTY0MWVkYjYzNDE2N2FiZGUzOGQ1YjI0NzQ4
10
+ OTgzMTgyNDI2M2RlMjJhMWQzMjk1MzZjOGFkODQ3NzFjZmEwOTQ2M2UzNDMy
11
+ MDJjY2Y0ZmFiZjU5NjY3MTE5MzQ3NDk0ZDg4MzdjZDJkOTZhNGI=
12
+ data.tar.gz: !binary |-
13
+ NWEyODY1M2JhZjEwMDI1MTUyNGQ1NzM4ZTU0NTgyYWRkOWM3NWI0ZmQ2ZmY1
14
+ ODkyMjc5OTQwNThlODA2YjY3MGM5MTQyZDA5NzMzZDBjOWYyYmI2NmMyNGQx
15
+ OTU3ZGM3OTEyOGJjMWIyMmEwYWY2MjkzOGJmM2Q3MzQ2MzgzMGE=
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .rvmrc
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
- services:
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
- class UsersIndex < Chewy::Index
29
+ class UsersIndex < Chewy::Index
27
30
 
28
- end
31
+ end
29
32
  ```
30
33
 
31
34
  2. Add one or more types mapping
32
35
 
33
36
  ```ruby
34
- class UsersIndex < Chewy::Index
35
- define_type User.active # or just model instead_of scope: define_type User
36
- end
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
- class UsersIndex < Chewy::Index
45
- define_type User.active.includes(:country, :bages, :projects) do
46
- field :first_name, :last_name # multiple fields without additional options
47
- field :email, analyzer: 'email' # elasticsearch-related options
48
- field :country, value: ->(user) { user.country.name } # custom value proc
49
- field :bages, value: ->(user) { user.bages.map(&:name) } # passing array values to index
50
- field :projects, type: 'object' do # the same syntax for `multi_field`
51
- field :title
52
- field :description # default data type is `string`
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
- class UsersIndex < Chewy::Index
66
- settings analysis: {
67
- analyzer: {
68
- email: {
69
- tokenizer: 'keyword',
70
- filter: ['lowercase']
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
- define_type User.active.includes(:country, :bages, :projects) do
76
- root _boost: { name: :_boost, null_value: 1.0 } do # optional `root` object settings
77
- field :first_name, :last_name # multiple fields without additional options
78
- field :email, analyzer: 'email' # elasticsearch-related options
79
- field :country, value: ->(user) { user.country.name } # custom value proc
80
- field :bages, value: ->(user) { user.bages.map(&:name) } # passing array values to index
81
- field :projects, type: 'object' do # the same syntax for `multi_field`
82
- field :title
83
- field :description # default data type is `string`
84
- end
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
- class User < ActiveRecord::Base
100
- update_elasticsearch('users#user') { self } # specifying index, type and backreference
101
- # for updating after user save or destroy
102
- end
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
- class Country < ActiveRecord::Base
105
- has_many :users
109
+ class Country < ActiveRecord::Base
110
+ has_many :users
106
111
 
107
- update_elasticsearch('users#user') { users } # return single object or collection
108
- end
112
+ update_index('users#user') { users } # return single object or collection
113
+ end
109
114
 
110
- class Project < ActiveRecord::Base
111
- update_elasticsearch('users#user') { user if user.active? } # you can return even `nil` from the backreference
112
- end
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
- class Bage < ActiveRecord::Base
115
- has_and_belongs_to_many :users
119
+ class Bage < ActiveRecord::Base
120
+ has_and_belongs_to_many :users
116
121
 
117
- update_elasticsearch('users') { users } # if index has only one type
118
- # there is no need to specify updated type
119
- end
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
- UsersIndex.index_delete # destroy index if exists
126
- UsersIndex.index_create! # use bang or non-bang methods
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
- UsersIndex.import User.where('rating > 100') # or import specified users
131
- UsersIndex.import [1, 2, 42] # pass even ids for import, it will be handled in the most effective way
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 2 strategies for index updating: updating right after save and cummulative update. The first is by default. To perform the second one, use `Chewy.atomic`:
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
- Chewy.atomic do
142
- user.each { |user| user.update_attributes(name: user.name.strip) }
143
- end
182
+ class User < ActiveRecord::Base
183
+ update_index 'users#user', 'self', urgent: true
184
+ end
144
185
  ```
145
186
 
146
- Index update will be performed once per Chewy.atomic block. This strategy is highly usable for rails actions:
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
- class ApplicationController < ActionController::Base
150
- around_action { |&block| Chewy.atomic(&block) }
151
- end
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
- scope = UsersIndex.search.query(term: {name: 'foo'})
158
- .filter({numeric_range: {rating: {gte: 100}}})
159
- .order(created_at: :desc)
160
- .limit(20).offset(100)
161
-
162
- scope.to_a # => will produce array of UserIndex::User or other types instances
163
- scope.map { |user| user.email }
164
- scope.total_count # => will return total objects count
165
-
166
- scope.per(10).page(3) # supports kaminari pagination
167
- scope.explain.map { |user| user._explanation }
168
- scope.only(:id, :email) # returns ids and emails only
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
- UsersIndex.search.query(term: {name: 'foo'}).count # will return UserIndex::User array only
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
- scope = UsersIndex.search.filter({numeric_range: {rating: {gte: 100}}})
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
- scope.load # => will return User instances array (not a scope because )
185
- scope.load(scopes: { user: ->(_) { includes(:country) }}) # => you can also pass loading scopes for each
186
- # possibly returned type
187
- scope.only(:id).load # it is optimal to request ids only if you are not planning to use type objects
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/<my-github-username>/chewy/fork )
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. Commit your changes (`git commit -am 'Add some feature'`)
207
- 4. Push to the branch (`git push origin my-new-feature`)
208
- 5. Create new Pull Request
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