stretchy-model 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -1
- data/README.md +19 -84
- data/lib/stretchy/associations.rb +155 -15
- data/lib/stretchy/attributes/type/array.rb +15 -0
- data/lib/stretchy/attributes/type/hash.rb +17 -0
- data/lib/stretchy/attributes/type/text.rb +12 -0
- data/lib/stretchy/attributes.rb +22 -2
- data/lib/stretchy/common.rb +2 -3
- data/lib/stretchy/delegation/gateway_delegation.rb +7 -1
- data/lib/stretchy/model/serialization.rb +1 -0
- data/lib/stretchy/querying.rb +1 -1
- data/lib/stretchy/record.rb +8 -9
- data/lib/stretchy/relation.rb +10 -16
- data/lib/stretchy/relations/merger.rb +5 -1
- data/lib/stretchy/relations/query_builder.rb +25 -2
- data/lib/stretchy/relations/query_methods.rb +66 -2
- data/lib/stretchy/shared_scopes.rb +1 -1
- data/lib/stretchy/version.rb +1 -1
- data/lib/stretchy.rb +0 -2
- data/lib/stretchy_model.rb +9 -0
- metadata +6 -4
- data/lib/active_model/type/array.rb +0 -13
- data/lib/active_model/type/hash.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd18b424c18bda352233d72af113e9bbc944277e03a02cfb950780f0fab9a405
|
4
|
+
data.tar.gz: 6eb86522e7bc91012cc4c743d0a9fccc54c7e75b14ff4d6f4fca5bdb9fa31626
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0fd2a0fb25439d79c799f43f43bb01df7c4dfe0ce9eb577d445b6d415eb9c38ecdc0f45c73d8255d2e079ff502e943f6491b874e3bb9c41fc9c853efed84d252
|
7
|
+
data.tar.gz: 8ea4cfa584ec825ac30aa4aa5dea88b04c709cbf7cc75089e28cd1e850318f150f1f68ead89bc571535739aa8ff8db777102d5dd05173087d4af499bac8d8bbb
|
data/.rspec
CHANGED
@@ -1 +1 @@
|
|
1
|
-
--require spec_helper
|
1
|
+
--require spec_helper
|
data/README.md
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
stretchy-model
|
2
2
|
===
|
3
|
-
|
4
3
|
<p>
|
5
4
|
<a href="https://stretchy.io/" target="_blank"><img src="./stretchy.logo.png" alt="Gum Image" width="450" /></a>
|
6
5
|
<br><br>
|
@@ -9,104 +8,42 @@ stretchy-model
|
|
9
8
|
|
10
9
|
</p>
|
11
10
|
|
12
|
-
Stretchy provides Elasticsearch models in a Rails environment with an integrated ActiveRecord-like interface and features.
|
13
11
|
|
14
12
|
## Features
|
15
13
|
Stretchy simplifies the process of querying, aggregating, and managing Elasticsearch-backed models, allowing Rails developers to work with search indices as comfortably as they would with traditional Rails models.
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
attribute :flagged, :boolean, default: false
|
25
|
-
attribute :author, :hash
|
26
|
-
attribute :tags, :array, default: []
|
27
|
-
|
28
|
-
end
|
29
|
-
```
|
30
|
-
>[!NOTE]
|
31
|
-
>`created_at`, `:updated_at` and `:id` are automatically added to all `Stretchy::Records`
|
32
|
-
|
33
|
-
|
34
|
-
## Query
|
35
|
-
```ruby
|
36
|
-
Post.where('author.name': "Jadzia", flagged: true).first
|
37
|
-
#=> <Post id: aW02w3092, title: "Fun Cats", body: "...", flagged: true,
|
38
|
-
# author: {name: "Jadzia", age: 20}, tags: ["cat", "amusing"]>
|
39
|
-
```
|
40
|
-
|
41
|
-
## Aggregations
|
42
|
-
```ruby
|
43
|
-
|
44
|
-
result = Post.filter(:range, 'author.age': {gte: 18})
|
45
|
-
.aggregation(:post_frequency, date_histogram: {
|
46
|
-
field: :created_at,
|
47
|
-
calender_interval: :month
|
48
|
-
})
|
49
|
-
|
50
|
-
result.aggregations.post_frequency
|
51
|
-
#=> {buckets: [{key_as_string: "2024-01-01", doc_count: 20}, ...]}
|
52
|
-
```
|
53
|
-
|
54
|
-
## Scopes
|
55
|
-
|
56
|
-
```ruby
|
57
|
-
class Post < Stretchy::Record
|
58
|
-
# ...attributes
|
59
|
-
|
60
|
-
# Scopes
|
61
|
-
scope :flagged, -> { where(flagged: true) }
|
62
|
-
scope :top_links, lambda do |size=10, url=".com"|
|
63
|
-
aggregation(:links,
|
64
|
-
terms: {
|
65
|
-
field: :links,
|
66
|
-
size: size,
|
67
|
-
include: ".*#{url}.*"
|
68
|
-
})
|
69
|
-
end
|
70
|
-
end
|
15
|
+
* Model fully back by Elasticsearch/Opensearch
|
16
|
+
* Chain queries, scopes and aggregations
|
17
|
+
* Reduce Elasticsearch query complexity
|
18
|
+
* Support for time-based indices and aliases
|
19
|
+
* Associations to both ActiveRecord models and Stretchy::Record
|
20
|
+
* Bulk Operations made easy
|
21
|
+
* Validations, custom attributes, and more...
|
71
22
|
|
72
|
-
|
73
|
-
# links in results.aggregations.links
|
74
|
-
result = Post.flagged.top_links(10, "youtube.com")
|
23
|
+
Follow the guides to learn more about:
|
75
24
|
|
76
|
-
|
25
|
+
* [Models](https://theablefew.github.io/stretchy/#/guides/models?id=models)
|
26
|
+
* [Querying](https://theablefew.github.io/stretchy/#/guides/querying?id=querying)
|
27
|
+
* [Aggregations](https://theablefew.github.io/stretchy/#/guides/aggregations?id=aggregations)
|
28
|
+
* [Scopes](https://theablefew.github.io/stretchy/#/guides/scopes?id=scopes)
|
77
29
|
|
78
|
-
## Bulk Operations
|
79
30
|
|
31
|
+
[Read the Documentation](https://theablefew.github.io/stretchy/#/) or walk through of a simple [Data Analysis](https://theablefew.github.io/stretchy/#/examples/data_analysis?id=data-analysis) example.
|
80
32
|
|
81
|
-
```ruby
|
82
|
-
Model.bulk(records_as_bulk_operations)
|
83
|
-
```
|
84
33
|
|
85
|
-
#### Bulk helper
|
86
|
-
Generates structure for the bulk operation
|
87
|
-
```ruby
|
88
|
-
record.to_bulk # default to_bulk(:index)
|
89
|
-
record.to_bulk(:delete)
|
90
|
-
record.to_bulk(:update)
|
91
|
-
```
|
92
|
-
|
93
|
-
#### In batches
|
94
|
-
Run bulk operations in batches specified by `size`
|
95
|
-
```ruby
|
96
|
-
Model.bulk_in_batches(records, size: 100) do |batch|
|
97
|
-
batch.map! { |record| Model.new(record).to_bulk }
|
98
|
-
end
|
99
|
-
```
|
100
34
|
|
101
35
|
## Installation
|
102
36
|
|
103
37
|
Install the gem and add to the application's Gemfile by executing:
|
104
38
|
|
105
|
-
|
39
|
+
```sh
|
40
|
+
bundle add stretchy-model
|
41
|
+
```
|
106
42
|
|
107
43
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
108
|
-
|
109
|
-
|
44
|
+
```sh
|
45
|
+
gem install stretchy-model
|
46
|
+
```
|
110
47
|
|
111
48
|
<details>
|
112
49
|
<summary>Rails Configuration</summary>
|
@@ -145,8 +82,6 @@ end
|
|
145
82
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
146
83
|
|
147
84
|
>[!TIP]
|
148
|
-
>This library is built on top of the excellent [elasticsearch-persistence](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-persistence) gem.
|
149
|
-
>
|
150
85
|
> Full documentation on [Elasticsearch Query DSL and Aggregation options](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-persistence)
|
151
86
|
|
152
87
|
## Testing
|
@@ -3,7 +3,11 @@ module Stretchy
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
def save!
|
6
|
+
if valid?
|
6
7
|
self.save
|
8
|
+
else
|
9
|
+
raise "Record is invalid"
|
10
|
+
end
|
7
11
|
end
|
8
12
|
|
9
13
|
# Required for Elasticsearch < 7
|
@@ -34,7 +38,7 @@ module Stretchy
|
|
34
38
|
end
|
35
39
|
|
36
40
|
def association_reflection(association)
|
37
|
-
|
41
|
+
Stretchy::Relation.new @@_associations[association], (dirty[association.to_sym] || [])
|
38
42
|
end
|
39
43
|
|
40
44
|
def _destroy=(bool)
|
@@ -48,6 +52,7 @@ module Stretchy
|
|
48
52
|
def save_associations
|
49
53
|
@_after_save_objects.each_pair do |association, collection|
|
50
54
|
collection.each do |instance|
|
55
|
+
# TODO: bulk update
|
51
56
|
instance.send("#{@@_association_options[association.to_sym][:foreign_key]}=", self.id)
|
52
57
|
instance.save
|
53
58
|
end
|
@@ -59,59 +64,194 @@ module Stretchy
|
|
59
64
|
@@_associations ||= {}
|
60
65
|
@@_association_options ||= {}
|
61
66
|
|
67
|
+
# The belongs_to method is used to set up a one-to-one connection with another model.
|
68
|
+
# This indicates that this model has exactly one instance of another model.
|
69
|
+
# For example, if your application includes authors and books, and each book can be assigned exactly one author,
|
70
|
+
# you'd declare the book model to belong to the author model.
|
71
|
+
#
|
72
|
+
# association:: [Symbol] the name of the association
|
73
|
+
# options:: [Hash] a hash to set up options for the association
|
74
|
+
# :foreign_key - the foreign key used for the association. Defaults to "#{association}_id"
|
75
|
+
# :primary_key - the primary key used for the association. Defaults to "id"
|
76
|
+
# :class_name - the name of the associated object's class. Defaults to the name of the association
|
77
|
+
#
|
78
|
+
# Example:
|
79
|
+
# belongs_to :author
|
80
|
+
#
|
81
|
+
# This creates a book.author method that returns the author of the book.
|
82
|
+
# It also creates an author= method that allows you to assign the author of the book.
|
83
|
+
#
|
62
84
|
def belongs_to(association, options = {})
|
63
85
|
@@_association_options[association] = {
|
64
86
|
foreign_key: "#{association}_id",
|
65
87
|
primary_key: "id",
|
66
88
|
class_name: association
|
67
|
-
}.
|
89
|
+
}.merge(options)
|
68
90
|
|
69
91
|
klass = @@_association_options[association][:class_name].to_s.singularize.classify.constantize
|
70
92
|
@@_associations[association] = klass
|
71
93
|
|
72
94
|
define_method(association.to_sym) do
|
73
|
-
|
95
|
+
instance_variable_get("@#{association}") ||
|
96
|
+
klass.where(_id: self.send(@@_association_options[association][:foreign_key].to_sym)).first
|
74
97
|
end
|
75
98
|
|
76
99
|
define_method("#{association}=".to_sym) do |val|
|
77
100
|
options = @@_association_options[association]
|
78
|
-
|
101
|
+
self.send("#{options[:foreign_key]}=", val.send(options[:primary_key]))
|
102
|
+
instance_variable_set("@#{association}", val)
|
103
|
+
end
|
104
|
+
|
105
|
+
define_method("build_#{association}") do |*args|
|
106
|
+
associated_object = klass.new(*args)
|
107
|
+
instance_variable_set("@#{association}", associated_object)
|
108
|
+
associated_object
|
109
|
+
end
|
110
|
+
|
111
|
+
before_save do
|
112
|
+
associated_object = instance_variable_get("@#{association}")
|
113
|
+
if associated_object && associated_object.new_record?
|
114
|
+
if associated_object.save!
|
115
|
+
self.send("#{@@_association_options[association][:foreign_key]}=", associated_object.id)
|
116
|
+
end
|
117
|
+
end
|
79
118
|
end
|
80
119
|
end
|
81
120
|
|
82
|
-
def has_one(association, class_name: nil, foreign_key: nil, dependent: :destroy)
|
83
121
|
|
84
|
-
|
85
|
-
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
|
128
|
+
|
129
|
+
|
130
|
+
# The has_one method is used to set up a one-to-one connection with another model.
|
131
|
+
# This indicates that this model contains the foreign key.
|
132
|
+
#
|
133
|
+
# association:: [Symbol] The name of the association.
|
134
|
+
# options:: [Hash] A hash to set up options for the association.
|
135
|
+
# :class_name - The name of the associated model. If not provided, it's derived from +association+.
|
136
|
+
# :foreign_key - The name of the foreign key on the associated model. If not provided, it's derived from the name of this model.
|
137
|
+
# :dependent - If set to +:destroy+, the associated object will be destroyed when this object is destroyed. This is the default behavior.
|
138
|
+
# :primary_key - The name of the primary key on the associated model. If not provided, it's assumed to be +id+.
|
139
|
+
#
|
140
|
+
#
|
141
|
+
# Example:
|
142
|
+
# has_one :profile
|
143
|
+
#
|
144
|
+
# This creates a user.profile method that returns the profile of the user.
|
145
|
+
# It also creates a profile= method that allows you to assign the profile of the user.
|
146
|
+
#
|
147
|
+
def has_one(association, options = {})
|
148
|
+
|
149
|
+
@@_association_options[association] = {
|
150
|
+
foreign_key: "#{self.name.underscore}_id",
|
151
|
+
primary_key: "id",
|
152
|
+
class_name: association
|
153
|
+
}.merge(options)
|
154
|
+
|
155
|
+
klass = @@_association_options[association][:class_name].to_s.singularize.classify.constantize
|
86
156
|
@@_associations[association] = klass
|
87
157
|
|
158
|
+
foreign_key = @@_association_options[association][:foreign_key]
|
159
|
+
|
88
160
|
define_method(association.to_sym) do
|
89
|
-
|
161
|
+
instance_variable_get("@#{association}") ||
|
162
|
+
klass.where("#{foreign_key}": self.id).first
|
163
|
+
end
|
164
|
+
|
165
|
+
define_method("#{association}=".to_sym) do |val|
|
166
|
+
instance_variable_set("@#{association}", val)
|
167
|
+
save!
|
168
|
+
end
|
169
|
+
|
170
|
+
before_save do
|
171
|
+
associated_object = instance_variable_get("@#{association}")
|
172
|
+
if associated_object
|
173
|
+
associated_object.send("#{foreign_key}=", self.id)
|
174
|
+
associated_object.save!
|
175
|
+
end
|
90
176
|
end
|
91
177
|
end
|
92
178
|
|
93
|
-
def has_many(association, klass, options = {})
|
94
|
-
@@_associations[association] = klass
|
95
179
|
|
96
|
-
opt_fk = options.delete(:foreign_key)
|
97
|
-
foreign_key = opt_fk ? opt_fk : "#{self.name.split("::").last.tableize.singularize}_id"
|
98
180
|
|
99
|
-
|
181
|
+
|
182
|
+
|
183
|
+
|
184
|
+
|
185
|
+
|
186
|
+
|
187
|
+
# The has_many method is used to set up a one-to-many connection with another model.
|
188
|
+
# This indicates that this model can be matched with zero or more instances of another model.
|
189
|
+
# For example, if your application includes authors and books, and each author can have many books,
|
190
|
+
# you'd declare the author model to have many books.
|
191
|
+
#
|
192
|
+
# association:: [Symbol] the name of the association
|
193
|
+
# options:: [Hash] a hash to set up options for the association
|
194
|
+
# :foreign_key - the foreign key used for the association. Defaults to "#{self.name.downcase}_id"
|
195
|
+
# :primary_key - the primary key used for the association. Defaults to "id"
|
196
|
+
# :class_name - the name of the associated object's class. Defaults to the name of the association
|
197
|
+
# :dependent - if set to :destroy, the associated object will be destroyed when this object is destroyed. This is the default behavior.
|
198
|
+
#
|
199
|
+
#
|
200
|
+
# Example:
|
201
|
+
# has_many :books
|
202
|
+
#
|
203
|
+
# This creates an author.books method that returns a collection of books for the author.
|
204
|
+
# It also creates a books= method that allows you to assign the books for the author.
|
205
|
+
#
|
206
|
+
def has_many(association, options = {})
|
207
|
+
@@_association_options[association] = {
|
208
|
+
foreign_key: "#{self.name.underscore}_id",
|
209
|
+
primary_key: "id",
|
210
|
+
class_name: association.to_s.singularize.to_sym
|
211
|
+
}.merge(options)
|
212
|
+
|
213
|
+
klass = @@_association_options[association][:class_name].to_s.classify.constantize
|
214
|
+
foreign_key = @@_association_options[association][:foreign_key]
|
215
|
+
primary_key = @@_association_options[association][:primary_key]
|
216
|
+
@@_associations[association] = klass
|
100
217
|
|
101
218
|
define_method(association.to_sym) do
|
102
219
|
args = {}
|
103
|
-
args[
|
220
|
+
args["_#{primary_key}"] = self.send("#{association.to_s.singularize}_ids")
|
104
221
|
self.new_record? ? association_reflection(association) : klass.where(args)
|
105
222
|
end
|
106
223
|
|
224
|
+
define_method("#{association.to_s.singularize}_ids") do
|
225
|
+
instance_variable_get("@#{association.to_s.singularize}_ids".to_sym)
|
226
|
+
end
|
227
|
+
|
228
|
+
define_method("#{association.to_s.singularize}_ids=") do |val|
|
229
|
+
instance_variable_set("@#{association.to_s.singularize}_ids".to_sym, val)
|
230
|
+
end
|
231
|
+
|
232
|
+
define_method("#{association}=".to_sym) do |val|
|
233
|
+
val.each { |v| after_save_objects(v.attributes, association)}
|
234
|
+
self.send("#{association.to_s.singularize}_ids=", val.map(&:id))
|
235
|
+
dirty
|
236
|
+
end
|
237
|
+
|
107
238
|
define_method("build_#{association}".to_sym) do |*args|
|
108
239
|
opts = {}
|
109
240
|
opts[foreign_key] = self.id
|
110
241
|
args.first.merge! opts
|
111
242
|
klass.new *args
|
112
243
|
end
|
244
|
+
|
245
|
+
after_save do
|
246
|
+
save_associations
|
247
|
+
end
|
113
248
|
end
|
114
249
|
|
250
|
+
|
251
|
+
|
252
|
+
|
253
|
+
|
254
|
+
|
115
255
|
def validates_associated(*attr_names)
|
116
256
|
validates_with AssociatedValidator, _merge_attributes(attr_names)
|
117
257
|
end
|
@@ -131,7 +271,7 @@ module Stretchy
|
|
131
271
|
end
|
132
272
|
|
133
273
|
def reflect_on_association(association)
|
134
|
-
|
274
|
+
Stretchy::Relation.new @@_associations[association]
|
135
275
|
end
|
136
276
|
|
137
277
|
def update_all(records, **attributes)
|
data/lib/stretchy/attributes.rb
CHANGED
@@ -1,10 +1,30 @@
|
|
1
1
|
module Stretchy
|
2
2
|
module Attributes
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def [](attribute)
|
6
|
+
self.send(attribute)
|
7
|
+
end
|
8
|
+
|
9
|
+
def []=(attribute, value)
|
10
|
+
self.send("#{attribute}=", value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"#<#{self.class.name} #{attributes.map { |k,v| "#{k}: #{v.blank? ? 'nil' : v}" }.join(', ')}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
def inspect
|
19
|
+
"#<#{self.name} #{attribute_types.map { |k,v| "#{k}: #{v.type}" }.join(', ')}>"
|
20
|
+
end
|
21
|
+
end
|
3
22
|
|
4
23
|
def self.register!
|
5
|
-
ActiveModel::Type.register(:array,
|
6
|
-
ActiveModel::Type.register(:hash,
|
24
|
+
ActiveModel::Type.register(:array, Stretchy::Attributes::Type::Array)
|
25
|
+
ActiveModel::Type.register(:hash, Stretchy::Attributes::Type::Hash)
|
7
26
|
ActiveModel::Type.register(:keyword, Stretchy::Attributes::Type::Keyword)
|
27
|
+
ActiveModel::Type.register(:text, Stretchy::Attributes::Type::Text)
|
8
28
|
end
|
9
29
|
end
|
10
30
|
end
|
data/lib/stretchy/common.rb
CHANGED
@@ -2,11 +2,10 @@ module Stretchy
|
|
2
2
|
module Common
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
def
|
6
|
-
|
5
|
+
def highlights_for(attribute)
|
6
|
+
highlights[attribute.to_s]
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
9
|
class_methods do
|
11
10
|
|
12
11
|
# Set the default sort key to be used in sort operations
|
@@ -28,11 +28,17 @@ module Stretchy
|
|
28
28
|
if @index_name.respond_to?(:call)
|
29
29
|
@index_name.call
|
30
30
|
else
|
31
|
-
@index_name || base_class.model_name.collection
|
31
|
+
@index_name || base_class.model_name.collection.parameterize.underscore
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
def reload_gateway_configuration!
|
36
|
+
@gateway = nil
|
37
|
+
end
|
38
|
+
|
35
39
|
def gateway(&block)
|
40
|
+
reload_gateway_configuration! if @gateway && @gateway.client != Stretchy.configuration.client
|
41
|
+
|
36
42
|
@gateway ||= Stretchy::Repository.create(client: Stretchy.configuration.client, index_name: index_name, klass: base_class)
|
37
43
|
block.arity < 1 ? @gateway.instance_eval(&block) : block.call(@gateway) if block_given?
|
38
44
|
@gateway
|
@@ -9,6 +9,7 @@ module Stretchy
|
|
9
9
|
|
10
10
|
def deserialize(document)
|
11
11
|
attribs = ActiveSupport::HashWithIndifferentAccess.new(document['_source']).deep_symbolize_keys
|
12
|
+
attribs[:_highlights] = document["highlight"] if document["highlight"]
|
12
13
|
_id = __get_id_from_document(document)
|
13
14
|
attribs[:id] = _id if _id
|
14
15
|
klass.new attribs
|
data/lib/stretchy/querying.rb
CHANGED
@@ -6,7 +6,7 @@ module Stretchy
|
|
6
6
|
delegate *Stretchy::Relations::AggregationMethods::AGGREGATION_METHODS, to: :all
|
7
7
|
|
8
8
|
delegate :skip_callbacks, :routing, :search_options, to: :all
|
9
|
-
delegate :must, :must_not, :should, :where_not, :where, :filter_query, :query_string, to: :all
|
9
|
+
delegate :must, :must_not, :should, :where_not, :where, :filter_query, :query_string, :regexp, to: :all
|
10
10
|
|
11
11
|
def fetch_results(es)
|
12
12
|
unless es.count?
|
data/lib/stretchy/record.rb
CHANGED
@@ -14,11 +14,6 @@ module Stretchy
|
|
14
14
|
include ActiveModel::Conversion
|
15
15
|
include ActiveModel::Serialization
|
16
16
|
include ActiveModel::Serializers::JSON
|
17
|
-
include ActiveModel::Validations
|
18
|
-
include ActiveModel::Validations::Callbacks
|
19
|
-
extend ActiveModel::Callbacks
|
20
|
-
|
21
|
-
|
22
17
|
|
23
18
|
include Stretchy::Model::Callbacks
|
24
19
|
include Stretchy::Indexing::Bulk
|
@@ -28,6 +23,8 @@ module Stretchy
|
|
28
23
|
include Stretchy::Common
|
29
24
|
include Stretchy::Scoping
|
30
25
|
include Stretchy::Utils
|
26
|
+
include Stretchy::SharedScopes
|
27
|
+
include Stretchy::Attributes
|
31
28
|
|
32
29
|
extend Stretchy::Delegation::DelegateCache
|
33
30
|
extend Stretchy::Querying
|
@@ -44,11 +41,13 @@ module Stretchy
|
|
44
41
|
# overriden by #size
|
45
42
|
default_size 10000
|
46
43
|
|
47
|
-
|
44
|
+
attr_accessor :highlights
|
45
|
+
|
46
|
+
def initialize(attributes = {})
|
47
|
+
@highlights = attributes.delete(:_highlights)
|
48
|
+
super(attributes)
|
49
|
+
end
|
48
50
|
|
49
|
-
def initialize(attributes = {})
|
50
|
-
self.assign_attributes(attributes) if attributes
|
51
|
-
super()
|
52
51
|
end
|
53
52
|
|
54
53
|
end
|
data/lib/stretchy/relation.rb
CHANGED
@@ -3,20 +3,16 @@ module Stretchy
|
|
3
3
|
# It provides methods for querying and manipulating the documents.
|
4
4
|
class Relation
|
5
5
|
|
6
|
-
# These methods can accept multiple values.
|
7
|
-
MULTI_VALUE_METHODS = [:order, :where, :or_filter, :filter_query, :bind, :extending, :unscope, :skip_callbacks]
|
8
|
-
|
9
|
-
# These methods can accept a single value.
|
10
|
-
SINGLE_VALUE_METHODS = [:limit, :offset, :routing, :size]
|
11
|
-
|
12
6
|
# These methods cannot be used with the `delete_all` method.
|
13
7
|
INVALID_METHODS_FOR_DELETE_ALL = [:limit, :offset]
|
14
8
|
|
15
|
-
# All value methods.
|
16
|
-
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
|
17
|
-
|
18
9
|
# Include modules.
|
19
|
-
include Relations::FinderMethods,
|
10
|
+
include Relations::FinderMethods,
|
11
|
+
Relations::SpawnMethods,
|
12
|
+
Relations::QueryMethods,
|
13
|
+
Relations::AggregationMethods,
|
14
|
+
Relations::SearchOptionMethods,
|
15
|
+
Delegation
|
20
16
|
|
21
17
|
# Getters.
|
22
18
|
attr_reader :klass, :loaded
|
@@ -49,14 +45,13 @@ module Stretchy
|
|
49
45
|
#
|
50
46
|
# @return [Array] The results of the relation.
|
51
47
|
def to_a
|
52
|
-
|
53
48
|
load
|
54
49
|
@records
|
55
50
|
end
|
56
51
|
alias :results :to_a
|
57
52
|
|
58
53
|
def response
|
59
|
-
|
54
|
+
results.response
|
60
55
|
end
|
61
56
|
|
62
57
|
# Returns the results of the relation as a JSON object.
|
@@ -64,7 +59,7 @@ module Stretchy
|
|
64
59
|
# @param options [Hash] The options to pass to the `as_json` method.
|
65
60
|
# @return [Hash] The results of the relation as a JSON object.
|
66
61
|
def as_json(options = nil)
|
67
|
-
|
62
|
+
results.as_json(options)
|
68
63
|
end
|
69
64
|
|
70
65
|
# Returns the Elasticsearch query for the relation.
|
@@ -98,7 +93,6 @@ module Stretchy
|
|
98
93
|
# @return [Relation] The relation object.
|
99
94
|
def load
|
100
95
|
exec_queries unless loaded?
|
101
|
-
|
102
96
|
self
|
103
97
|
end
|
104
98
|
alias :fetch :load
|
@@ -146,8 +140,8 @@ module Stretchy
|
|
146
140
|
begin
|
147
141
|
entries = to_a.results.take([size_value.to_i + 1, 11].compact.min).map!(&:inspect)
|
148
142
|
message = {}
|
149
|
-
message = {total:
|
150
|
-
message.merge!(aggregations:
|
143
|
+
message = {total: results.total, max: results.total}
|
144
|
+
message.merge!(aggregations: response.aggregations.keys) unless response.aggregations.nil?
|
151
145
|
message = message.each_pair.collect { |k,v| "#{k}: #{v}" }
|
152
146
|
message.unshift entries.join(', ') unless entries.size.zero?
|
153
147
|
"#<#{self.class.name} #{message.join(', ')}>"
|
@@ -7,8 +7,12 @@ module Stretchy
|
|
7
7
|
class HashMerger # :nodoc:
|
8
8
|
attr_reader :relation, :hash
|
9
9
|
|
10
|
+
VALUE_METHODS = Stretchy::Relations::QueryMethods::MULTI_VALUE_METHODS.concat(
|
11
|
+
Stretchy::Relations::QueryMethods::SINGLE_VALUE_METHODS
|
12
|
+
)
|
13
|
+
|
10
14
|
def initialize(relation, hash)
|
11
|
-
hash.assert_valid_keys(*
|
15
|
+
hash.assert_valid_keys(*VALUE_METHODS)
|
12
16
|
|
13
17
|
@relation = relation
|
14
18
|
@hash = hash
|
@@ -38,6 +38,10 @@ module Stretchy
|
|
38
38
|
@shoulds ||= compact_where(values[:should])
|
39
39
|
end
|
40
40
|
|
41
|
+
def regexes
|
42
|
+
@regexes ||= values[:regexp]
|
43
|
+
end
|
44
|
+
|
41
45
|
def fields
|
42
46
|
values[:field]
|
43
47
|
end
|
@@ -88,7 +92,7 @@ module Stretchy
|
|
88
92
|
private
|
89
93
|
|
90
94
|
def missing_bool_query?
|
91
|
-
query.nil? && must_nots.nil? && shoulds.nil?
|
95
|
+
query.nil? && must_nots.nil? && shoulds.nil? && regexes.nil?
|
92
96
|
end
|
93
97
|
|
94
98
|
def missing_query_string?
|
@@ -102,7 +106,12 @@ module Stretchy
|
|
102
106
|
def build_query
|
103
107
|
return if missing_bool_query? && missing_query_string? && missing_query_filter?
|
104
108
|
structure.query do
|
109
|
+
structure.regexp do
|
110
|
+
build_regexp unless regexes.nil?
|
111
|
+
end
|
112
|
+
|
105
113
|
structure.bool do
|
114
|
+
|
106
115
|
structure.must query unless missing_bool_query?
|
107
116
|
structure.must_not must_nots unless must_nots.nil?
|
108
117
|
structure.set! :should, shoulds unless shoulds.nil?
|
@@ -120,6 +129,14 @@ module Stretchy
|
|
120
129
|
end.with_indifferent_access
|
121
130
|
end
|
122
131
|
|
132
|
+
def build_regexp
|
133
|
+
regexes.each do |args|
|
134
|
+
target_field = args.first.keys.first
|
135
|
+
value_field = args.first.values.first
|
136
|
+
structure.set! target_field, args.last.merge(value: value_field)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
123
140
|
def build_filtered_query
|
124
141
|
structure.filter do
|
125
142
|
structure.or do
|
@@ -157,7 +174,13 @@ module Stretchy
|
|
157
174
|
structure.highlight do
|
158
175
|
structure.fields do
|
159
176
|
highlights.each do |highlight|
|
160
|
-
|
177
|
+
if highlight.is_a?(String) || highlight.is_a?(Symbol)
|
178
|
+
structure.set! highlight, {}
|
179
|
+
elsif highlight.is_a?(Hash)
|
180
|
+
highlight.each_pair do |k,v|
|
181
|
+
structure.set! k, v
|
182
|
+
end
|
183
|
+
end
|
161
184
|
end
|
162
185
|
end
|
163
186
|
end
|
@@ -18,7 +18,8 @@ module Stretchy
|
|
18
18
|
:filter_query,
|
19
19
|
:or_filter,
|
20
20
|
:extending,
|
21
|
-
:skip_callbacks
|
21
|
+
:skip_callbacks,
|
22
|
+
:regexp
|
22
23
|
]
|
23
24
|
|
24
25
|
SINGLE_VALUE_METHODS = [:size]
|
@@ -173,6 +174,16 @@ module Stretchy
|
|
173
174
|
# }
|
174
175
|
# }
|
175
176
|
#
|
177
|
+
# .where acts as a convienence method for adding conditions to the query. It can also be used to add
|
178
|
+
# range , regex, terms, and id queries through shorthand parameters.
|
179
|
+
#
|
180
|
+
# @example
|
181
|
+
# Model.where(price: {gte: 10, lte: 20})
|
182
|
+
# Model.where(age: 19..33)
|
183
|
+
# Model.where(color: /gr(a|e)y/)
|
184
|
+
# Model.where(id: [10, 22, 18])
|
185
|
+
# Model.where(names: ['John', 'Jane'])
|
186
|
+
#
|
176
187
|
# @return [ActiveRecord::Relation, WhereChain] a new relation, which reflects the conditions, or a WhereChain if opts is :chain
|
177
188
|
# @see #must
|
178
189
|
def where(opts = :chain, *rest)
|
@@ -181,7 +192,28 @@ module Stretchy
|
|
181
192
|
elsif opts.blank?
|
182
193
|
self
|
183
194
|
else
|
184
|
-
|
195
|
+
opts.each do |key, value|
|
196
|
+
case value
|
197
|
+
when Range
|
198
|
+
between(value, key)
|
199
|
+
when Hash
|
200
|
+
filter_query(:range, key => value) if value.keys.any? { |k| [:gte, :lte, :gt, :lt].include?(k) }
|
201
|
+
when Regexp
|
202
|
+
regexp(Hash[key, value])
|
203
|
+
when Array
|
204
|
+
# handle ID queries
|
205
|
+
# if [:id, :_id].include?(key)
|
206
|
+
|
207
|
+
# else
|
208
|
+
spawn.where!(opts, *rest)
|
209
|
+
# end
|
210
|
+
else
|
211
|
+
spawn.where!(opts, *rest)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
self
|
216
|
+
|
185
217
|
end
|
186
218
|
end
|
187
219
|
|
@@ -199,6 +231,38 @@ module Stretchy
|
|
199
231
|
# @see #where
|
200
232
|
alias :must :where
|
201
233
|
|
234
|
+
# Adds a regexp condition to the query.
|
235
|
+
#
|
236
|
+
# @param field [Hash] the field to filter by and the Regexp to match
|
237
|
+
# @param opts [Hash] additional options for the regexp query
|
238
|
+
# - :flags [String] the flags to use for the regexp query (e.g. 'ALL')
|
239
|
+
# - :use_keyword [Boolean] whether to use the .keyword field for the regexp query. Default: true
|
240
|
+
# - :case_insensitive [Boolean] whether to use case insensitive matching. If the regexp has ignore case flag `/regex/i`, this is automatically set to true
|
241
|
+
# - :max_determinized_states [Integer] the maximum number of states that the regexp query can produce
|
242
|
+
# - :rewrite [String] the rewrite method to use for the regexp query
|
243
|
+
#
|
244
|
+
#
|
245
|
+
# @example
|
246
|
+
# Model.regexp(:name, /john|jane/)
|
247
|
+
# Model.regexp(:name, /john|jane/i)
|
248
|
+
# Model.regexp(:name, /john|jane/i, flags: 'ALL')
|
249
|
+
#
|
250
|
+
# @return [Stretchy::Relation] a new relation, which reflects the regexp condition
|
251
|
+
# @see #where
|
252
|
+
def regexp(args)
|
253
|
+
spawn.regexp!(args)
|
254
|
+
end
|
255
|
+
|
256
|
+
def regexp!(args) # :nodoc:
|
257
|
+
args = args.to_a
|
258
|
+
target_field, regex = args.shift
|
259
|
+
opts = args.to_h
|
260
|
+
opts.reverse_merge!(use_keyword: true)
|
261
|
+
target_field = "#{target_field}.keyword" if opts.delete(:use_keyword)
|
262
|
+
opts.merge!(case_insensitive: true) if regex.casefold?
|
263
|
+
self.regexp_values += [[Hash[target_field, regex.source], opts]]
|
264
|
+
self
|
265
|
+
end
|
202
266
|
|
203
267
|
|
204
268
|
|
@@ -4,7 +4,7 @@ module Stretchy
|
|
4
4
|
|
5
5
|
included do
|
6
6
|
|
7
|
-
scope :between,
|
7
|
+
scope :between, ->(range, range_field = "created_at") { filter_query(:range, range_field => {gte: range.begin, lte: range.end}) }
|
8
8
|
scope :using_time_based_indices, lambda { |range| search_options(index: time_based_indices(range)) }
|
9
9
|
|
10
10
|
end
|
data/lib/stretchy/version.rb
CHANGED
data/lib/stretchy.rb
CHANGED
@@ -7,8 +7,6 @@ require 'elasticsearch/model'
|
|
7
7
|
require 'elasticsearch/persistence'
|
8
8
|
require 'active_model'
|
9
9
|
require 'active_support/all'
|
10
|
-
require 'active_model/type/array'
|
11
|
-
require 'active_model/type/hash'
|
12
10
|
|
13
11
|
require_relative "stretchy/version"
|
14
12
|
require_relative "rails/instrumentation/railtie" if defined?(Rails)
|
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.5.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-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -224,8 +224,6 @@ files:
|
|
224
224
|
- containers/Dockerfile.elasticsearch
|
225
225
|
- containers/Dockerfile.opensearch
|
226
226
|
- docker-compose.yml
|
227
|
-
- lib/active_model/type/array.rb
|
228
|
-
- lib/active_model/type/hash.rb
|
229
227
|
- lib/rails/instrumentation/publishers.rb
|
230
228
|
- lib/rails/instrumentation/railtie.rb
|
231
229
|
- lib/stretchy.rb
|
@@ -234,7 +232,10 @@ files:
|
|
234
232
|
- lib/stretchy/associations/elastic_relation.rb
|
235
233
|
- lib/stretchy/attributes.rb
|
236
234
|
- lib/stretchy/attributes/transformers/keyword_transformer.rb
|
235
|
+
- lib/stretchy/attributes/type/array.rb
|
236
|
+
- lib/stretchy/attributes/type/hash.rb
|
237
237
|
- lib/stretchy/attributes/type/keyword.rb
|
238
|
+
- lib/stretchy/attributes/type/text.rb
|
238
239
|
- lib/stretchy/common.rb
|
239
240
|
- lib/stretchy/delegation/delegate_cache.rb
|
240
241
|
- lib/stretchy/delegation/gateway_delegation.rb
|
@@ -263,6 +264,7 @@ files:
|
|
263
264
|
- lib/stretchy/shared_scopes.rb
|
264
265
|
- lib/stretchy/utils.rb
|
265
266
|
- lib/stretchy/version.rb
|
267
|
+
- lib/stretchy_model.rb
|
266
268
|
- sig/stretchy.rbs
|
267
269
|
- stretchy-model/lib/stretchy-model.rb
|
268
270
|
- stretchy.logo.png
|