chewy 0.1.0 → 0.2.0

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