chewy_query 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +37 -0
  7. data/Rakefile +2 -0
  8. data/chewy_query.gemspec +27 -0
  9. data/lib/chewy_query.rb +12 -0
  10. data/lib/chewy_query/builder.rb +865 -0
  11. data/lib/chewy_query/builder/compose.rb +64 -0
  12. data/lib/chewy_query/builder/criteria.rb +182 -0
  13. data/lib/chewy_query/builder/filters.rb +227 -0
  14. data/lib/chewy_query/builder/nodes/and.rb +26 -0
  15. data/lib/chewy_query/builder/nodes/base.rb +17 -0
  16. data/lib/chewy_query/builder/nodes/bool.rb +33 -0
  17. data/lib/chewy_query/builder/nodes/equal.rb +34 -0
  18. data/lib/chewy_query/builder/nodes/exists.rb +20 -0
  19. data/lib/chewy_query/builder/nodes/expr.rb +28 -0
  20. data/lib/chewy_query/builder/nodes/field.rb +106 -0
  21. data/lib/chewy_query/builder/nodes/has_child.rb +14 -0
  22. data/lib/chewy_query/builder/nodes/has_parent.rb +14 -0
  23. data/lib/chewy_query/builder/nodes/has_relation.rb +61 -0
  24. data/lib/chewy_query/builder/nodes/match_all.rb +11 -0
  25. data/lib/chewy_query/builder/nodes/missing.rb +20 -0
  26. data/lib/chewy_query/builder/nodes/not.rb +26 -0
  27. data/lib/chewy_query/builder/nodes/or.rb +26 -0
  28. data/lib/chewy_query/builder/nodes/prefix.rb +18 -0
  29. data/lib/chewy_query/builder/nodes/query.rb +20 -0
  30. data/lib/chewy_query/builder/nodes/range.rb +63 -0
  31. data/lib/chewy_query/builder/nodes/raw.rb +15 -0
  32. data/lib/chewy_query/builder/nodes/regexp.rb +33 -0
  33. data/lib/chewy_query/builder/nodes/script.rb +20 -0
  34. data/lib/chewy_query/version.rb +3 -0
  35. data/spec/chewy_query/builder/context_spec.rb +529 -0
  36. data/spec/chewy_query/builder/filters_spec.rb +181 -0
  37. data/spec/chewy_query/builder/nodes/and_spec.rb +16 -0
  38. data/spec/chewy_query/builder/nodes/bool_spec.rb +22 -0
  39. data/spec/chewy_query/builder/nodes/equal_spec.rb +58 -0
  40. data/spec/chewy_query/builder/nodes/exists_spec.rb +16 -0
  41. data/spec/chewy_query/builder/nodes/has_child_spec.rb +79 -0
  42. data/spec/chewy_query/builder/nodes/has_parent_spec.rb +84 -0
  43. data/spec/chewy_query/builder/nodes/match_all_spec.rb +11 -0
  44. data/spec/chewy_query/builder/nodes/missing_spec.rb +14 -0
  45. data/spec/chewy_query/builder/nodes/not_spec.rb +14 -0
  46. data/spec/chewy_query/builder/nodes/or_spec.rb +16 -0
  47. data/spec/chewy_query/builder/nodes/prefix_spec.rb +15 -0
  48. data/spec/chewy_query/builder/nodes/query_spec.rb +17 -0
  49. data/spec/chewy_query/builder/nodes/range_spec.rb +36 -0
  50. data/spec/chewy_query/builder/nodes/raw_spec.rb +11 -0
  51. data/spec/chewy_query/builder/nodes/regexp_spec.rb +45 -0
  52. data/spec/chewy_query/builder/nodes/script_spec.rb +16 -0
  53. data/spec/chewy_query/builder_spec.rb +196 -0
  54. data/spec/chewy_query_spec.rb +0 -0
  55. data/spec/spec_helper.rb +8 -0
  56. metadata +191 -0
@@ -0,0 +1,64 @@
1
+ module ChewyQuery
2
+ class Builder
3
+ module Compose
4
+
5
+ protected
6
+
7
+ def _filtered_query(query, filter, options = {})
8
+ query = { match_all: {} } if !query.present? && filter.present?
9
+
10
+ if filter.present?
11
+ filtered = if query.present?
12
+ { query: { filtered: { query: query, filter: filter } } }
13
+ else
14
+ { query: { filtered: { filter: filter } } }
15
+ end
16
+ filtered[:query][:filtered].merge!(strategy: options[:strategy].to_s) if options[:strategy].present?
17
+ filtered
18
+ elsif query.present?
19
+ { query: query }
20
+ else
21
+ { }
22
+ end
23
+ end
24
+
25
+ def _queries_join(queries, logic)
26
+ queries = queries.compact
27
+
28
+ if queries.many? || (queries.any? && logic == :must_not)
29
+ case logic
30
+ when :dis_max
31
+ { dis_max: { queries: queries } }
32
+ when :must, :should, :must_not
33
+ { bool: { logic => queries } }
34
+ else
35
+ if logic.is_a?(Float)
36
+ { dis_max: { queries: queries, tie_breaker: logic } }
37
+ else
38
+ { bool: { should: queries, minimum_should_match: logic } }
39
+ end
40
+ end
41
+ else
42
+ queries.first
43
+ end
44
+ end
45
+
46
+ def _filters_join(filters, logic)
47
+ filters = filters.compact
48
+
49
+ if filters.many? || (filters.any? && logic == :must_not)
50
+ case logic
51
+ when :and, :or
52
+ { logic => filters }
53
+ when :must, :should, :must_not
54
+ { bool: { logic => filters } }
55
+ else
56
+ { bool: { should: filters, minimum_should_match: logic } }
57
+ end
58
+ else
59
+ filters.first
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,182 @@
1
+ require 'chewy_query/builder/compose'
2
+
3
+ module ChewyQuery
4
+ class Builder
5
+ class Criteria
6
+ include Compose
7
+
8
+ ARRAY_STORAGES = [:queries, :filters, :post_filters, :sort, :fields, :types, :scores]
9
+ HASH_STORAGES = [:options, :request_options, :facets, :aggregations, :suggest]
10
+ STORAGES = ARRAY_STORAGES + HASH_STORAGES
11
+
12
+ def initialize(options = {})
13
+ @options = { query_mode: :must, filter_mode: :and, post_filter_mode: nil }.merge(options)
14
+ @options[:post_filter_mode] = @options[:filter_mode] unless @options[:post_filter_mode]
15
+ end
16
+
17
+ def ==(other)
18
+ other.is_a?(self.class) && storages == other.storages
19
+ end
20
+
21
+ { ARRAY_STORAGES => '[]', HASH_STORAGES => '{}' }.each do |storages, default|
22
+ storages.each do |storage|
23
+ class_eval <<-METHODS, __FILE__, __LINE__ + 1
24
+ def #{storage}
25
+ @#{storage} ||= #{default}
26
+ end
27
+ METHODS
28
+ end
29
+ end
30
+
31
+ STORAGES.each do |storage|
32
+ define_method "#{storage}?" do
33
+ send(storage).any?
34
+ end
35
+ end
36
+
37
+ def none?
38
+ !!options[:none]
39
+ end
40
+
41
+ def update_options(modifer)
42
+ options.merge!(modifer)
43
+ end
44
+
45
+ def update_request_options(modifer)
46
+ request_options.merge!(modifer)
47
+ end
48
+
49
+ def update_facets(modifer)
50
+ facets.merge!(modifer)
51
+ end
52
+
53
+ def update_scores(modifer)
54
+ @scores = scores + Array.wrap(modifer).reject(&:blank?)
55
+ end
56
+
57
+ def update_aggregations(modifer)
58
+ aggregations.merge!(modifer)
59
+ end
60
+
61
+ def update_suggest(modifier)
62
+ suggest.merge!(modifier)
63
+ end
64
+
65
+ [:filters, :queries, :post_filters].each do |storage|
66
+ class_eval <<-RUBY
67
+ def update_#{storage}(modifer)
68
+ @#{storage} = #{storage} + Array.wrap(modifer).reject(&:blank?)
69
+ end
70
+ RUBY
71
+ end
72
+
73
+ def update_sort(modifer, options = {})
74
+ @sort = nil if options[:purge]
75
+ modifer = Array.wrap(modifer).flatten.map do |element|
76
+ element.is_a?(Hash) ? element.map{|k, v| { k => v } } : element
77
+ end.flatten
78
+ @sort = sort + modifer
79
+ end
80
+
81
+ %w(fields types).each do |storage|
82
+ define_method "update_#{storage}" do |modifer, options = {}|
83
+ variable = "@#{storage}"
84
+ instance_variable_set(variable, nil) if options[:purge]
85
+ modifer = send(storage) | Array.wrap(modifer).flatten.map(&:to_s).reject(&:blank?)
86
+ instance_variable_set(variable, modifer)
87
+ end
88
+ end
89
+
90
+ def merge!(other)
91
+ STORAGES.each do |storage|
92
+ send("update_#{storage}", other.send(storage))
93
+ end
94
+ self
95
+ end
96
+
97
+ def merge(other)
98
+ clone.merge!(other)
99
+ end
100
+
101
+ def request_body
102
+ body = {}
103
+
104
+ body.merge!(_filtered_query(_request_query, _request_filter, options.slice(:strategy)))
105
+ body.merge!(post_filter: _request_post_filter) if post_filters?
106
+ body.merge!(facets: facets) if facets?
107
+ body.merge!(aggregations: aggregations) if aggregations?
108
+ body.merge!(suggest: suggest) if suggest?
109
+ body.merge!(sort: sort) if sort?
110
+ body.merge!(_source: fields) if fields?
111
+
112
+ body = _boost_query(body)
113
+
114
+ { body: body.merge!(request_options) }
115
+ end
116
+
117
+ def delete_all_request_body
118
+ filtered_query = _filtered_query(_request_query, _request_filter, options.slice(:strategy))
119
+ { body: filtered_query.presence || { query: { match_all: {} } } }
120
+ end
121
+
122
+ protected
123
+
124
+ def storages
125
+ STORAGES.map{|storage| send(storage) }
126
+ end
127
+
128
+ def initialize_clone(other)
129
+ STORAGES.each do |storage|
130
+ value = other.send(storage)
131
+ instance_variable_set("@#{storage}", value.deep_dup)
132
+ end
133
+ end
134
+
135
+ def _boost_query(body)
136
+ scores? or return body
137
+ query = body.delete(:query)
138
+ filter = body.delete(:filter)
139
+
140
+ if query && filter
141
+ query = { filtered: { query: query, filter: filter } }
142
+ filter = nil
143
+ end
144
+
145
+ score = { }
146
+ score[:functions] = scores
147
+ score[:boost_mode] = options[:boost_mode] if options[:boost_mode]
148
+ score[:score_mode] = options[:score_mode] if options[:score_mode]
149
+ score[:query] = query if query
150
+ score[:filter] = filter if filter
151
+ body.tap{|b| b[:query] = { function_score: score } }
152
+ end
153
+
154
+ def _request_options
155
+ options.slice(:size, :from, :explain, :highlight, :rescore)
156
+ end
157
+
158
+ def _request_query
159
+ _queries_join(queries, options[:query_mode])
160
+ end
161
+
162
+ def _request_filter
163
+ filter_mode = options[:filter_mode]
164
+ request_filter = if filter_mode == :and
165
+ filters
166
+ else
167
+ [_filters_join(filters, filter_mode)]
168
+ end
169
+
170
+ _filters_join([_request_types, *request_filter], :and)
171
+ end
172
+
173
+ def _request_types
174
+ _filters_join(types.map{|type| { type: { value: type } } }, :or)
175
+ end
176
+
177
+ def _request_post_filter
178
+ _filters_join(post_filters, options[:post_filter_mode])
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,227 @@
1
+ require 'chewy_query/builder/nodes/base'
2
+ require 'chewy_query/builder/nodes/expr'
3
+ require 'chewy_query/builder/nodes/field'
4
+ require 'chewy_query/builder/nodes/bool'
5
+ require 'chewy_query/builder/nodes/and'
6
+ require 'chewy_query/builder/nodes/or'
7
+ require 'chewy_query/builder/nodes/not'
8
+ require 'chewy_query/builder/nodes/raw'
9
+ require 'chewy_query/builder/nodes/exists'
10
+ require 'chewy_query/builder/nodes/missing'
11
+ require 'chewy_query/builder/nodes/range'
12
+ require 'chewy_query/builder/nodes/prefix'
13
+ require 'chewy_query/builder/nodes/regexp'
14
+ require 'chewy_query/builder/nodes/equal'
15
+ require 'chewy_query/builder/nodes/query'
16
+ require 'chewy_query/builder/nodes/script'
17
+ require 'chewy_query/builder/nodes/has_child'
18
+ require 'chewy_query/builder/nodes/has_parent'
19
+ require 'chewy_query/builder/nodes/match_all'
20
+
21
+ module ChewyQuery
22
+ class Builder
23
+ # Context provides simplified DSL functionality for filters declaring.
24
+ # You can use logic operations <tt>&</tt> and <tt>|</tt> to concat
25
+ # expressions.
26
+ #
27
+ # builder.filter{ (article.title =~ /Honey/) & (age < 42) & !rate }
28
+ #
29
+ #
30
+ class Filters
31
+ def initialize(outer = nil, &block)
32
+ @block = block
33
+ @outer = outer || eval('self', block.binding)
34
+ end
35
+
36
+ # Outer scope call
37
+ # Block evaluates in the external context
38
+ #
39
+ # def name
40
+ # 'Friend'
41
+ # end
42
+ #
43
+ # builder.filter{ name == o{ name } } # => {filter: {term: {name: 'Friend'}}}
44
+ #
45
+ def o(&block)
46
+ @outer.instance_exec(&block)
47
+ end
48
+
49
+ # Returns field node
50
+ # Used if method_missing is not working by some reason.
51
+ # Additional expression options might be passed as second argument hash.
52
+ #
53
+ # builder.filter{ f(:name) == 'Name' } == builder.filter{ name == 'Name' } # => true
54
+ # builder.filter{ f(:name, execution: :bool) == ['Name1', 'Name2'] } ==
55
+ # builder.filter{ name(execution: :bool) == ['Name1', 'Name2'] } # => true
56
+ #
57
+ # Supports block for getting field name from the outer scope
58
+ #
59
+ # def field
60
+ # :name
61
+ # end
62
+ #
63
+ # builder.filter{ f{ field } == 'Name' } == builder.filter{ name == 'Name' } # => true
64
+ #
65
+ def f(name = nil, *args, &block)
66
+ name = block ? o(&block) : name
67
+ Nodes::Field.new(name, *args)
68
+ end
69
+
70
+ # Returns script filter
71
+ # Just script filter. Supports additional params.
72
+ #
73
+ # builder.filter{ s('doc["num1"].value > 1') }
74
+ # builder.filter{ s('doc["num1"].value > param1', param1: 42) }
75
+ #
76
+ # Supports block for getting script from the outer scope
77
+ #
78
+ # def script
79
+ # 'doc["num1"].value > param1 || 1'
80
+ # end
81
+ #
82
+ # builder.filter{ s{ script } } == builder.filter{ s('doc["num1"].value > 1') } # => true
83
+ # builder.filter{ s(param1: 42) { script } } == builder.filter{ s('doc["num1"].value > 1', param1: 42) } # => true
84
+ #
85
+ def s(*args, &block)
86
+ params = args.extract_options!
87
+ script = block ? o(&block) : args.first
88
+ Nodes::Script.new(script, params)
89
+ end
90
+
91
+ # Returns query filter
92
+ #
93
+ # builder.filter{ q(query_string: {query: 'name: hello'}) }
94
+ #
95
+ # Supports block for getting query from the outer scope
96
+ #
97
+ # def query
98
+ # {query_string: {query: 'name: hello'}}
99
+ # end
100
+ #
101
+ # builder.filter{ q{ query } } == builder.filter{ q(query_string: {query: 'name: hello'}) } # => true
102
+ #
103
+ def q(query = nil, &block)
104
+ Nodes::Query.new(block ? o(&block) : query)
105
+ end
106
+
107
+ # Returns raw expression
108
+ # Same as filter with arguments instead of block, but can participate in expressions
109
+ #
110
+ # builder.filter{ r(term: {name: 'Name'}) }
111
+ # builder.filter{ r(term: {name: 'Name'}) & (age < 42) }
112
+ #
113
+ # Supports block for getting raw filter from the outer scope
114
+ #
115
+ # def filter
116
+ # {term: {name: 'Name'}}
117
+ # end
118
+ #
119
+ # builder.filter{ r{ filter } } == builder.filter{ r(term: {name: 'Name'}) } # => true
120
+ # builder.filter{ r{ filter } } == builder.filter(term: {name: 'Name'}) # => true
121
+ #
122
+ def r(raw = nil, &block)
123
+ Nodes::Raw.new(block ? o(&block) : raw)
124
+ end
125
+
126
+ # Bool filter chainable methods
127
+ # Used to create bool query. Nodes are passed as arguments.
128
+ #
129
+ # builder.filter{ must(age < 42, name == 'Name') }
130
+ # builder.filter{ should(age < 42, name == 'Name') }
131
+ # builder.filter{ must(age < 42).should(name == 'Name1', name == 'Name2') }
132
+ # builder.filter{ should_not(age >= 42).must(name == 'Name1') }
133
+ #
134
+ %w(must must_not should).each do |method|
135
+ define_method method do |*exprs|
136
+ Nodes::Bool.new.send(method, *exprs)
137
+ end
138
+ end
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
+ # builder.filter{ has_child('user').filter(term: {role: 'Admin'}) }
145
+ # builder.filter{ has_child('user').filter{ role == 'Admin' } }
146
+ # builder.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
+ # builder.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
+ # builder.filter{ has_parent('user').filter(term: {role: 'Admin'}) }
167
+ # builder.filter{ has_parent('user').filter{ role == 'Admin' } }
168
+ # builder.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
+ # builder.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
+
190
+ # Creates field or exists node
191
+ # Additional options for further expression might be passed as hash
192
+ #
193
+ # builder.filter{ name == 'Name' } == builder.filter(term: {name: 'Name'}) # => true
194
+ # builder.filter{ name? } == builder.filter(exists: {term: 'name'}) # => true
195
+ # builder.filter{ name(execution: :bool) == ['Name1', 'Name2'] } ==
196
+ # builder.filter(terms: {name: ['Name1', 'Name2'], execution: :bool}) # => true
197
+ #
198
+ # Also field names might be chained to use dot-notation for ES field names
199
+ #
200
+ # builder.filter{ article.title =~ 'Hello' }
201
+ # builder.filter{ article.tags? }
202
+ #
203
+ def method_missing(method, *args, &block)
204
+ method = method.to_s
205
+ if method =~ /\?\Z/
206
+ Nodes::Exists.new(method.gsub(/\?\Z/, ''))
207
+ else
208
+ f(method, *args)
209
+ end
210
+ end
211
+
212
+ # Evaluates context block, returns top node.
213
+ # For internal usage.
214
+ #
215
+ def __result__
216
+ instance_exec(&@block)
217
+ end
218
+
219
+ # Renders evaluated filters.
220
+ # For internal usage.
221
+ #
222
+ def __render__
223
+ __result__.__render__ # haha, wtf?
224
+ end
225
+ end
226
+ end
227
+ end