chewy 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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