chewy_query 0.0.1

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 (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