stretchy-model 0.1.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.
- 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
|