stretchy-model 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +146 -0
- data/Rakefile +4 -0
- data/containers/Dockerfile.elasticsearch +7 -0
- data/containers/Dockerfile.opensearch +19 -0
- data/docker-compose.yml +52 -0
- data/lib/active_model/type/array.rb +13 -0
- data/lib/active_model/type/hash.rb +15 -0
- data/lib/rails/instrumentation/publishers.rb +29 -0
- data/lib/rails/instrumentation/railtie.rb +29 -0
- data/lib/stretchy/associations/associated_validator.rb +17 -0
- data/lib/stretchy/associations/elastic_relation.rb +38 -0
- data/lib/stretchy/associations.rb +161 -0
- data/lib/stretchy/common.rb +33 -0
- data/lib/stretchy/delegation/delegate_cache.rb +131 -0
- data/lib/stretchy/delegation/gateway_delegation.rb +43 -0
- data/lib/stretchy/indexing/bulk.rb +48 -0
- data/lib/stretchy/model/callbacks.rb +31 -0
- data/lib/stretchy/model/serialization.rb +20 -0
- data/lib/stretchy/null_relation.rb +53 -0
- data/lib/stretchy/persistence.rb +43 -0
- data/lib/stretchy/querying.rb +20 -0
- data/lib/stretchy/record.rb +57 -0
- data/lib/stretchy/refreshable.rb +15 -0
- data/lib/stretchy/relation.rb +169 -0
- data/lib/stretchy/relations/finder_methods.rb +39 -0
- data/lib/stretchy/relations/merger.rb +179 -0
- data/lib/stretchy/relations/query_builder.rb +265 -0
- data/lib/stretchy/relations/query_methods.rb +578 -0
- data/lib/stretchy/relations/search_option_methods.rb +34 -0
- data/lib/stretchy/relations/spawn_methods.rb +60 -0
- data/lib/stretchy/repository.rb +10 -0
- data/lib/stretchy/scoping/default.rb +134 -0
- data/lib/stretchy/scoping/named.rb +68 -0
- data/lib/stretchy/scoping/scope_registry.rb +34 -0
- data/lib/stretchy/scoping.rb +28 -0
- data/lib/stretchy/shared_scopes.rb +34 -0
- data/lib/stretchy/utils.rb +69 -0
- data/lib/stretchy/version.rb +5 -0
- data/lib/stretchy.rb +38 -0
- data/sig/stretchy.rbs +4 -0
- data/stretchy.logo.png +0 -0
- metadata +247 -0
@@ -0,0 +1,578 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Relations
|
3
|
+
module QueryMethods
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
|
7
|
+
MULTI_VALUE_METHODS = [
|
8
|
+
:where,
|
9
|
+
:order,
|
10
|
+
:field,
|
11
|
+
:highlight,
|
12
|
+
:source,
|
13
|
+
:must_not,
|
14
|
+
:should,
|
15
|
+
:query_string,
|
16
|
+
:aggregation,
|
17
|
+
:search_option,
|
18
|
+
:filter,
|
19
|
+
:or_filter,
|
20
|
+
:extending,
|
21
|
+
:skip_callbacks
|
22
|
+
]
|
23
|
+
|
24
|
+
SINGLE_VALUE_METHODS = [:size]
|
25
|
+
|
26
|
+
class WhereChain
|
27
|
+
def initialize(scope)
|
28
|
+
@scope = scope
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
MULTI_VALUE_METHODS.each do |name|
|
34
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
35
|
+
def #{name}_values # def select_values
|
36
|
+
@values[:#{name}] || [] # @values[:select] || []
|
37
|
+
end # end
|
38
|
+
#
|
39
|
+
def #{name}_values=(values) # def select_values=(values)
|
40
|
+
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
|
41
|
+
@values[:#{name}] = values # @values[:select] = values
|
42
|
+
end # end
|
43
|
+
CODE
|
44
|
+
end
|
45
|
+
|
46
|
+
SINGLE_VALUE_METHODS.each do |name|
|
47
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
48
|
+
def #{name}_value # def readonly_value
|
49
|
+
@values[:#{name}] # @values[:readonly]
|
50
|
+
end # end
|
51
|
+
CODE
|
52
|
+
end
|
53
|
+
|
54
|
+
SINGLE_VALUE_METHODS.each do |name|
|
55
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
56
|
+
def #{name}_value=(value) # def readonly_value=(value)
|
57
|
+
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
|
58
|
+
@values[:#{name}] = value # @values[:readonly] = value
|
59
|
+
end # end
|
60
|
+
CODE
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Allows you to add one or more sorts on specified fields.
|
65
|
+
#
|
66
|
+
# @overload order(attribute: direction, ...)
|
67
|
+
# @param attribute [Symbol] the attribute to sort by
|
68
|
+
# @param direction [Symbol] the direction to sort in (:asc or :desc)
|
69
|
+
#
|
70
|
+
# @overload order(attribute: {order: direction, mode: mode, ...}, ...)
|
71
|
+
# @param params [Hash] attributes to sort by
|
72
|
+
# @param params [Symbol] :attribute the attribute name as key to sort by
|
73
|
+
# @param options [Hash] a hash containing possible sorting options
|
74
|
+
# @option options [Symbol] :order the direction to sort in (:asc or :desc)
|
75
|
+
# @option options [Symbol] :mode the mode to use for sorting (:avg, :min, :max, :sum, :median)
|
76
|
+
# @option options [Symbol] :numeric_type the numeric type to use for sorting (:double, :long, :date, :date_nanos)
|
77
|
+
# @option options [Symbol] :missing the value to use for documents without the field
|
78
|
+
# @option options [Hash] :nested the nested sorting options
|
79
|
+
# @option nested [String] :path the path to the nested object
|
80
|
+
# @option nested [Hash] :filter the filter to apply to the nested object
|
81
|
+
# @option nested [Hash] :max_children the maximum number of children to consider per root document when picking the sort value. Defaults to unlimited
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# Model.order(created_at: :asc)
|
85
|
+
# # Elasticsearch equivalent
|
86
|
+
# #=> "sort" : [{"created_at" : "asc"}]
|
87
|
+
#
|
88
|
+
# Model.order(age: :desc, name: :asc, price: {order: :desc, mode: :avg})
|
89
|
+
#
|
90
|
+
# # Elasticsearch equivalent
|
91
|
+
# #=> "sort" : [
|
92
|
+
# { "price" : {"order" : "desc", "mode": "avg"}},
|
93
|
+
# { "name" : "asc" },
|
94
|
+
# { "age" : "desc" }
|
95
|
+
# ]
|
96
|
+
#
|
97
|
+
# @return [Stretchy::Relation] a new relation with the specified order
|
98
|
+
# @see #sort
|
99
|
+
def order(*args)
|
100
|
+
check_if_method_has_arguments!(:order, args)
|
101
|
+
spawn.order!(*args)
|
102
|
+
end
|
103
|
+
|
104
|
+
def order!(*args) # :nodoc:
|
105
|
+
self.order_values += args.first.zip.map(&:to_h)
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
# Alias for {#order}
|
110
|
+
# @see #order
|
111
|
+
alias :sort :order
|
112
|
+
|
113
|
+
|
114
|
+
# Allows you to skip callbacks for the specified fields that are added by query_must_have for
|
115
|
+
# the current query.
|
116
|
+
#
|
117
|
+
# @example
|
118
|
+
# Model.skip_callbacks(:routing)
|
119
|
+
def skip_callbacks(*args)
|
120
|
+
spawn.skip_callbacks!(*args)
|
121
|
+
end
|
122
|
+
|
123
|
+
def skip_callbacks!(*args) # :nodoc:
|
124
|
+
self.skip_callbacks_values += args
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
alias :sort :order
|
129
|
+
|
130
|
+
|
131
|
+
# Sets the maximum number of records to be retrieved.
|
132
|
+
#
|
133
|
+
# @param args [Integer] the maximum number of records to retrieve
|
134
|
+
#
|
135
|
+
# @example
|
136
|
+
# Model.size(10)
|
137
|
+
#
|
138
|
+
# @return [ActiveRecord::Relation] a new relation, which reflects the limit
|
139
|
+
# @see #limit
|
140
|
+
def size(args)
|
141
|
+
spawn.size!(args)
|
142
|
+
end
|
143
|
+
|
144
|
+
def size!(args) # :nodoc:
|
145
|
+
self.size_value = args
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
# Alias for {#size}
|
150
|
+
# @see #size
|
151
|
+
alias :limit :size
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
# Adds conditions to the query.
|
157
|
+
#
|
158
|
+
# Each argument is a hash where the key is the attribute to filter by and the value is the value to match.
|
159
|
+
#
|
160
|
+
# @overload where(*rest)
|
161
|
+
# @param rest [Array<Hash>] keywords containing attribute-value pairs to match
|
162
|
+
#
|
163
|
+
# @example
|
164
|
+
# Model.where(price: 10, color: :green)
|
165
|
+
#
|
166
|
+
# # Elasticsearch equivalent
|
167
|
+
# # => "query" : {
|
168
|
+
# "bool" : {
|
169
|
+
# "must" : [
|
170
|
+
# { "term" : { "price" : 10 } },
|
171
|
+
# { "term" : { "color" : "green" } }
|
172
|
+
# ]
|
173
|
+
# }
|
174
|
+
# }
|
175
|
+
#
|
176
|
+
# @return [ActiveRecord::Relation, WhereChain] a new relation, which reflects the conditions, or a WhereChain if opts is :chain
|
177
|
+
# @see #must
|
178
|
+
def where(opts = :chain, *rest)
|
179
|
+
if opts == :chain
|
180
|
+
WhereChain.new(spawn)
|
181
|
+
elsif opts.blank?
|
182
|
+
self
|
183
|
+
else
|
184
|
+
spawn.where!(opts, *rest)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
def where!(opts, *rest) # :nodoc:
|
190
|
+
if opts == :chain
|
191
|
+
WhereChain.new(self)
|
192
|
+
else
|
193
|
+
self.where_values += build_where(opts, rest)
|
194
|
+
self
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Alias for {#where}
|
199
|
+
# @see #where
|
200
|
+
alias :must :where
|
201
|
+
|
202
|
+
|
203
|
+
|
204
|
+
|
205
|
+
# Adds a query string to the search.
|
206
|
+
#
|
207
|
+
# The query string uses Elasticsearch's Query String Query syntax, which includes a series of terms and operators.
|
208
|
+
# Terms can be single words or phrases. Operators include AND, OR, and NOT, among others.
|
209
|
+
# Field names can be included in the query string to search for specific values in specific fields. (e.g. "eye_color: green")
|
210
|
+
# The default operator between terms are treated as OR operators.
|
211
|
+
#
|
212
|
+
# @param query [String] the query string
|
213
|
+
# @param rest [Array] additional arguments (not normally used)
|
214
|
+
#
|
215
|
+
# @example
|
216
|
+
# Model.query_string("((big cat) OR (domestic cat)) AND NOT panther eye_color: green")
|
217
|
+
#
|
218
|
+
# @return [Stretchy::Relation] a new relation, which reflects the query string
|
219
|
+
def query_string(opts = :chain, *rest)
|
220
|
+
if opts == :chain
|
221
|
+
WhereChain.new(spawn)
|
222
|
+
elsif opts.blank?
|
223
|
+
self
|
224
|
+
else
|
225
|
+
spawn.query_string!(opts, *rest)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def query_string!(opts, *rest) # :nodoc:
|
230
|
+
if opts == :chain
|
231
|
+
WhereChain.new(self)
|
232
|
+
else
|
233
|
+
self.query_string_values += build_where(opts, rest)
|
234
|
+
self
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
|
240
|
+
# Adds negated conditions to the query.
|
241
|
+
#
|
242
|
+
# Each argument is a hash where the key is the attribute to filter by and the value is the value to exclude.
|
243
|
+
#
|
244
|
+
# @overload must_not(*rest)
|
245
|
+
# @param rest [Array<Hash>] a hash containing attribute-value pairs to exclude
|
246
|
+
#
|
247
|
+
# @example
|
248
|
+
# Model.must_not(color: 'blue', size: :large)
|
249
|
+
#
|
250
|
+
# @return [Stretchy::Relation] a new relation, which reflects the negated conditions
|
251
|
+
# @see #where_not
|
252
|
+
def must_not(opts = :chain, *rest)
|
253
|
+
if opts == :chain
|
254
|
+
WhereChain.new(spawn)
|
255
|
+
elsif opts.blank?
|
256
|
+
self
|
257
|
+
else
|
258
|
+
spawn.must_not!(opts, *rest)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
|
263
|
+
def must_not!(opts, *rest) # :nodoc:
|
264
|
+
if opts == :chain
|
265
|
+
WhereChain.new(self)
|
266
|
+
else
|
267
|
+
self.must_not_values += build_where(opts, rest)
|
268
|
+
self
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Alias for {#must_not}
|
273
|
+
# @see #must_not
|
274
|
+
alias :where_not :must_not
|
275
|
+
|
276
|
+
|
277
|
+
|
278
|
+
# Adds optional conditions to the query.
|
279
|
+
#
|
280
|
+
# Each argument is a hash where the key is the attribute to filter by and the value is the value to match optionally.
|
281
|
+
#
|
282
|
+
# @overload should(*rest)
|
283
|
+
# @param rest [Array<Hash>] additional keywords containing attribute-value pairs to match optionally
|
284
|
+
#
|
285
|
+
# @example
|
286
|
+
# Model.should(color: :pink, size: :medium)
|
287
|
+
#
|
288
|
+
# @return [Stretchy::Relation] a new relation, which reflects the optional conditions
|
289
|
+
def should(opts = :chain, *rest)
|
290
|
+
if opts == :chain
|
291
|
+
WhereChain.new(spawn)
|
292
|
+
elsif opts.blank?
|
293
|
+
self
|
294
|
+
else
|
295
|
+
spawn.should!(opts, *rest)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def should!(opts, *rest) # :nodoc:
|
300
|
+
if opts == :chain
|
301
|
+
WhereChain.new(self)
|
302
|
+
else
|
303
|
+
self.should_values += build_where(opts, rest)
|
304
|
+
self
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
|
309
|
+
|
310
|
+
|
311
|
+
# @deprecated in elasticsearch 7.x+ use {#filter} instead
|
312
|
+
def or_filter(name, options = {}, &block)
|
313
|
+
spawn.or_filter!(name, options, &block)
|
314
|
+
end
|
315
|
+
|
316
|
+
def or_filter!(name, options = {}, &block) # :nodoc:
|
317
|
+
self.or_filter_values += [{name: name, args: options}]
|
318
|
+
self
|
319
|
+
end
|
320
|
+
|
321
|
+
# Adds a filter to the query.
|
322
|
+
#
|
323
|
+
# This method supports all filters supported by Elasticsearch.
|
324
|
+
#
|
325
|
+
# @overload filter(type, opts)
|
326
|
+
# @param type [Symbol] the type of filter to add (:range, :term, etc.)
|
327
|
+
# @param opts [Hash] a hash containing the attribute and value to filter by
|
328
|
+
#
|
329
|
+
# @example
|
330
|
+
# Model.filter(:range, age: {gte: 30})
|
331
|
+
# Model.filter(:term, color: :blue)
|
332
|
+
#
|
333
|
+
# @return [Stretchy::Relation] a new relation, which reflects the filter
|
334
|
+
def filter(name, options = {}, &block)
|
335
|
+
spawn.filter!(name, options, &block)
|
336
|
+
end
|
337
|
+
|
338
|
+
def filter!(name, options = {}, &block) # :nodoc:
|
339
|
+
self.filter_values += [{name: name, args: options}]
|
340
|
+
self
|
341
|
+
end
|
342
|
+
|
343
|
+
|
344
|
+
|
345
|
+
# Adds an aggregation to the query.
|
346
|
+
#
|
347
|
+
# @param name [Symbol, String] the name of the aggregation
|
348
|
+
# @param options [Hash] a hash of options for the aggregation
|
349
|
+
# @param block [Proc] an optional block to further configure the aggregation
|
350
|
+
#
|
351
|
+
# @example
|
352
|
+
# Model.aggregation(:avg_price, field: :price)
|
353
|
+
# Model.aggregation(:price_ranges) do
|
354
|
+
# range field: :price, ranges: [{to: 100}, {from: 100, to: 200}, {from: 200}]
|
355
|
+
# end
|
356
|
+
#
|
357
|
+
# Aggregation results are available in the `aggregations` method of the results under name provided in the aggregation.
|
358
|
+
#
|
359
|
+
# @example
|
360
|
+
# results = Model.where(color: :blue).aggregation(:avg_price, field: :price)
|
361
|
+
# results.aggregations.avg_price
|
362
|
+
#
|
363
|
+
# @return [Stretchy::Relation] a new relation
|
364
|
+
def aggregation(name, options = {}, &block)
|
365
|
+
spawn.aggregation!(name, options, &block)
|
366
|
+
end
|
367
|
+
|
368
|
+
def aggregation!(name, options = {}, &block) # :nodoc:
|
369
|
+
self.aggregation_values += [{name: name, args: assume_keyword_field(options)}]
|
370
|
+
self
|
371
|
+
end
|
372
|
+
|
373
|
+
|
374
|
+
|
375
|
+
|
376
|
+
def field(*args)
|
377
|
+
spawn.field!(*args)
|
378
|
+
end
|
379
|
+
alias :fields :field
|
380
|
+
|
381
|
+
def field!(*args) # :nodoc:
|
382
|
+
self.field_values += args
|
383
|
+
self
|
384
|
+
end
|
385
|
+
|
386
|
+
|
387
|
+
|
388
|
+
# Controls which fields of the source are returned.
|
389
|
+
#
|
390
|
+
# This method supports source filtering, which allows you to include or exclude fields from the source.
|
391
|
+
# You can specify fields directly, use wildcard patterns, or use an object containing arrays
|
392
|
+
# of includes and excludes patterns.
|
393
|
+
#
|
394
|
+
# If the includes property is specified, only source fields that match one of its patterns are returned.
|
395
|
+
# You can exclude fields from this subset using the excludes property.
|
396
|
+
#
|
397
|
+
# If the includes property is not specified, the entire document source is returned, excluding any
|
398
|
+
# fields that match a pattern in the excludes property.
|
399
|
+
#
|
400
|
+
# @overload source(opts)
|
401
|
+
# @param opts [Hash, Boolean] a hash containing :includes and/or :excludes arrays, or a boolean indicating whether
|
402
|
+
# to include the source
|
403
|
+
#
|
404
|
+
# @example
|
405
|
+
# Model.source(includes: [:name, :email])
|
406
|
+
# Model.source(excludes: [:name, :email])
|
407
|
+
# Model.source(false) # don't include source
|
408
|
+
#
|
409
|
+
# @return [Stretchy::Relation] a new relation, which reflects the source filtering
|
410
|
+
def source(*args)
|
411
|
+
spawn.source!(*args)
|
412
|
+
end
|
413
|
+
|
414
|
+
def source!(*args) # :nodoc:
|
415
|
+
self.source_values += args
|
416
|
+
self
|
417
|
+
end
|
418
|
+
|
419
|
+
|
420
|
+
|
421
|
+
# Checks if a field exists in the documents.
|
422
|
+
#
|
423
|
+
# This is a helper for the exists filter in Elasticsearch, which returns documents
|
424
|
+
# that have at least one non-null value in the specified field.
|
425
|
+
#
|
426
|
+
# @param field [Symbol, String] the field to check for existence
|
427
|
+
#
|
428
|
+
# @example
|
429
|
+
# Model.has_field(:name)
|
430
|
+
#
|
431
|
+
# @return [ActiveRecord::Relation] a new relation, which reflects the exists filter
|
432
|
+
def has_field(field)
|
433
|
+
spawn.filter(:exists, {field: field})
|
434
|
+
end
|
435
|
+
|
436
|
+
|
437
|
+
|
438
|
+
|
439
|
+
def bind(value)
|
440
|
+
spawn.bind!(value)
|
441
|
+
end
|
442
|
+
|
443
|
+
def bind!(value) # :nodoc:
|
444
|
+
self.bind_values += [value]
|
445
|
+
self
|
446
|
+
end
|
447
|
+
|
448
|
+
|
449
|
+
|
450
|
+
|
451
|
+
|
452
|
+
# Highlights the specified fields in the search results.
|
453
|
+
#
|
454
|
+
# @example
|
455
|
+
# Model.where(body: "turkey").highlight(:body)
|
456
|
+
#
|
457
|
+
# @param [Hash] args The fields to highlight. Each field is a key in the hash,
|
458
|
+
# and the value is another hash specifying the type of highlighting.
|
459
|
+
# For example, `{body: {type: :plain}}` will highlight the 'body' field
|
460
|
+
# with plain type highlighting.
|
461
|
+
#
|
462
|
+
# @return [Stretchy::Relation] Returns a Stretchy::Relation object, which can be used
|
463
|
+
# for chaining further query methods.
|
464
|
+
def highlight(*args)
|
465
|
+
spawn.highlight!(*args)
|
466
|
+
end
|
467
|
+
|
468
|
+
def highlight!(*args) # :nodoc:
|
469
|
+
self.highlight_values += args
|
470
|
+
self
|
471
|
+
end
|
472
|
+
|
473
|
+
|
474
|
+
# Returns a chainable relation with zero records.
|
475
|
+
def none
|
476
|
+
extending(NullRelation)
|
477
|
+
end
|
478
|
+
|
479
|
+
def none! # :nodoc:
|
480
|
+
extending!(NullRelation)
|
481
|
+
end
|
482
|
+
|
483
|
+
|
484
|
+
def extending(*modules, &block)
|
485
|
+
if modules.any? || block
|
486
|
+
spawn.extending!(*modules, &block)
|
487
|
+
else
|
488
|
+
self
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
def extending!(*modules, &block) # :nodoc:
|
493
|
+
modules << Module.new(&block) if block
|
494
|
+
modules.flatten!
|
495
|
+
|
496
|
+
self.extending_values += modules
|
497
|
+
extend(*extending_values) if extending_values.any?
|
498
|
+
|
499
|
+
self
|
500
|
+
end
|
501
|
+
|
502
|
+
def build_where(opts, other = [])
|
503
|
+
case opts
|
504
|
+
when String, Array
|
505
|
+
#TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113
|
506
|
+
values = Hash === other.first ? other.first.values : other
|
507
|
+
|
508
|
+
values.grep(Stretchy::Relation) do |rel|
|
509
|
+
self.bind_values += rel.bind_values
|
510
|
+
end
|
511
|
+
|
512
|
+
[other.empty? ? opts : ([opts] + other)]
|
513
|
+
when Hash
|
514
|
+
[other.empty? ? opts : ([opts] + other)]
|
515
|
+
else
|
516
|
+
[opts]
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
private
|
521
|
+
|
522
|
+
# If terms are used, we assume that the field is a keyword field
|
523
|
+
# and append .keyword to the field name
|
524
|
+
# {terms: {field: 'gender'}}
|
525
|
+
# or nested aggs
|
526
|
+
# {terms: {field: 'gender'}, aggs: {name: {terms: {field: 'position.name'}}}}
|
527
|
+
# should be converted to
|
528
|
+
# {terms: {field: 'gender.keyword'}, aggs: {name: {terms: {field: 'position.name.keyword'}}}}
|
529
|
+
# {date_histogram: {field: 'created_at', interval: 'day'}}
|
530
|
+
# TODO: There may be cases where we don't want to add .keyword to the field and there should be a way to override this
|
531
|
+
KEYWORD_AGGREGATION_FIELDS = [:terms, :rare_terms, :significant_terms, :cardinality, :string_stats]
|
532
|
+
def assume_keyword_field(args={}, parent_match=false)
|
533
|
+
if args.is_a?(Hash)
|
534
|
+
args.each do |k, v|
|
535
|
+
if v.is_a?(Hash)
|
536
|
+
assume_keyword_field(v, KEYWORD_AGGREGATION_FIELDS.include?(k))
|
537
|
+
else
|
538
|
+
next unless v.is_a?(String) || v.is_a?(Symbol)
|
539
|
+
args[k] = ([:field, :fields].include?(k.to_sym) && v !~ /\.keyword$/ && parent_match) ? "#{v}.keyword" : v.to_s
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
def check_if_method_has_arguments!(method_name, args)
|
546
|
+
if args.blank?
|
547
|
+
raise ArgumentError, "The method .#{method_name}() must contain arguments."
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
|
552
|
+
'asc', 'desc', 'ASC', 'DESC'] # :nodoc:
|
553
|
+
|
554
|
+
def validate_order_args(args)
|
555
|
+
args.each do |arg|
|
556
|
+
next unless arg.is_a?(Hash)
|
557
|
+
arg.each do |_key, value|
|
558
|
+
raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \
|
559
|
+
"directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value)
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
def add_relations_to_bind_values(attributes)
|
565
|
+
if attributes.is_a?(Hash)
|
566
|
+
attributes.each_value do |value|
|
567
|
+
if value.is_a?(ActiveRecord::Relation)
|
568
|
+
self.bind_values += value.bind_values
|
569
|
+
else
|
570
|
+
add_relations_to_bind_values(value)
|
571
|
+
end
|
572
|
+
end
|
573
|
+
end
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
end
|
578
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Stretchy
|
2
|
+
module Relations
|
3
|
+
|
4
|
+
module SearchOptionMethods
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def routing(args)
|
8
|
+
check_if_method_has_arguments!(:routing, args)
|
9
|
+
spawn.routing!(args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def routing!(args)
|
13
|
+
merge_search_option_values(:routing, args)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def search_options(*args)
|
18
|
+
spawn.search_options!(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
def search_options!(*args)
|
22
|
+
self.search_option_values += args
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def merge_search_option_values(key, value)
|
29
|
+
self.search_option_values += [Hash[key,value]]
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'active_support/core_ext/hash/except'
|
2
|
+
require 'active_support/core_ext/hash/slice'
|
3
|
+
|
4
|
+
module Stretchy
|
5
|
+
module Relations
|
6
|
+
|
7
|
+
module SpawnMethods
|
8
|
+
def spawn
|
9
|
+
clone
|
10
|
+
end
|
11
|
+
|
12
|
+
def merge(other)
|
13
|
+
if other.is_a?(Array)
|
14
|
+
to_a & other
|
15
|
+
elsif other
|
16
|
+
spawn.merge!(other)
|
17
|
+
else
|
18
|
+
self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def merge!(other) # :nodoc:
|
23
|
+
if !other.is_a?(Relation) && other.respond_to?(:to_proc)
|
24
|
+
instance_exec(&other)
|
25
|
+
else
|
26
|
+
klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger
|
27
|
+
klass.new(self, other).merge
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Removes from the query the condition(s) specified in +skips+.
|
32
|
+
#
|
33
|
+
# Post.order('id asc').except(:order) # discards the order condition
|
34
|
+
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
|
35
|
+
def except(*skips)
|
36
|
+
relation_with values.except(*skips)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Removes any condition from the query other than the one(s) specified in +onlies+.
|
40
|
+
#
|
41
|
+
# Post.order('id asc').only(:where) # discards the order condition
|
42
|
+
# Post.order('id asc').only(:where, :order) # uses the specified order
|
43
|
+
def only(*onlies)
|
44
|
+
if onlies.any? { |o| o == :where }
|
45
|
+
onlies << :bind
|
46
|
+
end
|
47
|
+
relation_with values.slice(*onlies)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def relation_with(values) # :nodoc:
|
53
|
+
result = Relation.create(klass, values)
|
54
|
+
result.extend(*extending_values) if extending_values.any?
|
55
|
+
result
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|