stretchy-model 0.4.0 → 0.5.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/.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
|