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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7dff610329ad21128c58429c6f9d08b62138a7fad9adb0f610984bd744db1cf
4
- data.tar.gz: 915256c1b413dd34d4777097b878f0c2c7886342b5d8dcfedc2159a405b3bdf2
3
+ metadata.gz: dd18b424c18bda352233d72af113e9bbc944277e03a02cfb950780f0fab9a405
4
+ data.tar.gz: 6eb86522e7bc91012cc4c743d0a9fccc54c7e75b14ff4d6f4fca5bdb9fa31626
5
5
  SHA512:
6
- metadata.gz: 66f98b878a8e78d9d79b53f05edf89a8d4a61d4fd3a5b98d93ef349491c7c87f36d4cadb857b8be5e9e992308609c22d1abc45703eb39b95e78240d87c6ae081
7
- data.tar.gz: 0eabc3b84d0aecb0f8051bf53238ec6da5d431c7ff5d0f7c3be20888a20252504e3324c7b558fe455d90c2cf0db827b1d0440e6d03d211569ed8ed7d60dade01
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
- ## Attributes
18
-
19
- ```ruby
20
- class Post < Stretchy::Record
21
-
22
- attribute :title, :string
23
- attribute :body, :string
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
- # Returns flagged posts and includes the top 10 'youtube.com'
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
- $ bundle add stretchy-model
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
- $ gem install stretchy-model
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
- ElasticRelation.new @@_associations[association], (dirty[association.to_sym] || [])
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
- }.reverse_merge(options)
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
- klass.where(_id: self.send(@@_association_options[association][:foreign_key].to_sym)).first
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
- instance_variable_set("@#{options[:foreign_key]}", val.send(options[:primary_key]))
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
- klass = association.to_s.singularize.classify.constantize unless class_name.present?
85
- foreign_key = "#{self.name.downcase}_id" unless foreign_key.present?
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
- klass.where("#{foreign_key}": self.id).first
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
- @@_association_options[association] = { foreign_key: foreign_key }
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[foreign_key] = self.id
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
- ElasticRelation.new @@_associations[association]
274
+ Stretchy::Relation.new @@_associations[association]
135
275
  end
136
276
 
137
277
  def update_all(records, **attributes)
@@ -0,0 +1,15 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Type
4
+
5
+ class Array < ActiveModel::Type::Value # :nodoc:
6
+
7
+ def type
8
+ :array
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Type
4
+ class Hash < ActiveModel::Type::Value # :nodoc:
5
+ def type
6
+ :hash
7
+ end
8
+
9
+ private
10
+
11
+ def cast_value(value)
12
+ Elasticsearch::Model::HashWrapper[value]
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module Stretchy
2
+ module Attributes
3
+ module Type
4
+ # alias for ActiveModel::Type::String
5
+ class Text < ActiveModel::Type::String
6
+ def type
7
+ :text
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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, ActiveModel::Type::Array)
6
- ActiveModel::Type.register(:hash, ActiveModel::Type::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
@@ -2,11 +2,10 @@ module Stretchy
2
2
  module Common
3
3
  extend ActiveSupport::Concern
4
4
 
5
- def inspect
6
- "#<#{self.class.name} #{attributes.map { |k,v| "#{k}: #{v.blank? ? 'nil' : v}" }.join(', ')}>"
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
@@ -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?
@@ -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
- end
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
@@ -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, Relations::SpawnMethods, Relations::QueryMethods, Relations::AggregationMethods, Relations::SearchOptionMethods, Delegation
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
- to_a.response
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
- to_a.as_json(options)
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: to_a.total, max: to_a.total}
150
- message.merge!(aggregations: results.response.aggregations.keys) unless results.response.aggregations.nil?
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(*Relation::VALUE_METHODS)
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
- structure.set! highlight, extract_highlighter(highlight)
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
- spawn.where!(opts, *rest)
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, lambda { |range| filter(:range, "date" => {gte: range.begin, lte: range.end}) }
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
@@ -1,3 +1,3 @@
1
1
  module Stretchy
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
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)
@@ -0,0 +1,9 @@
1
+ # This is a simple model that inherits from Stretchy::Record
2
+ # for aesthetic purposes.
3
+ #
4
+ # In Rails applications, you can use this model as a base class
5
+ # for your models.
6
+ #
7
+ class StretchyModel < Stretchy::Record
8
+
9
+ end
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.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-10 00:00:00.000000000 Z
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
@@ -1,13 +0,0 @@
1
- module ActiveModel
2
- module Type
3
-
4
- class Array < Value # :nodoc:
5
-
6
- def type
7
- :array
8
- end
9
-
10
- end
11
-
12
- end
13
- end
@@ -1,15 +0,0 @@
1
- module ActiveModel
2
- module Type
3
- class Hash < Value # :nodoc:
4
- def type
5
- :hash
6
- end
7
-
8
- private
9
-
10
- def cast_value(value)
11
- Elasticsearch::Model::HashWrapper[value]
12
- end
13
- end
14
- end
15
- end