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 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