stretchy-model 0.2.0 → 0.4.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 +4 -4
- data/README.md +66 -10
- data/Rakefile +92 -0
- data/lib/stretchy/attributes/transformers/keyword_transformer.rb +85 -0
- data/lib/stretchy/attributes/type/keyword.rb +11 -0
- data/lib/stretchy/attributes.rb +10 -0
- data/lib/stretchy/open_search_compatibility.rb +86 -0
- data/lib/stretchy/querying.rb +6 -5
- data/lib/stretchy/relation.rb +3 -3
- data/lib/stretchy/relations/aggregation_methods.rb +758 -0
- data/lib/stretchy/relations/finder_methods.rb +21 -3
- data/lib/stretchy/relations/merger.rb +6 -6
- data/lib/stretchy/relations/query_builder.rb +23 -9
- data/lib/stretchy/relations/query_methods.rb +10 -36
- data/lib/stretchy/utils.rb +21 -0
- data/lib/stretchy/version.rb +1 -3
- data/lib/stretchy.rb +37 -8
- metadata +50 -3
@@ -9,7 +9,15 @@ module Stretchy
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def first!
|
12
|
-
|
12
|
+
spawned = spawn
|
13
|
+
if spawned.order_values.length.zero?
|
14
|
+
spawn.sort(Hash[default_sort_key, :asc]).spawn.size(1)
|
15
|
+
elsif spawned.order_values.length >= 1
|
16
|
+
first_order_value = spawned.order_values.shift
|
17
|
+
new_direction = Hash[first_order_value.keys.first, :asc]
|
18
|
+
spawned.order_values.unshift(new_direction)
|
19
|
+
spawned.size(1)
|
20
|
+
end
|
13
21
|
self
|
14
22
|
end
|
15
23
|
|
@@ -19,7 +27,15 @@ module Stretchy
|
|
19
27
|
end
|
20
28
|
|
21
29
|
def last!
|
22
|
-
|
30
|
+
spawned = spawn
|
31
|
+
if spawned.order_values.length.zero?
|
32
|
+
spawn.sort(Hash[default_sort_key, :desc]).spawn.size(1)
|
33
|
+
elsif spawned.order_values.length >= 1
|
34
|
+
first_order_value = spawned.order_values.shift
|
35
|
+
new_direction = Hash[first_order_value.keys.first, :desc]
|
36
|
+
spawned.order_values.unshift(new_direction)
|
37
|
+
spawned.size(1)
|
38
|
+
end
|
23
39
|
self
|
24
40
|
end
|
25
41
|
|
@@ -31,7 +47,9 @@ module Stretchy
|
|
31
47
|
def count!
|
32
48
|
@values[:count] = true
|
33
49
|
@values.delete(:size)
|
34
|
-
spawn
|
50
|
+
spawned = spawn
|
51
|
+
spawned.order_values.clear
|
52
|
+
spawned.results
|
35
53
|
end
|
36
54
|
|
37
55
|
end
|
@@ -50,7 +50,7 @@ module Stretchy
|
|
50
50
|
@other = other
|
51
51
|
end
|
52
52
|
|
53
|
-
NORMAL_VALUES = [:where, :first, :last, :
|
53
|
+
NORMAL_VALUES = [:where, :first, :last, :filter_query]
|
54
54
|
|
55
55
|
def normal_values
|
56
56
|
NORMAL_VALUES
|
@@ -67,7 +67,7 @@ module Stretchy
|
|
67
67
|
unless value.nil? || (value.blank? && false != value)
|
68
68
|
if name == :select
|
69
69
|
relation._select!(*value)
|
70
|
-
elsif name == :
|
70
|
+
elsif name == :filter_query
|
71
71
|
values.each do |v|
|
72
72
|
relation.send("#{name}!", v.first, v.last)
|
73
73
|
end
|
@@ -114,19 +114,19 @@ module Stretchy
|
|
114
114
|
lhs_wheres = relation.where_values
|
115
115
|
rhs_wheres = values[:where] || []
|
116
116
|
|
117
|
-
lhs_filters = relation.
|
118
|
-
rhs_filters = values[:
|
117
|
+
lhs_filters = relation.filter_query_values
|
118
|
+
rhs_filters = values[:filter_query] || []
|
119
119
|
|
120
120
|
removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
|
121
121
|
|
122
122
|
where_values = kept + rhs_wheres
|
123
123
|
|
124
124
|
filters_removed, filters_kept = partition_overwrites(lhs_wheres, rhs_wheres)
|
125
|
-
|
125
|
+
filter_query_values = rhs_filters
|
126
126
|
|
127
127
|
|
128
128
|
relation.where_values = where_values.empty? ? nil : where_values
|
129
|
-
relation.
|
129
|
+
relation.filter_query_values = filter_query_values.empty? ? nil : filter_query_values
|
130
130
|
|
131
131
|
if values[:reordering]
|
132
132
|
# override any order specified in the original relation
|
@@ -2,9 +2,10 @@ module Stretchy
|
|
2
2
|
module Relations
|
3
3
|
class QueryBuilder
|
4
4
|
|
5
|
-
attr_reader :structure, :values
|
5
|
+
attr_reader :structure, :values, :attribute_types
|
6
6
|
|
7
|
-
def initialize(values)
|
7
|
+
def initialize(values, attribute_types = nil)
|
8
|
+
@attribute_types = attribute_types
|
8
9
|
@structure = Jbuilder.new ignore_nil: true
|
9
10
|
@values = values
|
10
11
|
end
|
@@ -14,7 +15,7 @@ module Stretchy
|
|
14
15
|
end
|
15
16
|
|
16
17
|
def filters
|
17
|
-
values[:
|
18
|
+
values[:filter_query]
|
18
19
|
end
|
19
20
|
|
20
21
|
def or_filters
|
@@ -58,7 +59,7 @@ module Stretchy
|
|
58
59
|
end
|
59
60
|
|
60
61
|
def query_filters
|
61
|
-
values[:
|
62
|
+
values[:filter_query]
|
62
63
|
end
|
63
64
|
|
64
65
|
def search_options
|
@@ -149,7 +150,7 @@ module Stretchy
|
|
149
150
|
end
|
150
151
|
|
151
152
|
def build_sort
|
152
|
-
structure.sort sort.
|
153
|
+
structure.sort sort.map { |arg| keyword_transformer.transform(arg) }.flatten
|
153
154
|
end
|
154
155
|
|
155
156
|
def build_highlights
|
@@ -165,7 +166,7 @@ module Stretchy
|
|
165
166
|
def build_aggregations
|
166
167
|
structure.aggregations do
|
167
168
|
aggregations.each do |agg|
|
168
|
-
structure.set! agg[:name], aggregation(agg[:name], agg[:args])
|
169
|
+
structure.set! agg[:name], aggregation(agg[:name], keyword_transformer.transform(agg[:args], :name))
|
169
170
|
end
|
170
171
|
end
|
171
172
|
end
|
@@ -199,13 +200,23 @@ module Stretchy
|
|
199
200
|
def as_must(q)
|
200
201
|
_must = []
|
201
202
|
q.each do |arg|
|
202
|
-
|
203
|
-
|
204
|
-
|
203
|
+
case arg
|
204
|
+
when Hash
|
205
|
+
arg = keyword_transformer.transform(arg)
|
206
|
+
arg.each_pair do |k,v|
|
207
|
+
# If v is an array, we build a terms query otherwise a term query
|
208
|
+
_must << (v.is_a?(Array) ? {terms: Hash[k,v]} : {term: Hash[k,v]})
|
209
|
+
end
|
210
|
+
when String
|
211
|
+
_must << {term: Hash[[arg.split(/:/).collect(&:strip)]]}
|
212
|
+
when Array
|
213
|
+
_must << arg.first
|
214
|
+
end
|
205
215
|
end
|
206
216
|
_must.length == 1 ? _must.first : _must
|
207
217
|
end
|
208
218
|
|
219
|
+
|
209
220
|
def as_query_string(q)
|
210
221
|
_and = []
|
211
222
|
|
@@ -260,6 +271,9 @@ module Stretchy
|
|
260
271
|
end
|
261
272
|
end
|
262
273
|
|
274
|
+
def keyword_transformer
|
275
|
+
@keyword_transformer ||= Stretchy::Attributes::Transformers::KeywordTransformer.new(@attribute_types)
|
276
|
+
end
|
263
277
|
end
|
264
278
|
end
|
265
279
|
end
|
@@ -15,7 +15,7 @@ module Stretchy
|
|
15
15
|
:query_string,
|
16
16
|
:aggregation,
|
17
17
|
:search_option,
|
18
|
-
:
|
18
|
+
:filter_query,
|
19
19
|
:or_filter,
|
20
20
|
:extending,
|
21
21
|
:skip_callbacks
|
@@ -308,7 +308,7 @@ module Stretchy
|
|
308
308
|
|
309
309
|
|
310
310
|
|
311
|
-
# @deprecated in elasticsearch 7.x+ use {#
|
311
|
+
# @deprecated in elasticsearch 7.x+ use {#filter_query} instead
|
312
312
|
def or_filter(name, options = {}, &block)
|
313
313
|
spawn.or_filter!(name, options, &block)
|
314
314
|
end
|
@@ -322,53 +322,27 @@ module Stretchy
|
|
322
322
|
#
|
323
323
|
# This method supports all filters supported by Elasticsearch.
|
324
324
|
#
|
325
|
-
# @overload
|
325
|
+
# @overload filter_query(type, opts)
|
326
326
|
# @param type [Symbol] the type of filter to add (:range, :term, etc.)
|
327
327
|
# @param opts [Hash] a hash containing the attribute and value to filter by
|
328
328
|
#
|
329
329
|
# @example
|
330
|
-
# Model.
|
331
|
-
# Model.
|
330
|
+
# Model.filter_query(:range, age: {gte: 30})
|
331
|
+
# Model.filter_query(:term, color: :blue)
|
332
332
|
#
|
333
333
|
# @return [Stretchy::Relation] a new relation, which reflects the filter
|
334
|
-
def
|
335
|
-
spawn.
|
334
|
+
def filter_query(name, options = {}, &block)
|
335
|
+
spawn.filter_query!(name, options, &block)
|
336
336
|
end
|
337
337
|
|
338
|
-
def
|
339
|
-
self.
|
338
|
+
def filter_query!(name, options = {}, &block) # :nodoc:
|
339
|
+
self.filter_query_values += [{name: name, args: options}]
|
340
340
|
self
|
341
341
|
end
|
342
342
|
|
343
343
|
|
344
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
345
|
|
368
|
-
def aggregation!(name, options = {}, &block) # :nodoc:
|
369
|
-
self.aggregation_values += [{name: name, args: assume_keyword_field(options)}]
|
370
|
-
self
|
371
|
-
end
|
372
346
|
|
373
347
|
|
374
348
|
|
@@ -430,7 +404,7 @@ module Stretchy
|
|
430
404
|
#
|
431
405
|
# @return [ActiveRecord::Relation] a new relation, which reflects the exists filter
|
432
406
|
def has_field(field)
|
433
|
-
spawn.
|
407
|
+
spawn.filter_query(:exists, {field: field})
|
434
408
|
end
|
435
409
|
|
436
410
|
|
data/lib/stretchy/utils.rb
CHANGED
@@ -2,6 +2,27 @@ module Stretchy
|
|
2
2
|
module Utils
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
+
concerning :ConsoleMethods do
|
6
|
+
def reload!
|
7
|
+
Stretchy.loader.reload
|
8
|
+
end
|
9
|
+
|
10
|
+
def banner
|
11
|
+
banner = <<~BANNER
|
12
|
+
|
13
|
+
d8b
|
14
|
+
d8P d8P ?88
|
15
|
+
d888888P d888888P 88b
|
16
|
+
.d888b, ?88' 88bd88b d8888b ?88' d8888b 888888b ?88 d8P
|
17
|
+
?8b, 88P 88P' `d8b_,dP 88P d8P' `P 88P `?8bd88 88
|
18
|
+
`?8b 88b d88 88b 88b 88b d88 88P?8( d88
|
19
|
+
`?888P' `?8b d88' `?888P' `?8b `?888P'd88' 88b`?88P'?8b
|
20
|
+
)88
|
21
|
+
,d8P
|
22
|
+
`?888P'
|
23
|
+
BANNER
|
24
|
+
end
|
25
|
+
end
|
5
26
|
|
6
27
|
def self.to_curl(klass, arguments = {}, end_point = "_search")
|
7
28
|
host = klass.gateway.client.transport.transport.hosts&.first || klass.gateway.client.transport.transport.options[:url]
|
data/lib/stretchy/version.rb
CHANGED
data/lib/stretchy.rb
CHANGED
@@ -10,13 +10,28 @@ require 'active_support/all'
|
|
10
10
|
require 'active_model/type/array'
|
11
11
|
require 'active_model/type/hash'
|
12
12
|
|
13
|
-
ActiveModel::Type.register(:array, ActiveModel::Type::Array)
|
14
|
-
ActiveModel::Type.register(:hash, ActiveModel::Type::Hash)
|
15
|
-
|
16
13
|
require_relative "stretchy/version"
|
17
14
|
require_relative "rails/instrumentation/railtie" if defined?(Rails)
|
18
15
|
|
16
|
+
|
17
|
+
|
19
18
|
module Stretchy
|
19
|
+
|
20
|
+
def self.loader
|
21
|
+
@loader ||= begin
|
22
|
+
loader = Zeitwerk::Loader.new
|
23
|
+
loader.tag = File.basename(__FILE__, ".rb")
|
24
|
+
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
25
|
+
loader.push_dir(__dir__)
|
26
|
+
loader
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.boot!
|
31
|
+
loader.setup
|
32
|
+
Stretchy::Attributes.register!
|
33
|
+
end
|
34
|
+
|
20
35
|
module Errors
|
21
36
|
class QueryOptionMissing < StandardError; end
|
22
37
|
end
|
@@ -24,9 +39,25 @@ module Stretchy
|
|
24
39
|
class Configuration
|
25
40
|
|
26
41
|
attr_accessor :client
|
42
|
+
attr_accessor :opensearch
|
27
43
|
|
28
44
|
def initialize
|
29
45
|
@client = Elasticsearch::Client.new url: 'http://localhost:9200'
|
46
|
+
@opensearch = false
|
47
|
+
end
|
48
|
+
|
49
|
+
def client=(client)
|
50
|
+
@client = client
|
51
|
+
self.opensearch = true if @client.class.name =~ /OpenSearch/
|
52
|
+
end
|
53
|
+
|
54
|
+
def opensearch=(bool)
|
55
|
+
@opensearch = bool
|
56
|
+
OpenSearchCompatibility.opensearch_patch! if bool
|
57
|
+
end
|
58
|
+
|
59
|
+
def opensearch?
|
60
|
+
@opensearch
|
30
61
|
end
|
31
62
|
|
32
63
|
end
|
@@ -48,8 +79,6 @@ module Stretchy
|
|
48
79
|
|
49
80
|
end
|
50
81
|
|
51
|
-
loader
|
52
|
-
|
53
|
-
|
54
|
-
loader.push_dir(__dir__)
|
55
|
-
loader.setup
|
82
|
+
Stretchy.loader.enable_reloading if defined?(Rails) && Rails.env.development? || ENV['RACK_ENV'] == 'development'
|
83
|
+
Stretchy.boot!
|
84
|
+
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stretchy-model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Spencer Markowski
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-03-
|
11
|
+
date: 2024-03-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -164,6 +164,48 @@ dependencies:
|
|
164
164
|
- - "~>"
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: 0.9.36
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: opensearch-ruby
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '3.0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '3.0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: octokit
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '4.20'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '4.20'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: versionomy
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: 0.5.0
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: 0.5.0
|
167
209
|
description: Provides a familiar ActiveRecord-like interface for working with Elasticsearch
|
168
210
|
email:
|
169
211
|
- spencer.markowski@theablefew.com
|
@@ -190,6 +232,9 @@ files:
|
|
190
232
|
- lib/stretchy/associations.rb
|
191
233
|
- lib/stretchy/associations/associated_validator.rb
|
192
234
|
- lib/stretchy/associations/elastic_relation.rb
|
235
|
+
- lib/stretchy/attributes.rb
|
236
|
+
- lib/stretchy/attributes/transformers/keyword_transformer.rb
|
237
|
+
- lib/stretchy/attributes/type/keyword.rb
|
193
238
|
- lib/stretchy/common.rb
|
194
239
|
- lib/stretchy/delegation/delegate_cache.rb
|
195
240
|
- lib/stretchy/delegation/gateway_delegation.rb
|
@@ -197,11 +242,13 @@ files:
|
|
197
242
|
- lib/stretchy/model/callbacks.rb
|
198
243
|
- lib/stretchy/model/serialization.rb
|
199
244
|
- lib/stretchy/null_relation.rb
|
245
|
+
- lib/stretchy/open_search_compatibility.rb
|
200
246
|
- lib/stretchy/persistence.rb
|
201
247
|
- lib/stretchy/querying.rb
|
202
248
|
- lib/stretchy/record.rb
|
203
249
|
- lib/stretchy/refreshable.rb
|
204
250
|
- lib/stretchy/relation.rb
|
251
|
+
- lib/stretchy/relations/aggregation_methods.rb
|
205
252
|
- lib/stretchy/relations/finder_methods.rb
|
206
253
|
- lib/stretchy/relations/merger.rb
|
207
254
|
- lib/stretchy/relations/query_builder.rb
|
@@ -242,7 +289,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
242
289
|
- !ruby/object:Gem::Version
|
243
290
|
version: '0'
|
244
291
|
requirements: []
|
245
|
-
rubygems_version: 3.3
|
292
|
+
rubygems_version: 3.5.3
|
246
293
|
signing_key:
|
247
294
|
specification_version: 4
|
248
295
|
summary: Rails ORM for Elasticsearch
|