elasticsearch_record 1.7.2 → 1.8.1

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: 9e957272489ab4ebcfc628f36441959f01e20264d215f3f8d020d461447b87bc
4
- data.tar.gz: a12a713313af1fd1dd83077b96255efe9cfeced3bcc5204739fd383499e6ad8a
3
+ metadata.gz: ef01a33de054cbae1fe582ba5f7d8a5910c73dd81e3395f8b6c07ec9d3a5fbe2
4
+ data.tar.gz: d574bafc90b201869727539fd0a0c148118d6f666f9f29400690613bd7cbbaa8
5
5
  SHA512:
6
- metadata.gz: 4ef80538ae0feca07575902168df24c56696b4c4cf608ca8466b4c495261296a4338709e302bf402b2596fe1bb0617faa3c1198a7fa6f6b96cc031bd4ea7831e
7
- data.tar.gz: 5ad60438e39b4f579d84959f897452d48ac1d4b261c7b65af7539ddb7b69601b1d963dd393ab647d5dbcef55c695ce0cf5e2ffbb6db217348ead523f8e373f4b
6
+ metadata.gz: 89ac7c99f35a36650c2ac8f00600f0c702ed8ec4c4aaa5d28ad86c329a66be3625d063ada9dd8c6630b8b30eea1a5283a0231a60c0d8021242adeae4efe86316
7
+ data.tar.gz: b44f713a1650100a925c94349fcb9fdce4b72bf5acb901c5ff04b871e0586115bf5e08e42f38cd9ecb0e550663977da48e430a739b67230ced4eb29aa4f5bb8b
data/README.md CHANGED
@@ -14,8 +14,8 @@ _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionalit
14
14
 
15
15
  **PLEASE NOTE:**
16
16
 
17
- - This is the `rails-7-0-stable`-branch, which only supports rails **7.0** _(see section 'Rails_Versions' for supported versions)_
18
- - supports ActiveRecord ~> 7.0 + Elasticsearch >= 7.17
17
+ - This is the `rails-7-1-stable`-branch, which only supports rails **7.1** _(see section 'Rails_Versions' for supported versions)_
18
+ - supports ActiveRecord ~> 7.1 + Elasticsearch >= 7.17
19
19
 
20
20
  -----
21
21
 
@@ -44,10 +44,10 @@ https://github.com/ruby-smart/elasticsearch_record/tree/rails-7-0-stable
44
44
  Add this line to your application's Gemfile:
45
45
 
46
46
  ```ruby
47
- gem 'elasticsearch_record', '~> 1.7.0'
47
+ gem 'elasticsearch_record', '~> 1.8'
48
48
 
49
49
  # alternative
50
- gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-7-0-stable'
50
+ gem 'elasticsearch_record', git: 'https://github.com/ruby-smart/elasticsearch_record', branch: 'rails-7-1-stable'
51
51
 
52
52
  ```
53
53
 
@@ -87,7 +87,7 @@ To raise an exception while using transactions on a ElasticsearchRecord model, t
87
87
  However enabling this flag will surely fail transactional tests _(prevent this with 'use_transactional_tests=false')_
88
88
 
89
89
  ```ruby
90
- # config/initializers/elasticsearch_record.yml
90
+ # config/initializers/elasticsearch_record.rb
91
91
 
92
92
  # enable transactional exceptions
93
93
  ElasticsearchRecord.error_on_transaction = true
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # ElasticsearchRecord - CHANGELOG
2
2
 
3
+ ## [1.8.1] - 2024-05-07
4
+ * [add] new elasticsearch mapping types _(percolator, geo, vector, texts, ...)_
5
+ * [ref] `ElasticsearchRecord::Relation#limit` to detect `Float::INFINITY` to also set the **max_result_window**
6
+ * [fix] `ElasticsearchRecord::SchemaMigration` only returning the first ten migrations (broke migrated migrations)
7
+ * [fix] `ElasticsearchRecord::Relation::CalculationMethods#calculate` method incompatibility - renamed to `#calculate_aggregation` (+ alias to `#calculate`)
8
+ * [fix] `ElasticsearchRecord::ModelApi#bulk` method not correctly generating data for 'delete'
9
+
10
+ ## [1.8.0] - 2024-01-10
11
+ * [ref] major method & dependency refactoring for `rails 7.1` - _(Does **NOT** work with rails 7.0)_
12
+ * [add] new repository branch `rails-7-1-stable` to support different rails version
13
+ * [ref] gemspec to lock on rails 7.1
14
+
15
+ ## [1.7.3] - 2024-05-07 _(no gem release)_
16
+ * [add] new elasticsearch mapping types _(percolator, geo, vector, texts, ...)_
17
+ * [ref] `ElasticsearchRecord::Relation#limit` to detect `Float::INFINITY` to also set the **max_result_window**
18
+ * [fix] `ElasticsearchRecord::SchemaMigration` only returning the first ten migrations (broke migrated migrations)
19
+ * [fix] `ElasticsearchRecord::Relation::CalculationMethods#calculate` method incompatibility - renamed to `#calculate_aggregation` (+ alias to `#calculate`)
20
+ * [fix] `ElasticsearchRecord::ModelApi#bulk` method not correctly generating data for 'delete'
21
+
3
22
  ## [1.7.2] - 2024-01-10
4
23
  * [ref] gemspec to lock on rails 7.0
5
24
 
@@ -32,7 +32,7 @@ DESC
32
32
 
33
33
  spec.require_paths = ["lib"]
34
34
 
35
- spec.add_dependency 'activerecord', '~> 7.0.0'
35
+ spec.add_dependency 'activerecord', '~> 7.1.0'
36
36
  spec.add_dependency 'elasticsearch', '>= 7.17'
37
37
 
38
38
  #spec.add_development_dependency 'coveralls_reborn', '~> 0.25'
@@ -50,43 +50,12 @@ module ActiveRecord
50
50
  query.write?
51
51
  end
52
52
 
53
- # Executes the query object in the context of this connection and returns the raw result
54
- # from the connection adapter.
55
- # @param [ElasticsearchRecord::Query] query
56
- # @param [String (frozen)] name
57
- # @param [Boolean] async
58
- # @return [ElasticsearchRecord::Result]
59
- def execute(query, name = nil, async: false)
60
- # validate the query
61
- raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is not a "ElasticsearchRecord::Query".' unless query.is_a?(ElasticsearchRecord::Query)
62
- raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is invalid.' unless query.valid?
63
-
64
- # checks for write query - raises an exception if connection is locked to readonly ...
65
- check_if_write_query(query)
66
-
67
- api(*query.gate, query.query_arguments, name, async: async)
68
- end
69
-
70
- # gets called for all queries - a +ElasticsearchRecord::Query+ must be provided.
71
- # @param [ElasticsearchRecord::Query] query
72
- # @param [String (frozen)] name
73
- # @param [Array] binds - not supported on the top-level and therefore ignored!
74
- # @param [Boolean] prepare - used by the default AbstractAdapter - but not supported and therefore never ignored!
75
- # @param [Boolean] async
76
- # @return [ElasticsearchRecord::Result]
77
- def exec_query(query, name = "QUERY", binds = [], prepare: false, async: false)
78
- build_result(
79
- execute(query, name, async: async),
80
- columns: query.columns
81
- )
82
- end
83
-
84
53
  # Executes insert +query+ statement in the context of this connection using
85
54
  # +binds+ as the bind substitutes. +name+ is logged along with
86
55
  # the executed +query+ arguments.
87
56
  # @return [ElasticsearchRecord::Result]
88
- def exec_insert(query, name = nil, binds = [], _pk = nil, _sequence_name = nil)
89
- result = exec_query(query, name, binds)
57
+ def exec_insert(query, name = nil, binds = [], _pk = nil, _sequence_name = nil, returning: nil)
58
+ result = internal_exec_query(query, name, binds)
90
59
 
91
60
  # fetch additional Elasticsearch response result
92
61
  # raise ::ElasticsearchRecord::ResponseResultError.new('created', result.result) unless result.result == 'created'
@@ -101,7 +70,7 @@ module ActiveRecord
101
70
  # expects a integer as return.
102
71
  # @return [Integer]
103
72
  def exec_update(query, name = nil, binds = [])
104
- result = exec_query(query, name, binds)
73
+ result = internal_exec_query(query, name, binds)
105
74
 
106
75
  # fetch additional Elasticsearch response result
107
76
  # raise ::ElasticsearchRecord::ResponseResultError.new('updated', result.result) unless result.result == 'updated'
@@ -115,7 +84,7 @@ module ActiveRecord
115
84
  # expects a integer as return.
116
85
  # @return [Integer]
117
86
  def exec_delete(query, name = nil, binds = [])
118
- result = exec_query(query, name, binds)
87
+ result = internal_exec_query(query, name, binds)
119
88
 
120
89
  # fetch additional Elasticsearch response result
121
90
  # raise ::ElasticsearchRecord::ResponseResultError.new('deleted', result.result) unless result.result == 'deleted'
@@ -135,7 +104,7 @@ module ActiveRecord
135
104
  type: ElasticsearchRecord::Query::TYPE_MSEARCH,
136
105
  body: queries.map { |q| { search: q.body } })
137
106
 
138
- exec_query(query, name, async: async)
107
+ internal_exec_query(query, name, async: async)
139
108
  end
140
109
 
141
110
  # executes a count query for provided arel
@@ -151,7 +120,7 @@ module ActiveRecord
151
120
  status: query.status,
152
121
  arguments: query.arguments)
153
122
 
154
- exec_query(query, name, async: async).response['count']
123
+ internal_exec_query(query, name, async: async).response['count']
155
124
  end
156
125
 
157
126
  # returns the last inserted id from the result.
@@ -159,6 +128,40 @@ module ActiveRecord
159
128
  def last_inserted_id(result)
160
129
  result.response['_id']
161
130
  end
131
+
132
+ private
133
+
134
+ # Executes the query object in the context of this connection and returns the raw result
135
+ # from the connection adapter.
136
+ # @param [ElasticsearchRecord::Query] query
137
+ # @param [String (frozen),nil] name
138
+ # @param [Boolean] async (default: false)
139
+ # @param [Boolean] allow_retry (default: false)
140
+ # @return [ElasticsearchRecord::Result]
141
+ def internal_execute(query, name = nil, async: false, allow_retry: false, materialize_transactions: nil)
142
+ # validate the query
143
+ raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is not a "ElasticsearchRecord::Query".' unless query.is_a?(ElasticsearchRecord::Query)
144
+ raise ActiveRecord::StatementInvalid, 'Unable to execute! Provided query is invalid.' unless query.valid?
145
+
146
+ # checks for write query - raises an exception if connection is locked to readonly ...
147
+ check_if_write_query(query)
148
+
149
+ api(*query.gate, query.query_arguments, name, async: async)
150
+ end
151
+
152
+ # gets called for all queries - a +ElasticsearchRecord::Query+ must be provided.
153
+ # @param [ElasticsearchRecord::Query] query
154
+ # @param [String (frozen),nil] name
155
+ # @param [Array] binds - not supported on the top-level and therefore ignored!
156
+ # @param [Boolean] prepare - used by the default AbstractAdapter - but not supported and therefore never ignored!
157
+ # @param [Boolean] async
158
+ # @return [ElasticsearchRecord::Result]
159
+ def internal_exec_query(query, name = "QUERY", binds = [], prepare: false, async: false)
160
+ build_result(
161
+ internal_execute(query, name, async: async),
162
+ columns: query.columns
163
+ )
164
+ end
162
165
  end
163
166
  end
164
167
  end
@@ -8,11 +8,20 @@ module ActiveRecord
8
8
 
9
9
  included do
10
10
  # see @ ::ActiveRecord::ConnectionAdapters::ElasticsearchAdapter::NATIVE_DATABASE_TYPES.keys
11
- define_column_methods :string, :blob, :datetime, :bigint, :json, :binary, :boolean, :keyword,
12
- :constant_keyword, :wildcard, :long, :integer, :short, :byte, :double, :float,
13
- :half_float, :scaled_float, :unsigned_long, :date, :object, :flattened, :nested,
11
+ define_column_methods :string, :blob, :datetime, :bigint, :json,
12
+ :binary, :boolean,
13
+ :keyword, :constant_keyword, :wildcard,
14
+ :long, :integer, :short, :byte, :double, :float, :half_float, :scaled_float, :unsigned_long,
15
+ :date, :date_nanos,
16
+ :alias,
17
+ :object, :flattened, :nested, :join,
14
18
  :integer_range, :float_range, :long_range, :double_range, :date_range, :ip_range,
15
- :ip, :version, :text
19
+ :ip, :version, :murmur3,
20
+ :aggregate_metric_double, :histogram,
21
+ :text, :match_only_text, :completion, :search_as_you_type, :token_count,
22
+ :dense_vector, :sparse_vector, :rank_feature, :rank_features,
23
+ :geo_point, :geo_shape, :point, :shape,
24
+ :percolator
16
25
 
17
26
  # Appends a primary key definition to the table definition.
18
27
  # Can be called multiple times, but this is probably not a good idea.
@@ -223,7 +223,7 @@ module ActiveRecord
223
223
  # @param [String] _table_name
224
224
  # @param [Hash] field
225
225
  # @return [ActiveRecord::ConnectionAdapters::Column]
226
- def new_column_from_field(_table_name, field)
226
+ def new_column_from_field(_table_name, field, _definitions)
227
227
  ActiveRecord::ConnectionAdapters::Elasticsearch::Column.new(
228
228
  field["name"],
229
229
  field["null_value"],
@@ -23,31 +23,18 @@ require 'elasticsearch'
23
23
 
24
24
  module ActiveRecord # :nodoc:
25
25
  module ConnectionHandling # :nodoc:
26
- def elasticsearch_connection(config)
27
- config = config.symbolize_keys
28
-
29
- # move 'username' to 'user'
30
- config[:user] = config.delete(:username) if config[:username]
31
-
32
- # append 'port' to 'host'
33
- config[:host] += ":#{config.delete(:port)}" if config[:port] && config[:host]
34
-
35
- # move 'host' to 'hosts'
36
- config[:hosts] = config.delete(:host) if config[:host]
37
-
38
- # enable logging (Rails.logger)
39
- config[:logger] = logger if config.delete(:log)
26
+ def elasticsearch_adapter_class
27
+ ConnectionAdapters::ElasticsearchAdapter
28
+ end
40
29
 
41
- ConnectionAdapters::ElasticsearchAdapter.new(
42
- ConnectionAdapters::ElasticsearchAdapter.new_client(config),
43
- logger,
44
- config
45
- )
30
+ def elasticsearch_connection(config)
31
+ elasticsearch_adapter_class.new(config)
46
32
  end
47
33
  end
48
34
 
49
35
  module ConnectionAdapters # :nodoc:
50
36
  class ElasticsearchAdapter < AbstractAdapter
37
+ # defines the *Elasticsearch* adapter name.
51
38
  ADAPTER_NAME = "Elasticsearch"
52
39
 
53
40
  # defines the Elasticsearch 'base' structure, which is always included but cannot be resolved through mappings ...
@@ -68,16 +55,20 @@ module ActiveRecord # :nodoc:
68
55
 
69
56
  class << self
70
57
  def base_structure_keys
71
- @base_structure_keys ||= BASE_STRUCTURE.map { |struct| struct['name'] }.freeze
58
+ # using a class_variable to not reinitialize for descendants
59
+ @@base_structure_keys ||= BASE_STRUCTURE.map { |struct| struct['name'] }.freeze
72
60
  end
73
61
 
74
62
  def new_client(config)
75
63
  # IMPORTANT: remove +adapter+ from config - otherwise we mess up with Faraday::AdapterRegistry
76
- client = ::Elasticsearch::Client.new(config.except(:adapter))
77
- client.ping unless config[:ping] == false
78
- client
64
+ client_config = config.except(:adapter)
65
+ # add rails logger manually, if +:log+ is true
66
+ client_config[:logger] = logger if client_config.delete(:log)
67
+
68
+ # build an return new client
69
+ ::Elasticsearch::Client.new(client_config)
79
70
  rescue ::Elastic::Transport::Transport::Errors::Unauthorized
80
- raise ActiveRecord::DatabaseConnectionError.username_error(config[:user])
71
+ raise ::ActiveRecord::DatabaseConnectionError.username_error(config[:user])
81
72
  rescue ::Elastic::Transport::Transport::ServerError => error
82
73
  raise ::ActiveRecord::ConnectionNotEstablished, error.message
83
74
  end
@@ -144,25 +135,35 @@ module ActiveRecord # :nodoc:
144
135
  # define native types - which will be used for schema-dumping
145
136
  NATIVE_DATABASE_TYPES = {
146
137
  primary_key: { name: 'long' }, # maybe this hae to changed to 'keyword'
147
- string: { name: 'keyword' },
148
- blob: { name: 'binary' },
149
- datetime: { name: 'date' },
150
- bigint: { name: 'long' },
151
- json: { name: 'object' }
138
+ string: { name: 'keyword' },
139
+ blob: { name: 'binary' },
140
+ datetime: { name: 'date' },
141
+ bigint: { name: 'long' },
142
+ json: { name: 'object' }
152
143
  }.merge(
153
144
  TYPE_MAP.keys.map { |key| [key.to_sym, { name: key }] }.to_h
154
145
  )
155
146
 
156
- def initialize(*args)
157
- super(*args)
147
+ def initialize(...)
148
+ super
149
+
150
+ # transform provided config
151
+ config = @config.dup
152
+
153
+ # move 'username' to 'user'
154
+ config[:user] = config.delete(:username) if config[:username]
155
+
156
+ # append 'port' to 'host'
157
+ config[:host] += ":#{config.delete(:port)}" if config[:port] && config[:host]
158
+
159
+ # move 'host' to 'hosts'
160
+ config[:hosts] = config.delete(:host) if config[:host]
158
161
 
159
- # prepared statements are not supported by Elasticsearch.
160
- # documentation for mysql prepares statements @ https://dev.mysql.com/doc/refman/8.0/en/sql-prepared-statements.html
161
- @prepared_statements = false
162
+ @connection_parameters = config
162
163
  end
163
164
 
164
165
  def schema_migration # :nodoc:
165
- @schema_migration ||= ElasticsearchRecord::SchemaMigration
166
+ ElasticsearchRecord::SchemaMigration.new(self)
166
167
  end
167
168
 
168
169
  # provide a table_name_prefix from the configuration to create & restrict schema creation
@@ -180,6 +181,12 @@ module ActiveRecord # :nodoc:
180
181
  @config[:migrations_paths] || ['db/migrate_elasticsearch']
181
182
  end
182
183
 
184
+ # prepared statements are not supported by Elasticsearch.
185
+ # documentation for mysql prepares statements @ https://dev.mysql.com/doc/refman/8.0/en/sql-prepared-statements.html
186
+ def default_prepared_statements
187
+ false
188
+ end
189
+
183
190
  # Does this adapter support transactions in general?
184
191
  # HINT: This is +NOT* an official setting and only introduced to ElasticsearchRecord
185
192
  def supports_transactions?
@@ -236,7 +243,7 @@ module ActiveRecord # :nodoc:
236
243
  raise ::StandardError, 'ASYNC api calls are not supported' if async
237
244
 
238
245
  # resolve the API target
239
- target = namespace == :core ? @connection : @connection.__send__(namespace)
246
+ target = namespace == :core ? raw_connection : raw_connection.__send__(namespace)
240
247
 
241
248
  __send__(:log, "#{namespace}.#{action}", arguments, name, async: async, log: log) do
242
249
  response = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@@ -258,6 +265,30 @@ module ActiveRecord # :nodoc:
258
265
 
259
266
  private
260
267
 
268
+ def connect
269
+ @raw_connection = self.class.new_client(@connection_parameters)
270
+ rescue ::ActiveRecord::ConnectionNotEstablished => ex
271
+ raise ex.set_pool(@pool)
272
+ end
273
+
274
+ def reconnect
275
+ @raw_connection = nil
276
+ connect
277
+ end
278
+
279
+ def active?
280
+ !!@raw_connection&.ping
281
+ end
282
+
283
+ # Disconnects from the database if already connected.
284
+ # Otherwise, this method does nothing.
285
+ def disconnect!
286
+ super
287
+ @raw_connection = nil
288
+ end
289
+
290
+ alias :reset! :reconnect!
291
+
261
292
  def type_map
262
293
  TYPE_MAP
263
294
  end
@@ -8,8 +8,8 @@ module ElasticsearchRecord
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 1
11
- MINOR = 7
12
- TINY = 2
11
+ MINOR = 8
12
+ TINY = 1
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -45,8 +45,8 @@ module ElasticsearchRecord
45
45
  query = payload[:arguments].except(:_qt).inspect.gsub(/:(\w+)=>/, '\1: ').truncate((payload[:truncate] || 1000), omission: color(' (pruned)', RED))
46
46
 
47
47
  # final coloring
48
- name = color(name, name_color(payload[:name]), true)
49
- query = color(query, gate_color(payload[:gate], payload[:name]), true) if colorize_logging
48
+ name = color(name, name_color(payload[:name]), bold: true)
49
+ query = color(query, gate_color(payload[:gate], payload[:name]), bold: true) if colorize_logging
50
50
 
51
51
  debug " #{name} #{query.presence || '-/-'}"
52
52
  end
@@ -269,8 +269,11 @@ module ElasticsearchRecord
269
269
 
270
270
  _connection.api(:core, :bulk, {
271
271
  index: _index_name,
272
- body: if operation == :update
273
- data.map { |item| { operation => { _id: (item[:_id].presence || item['_id']), data: { doc: item.except(:_id, '_id') } } } }
272
+ body: case operation
273
+ when :update
274
+ data.map { |item| { update: { _id: (item[:_id].presence || item['_id']), data: { doc: item.except(:_id, '_id') } } } }
275
+ when :delete
276
+ data.map { |item| { delete: { _id: (item[:_id].presence || item['_id']) } } }
274
277
  else
275
278
  data.map { |item| { operation => { _id: (item[:_id].presence || item['_id']), data: item.except(:_id, '_id') } } }
276
279
  end,
@@ -58,7 +58,7 @@ module ElasticsearchRecord
58
58
 
59
59
  # clears schema-related instance variables.
60
60
  # @see ActiveRecord::ModelSchema::ClassMethods#reload_schema_from_cache
61
- def reload_schema_from_cache
61
+ def reload_schema_from_cache(recursive = true)
62
62
  # we also need to clear our custom-defined variables
63
63
  @source_column_names = nil
64
64
  @searchable_column_names = nil
@@ -7,7 +7,7 @@ module ElasticsearchRecord
7
7
  # NOTICE: We don't want to mess up with the Arel-builder - so we send new data directly to the API
8
8
  # @param [ActiveModel::Attribute] values
9
9
  # @return [Object] id
10
- def _insert_record(values)
10
+ def _insert_record(values, returning)
11
11
  # values is not a "key=>values"-Hash, but a +ActiveModel::Attribute+ - so the casted values gets resolved here
12
12
  values = values.transform_values(&:value)
13
13
 
@@ -23,7 +23,7 @@ module ElasticsearchRecord
23
23
  refresh: true)
24
24
 
25
25
  # execute query and return inserted id
26
- connection.insert(query, "#{self} Create")
26
+ connection.insert(query, "#{self} Create", returning: returning)
27
27
  end
28
28
  end
29
29
 
@@ -105,7 +105,7 @@ module ElasticsearchRecord
105
105
  # IMPORTANT: Always provide all columns
106
106
  columns: source_column_names)
107
107
 
108
- connection.exec_query(query, "#{name} ES|QL", async: async)
108
+ connection.internal_exec_query(query, "#{name} ES|QL", async: async)
109
109
  end
110
110
 
111
111
 
@@ -122,7 +122,7 @@ module ElasticsearchRecord
122
122
  # IMPORTANT: Always provide all columns
123
123
  columns: source_column_names)
124
124
 
125
- connection.exec_query(query, "#{name} Msearch", async: async)
125
+ connection.internal_exec_query(query, "#{name} Msearch", async: async)
126
126
  end
127
127
 
128
128
  # executes a search by provided +RAW+ query - supports +Elasticsearch::DSL+ gem if loaded
@@ -64,9 +64,12 @@ module ElasticsearchRecord
64
64
  #
65
65
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-boxplot-aggregation.html
66
66
  #
67
+ # @note returns *nil* on a *NullRelation*
68
+ #
67
69
  # @param [Symbol, String] column_name
70
+ # @return [Hash,nil]
68
71
  def boxplot(column_name)
69
- calculate(:boxplot, column_name)
72
+ calculate_aggregation(:boxplot, column_name)
70
73
  end
71
74
 
72
75
  # A multi-value metrics aggregation that computes stats over numeric values extracted from the aggregated documents. #
@@ -83,9 +86,12 @@ module ElasticsearchRecord
83
86
  #
84
87
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-stats-aggregation.html
85
88
  #
89
+ # @note returns *nil* on a *NullRelation*
90
+ #
86
91
  # @param [Symbol, String] column_name
92
+ # @return [Hash,nil]
87
93
  def stats(column_name)
88
- calculate(:stats, column_name)
94
+ calculate_aggregation(:stats, column_name)
89
95
  end
90
96
 
91
97
  # A multi-value metrics aggregation that computes statistics over string values extracted from the aggregated documents.
@@ -102,9 +108,12 @@ module ElasticsearchRecord
102
108
  #
103
109
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-string-stats-aggregation.html
104
110
  #
111
+ # @note returns *nil* on a *NullRelation*
112
+ #
105
113
  # @param [Symbol, String] column_name
114
+ # @return [Hash,nil]
106
115
  def string_stats(column_name)
107
- calculate(:string_stats, column_name)
116
+ calculate_aggregation(:string_stats, column_name)
108
117
  end
109
118
 
110
119
  # The matrix_stats aggregation is a numeric aggregation that computes the following statistics over a set of document fields:
@@ -118,9 +127,12 @@ module ElasticsearchRecord
118
127
  #
119
128
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-matrix-stats-aggregation.html
120
129
  #
130
+ # @note returns *nil* on a *NullRelation*
131
+ #
121
132
  # @param [Array<Symbol|String>] column_names
133
+ # @return [Hash,nil]
122
134
  def matrix_stats(*column_names)
123
- calculate(:matrix_stats, *column_names)
135
+ calculate_aggregation(:matrix_stats, *column_names)
124
136
  end
125
137
 
126
138
  # A multi-value metrics aggregation that calculates one or more
@@ -140,9 +152,12 @@ module ElasticsearchRecord
140
152
  #
141
153
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html
142
154
  #
155
+ # @note returns *nil* on a *NullRelation*
156
+ #
143
157
  # @param [Symbol, String] column_name
158
+ # @return [Hash,nil]
144
159
  def percentiles(column_name)
145
- calculate(:percentiles, column_name, node: :values)
160
+ calculate_aggregation(:percentiles, column_name, node: :values)
146
161
  end
147
162
 
148
163
  # A multi-value metrics aggregation that calculates one or more
@@ -165,10 +180,13 @@ module ElasticsearchRecord
165
180
  #
166
181
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-rank-aggregation.html
167
182
  #
183
+ # @note returns *nil* on a *NullRelation*
184
+ #
168
185
  # @param [Symbol, String] column_name
169
186
  # @param [Array] values
187
+ # @return [Hash,nil]
170
188
  def percentile_ranks(column_name, values)
171
- calculate(:percentile_ranks, column_name, opts: { values: values }, node: :values)
189
+ calculate_aggregation(:percentile_ranks, column_name, opts: { values: values }, node: :values)
172
190
  end
173
191
 
174
192
  # Calculates the cardinality on a given column. Returns +0+ if there's no row.
@@ -178,9 +196,12 @@ module ElasticsearchRecord
178
196
  #
179
197
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html
180
198
  #
199
+ # @note returns *nil* on a *NullRelation*
200
+ #
181
201
  # @param [Symbol, String] column_name
202
+ # @return [Integer,nil]
182
203
  def cardinality(column_name)
183
- calculate(:cardinality, column_name, node: :value)
204
+ calculate_aggregation(:cardinality, column_name, node: :value)
184
205
  end
185
206
 
186
207
  # Calculates the average value on a given column. Returns +nil+ if there's no row. See #calculate for examples with options.
@@ -189,9 +210,12 @@ module ElasticsearchRecord
189
210
  #
190
211
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html
191
212
  #
213
+ # @note returns *nil* on a *NullRelation*
214
+ #
192
215
  # @param [Symbol, String] column_name
216
+ # @return [Float,nil]
193
217
  def average(column_name)
194
- calculate(:avg, column_name, node: :value)
218
+ calculate_aggregation(:avg, column_name, node: :value)
195
219
  end
196
220
 
197
221
  # Calculates the minimum value on a given column. The value is returned
@@ -202,9 +226,12 @@ module ElasticsearchRecord
202
226
  #
203
227
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-min-aggregation.html
204
228
  #
229
+ # @note returns *nil* on a *NullRelation*
230
+ #
205
231
  # @param [Symbol, String] column_name
232
+ # @return [Float,nil]
206
233
  def minimum(column_name)
207
- calculate(:min, column_name, node: :value)
234
+ calculate_aggregation(:min, column_name, node: :value)
208
235
  end
209
236
 
210
237
  # Calculates the maximum value on a given column. The value is returned
@@ -215,9 +242,12 @@ module ElasticsearchRecord
215
242
  #
216
243
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-max-aggregation.html
217
244
  #
245
+ # @note returns *nil* on a *NullRelation*
246
+ #
218
247
  # @param [Symbol, String] column_name
248
+ # @return [Float,nil]
219
249
  def maximum(column_name)
220
- calculate(:max, column_name, node: :value)
250
+ calculate_aggregation(:max, column_name, node: :value)
221
251
  end
222
252
 
223
253
  # This single-value aggregation approximates the median absolute deviation of its search results.
@@ -232,9 +262,12 @@ module ElasticsearchRecord
232
262
  #
233
263
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-median-absolute-deviation-aggregation.html
234
264
  #
265
+ # @note returns *nil* on a *NullRelation*
266
+ #
235
267
  # @param [Symbol, String] column_name
268
+ # @return [Float,nil]
236
269
  def median_absolute_deviation(column_name)
237
- calculate(:median_absolute_deviation, column_name)
270
+ calculate_aggregation(:median_absolute_deviation, column_name)
238
271
  end
239
272
 
240
273
  # Calculates the sum of values on a given column. The value is returned
@@ -245,18 +278,27 @@ module ElasticsearchRecord
245
278
  #
246
279
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html
247
280
  #
281
+ # @note returns *nil* on a *NullRelation*
282
+ #
248
283
  # @param [Symbol, String] column_name (optional)
284
+ # @return [Float,nil]
249
285
  def sum(column_name)
250
- calculate(:sum, column_name, node: :value)
286
+ calculate_aggregation(:sum, column_name, node: :value)
251
287
  end
252
288
 
253
289
  # creates a aggregation with the provided metric (e.g. :sum) and columns.
254
290
  # returns the metric node (default: :value) from the aggregations result.
291
+ #
292
+ # @note returns *nil* on a *NullRelation*
293
+ #
255
294
  # @param [Symbol, String] metric
256
295
  # @param [Array<Symbol|String>] columns
257
296
  # @param [Hash] opts - additional arguments that get merged with the metric definition
258
297
  # @param [Symbol] node (default: nil)
259
- def calculate(metric, *columns, opts: {}, node: nil)
298
+ def calculate_aggregation(metric, *columns, opts: {}, node: nil)
299
+ # prevent execution on a *NullRelation*
300
+ return if null_relation?
301
+
260
302
  metric_key = "calculate_#{metric}"
261
303
 
262
304
  # spawn a new aggregation and return the aggs
@@ -272,6 +314,8 @@ module ElasticsearchRecord
272
314
  response[metric_key]
273
315
  end
274
316
  end
317
+
318
+ alias_method :calculate, :calculate_aggregation
275
319
  end
276
320
  end
277
321
  end
@@ -188,8 +188,9 @@ module ElasticsearchRecord
188
188
  end
189
189
 
190
190
  def none! # :nodoc:
191
+ @none = true
191
192
  # tell the query it 'failed'
192
- configure!(:__query__, status: :failed).extending!(ActiveRecord::NullRelation)
193
+ configure!(:__query__, status: :failed)
193
194
  end
194
195
 
195
196
  # creates a condition on the relation.
@@ -132,11 +132,10 @@ module ElasticsearchRecord
132
132
  # resolve only data from hits->hits[{_source}]
133
133
  current_results = if ids_only
134
134
  current_response['hits']['hits'].map { |result| result['_id'] }
135
- # future with helper
136
- # current_response['hits']['hits'].map.from_hash('_id')
137
135
  else
138
136
  current_response['hits']['hits'].map { |result| result['_source'].merge('_id' => result['_id']) }
139
137
  end
138
+
140
139
  current_results_length = current_results.length
141
140
 
142
141
  # check if we reached the required offset
@@ -45,7 +45,7 @@ module ElasticsearchRecord
45
45
 
46
46
  # overwrite the limit_value setter, to provide a special behaviour of auto-setting the +max_result_window+.
47
47
  def limit_value=(limit)
48
- if limit == '__max__' || (limit.nil? && delegate_query_nil_limit?)
48
+ if limit == '__max__' || limit == Float::INFINITY || (limit.nil? && delegate_query_nil_limit?)
49
49
  super(max_result_window)
50
50
  else
51
51
  super(limit)
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_record/future_result"
4
+
3
5
  module ElasticsearchRecord
4
6
  class Result
5
7
  include Enumerable
6
8
 
7
9
  # creates an empty response
8
10
  # @return [ElasticsearchRecord::Result (frozen)]
9
- def self.empty
10
- new(nil).freeze
11
+ def self.empty(async: false) # :nodoc:
12
+ if async
13
+ EMPTY_ASYNC
14
+ else
15
+ EMPTY
16
+ end
11
17
  end
12
18
 
13
19
  attr_reader :response, :columns, :column_types
@@ -272,5 +278,11 @@ module ElasticsearchRecord
272
278
  []
273
279
  end
274
280
  end
281
+
282
+ EMPTY = new([].freeze, [].freeze, {}.freeze).freeze
283
+ private_constant :EMPTY
284
+
285
+ EMPTY_ASYNC = ::ActiveRecord::FutureResult::Complete.new(EMPTY).freeze
286
+ private_constant :EMPTY_ASYNC
275
287
  end
276
288
  end
@@ -3,47 +3,20 @@
3
3
  require "active_record/schema_migration"
4
4
 
5
5
  module ElasticsearchRecord
6
- class SchemaMigration < ElasticsearchRecord::Base # :nodoc:
7
- class << self
8
- def primary_key
9
- "version"
10
- end
6
+ class SchemaMigration < ::ActiveRecord::SchemaMigration
11
7
 
12
- def table_name
13
- "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
14
- end
15
-
16
- def create_table
17
- unless connection.table_exists?(table_name)
18
- connection.create_table(table_name, id: false) do |t|
19
- t.string :version, **connection.internal_string_options_for_primary_key
20
- end
21
- end
22
- end
23
-
24
- def drop_table
25
- connection.drop_table table_name, if_exists: true
26
- end
27
-
28
- def normalize_migration_number(number)
29
- "%.3d" % number.to_i
30
- end
31
-
32
- def normalized_versions
33
- all_versions.map { |v| normalize_migration_number v }
34
- end
35
-
36
- def all_versions
37
- order(:version).pluck(:version)
38
- end
39
-
40
- def table_exists?
41
- connection.data_source_exists?(table_name)
42
- end
8
+ def table_name
9
+ "#{ElasticsearchRecord::Base.table_name_prefix}#{ElasticsearchRecord::Base.schema_migrations_table_name}#{ElasticsearchRecord::Base.table_name_suffix}"
43
10
  end
44
11
 
45
- def version
46
- super.to_i
12
+ # overwrite method to fix the default limit (= 10) to support returning more than 10 migrations
13
+ def versions
14
+ sm = Arel::SelectManager.new(arel_table)
15
+ sm.project(arel_table[primary_key])
16
+ sm.order(arel_table[primary_key].asc)
17
+ sm.take(connection.max_result_window(table_name))
18
+
19
+ connection.select_values(sm, "#{self.class} Load")
47
20
  end
48
21
  end
49
22
  end
@@ -70,7 +70,6 @@ end
70
70
  ActiveSupport.on_load(:active_record) do
71
71
  # load patches
72
72
  require 'elasticsearch_record/patches/active_record/relation_merger_patch'
73
-
74
73
  require 'elasticsearch_record/patches/arel/select_core_patch'
75
74
  require 'elasticsearch_record/patches/arel/select_manager_patch'
76
75
  require 'elasticsearch_record/patches/arel/select_statement_patch'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.2
4
+ version: 1.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Gonsior
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-10 00:00:00.000000000 Z
11
+ date: 2024-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 7.0.0
19
+ version: 7.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 7.0.0
26
+ version: 7.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: elasticsearch
29
29
  requirement: !ruby/object:Gem::Requirement