chewy 0.1.0 → 0.2.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 (55) hide show
  1. checksums.yaml +5 -13
  2. data/.travis.yml +3 -2
  3. data/CHANGELOG.md +28 -2
  4. data/README.md +156 -7
  5. data/filters +6 -4
  6. data/lib/chewy/index.rb +114 -18
  7. data/lib/chewy/index/actions.rb +117 -14
  8. data/lib/chewy/index/aliases.rb +21 -0
  9. data/lib/chewy/query.rb +5 -5
  10. data/lib/chewy/query/compose.rb +59 -0
  11. data/lib/chewy/query/criteria.rb +8 -55
  12. data/lib/chewy/query/{context.rb → filters.rb} +60 -7
  13. data/lib/chewy/query/loading.rb +1 -1
  14. data/lib/chewy/query/nodes/has_child.rb +14 -0
  15. data/lib/chewy/query/nodes/has_parent.rb +14 -0
  16. data/lib/chewy/query/nodes/has_relation.rb +61 -0
  17. data/lib/chewy/query/nodes/match_all.rb +11 -0
  18. data/lib/chewy/railtie.rb +2 -2
  19. data/lib/chewy/rspec/update_index.rb +1 -0
  20. data/lib/chewy/type.rb +1 -1
  21. data/lib/chewy/type/adapter/active_record.rb +28 -12
  22. data/lib/chewy/type/adapter/object.rb +12 -2
  23. data/lib/chewy/type/import.rb +60 -20
  24. data/lib/chewy/type/wrapper.rb +1 -1
  25. data/lib/chewy/version.rb +1 -1
  26. data/lib/tasks/chewy.rake +41 -8
  27. data/spec/chewy/index/actions_spec.rb +282 -2
  28. data/spec/chewy/index/aliases_spec.rb +50 -0
  29. data/spec/chewy/index_spec.rb +64 -0
  30. data/spec/chewy/query/criteria_spec.rb +11 -11
  31. data/spec/chewy/query/{context_spec.rb → filters_spec.rb} +2 -2
  32. data/spec/chewy/query/loading_spec.rb +5 -3
  33. data/spec/chewy/query/nodes/and_spec.rb +1 -1
  34. data/spec/chewy/query/nodes/bool_spec.rb +1 -1
  35. data/spec/chewy/query/nodes/equal_spec.rb +1 -1
  36. data/spec/chewy/query/nodes/exists_spec.rb +1 -1
  37. data/spec/chewy/query/nodes/has_child_spec.rb +40 -0
  38. data/spec/chewy/query/nodes/has_parent_spec.rb +40 -0
  39. data/spec/chewy/query/nodes/match_all_spec.rb +11 -0
  40. data/spec/chewy/query/nodes/missing_spec.rb +1 -1
  41. data/spec/chewy/query/nodes/not_spec.rb +1 -1
  42. data/spec/chewy/query/nodes/or_spec.rb +1 -1
  43. data/spec/chewy/query/nodes/prefix_spec.rb +1 -1
  44. data/spec/chewy/query/nodes/query_spec.rb +1 -1
  45. data/spec/chewy/query/nodes/range_spec.rb +1 -1
  46. data/spec/chewy/query/nodes/raw_spec.rb +1 -1
  47. data/spec/chewy/query/nodes/regexp_spec.rb +1 -1
  48. data/spec/chewy/query/nodes/script_spec.rb +1 -1
  49. data/spec/chewy/query/pagination_spec.rb +7 -7
  50. data/spec/chewy/query_spec.rb +60 -0
  51. data/spec/chewy/type/adapter/active_record_spec.rb +68 -9
  52. data/spec/chewy/type/adapter/object_spec.rb +18 -0
  53. data/spec/chewy/type/import_spec.rb +87 -2
  54. data/spec/spec_helper.rb +1 -0
  55. metadata +47 -33
@@ -1,41 +1,144 @@
1
1
  module Chewy
2
2
  class Index
3
+ # Module provides per-index actions, such as deletion,
4
+ # creation and existance check.
5
+ #
3
6
  module Actions
4
7
  extend ActiveSupport::Concern
5
8
 
6
9
  module ClassMethods
10
+ # Checks index existance. Returns true or false
11
+ #
12
+ # UsersIndex.exist? #=> true
13
+ #
7
14
  def exists?
8
15
  client.indices.exists(index: index_name)
9
16
  end
10
17
 
11
- def create
12
- create!
18
+ # Creates index and applies mappings and settings.
19
+ # Returns false in case of unsuccessful creation.
20
+ #
21
+ # UsersIndex.create # creates index named `users`
22
+ #
23
+ # Index name suffix might be passed optionally. In this case,
24
+ # method creates index with suffix and makes unsuffixed alias
25
+ # for it.
26
+ #
27
+ # UsersIndex.create '01-2013' # creates index `uses_01-2013` and alias `users` for it
28
+ # UsersIndex.create '01-2013', alias: false # creates index `uses_01-2013` only and no alias
29
+ #
30
+ # Suffixed index names might be used for zero-downtime mapping change, for example.
31
+ # Description: (http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/).
32
+ #
33
+ def create *args
34
+ create! *args
13
35
  rescue Elasticsearch::Transport::Transport::Errors::BadRequest
14
36
  false
15
37
  end
16
38
 
17
- def create!
18
- client.indices.create(index: index_name, body: index_params)
39
+ # Creates index and applies mappings and settings.
40
+ # Raises elasticsearch-ruby transport error in case of
41
+ # unsuccessfull creation.
42
+ #
43
+ # UsersIndex.create! # creates index named `users`
44
+ #
45
+ # Index name suffix might be passed optionally. In this case,
46
+ # method creates index with suffix and makes unsuffixed alias
47
+ # for it.
48
+ #
49
+ # UsersIndex.create! '01-2014' # creates index `users_01-2014` and alias `users` for it
50
+ # UsersIndex.create! '01-2014', alias: false # creates index `users_01-2014` only and no alias
51
+ #
52
+ # Suffixed index names might be used for zero-downtime mapping change, for example.
53
+ # Description: (http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/).
54
+ #
55
+ def create! *args
56
+ options = args.extract_options!.reverse_merge!(alias: true)
57
+ name = build_index_name(suffix: args.first)
58
+ result = client.indices.create(index: name, body: index_params)
59
+ result &&= client.indices.put_alias(index: name, name: index_name) if options[:alias] && name != index_name
60
+ result
19
61
  end
20
62
 
21
- def delete
22
- delete!
63
+ # Deletes ES index. Returns false in case of error.
64
+ #
65
+ # UsersIndex.delete # deletes `users` index
66
+ #
67
+ # Supports index suffix passed as the first argument
68
+ #
69
+ # UsersIndex.delete '01-2014' # deletes `users_01-2014` index
70
+ #
71
+ def delete suffix = nil
72
+ delete! suffix
23
73
  rescue Elasticsearch::Transport::Transport::Errors::NotFound
24
74
  false
25
75
  end
26
76
 
27
- def delete!
28
- client.indices.delete(index: index_name)
77
+ # Deletes ES index. Raises elasticsearch-ruby transport error
78
+ # in case of error.
79
+ #
80
+ # UsersIndex.delete # deletes `users` index
81
+ #
82
+ # Supports index suffix passed as the first argument
83
+ #
84
+ # UsersIndex.delete '01-2014' # deletes `users_01-2014` index
85
+ #
86
+ def delete! suffix = nil
87
+ client.indices.delete index: build_index_name(suffix: suffix)
29
88
  end
30
89
 
31
- def purge
32
- delete
33
- create
90
+ # Deletes and recreates index. Supports suffixes.
91
+ # Returns result of index creation.
92
+ #
93
+ # UsersIndex.purge # deletes and creates `users` index
94
+ # UsersIndex.purge '01-2014' # deletes `users` and `users_01-2014` indexes, creates `users_01-2014`
95
+ #
96
+ def purge suffix = nil
97
+ delete if suffix.present?
98
+ delete suffix
99
+ create suffix
34
100
  end
35
101
 
36
- def purge!
37
- delete
38
- create!
102
+ # Deletes and recreates index. Supports suffixes.
103
+ # Returns result of index creation. Raises error in case
104
+ # of unsuccessfull creation
105
+ #
106
+ # UsersIndex.purge! # deletes and creates `users` index
107
+ # UsersIndex.purge! '01-2014' # deletes `users` and `users_01-2014` indexes, creates `users_01-2014`
108
+ #
109
+ def purge! suffix = nil
110
+ delete if suffix.present?
111
+ delete suffix
112
+ create! suffix
113
+ end
114
+
115
+ # Deletes, creates and imports data to the index.
116
+ # Returns import result
117
+ #
118
+ # UsersIndex.reset!
119
+ #
120
+ # If index name suffix passed as the first argument - performs
121
+ # zero-downtime index resetting (described here:
122
+ # http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/).
123
+ #
124
+ # UsersIndex.reset! Time.now.to_i
125
+ #
126
+ def reset! suffix = nil
127
+ if suffix.present? && (indexes = self.indexes).any?
128
+ create! suffix, alias: false
129
+ result = import suffix: suffix
130
+ client.indices.update_aliases body: {actions: [
131
+ *indexes.map do |index|
132
+ {remove: {index: index, alias: index_name}}
133
+ end,
134
+ {add: {index: build_index_name(suffix: suffix), alias: index_name}}
135
+ ]}
136
+ client.indices.delete index: indexes if indexes.any?
137
+ result
138
+ else
139
+ purge! suffix
140
+ import
141
+ end
39
142
  end
40
143
  end
41
144
  end
@@ -0,0 +1,21 @@
1
+ module Chewy
2
+ class Index
3
+ module Aliases
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def indexes
8
+ client.indices.get_alias(name: index_name).keys
9
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
10
+ []
11
+ end
12
+
13
+ def aliases
14
+ client.indices.get_alias(index: index_name, name: '*')[index_name].try(:[], 'aliases').try(:keys) || []
15
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
16
+ []
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/chewy/query.rb CHANGED
@@ -4,7 +4,7 @@ rescue LoadError
4
4
  end
5
5
 
6
6
  require 'chewy/query/criteria'
7
- require 'chewy/query/context'
7
+ require 'chewy/query/filters'
8
8
  require 'chewy/query/loading'
9
9
  require 'chewy/query/pagination'
10
10
 
@@ -281,7 +281,7 @@ module Chewy
281
281
  # See <tt>#filter_mode</tt> chainable method for more info.
282
282
  #
283
283
  # Also this method supports block DSL.
284
- # See <tt>Chewy::Query::Context</tt> for more info.
284
+ # See <tt>Chewy::Query::Filters</tt> for more info.
285
285
  #
286
286
  # UsersIndex.filter(term: {name: 'Johny'}).filter(range: {age: {lte: 42}})
287
287
  # UsersIndex::User.filter(term: {name: 'Johny'}).filter(range: {age: {lte: 42}})
@@ -301,7 +301,7 @@ module Chewy
301
301
  # }}}}
302
302
  #
303
303
  def filter params = nil, &block
304
- params = Context.new(&block).__render__ if block
304
+ params = Filters.new(&block).__render__ if block
305
305
  chain { criteria.update_filters params }
306
306
  end
307
307
 
@@ -450,8 +450,8 @@ module Chewy
450
450
  def _results
451
451
  @_results ||= _response['hits']['hits'].map do |hit|
452
452
  attributes = hit['_source'] || hit['fields'] || {}
453
- attributes.reverse_merge!(id: hit['_id']).merge!(_score: hit['_score'])
454
- attributes.merge!(_explain: hit['_explanation']) if hit['_explanation']
453
+ attributes.reverse_merge!(id: hit['_id'])
454
+ .merge!(_score: hit['_score'], _explanation: hit['_explanation'])
455
455
  index.type_hash[hit['_type']].new attributes
456
456
  end
457
457
  end
@@ -0,0 +1,59 @@
1
+ module Chewy
2
+ class Query
3
+ module Compose
4
+
5
+ protected
6
+
7
+ def _composed_query queries, filters
8
+ if filters
9
+ {query: {
10
+ filtered: {
11
+ query: queries ? queries : {match_all: {}},
12
+ filter: filters
13
+ }
14
+ }}
15
+ elsif queries
16
+ {query: queries}
17
+ end
18
+ end
19
+
20
+ def _queries_join queries, logic
21
+ queries = queries.compact
22
+
23
+ if queries.many?
24
+ case logic
25
+ when :dis_max
26
+ {dis_max: {queries: queries}}
27
+ when :must, :should
28
+ {bool: {logic => queries}}
29
+ else
30
+ if logic.is_a?(Float)
31
+ {dis_max: {queries: queries, tie_breaker: logic}}
32
+ else
33
+ {bool: {should: queries, minimum_should_match: logic}}
34
+ end
35
+ end
36
+ else
37
+ queries.first
38
+ end
39
+ end
40
+
41
+ def _filters_join filters, logic
42
+ filters = filters.compact
43
+
44
+ if filters.many?
45
+ case logic
46
+ when :and, :or
47
+ {logic => filters}
48
+ when :must, :should
49
+ {bool: {logic => filters}}
50
+ else
51
+ {bool: {should: filters, minimum_should_match: logic}}
52
+ end
53
+ else
54
+ filters.first
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,6 +1,9 @@
1
+ require 'chewy/query/compose'
2
+
1
3
  module Chewy
2
4
  class Query
3
5
  class Criteria
6
+ include Compose
4
7
  STORAGES = [:options, :queries, :facets, :filters, :sort, :fields, :types]
5
8
 
6
9
  def initialize options = {}
@@ -36,11 +39,11 @@ module Chewy
36
39
  end
37
40
 
38
41
  def update_queries(modifer)
39
- @queries = queries + Array.wrap(modifer).delete_if(&:blank?)
42
+ @queries = queries + Array.wrap(modifer).reject(&:blank?)
40
43
  end
41
44
 
42
45
  def update_filters(modifer)
43
- @filters = filters + Array.wrap(modifer).delete_if(&:blank?)
46
+ @filters = filters + Array.wrap(modifer).reject(&:blank?)
44
47
  end
45
48
 
46
49
  def update_sort(modifer, options = {})
@@ -55,7 +58,7 @@ module Chewy
55
58
  define_method "update_#{storage}" do |modifer, options = {}|
56
59
  variable = "@#{storage}"
57
60
  instance_variable_set(variable, nil) if options[:purge]
58
- modifer = send(storage) | Array.wrap(modifer).flatten.map(&:to_s).delete_if(&:blank?)
61
+ modifer = send(storage) | Array.wrap(modifer).flatten.map(&:to_s).reject(&:blank?)
59
62
  instance_variable_set(variable, modifer)
60
63
  end
61
64
  end
@@ -72,7 +75,7 @@ module Chewy
72
75
  end
73
76
 
74
77
  def request_body
75
- body = (_request_query || {}).tap do |body|
78
+ body = (_composed_query(_request_query, _request_filter) || {}).tap do |body|
76
79
  body.merge!(facets: facets) if facets?
77
80
  body.merge!(sort: sort) if sort?
78
81
  body.merge!(fields: fields) if fields?
@@ -102,19 +105,7 @@ module Chewy
102
105
  end
103
106
 
104
107
  def _request_query
105
- request_filter = _request_filter
106
- request_query = _queries_join(queries, options[:query_mode])
107
-
108
- if request_filter
109
- {query: {
110
- filtered: {
111
- query: request_query ? request_query : {match_all: {}},
112
- filter: request_filter
113
- }
114
- }}
115
- elsif request_query
116
- {query: request_query}
117
- end
108
+ _queries_join(queries, options[:query_mode])
118
109
  end
119
110
 
120
111
  def _request_filter
@@ -131,44 +122,6 @@ module Chewy
131
122
  def _request_types
132
123
  _filters_join(types.map { |type| {type: {value: type}} }, :or)
133
124
  end
134
-
135
- def _queries_join queries, logic
136
- queries = queries.compact
137
-
138
- if queries.many?
139
- case logic
140
- when :dis_max
141
- {dis_max: {queries: queries}}
142
- when :must, :should
143
- {bool: {logic => queries}}
144
- else
145
- if logic.is_a?(Float)
146
- {dis_max: {queries: queries, tie_breaker: logic}}
147
- else
148
- {bool: {should: queries, minimum_should_match: logic}}
149
- end
150
- end
151
- else
152
- queries.first
153
- end
154
- end
155
-
156
- def _filters_join filters, logic
157
- filters = filters.compact
158
-
159
- if filters.many?
160
- case logic
161
- when :and, :or
162
- {logic => filters}
163
- when :must, :should
164
- {bool: {logic => filters}}
165
- else
166
- {bool: {should: filters, minimum_should_match: logic}}
167
- end
168
- else
169
- filters.first
170
- end
171
- end
172
125
  end
173
126
  end
174
127
  end
@@ -14,6 +14,9 @@ require 'chewy/query/nodes/regexp'
14
14
  require 'chewy/query/nodes/equal'
15
15
  require 'chewy/query/nodes/query'
16
16
  require 'chewy/query/nodes/script'
17
+ require 'chewy/query/nodes/has_child'
18
+ require 'chewy/query/nodes/has_parent'
19
+ require 'chewy/query/nodes/match_all'
17
20
 
18
21
  module Chewy
19
22
  class Query
@@ -24,10 +27,10 @@ module Chewy
24
27
  # UsersIndex.filter{ (article.title =~ /Honey/) & (age < 42) & !rate }
25
28
  #
26
29
  #
27
- class Context
28
- def initialize &block
30
+ class Filters
31
+ def initialize outer = nil, &block
29
32
  @block = block
30
- @outer = eval('self', block.binding)
33
+ @outer = outer || eval('self', block.binding)
31
34
  end
32
35
 
33
36
  # Outer scope call
@@ -123,10 +126,10 @@ module Chewy
123
126
  # Bool filter chainable methods
124
127
  # Used to create bool query. Nodes are passed as arguments.
125
128
  #
126
- # UsersIndex.filter{ must(age < 42, name == 'Name') }
127
- # UsersIndex.filter{ should(age < 42, name == 'Name') }
128
- # UsersIndex.filter{ must(age < 42).should(name == 'Name1', name == 'Name2') }
129
- # UsersIndex.filter{ should_not(age >= 42).must(name == 'Name1') }
129
+ # UsersIndex.filter{ must(age < 42, name == 'Name') }
130
+ # UsersIndex.filter{ should(age < 42, name == 'Name') }
131
+ # UsersIndex.filter{ must(age < 42).should(name == 'Name1', name == 'Name2') }
132
+ # UsersIndex.filter{ should_not(age >= 42).must(name == 'Name1') }
130
133
  #
131
134
  %w(must must_not should).each do |method|
132
135
  define_method method do |*exprs|
@@ -134,6 +137,56 @@ module Chewy
134
137
  end
135
138
  end
136
139
 
140
+ # Initializes has_child filter.
141
+ # Chainable interface acts the same as main query interface. You can pass plain
142
+ # filters or plain queries or filter with DSL block.
143
+ #
144
+ # UsersIndex.filter{ has_child('user').filter(term: {role: 'Admin'}) }
145
+ # UsersIndex.filter{ has_child('user').filter{ role == 'Admin' } }
146
+ # UsersIndex.filter{ has_child('user').query(match: {name: 'borogoves'}) }
147
+ #
148
+ # Filters and queries might be combined and filter_mode and query_mode are configurable:
149
+ #
150
+ # UsersIndex.filter do
151
+ # has_child('user')
152
+ # .filter{ name: 'Peter' }
153
+ # .query(match: {name: 'Peter'})
154
+ # .filter{ age > 42 }
155
+ # .filter_mode(:or)
156
+ # end
157
+ #
158
+ def has_child type
159
+ Nodes::HasChild.new(type, @outer)
160
+ end
161
+
162
+ # Initializes has_parent filter.
163
+ # Chainable interface acts the same as main query interface. You can pass plain
164
+ # filters or plain queries or filter with DSL block.
165
+ #
166
+ # UsersIndex.filter{ has_parent('user').filter(term: {role: 'Admin'}) }
167
+ # UsersIndex.filter{ has_parent('user').filter{ role == 'Admin' } }
168
+ # UsersIndex.filter{ has_parent('user').query(match: {name: 'borogoves'}) }
169
+ #
170
+ # Filters and queries might be combined and filter_mode and query_mode are configurable:
171
+ #
172
+ # UsersIndex.filter do
173
+ # has_parent('user')
174
+ # .filter{ name: 'Peter' }
175
+ # .query(match: {name: 'Peter'})
176
+ # .filter{ age > 42 }
177
+ # .filter_mode(:or)
178
+ # end
179
+ #
180
+ def has_parent type
181
+ Nodes::HasParent.new(type, @outer)
182
+ end
183
+
184
+ # Just simple match_all filter.
185
+ #
186
+ def match_all
187
+ Nodes::MatchAll.new
188
+ end
189
+
137
190
  # Creates field or exists node
138
191
  # Additional options for further expression might be passed as hash
139
192
  #