elasticsearch_record 1.3.1 → 1.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: e8e00649a9c1e9451d8ea006078889081a70cb178a3d0f23336d157929eefafc
4
- data.tar.gz: '0596785a16b974b95a48ccb2bb3cb6a334ce553e8d80b1b82691ee98df18fe3b'
3
+ metadata.gz: 3f50568400141462093f5d91bf5b26efb44a52b9529577d5e72a4d3699a57717
4
+ data.tar.gz: 89536ffacc4ddd5fb4d0334eda843d206c25fc867664f6f9454ee07d921ec712
5
5
  SHA512:
6
- metadata.gz: 61301e89b67d34fd4ed604a782c85b84bd2316f98c7b67ddf293ad2a27e17e30531b905e7ecdc5a4d9dad34099d9feab8603269b2ee0e680b2a0cd2ff39835d3
7
- data.tar.gz: d7d208605dbbe0c09acccacda2ac6deba8a69f85540046ac098b555b9bc5212e7c19c137df1957f0b692112dc694f2a3913538671183e18052061f417f1dbd4b
6
+ metadata.gz: 20d82567d738f3f5fa78cdd1bb2595ea7cd068dd16a14fbda56b4c88450a33cac5b32106652401854e0085e1c4378cce6b3ef23983b04ec71fb14f931628c19e
7
+ data.tar.gz: 3bcd72650c2d0817048edf3eff0be6ba8de4cc568b04203c3ec9631ccd4ef269b1c91f6f70d90dc05419357e5f4d3c3f164b4b4345f72c54c9d0cfabe15d3103
data/README.md CHANGED
@@ -14,10 +14,8 @@ _ElasticsearchRecord is a ActiveRecord adapter and provides similar functionalit
14
14
 
15
15
  **PLEASE NOTE:**
16
16
 
17
- - This is still in **development**!
18
- - Specs & documentation will follow.
19
- - You might experience BUGs and Exceptions...
20
- - Currently supports only ActiveRecord 7.0 + Elasticsearch 8.4 _(downgrade for rails 6.x is planned in future versions)_
17
+ - Specs & documentation are still missing, but will follow.
18
+ - Currently supports ActiveRecord ~> 7.0 + Elasticsearch >= 7.17
21
19
 
22
20
  -----
23
21
 
@@ -55,10 +53,6 @@ Or install it yourself as:
55
53
  * logs Elasticsearch API-calls
56
54
  * shows Runtime in logs
57
55
 
58
- ## Contra - what it _(currently)_ can not
59
- * Joins to other indexes or databases
60
- * complex, combined or nested queries ```and, or, Model.arel ...```
61
-
62
56
  ## Setup
63
57
 
64
58
  ### a) Update your **database.yml** and add a elasticsearch connection:
@@ -67,7 +61,7 @@ Or install it yourself as:
67
61
 
68
62
  development:
69
63
  primary:
70
- # <...>
64
+ # <...>
71
65
 
72
66
  # elasticsearch
73
67
  elasticsearch:
@@ -75,10 +69,24 @@ Or install it yourself as:
75
69
  host: localhost:9200
76
70
  user: elastic
77
71
  password: '****'
78
- log: true
72
+
73
+ # enable ES verbose logging
74
+ # log: true
75
+
76
+ # add table (index) prefix & suffix to all 'tables'
77
+ # table_name_prefix: 'app-'
78
+ # table_name_suffix: '-development'
79
79
 
80
80
  production:
81
- ...
81
+ # <...>
82
+
83
+ # elasticsearch
84
+ elasticsearch:
85
+ # <...>
86
+
87
+ # add table (index) prefix & suffix to all 'tables'
88
+ # table_name_prefix: 'app-'
89
+ # table_name_suffix: '-production'
82
90
 
83
91
  test:
84
92
  ...
@@ -86,15 +94,19 @@ Or install it yourself as:
86
94
 
87
95
  ```
88
96
 
89
- ### b) Require ```elasticsearch_record/instrumentation``` in your application.rb (if you want to...):
97
+ ### b) Require `elasticsearch_record/instrumentation` in your application.rb (if you want to...):
98
+
90
99
  ```ruby
91
100
  # config/application.rb
101
+
92
102
  require_relative "boot"
93
103
 
94
104
  require "rails"
95
105
  # Pick the frameworks you want:
96
106
 
97
- # ...
107
+ # <...>
108
+
109
+ # add instrumentation
98
110
  require 'elasticsearch_record/instrumentation'
99
111
 
100
112
  module Application
@@ -102,14 +114,24 @@ module Application
102
114
  end
103
115
  ```
104
116
 
105
- ### c) Create a model that inherits from ```ElasticsearchRecord::Base``` model.
117
+ ### c) Create a model that inherits from `ElasticsearchRecord::Base` model.
106
118
  ```ruby
107
119
  # app/models/application_elasticsearch_record.rb
108
-
109
- class Search < ElasticsearchRecord::Base
110
-
120
+
121
+ class ApplicationElasticsearchRecord < ElasticsearchRecord::Base
122
+ # needs to be abstract
123
+ self.abstract_class = true
111
124
  end
125
+ ```
126
+
127
+ Example class, that inherits from **ApplicationElasticsearchRecord**
112
128
 
129
+ ```ruby
130
+ # app/models/search.rb
131
+
132
+ class Search < ApplicationElasticsearchRecord
133
+
134
+ end
113
135
  ```
114
136
 
115
137
  ### d) have FUN with your model:
@@ -243,11 +265,80 @@ _see simple documentation about these methods @ [rubydoc](https://rubydoc.info/g
243
265
 
244
266
  -----
245
267
 
246
- ### Useful model class attributes
247
- - index_base_name
248
- - relay_id_attribute
268
+ ## Useful model class attributes
269
+
270
+ ### index_base_name
271
+ Rails resolves a pluralized underscore table_name from the class name by default - which will not work for some models.
272
+
273
+ To support a generic +table_name_prefix+ & +table_name_suffix+ from the _database.yml_,
274
+ the 'index_base_name' provides a possibility to chain prefix, **base** and suffix.
275
+
276
+ ```ruby
277
+ class UnusalStat < ApplicationElasticsearchRecord
278
+ self.index_base_name = 'unusal-stats'
279
+ end
280
+
281
+ UnusalStat.where(year: 2023).to_query
282
+ # => {:index=>"app-unusal-stats-development", :body ...
283
+ ```
284
+
285
+ ### delegate_id_attribute
286
+ Rails resolves the primary_key's value by accessing the **#id** method.
287
+
288
+ Since Elasticsearch also supports an additional, independent **id** attribute,
289
+ it would only be able to access this through `_read_attribute(:id)`.
290
+
291
+ To also have the ability of accessing this attribute through the default, this flag can be enabled.
292
+
293
+ ```ruby
294
+ class SearchUser < ApplicationElasticsearchRecord
295
+ # attributes: id, name
296
+ end
297
+
298
+ # create new user within the index
299
+ user = SearchUser.create(id: 8, name: 'Parker')
300
+
301
+ # accessing the id, does NOT return the stored id by default - this will be delegated to the primary_key '_id'.
302
+ user.id
303
+ # => 'b2e34xa2'
304
+
305
+ # -- ENABLE delegation -------------------------------------------------------------------
306
+ SearchUser.delegate_id_attribute = true
307
+
308
+ # create new user within the index
309
+ user = SearchUser.create(id: 9, name: 'Pam')
310
+
311
+ # accessing the id accesses the stored attribute now
312
+ user.id
313
+ # => 9
314
+
315
+ # accessing the ES index id
316
+ user._id
317
+ # => 'xtf31bh8x'
318
+ ```
319
+
320
+ ## delegate_query_nil_limit
321
+ Elasticsearch's default value for queries without a **size** is forced to **10**.
322
+ To provide a similar behaviour as the (my)SQL interface,
323
+ this can be automatically set to the `max_result_window` value by calling `.limit(nil)` on the models' relation.
324
+
325
+
326
+ ```ruby
327
+ SearchUser.where(name: 'Peter').limit(nil)
328
+ # returns a maximum of 10 items ...
329
+ # => [...]
249
330
 
250
- ### Useful model class methods
331
+ # -- ENABLE delegation -------------------------------------------------------------------
332
+ SearchUser.delegate_query_nil_limit = true
333
+
334
+ SearchUser.where(name: 'Peter').limit(nil)
335
+ # returns up to 10_000 items ...
336
+ # => [...]
337
+
338
+ # hint: if you want more than 10_000 use the +#pit_results+ method!
339
+ ```
340
+
341
+ ## Useful model class methods
251
342
  - auto_increment?
252
343
  - max_result_window
253
344
  - source_column_names
@@ -255,12 +346,55 @@ _see simple documentation about these methods @ [rubydoc](https://rubydoc.info/g
255
346
  - find_by_query
256
347
  - msearch
257
348
 
349
+ ## Useful model API methods
350
+ Quick access to model-related methods for easier access without creating a overcomplicated method call on the models connection...
351
+
352
+ Access these methods through the model class method `.api`.
353
+ ```ruby
354
+ # returns mapping of model class
355
+ klass.api.mappings
356
+
357
+ # e.g. for ElasticUser model
358
+ SearchUser.api.mappings
359
+
360
+ # insert new raw data
361
+ SearchUser.api.insert([{name: 'Hans', age: 34}, {name: 'Peter', age: 22}])
362
+ ```
363
+
364
+ * open!
365
+ * close!
366
+ * refresh!
367
+ * block!
368
+ * unblock!
369
+ * drop!(confirm: true)
370
+ * truncate!(confirm: true)
371
+ * mappings
372
+ * metas
373
+ * settings
374
+ * aliases
375
+ * state
376
+ * schema
377
+ * exists?
378
+ * alias_exists?
379
+ * setting_exists?
380
+ * mapping_exists?
381
+ * meta_exists?
382
+
383
+ Fast insert, update, delete raw data
384
+ * index
385
+ * insert
386
+ * update
387
+ * delete
388
+ * bulk
389
+
390
+ -----
391
+
258
392
  ## ActiveRecord ConnectionAdapters table-methods
259
- Access these methods through the model's connection.
393
+ Access these methods through the model class method `.connection`.
260
394
 
261
395
  ```ruby
262
- # returns mapping of provided table (index)
263
- model.connection.table_mappings('table-name')
396
+ # returns mapping of provided table (index)
397
+ klass.connection.table_mappings('table-name')
264
398
  ```
265
399
 
266
400
  - table_mappings
@@ -281,7 +415,7 @@ Access these methods through the model's connection.
281
415
  ## Active Record Schema migration methods
282
416
  Access these methods through the model's connection or within any `Migration`.
283
417
 
284
- **cluster actions:**
418
+ ### cluster actions:
285
419
  - open_table
286
420
  - open_tables
287
421
  - close_table
@@ -298,7 +432,7 @@ Access these methods through the model's connection or within any `Migration`.
298
432
  - change_table
299
433
  - rename_table
300
434
 
301
- **table actions:**
435
+ ### table actions:
302
436
  - change_meta
303
437
  - remove_meta
304
438
  - add_mapping
@@ -313,8 +447,10 @@ Access these methods through the model's connection or within any `Migration`.
313
447
  - change_alias
314
448
  - remove_alias
315
449
 
450
+
451
+ **Example migration:**
452
+
316
453
  ```ruby
317
- # Example migration
318
454
  class AddTests < ActiveRecord::Migration[7.0]
319
455
  def up
320
456
  create_table "assignments", if_not_exists: true do |t|
@@ -387,6 +523,47 @@ class AddTests < ActiveRecord::Migration[7.0]
387
523
  end
388
524
  ```
389
525
 
526
+
527
+ ## environment-related-table-name:
528
+ Using the `_env_table_name`-method will resolve the table (index) name within the current environment,
529
+ even if the environments shares the same cluster ...
530
+
531
+ This can be provided through the `database.yml` by using the `table_name_prefix/suffix` configuration keys.
532
+ Within the migration the `_env_table_name`-method must be used in combination with the table (index) base name.
533
+
534
+ **Example:**
535
+ Production uses a index suffix with '-pro', development uses '-dev' - they share the same cluster, but different indexes.
536
+ For the **settings** table:
537
+ - settings-pro
538
+ - settings-dev
539
+
540
+ A single migration can be created to be used within each environment:
541
+ ```ruby
542
+ # Example migration
543
+ class AddSettings < ActiveRecord::Migration[7.0]
544
+ def up
545
+ create_table _env_table_name("settings"), force: true do |t|
546
+ t.mapping :created_at, :date
547
+ t.mapping :key, :integer do |m|
548
+ m.primary_key = true
549
+ m.auto_increment = 10
550
+ end
551
+ t.mapping :status, :keyword
552
+ t.mapping :updated_at, :date
553
+ t.mapping :value, :text
554
+
555
+ t.setting "index.number_of_replicas", "0"
556
+ t.setting "index.number_of_shards", "1"
557
+ t.setting "index.routing.allocation.include._tier_preference", "data_content"
558
+ end
559
+ end
560
+
561
+ def down
562
+ drop_table _env_table_name("settings")
563
+ end
564
+ end
565
+ ```
566
+
390
567
  ## Docs
391
568
 
392
569
  [CHANGELOG](docs/CHANGELOG.md)
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # ElasticsearchRecord - CHANGELOG
2
2
 
3
+ ## [1.5.0] - 2023-07-10
4
+ * [add] additional `ElasticsearchRecord::ModelApi` methods **drop!** & **truncate!**, which have to be called with a `confirm:true` parameter
5
+ * [add] `.ElasticsearchRecord::Base.delegate_query_nil_limit` to automatically delegate a relations `limit(nil)`-call to the **max_result_window** _(set to 10.000 as default)_
6
+ * [add] `ActiveRecord::ConnectionAdapters::Elasticsearch::SchemaStatements#access_shard_doc?` which checks, if the **PIT**-shard_doc order is available
7
+ * [add] support for **_shard_doc** as a default order for `ElasticsearchRecord::Relation#pit_results`
8
+ * [ref] `.ElasticsearchRecord::Base.relay_id_attribute` to a more coherent name: `delegate_id_attribute`
9
+ * [ref] `ElasticsearchRecord::Relation#ordered_relation` to optimize already ordered relations
10
+ * [ref] gemspecs to support different versions of Elasticsearch
11
+ * [ref] improved README
12
+ * [fix] `ElasticsearchRecord::Relation#pit_results` infinite loop _(caused by missing order)_
13
+ * [fix] `ElasticsearchRecord::Relation#pit_results` results generation without 'uniq' check of the array
14
+
15
+ ## [1.4.0] - 2023-01-27
16
+ * [add] `ElasticsearchRecord::ModelApi` for fast & easy access the elasticsearch index - callable through `.api` (e.g. ElasticUser.api.mappings)
17
+ * [ref] `ElasticsearchRecord::Instrumentation::LogSubscriber` to truncate the query-string (default: 1000)
18
+ * [ref] `ActiveRecord::ConnectionAdapters::ElasticsearchAdapter#log` with extra attribute (log: true) to prevent logging (e.g. on custom api calls)
19
+ * [fix] `ElasticsearchRecord::Result#bucket` to prevent resolving additional meta key (key_as_string)
20
+
3
21
  ## [1.3.1] - 2023-01-18
4
22
  * [fix] `#none!` method to correctly invalidate the query (String(s) in where-queries like '1=0' will raise now)
5
23
  * [fix] missing 'ChangeSettingDefinition' & 'RemoveSettingDefinition' @ `ActiveRecord::ConnectionAdapters::Elasticsearch::UpdateTableDefinition::COMPOSITE_DEFINITIONS` to composite in a single query
@@ -32,8 +32,8 @@ DESC
32
32
 
33
33
  spec.require_paths = ["lib"]
34
34
 
35
- spec.add_dependency 'activerecord', '~> 7.0.0'
36
- spec.add_dependency 'elasticsearch', '~> 8.4'
35
+ spec.add_dependency 'activerecord', '~> 7.0'
36
+ spec.add_dependency 'elasticsearch', '>= 7.17'
37
37
 
38
38
  #spec.add_development_dependency 'coveralls_reborn', '~> 0.25'
39
39
  spec.add_development_dependency 'rspec', '~> 3.0'
@@ -376,6 +376,14 @@ module ActiveRecord
376
376
  @access_id_fielddata
377
377
  end
378
378
 
379
+ # returns true if +_shard_doc+ field can be accessed through PIT-search.
380
+ # @return [Boolean]
381
+ def access_shard_doc?
382
+ @access_shard_doc = cluster_info[:version] >= "7.12" if @access_shard_doc.nil?
383
+
384
+ @access_shard_doc
385
+ end
386
+
379
387
  # Returns basic information about the cluster.
380
388
  # @return [Hash{Symbol->Unknown}]
381
389
  def cluster_info
@@ -216,14 +216,15 @@ module ActiveRecord # :nodoc:
216
216
  # @param [Hash] arguments - action arguments
217
217
  # @param [String (frozen)] name - the logging name
218
218
  # @param [Boolean] async - send async (default: false) - currently not supported
219
+ # @param [Boolean] log - send log to instrumenter (default: true)
219
220
  # @return [Elasticsearch::API::Response, Object]
220
- def api(namespace, action, arguments = {}, name = 'API', async: false)
221
+ def api(namespace, action, arguments = {}, name = 'API', async: false, log: true)
221
222
  raise ::StandardError, 'ASYNC api calls are not supported' if async
222
223
 
223
224
  # resolve the API target
224
225
  target = namespace == :core ? @connection : @connection.__send__(namespace)
225
226
 
226
- log("#{namespace}.#{action}", arguments, name, async: async) do
227
+ log("#{namespace}.#{action}", arguments, name, async: async, log: log) do
227
228
  response = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
228
229
  target.__send__(action, arguments)
229
230
  end
@@ -274,16 +275,24 @@ module ActiveRecord # :nodoc:
274
275
  end
275
276
 
276
277
  # provide a custom log instrumenter for elasticsearch subscribers
277
- def log(gate, arguments, name, async: false, &block)
278
- @instrumenter.instrument(
279
- "query.elasticsearch_record",
280
- gate: gate,
281
- name: name,
282
- arguments: gate == 'core.msearch' ? arguments.deep_dup : arguments,
283
- async: async) do
284
- @lock.synchronize(&block)
285
- rescue => e
286
- raise translate_exception_class(e, arguments, [])
278
+ def log(gate, arguments, name, async: false, log: true, &block)
279
+ if log
280
+ @instrumenter.instrument(
281
+ "query.elasticsearch_record",
282
+ gate: gate,
283
+ name: name,
284
+ arguments: gate == 'core.msearch' ? arguments.deep_dup : arguments,
285
+ async: async) do
286
+ @lock.synchronize(&block)
287
+ rescue => e
288
+ raise translate_exception_class(e, arguments, [])
289
+ end
290
+ else
291
+ begin
292
+ @lock.synchronize(&block)
293
+ rescue => e
294
+ raise translate_exception_class(e, arguments, [])
295
+ end
287
296
  end
288
297
  end
289
298
 
@@ -3,23 +3,27 @@ module ElasticsearchRecord
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
-
7
6
  # Rails resolves the primary_key's value by accessing the +#id+ method.
8
7
  # Since Elasticsearch also supports an additional, independent +id+ attribute, it would only be able to access
9
- # this through +read_attribute(:id)+.
8
+ # this through +_read_attribute(:id)+.
10
9
  # To also have the ability of accessing this attribute through the default, this flag can be enabled.
11
10
  # @attribute! Boolean
12
- class_attribute :relay_id_attribute, instance_writer: false, default: false
11
+ class_attribute :delegate_id_attribute, instance_writer: false, default: false
12
+
13
+ # Elasticsearch's default value for queries without a +size+ is forced to +10+.
14
+ # To provide a similar behaviour as SQL, this can be automatically set to the +max_result_window+ value.
15
+ # @attribute! Boolean
16
+ class_attribute :delegate_query_nil_limit, instance_writer: false, default: false
13
17
  end
14
18
 
15
19
  # overwrite to provide a Elasticsearch version of returning a 'primary_key' attribute.
16
20
  # Elasticsearch uses the static +_id+ column as primary_key, but also supports an additional +id+ column.
17
21
  # To provide functionality of returning the +id+ attribute, this method must also support it
18
- # with enabled +relay_id_attribute+.
22
+ # with enabled +delegate_id_attribute+.
19
23
  # @return [Object]
20
24
  def id
21
25
  # check, if the model has a +id+ attribute
22
- return _read_attribute('id') if relay_id_attribute? && has_attribute?('id')
26
+ return _read_attribute('id') if delegate_id_attribute? && has_attribute?('id')
23
27
 
24
28
  super
25
29
  end
@@ -27,11 +31,11 @@ module ElasticsearchRecord
27
31
  # overwrite to provide a Elasticsearch version of setting a 'primary_key' attribute.
28
32
  # Elasticsearch uses the static +_id+ column as primary_key, but also supports an additional +id+ column.
29
33
  # To provide functionality of setting the +id+ attribute, this method must also support it
30
- # with enabled +relay_id_attribute+.
34
+ # with enabled +delegate_id_attribute+.
31
35
  # @param [Object] value
32
36
  def id=(value)
33
37
  # check, if the model has a +id+ attribute
34
- return _write_attribute('id', value) if relay_id_attribute? && has_attribute?('id')
38
+ return _write_attribute('id', value) if delegate_id_attribute? && has_attribute?('id')
35
39
 
36
40
  # auxiliary update the +_id+ virtual column if we have a different primary_key
37
41
  _write_attribute('_id', value) if @primary_key != '_id'
@@ -42,13 +46,13 @@ module ElasticsearchRecord
42
46
  # overwrite to provide a Elasticsearch version of returning a 'primary_key' was attribute.
43
47
  # Elasticsearch uses the static +_id+ column as primary_key, but also supports an additional +id+ column.
44
48
  # To provide functionality of returning the +id_Was+ attribute, this method must also support it
45
- # with enabled +relay_id_attribute+.
49
+ # with enabled +delegate_id_attribute+.
46
50
  def id_was
47
- relay_id_attribute? && has_attribute?('id') ? attribute_was('id') : super
51
+ delegate_id_attribute? && has_attribute?('id') ? attribute_was('id') : super
48
52
  end
49
53
 
50
54
  # overwrite the write_attribute method to always write to the 'id'-attribute, if present.
51
- # This methods does not check for +relay_id_attribute+ flag!
55
+ # This methods does not check for +delegate_id_attribute+ flag!
52
56
  # see @ ActiveRecord::AttributeMethods::Write#write_attribute
53
57
  def write_attribute(attr_name, value)
54
58
  return _write_attribute('id', value) if attr_name.to_s == 'id' && has_attribute?('id')
@@ -57,7 +61,7 @@ module ElasticsearchRecord
57
61
  end
58
62
 
59
63
  # overwrite read_attribute method to read from the 'id'-attribute, if present.
60
- # This methods does not check for +relay_id_attribute+ flag!
64
+ # This methods does not check for +delegate_id_attribute+ flag!
61
65
  # see @ ActiveRecord::AttributeMethods::Read#read_attribute
62
66
  def read_attribute(attr_name, &block)
63
67
  return _read_attribute('id', &block) if attr_name.to_s == 'id' && has_attribute?('id')
@@ -84,10 +88,11 @@ module ElasticsearchRecord
84
88
  cache.compute_if_absent(key) { ElasticsearchRecord::StatementCache.create(connection, &block) }
85
89
  end
86
90
 
87
- # not supported atm - maybe enable in future versions to resolve migrations easier
88
- # def primary_class? # :nodoc:
89
- # self == ::ElasticsearchRecord::Base
90
- # end
91
+ # used to provide fast access to the connection API without explicit providing table-related parameters.
92
+ # @return [anonymous Struct]
93
+ def api
94
+ ElasticsearchRecord::ModelApi.new(self)
95
+ end
91
96
 
92
97
  private
93
98
 
@@ -8,8 +8,8 @@ module ElasticsearchRecord
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 1
11
- MINOR = 3
12
- TINY = 1
11
+ MINOR = 5
12
+ TINY = 0
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -5,7 +5,7 @@ module ElasticsearchRecord
5
5
  # attach to ElasticsearchRecord related events
6
6
  class LogSubscriber < ActiveSupport::LogSubscriber
7
7
 
8
- IGNORE_PAYLOAD_NAMES = %w[SCHEMA EXPLAIN]
8
+ IGNORE_PAYLOAD_NAMES = %w[SCHEMA EXPLAIN EXCLUDE]
9
9
 
10
10
  def self.runtime=(value)
11
11
  Thread.current["elasticsearch_record_runtime"] = value
@@ -37,17 +37,18 @@ module ElasticsearchRecord
37
37
  end
38
38
  name = "CACHE #{name}" if payload[:cached]
39
39
 
40
- # nice feature: displays the REAL query-time (_qt)
40
+ # nice feature: displays the REAL query-time from elasticsearch response (_qt)
41
+ # this is handled through the +::ActiveRecord::ConnectionAdapters::ElasticsearchAdapter#api+ method
41
42
  name = "#{name} (took: #{payload[:arguments][:_qt].round(1)}ms)" if payload[:arguments][:_qt]
42
43
 
43
44
  # build query
44
- query = payload[:arguments].except(:_qt).inspect.gsub(/:(\w+)=>/, '\1: ').presence || '-'
45
+ query = payload[:arguments].except(:_qt).inspect.gsub(/:(\w+)=>/, '\1: ').truncate((payload[:truncate] || 1000), omission: color(' (pruned)', RED))
45
46
 
46
47
  # final coloring
47
48
  name = color(name, name_color(payload[:name]), true)
48
49
  query = color(query, gate_color(payload[:gate]), true) if colorize_logging
49
50
 
50
- debug " #{name} #{query}"
51
+ debug " #{name} #{query.presence || '-/-'}"
51
52
  end
52
53
 
53
54
  private
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticsearchRecord
4
+ class ModelApi
5
+ attr_reader :klass
6
+
7
+ def initialize(klass)
8
+ @klass = klass
9
+ end
10
+
11
+ # undelegated schema methods: clone rename create
12
+ # those should not be quick-accessible, since they might end in heavily broken index
13
+
14
+ # delegated dangerous methods (created with exclamation mark)
15
+ # not able to provide individual arguments - always the defaults will be used!
16
+ #
17
+ # @example
18
+ # open!
19
+ # close!
20
+ # refresh!
21
+ # block!
22
+ # unblock!
23
+ %w(open close refresh block unblock).each do |method|
24
+ define_method("#{method}!") do
25
+ _connection.send("#{method}_table", _index_name)
26
+ end
27
+ end
28
+
29
+ # delegated dangerous methods with confirm parameter (created with exclamation mark)
30
+ # a exception will be raised, if +confirm:true+ is missing.
31
+ #
32
+ # @example
33
+ # drop!(confirm: true)
34
+ # truncate!(confirm: true)
35
+ %w(drop truncate).each do |method|
36
+ define_method("#{method}!") do |confirmed: false|
37
+ raise "#{method} of table '#{_index_name}' aborted!\nexecution not confirmed!\ncall with: #{klass}.api.#{method}!(confirmed: true)" unless confirmed
38
+ _connection.send("#{method}_table", _index_name)
39
+ end
40
+ end
41
+
42
+ # delegated table methods
43
+ #
44
+ # @example
45
+ # mappings
46
+ # metas
47
+ # settings
48
+ # aliases
49
+ # state
50
+ # schema
51
+ # exists?
52
+ %w(mappings metas settings aliases state schema exists?).each do |method|
53
+ define_method(method) do |*args|
54
+ _connection.send("table_#{method}", _index_name, *args)
55
+ end
56
+ end
57
+
58
+ # delegated plain methods
59
+ #
60
+ # @example
61
+ # alias_exists?
62
+ # setting_exists?
63
+ # mapping_exists?
64
+ # meta_exists?
65
+ %w(alias_exists? setting_exists? mapping_exists? meta_exists?).each do |method|
66
+ define_method(method) do |*args|
67
+ _connection.send(method, _index_name, *args)
68
+ end
69
+ end
70
+
71
+ # fast insert/update data.
72
+ #
73
+ # @example
74
+ # index([{name: 'Hans', age: 34}, {name: 'Peter', age: 22}])
75
+ #
76
+ # index({id: 5, name: 'Georg', age: 87})
77
+ #
78
+ # @param [Array<Hash>,Hash] data
79
+ # @param [Hash] options
80
+ def index(data, **options)
81
+ bulk(data, :index, **options)
82
+ end
83
+
84
+ # fast insert new data.
85
+ #
86
+ # @example
87
+ # insert([{name: 'Hans', age: 34}, {name: 'Peter', age: 22}])
88
+ #
89
+ # insert({name: 'Georg', age: 87})
90
+ #
91
+ # @param [Array<Hash>,Hash] data
92
+ # @param [Hash] options
93
+ def insert(data, **options)
94
+ bulk(data, :create, **options)
95
+ end
96
+
97
+ # fast update existing data.
98
+ #
99
+ # @example
100
+ # update([{id: 1, name: 'Hansi'}, {id: 2, name: 'Peter Parker', age: 42}])
101
+ #
102
+ # update({id: 3, name: 'Georg McCain'})
103
+ #
104
+ # @param [Array<Hash>,Hash] data
105
+ # @param [Hash] options
106
+ def update(data, **options)
107
+ bulk(data, :update, **options)
108
+ end
109
+
110
+ # fast delete data.
111
+ #
112
+ # @example
113
+ # delete([1,2,3,5])
114
+ #
115
+ # delete(3)
116
+ #
117
+ # delete({id: 2})
118
+ #
119
+ # @param [Array<Hash>,Hash] data
120
+ # @param [Hash] options
121
+ def delete(data, **options)
122
+ data = [data] unless data.is_a?(Array)
123
+
124
+ if data[0].is_a?(Hash)
125
+ bulk(data, :delete, **options)
126
+ else
127
+ bulk(data.map { |id| { id: id } }, :delete, **options)
128
+ end
129
+ end
130
+
131
+ # bulk handle provided data (single Hash or multiple Array<Hash>).
132
+ # @param [Hash,Array<Hash>] data - the data to insert/update/delete ...
133
+ # @param [Symbol] operation
134
+ # @param [Boolean, Symbol] refresh
135
+ def bulk(data, operation = :index, refresh: true, **options)
136
+ data = [data] unless data.is_a?(Array)
137
+
138
+ _connection.api(:core, :bulk, {
139
+ index: _index_name,
140
+ body: data.map { |item| { operation => { _id: item[:id], data: item.except(:id) } } },
141
+ refresh: refresh
142
+ }, "BULK #{operation.to_s.upcase}", **options)
143
+ end
144
+
145
+ private
146
+
147
+ def _index_name
148
+ klass.index_name
149
+ end
150
+
151
+ def _connection
152
+ klass.connection
153
+ end
154
+ end
155
+ end
@@ -99,21 +99,29 @@ module ElasticsearchRecord
99
99
  # overwrite original methods to provide a elasticsearch version:
100
100
  # checks against the +#access_id_fielddata?+ to ensure the Elasticsearch Cluster allows access on the +_id+ field.
101
101
  def ordered_relation
102
- # resolve valid primary_key (either not the '_id' or +access_id_fielddata?+ is enabled)
102
+ # order values already exist
103
+ return self unless order_values.empty?
104
+
105
+ # resolve valid primary_key
106
+ # - either it is NOT the '_id' column
107
+ # OR
108
+ # - it is the '_id'-column, but +access_id_fielddata?+ is also enabled!
103
109
  valid_primary_key = if primary_key != '_id' || klass.connection.access_id_fielddata?
104
- primary_key
105
- else
106
- nil
107
- end
110
+ primary_key
111
+ else
112
+ nil
113
+ end
108
114
 
109
115
  # slightly changed original methods content
110
- if order_values.empty? && (implicit_order_column || valid_primary_key)
116
+ if implicit_order_column || valid_primary_key
117
+ # order by +implicit_order_column+ AND +primary_key+
111
118
  if implicit_order_column && valid_primary_key && implicit_order_column != valid_primary_key
112
119
  order(table[implicit_order_column].asc, table[valid_primary_key].asc)
113
120
  else
114
121
  order(table[implicit_order_column || valid_primary_key].asc)
115
122
  end
116
123
  else
124
+ # order is not possible due restricted settings
117
125
  self
118
126
  end
119
127
  end
@@ -91,15 +91,20 @@ module ElasticsearchRecord
91
91
  # @param [String] keep_alive - how long to keep alive (for each single request) - default: '1m'
92
92
  # @param [Integer] batch_size - how many results per query (default: 1000 - this means at least 10 queries before reaching the +max_result_window+)
93
93
  def pit_results(keep_alive: '1m', batch_size: 1000)
94
- raise ArgumentError, "Batch size cannot be above the 'max_result_window' (#{klass.max_result_window}) !" if batch_size > klass.max_result_window
94
+ raise(ArgumentError, "Batch size cannot be above the 'max_result_window' (#{klass.max_result_window}) !") if batch_size > klass.max_result_window
95
95
 
96
- # check if a limit or offset values was provided
96
+ # check if limit or offset values where provided
97
97
  results_limit = limit_value ? limit_value : Float::INFINITY
98
98
  results_offset = offset_value ? offset_value : 0
99
99
 
100
100
  # search_after requires a order - we resolve a order either from provided value or by default ...
101
101
  relation = ordered_relation
102
102
 
103
+ # FALLBACK (without any order) for restricted access to the '_id' field.
104
+ # with PIT a order by '_shard_doc' can also be used
105
+ # see @ https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html
106
+ relation.order!(_shard_doc: :asc) if relation.order_values.empty? && klass.connection.access_shard_doc?
107
+
103
108
  # clear limit & offset
104
109
  relation.offset!(nil).limit!(nil)
105
110
 
@@ -135,7 +140,7 @@ module ElasticsearchRecord
135
140
  if block_given?
136
141
  yield ranged_results
137
142
  else
138
- results |= ranged_results
143
+ results += ranged_results
139
144
  end
140
145
 
141
146
  # add to total
@@ -150,8 +155,8 @@ module ElasticsearchRecord
150
155
  # we ran out of data
151
156
  break if current_results_length < batch_size
152
157
 
153
- # additional security - required?
154
- # break if current_pit_hash[:search_after] == current_response['hits']['hits'][-1]['sort']
158
+ # additional security - prevents infinite loops
159
+ raise(::ActiveRecord::StatementInvalid, "'pit_results' aborted due an infinite loop error (invalid or missing order)") if current_pit_hash[:search_after] == current_response['hits']['hits'][-1]['sort'] && current_pit_hash[:pit][:id] == current_response['pit_id']
155
160
 
156
161
  # -------- NEXT LOOP changes --------
157
162
 
@@ -43,6 +43,15 @@ module ElasticsearchRecord
43
43
  @values[:aggs] = value
44
44
  end
45
45
 
46
+ # overwrite the limit_value setter, to provide a special behaviour of auto-setting the +max_result_window+.
47
+ def limit=(limit)
48
+ if limit == '__max__' || (limit.nil? && delegate_query_nil_limit?)
49
+ super(max_result_window)
50
+ else
51
+ super
52
+ end
53
+ end
54
+
46
55
  private
47
56
 
48
57
  # alternative method to avoid redefining the const +VALID_UNSCOPING_VALUES+
@@ -256,7 +256,7 @@ module ElasticsearchRecord
256
256
  else
257
257
  # resolve sub-aggregations / nodes without 'meta' keys.
258
258
  # if this results in an empty hash, the return will be nil
259
- node.except(:key, :doc_count, :doc_count_error_upper_bound, :sum_other_doc_count).transform_values { |val| _resolve_bucket(val) }.presence
259
+ node.except(:key, :doc_count, :doc_count_error_upper_bound, :sum_other_doc_count, :key_as_string).transform_values { |val| _resolve_bucket(val) }.presence
260
260
  end
261
261
  end
262
262
 
@@ -23,6 +23,7 @@ module ElasticsearchRecord
23
23
  autoload :Base
24
24
  autoload :Core
25
25
  autoload :ModelSchema
26
+ autoload :ModelApi
26
27
  autoload :Persistence
27
28
  autoload :Querying
28
29
  autoload :Query
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.3.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Gonsior
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-18 00:00:00.000000000 Z
11
+ date: 2023-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 7.0.0
19
+ version: '7.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.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: elasticsearch
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '8.4'
33
+ version: '7.17'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '8.4'
40
+ version: '7.17'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -97,7 +97,7 @@ dependencies:
97
97
  description: 'ElasticsearchRecord is a ActiveRecord adapter and provides similar functionality
98
98
  for Elasticsearch.
99
99
 
100
- '
100
+ '
101
101
  email:
102
102
  - info@ruby-smart.org
103
103
  executables: []
@@ -107,7 +107,6 @@ files:
107
107
  - ".rspec"
108
108
  - ".yardopts"
109
109
  - Gemfile
110
- - Gemfile.lock
111
110
  - README.md
112
111
  - Rakefile
113
112
  - docs/CHANGELOG.md
@@ -159,6 +158,7 @@ files:
159
158
  - lib/elasticsearch_record/instrumentation/controller_runtime.rb
160
159
  - lib/elasticsearch_record/instrumentation/log_subscriber.rb
161
160
  - lib/elasticsearch_record/instrumentation/railtie.rb
161
+ - lib/elasticsearch_record/model_api.rb
162
162
  - lib/elasticsearch_record/model_schema.rb
163
163
  - lib/elasticsearch_record/patches/active_record/relation_merger_patch.rb
164
164
  - lib/elasticsearch_record/patches/arel/select_core_patch.rb
@@ -191,7 +191,7 @@ metadata:
191
191
  source_code_uri: https://github.com/ruby-smart/elasticsearch_record
192
192
  documentation_uri: https://rubydoc.info/gems/elasticsearch_record
193
193
  changelog_uri: https://github.com/ruby-smart/elasticsearch_record/blob/main/docs/CHANGELOG.md
194
- post_install_message:
194
+ post_install_message:
195
195
  rdoc_options: []
196
196
  require_paths:
197
197
  - lib
@@ -206,8 +206,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
206
206
  - !ruby/object:Gem::Version
207
207
  version: '0'
208
208
  requirements: []
209
- rubygems_version: 3.3.7
210
- signing_key:
209
+ rubygems_version: 3.1.6
210
+ signing_key:
211
211
  specification_version: 4
212
212
  summary: ActiveRecord adapter for Elasticsearch
213
213
  test_files: []
data/Gemfile.lock DELETED
@@ -1,73 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- elasticsearch_record (1.2.4)
5
- activerecord (~> 7.0.0)
6
- elasticsearch (~> 8.4)
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- activemodel (7.0.4)
12
- activesupport (= 7.0.4)
13
- activerecord (7.0.4)
14
- activemodel (= 7.0.4)
15
- activesupport (= 7.0.4)
16
- activesupport (7.0.4)
17
- concurrent-ruby (~> 1.0, >= 1.0.2)
18
- i18n (>= 1.6, < 2)
19
- minitest (>= 5.1)
20
- tzinfo (~> 2.0)
21
- concurrent-ruby (1.1.10)
22
- diff-lcs (1.5.0)
23
- elastic-transport (8.1.0)
24
- faraday (< 3)
25
- multi_json
26
- elasticsearch (8.5.2)
27
- elastic-transport (~> 8)
28
- elasticsearch-api (= 8.5.2)
29
- elasticsearch-api (8.5.2)
30
- multi_json
31
- faraday (2.7.2)
32
- faraday-net_http (>= 2.0, < 3.1)
33
- ruby2_keywords (>= 0.0.4)
34
- faraday-net_http (3.0.2)
35
- i18n (1.12.0)
36
- concurrent-ruby (~> 1.0)
37
- minitest (5.16.3)
38
- multi_json (1.15.0)
39
- rake (13.0.6)
40
- rspec (3.11.0)
41
- rspec-core (~> 3.11.0)
42
- rspec-expectations (~> 3.11.0)
43
- rspec-mocks (~> 3.11.0)
44
- rspec-core (3.11.0)
45
- rspec-support (~> 3.11.0)
46
- rspec-expectations (3.11.1)
47
- diff-lcs (>= 1.2.0, < 2.0)
48
- rspec-support (~> 3.11.0)
49
- rspec-mocks (3.11.1)
50
- diff-lcs (>= 1.2.0, < 2.0)
51
- rspec-support (~> 3.11.0)
52
- rspec-support (3.11.1)
53
- ruby2_keywords (0.0.5)
54
- tzinfo (2.0.5)
55
- concurrent-ruby (~> 1.0)
56
- webrick (1.7.0)
57
- yard (0.9.28)
58
- webrick (~> 1.7.0)
59
- yard-activesupport-concern (0.0.1)
60
- yard (>= 0.8)
61
-
62
- PLATFORMS
63
- x86_64-linux
64
-
65
- DEPENDENCIES
66
- elasticsearch_record!
67
- rake (~> 13.0)
68
- rspec (~> 3.0)
69
- yard (~> 0.9)
70
- yard-activesupport-concern (~> 0.0.1)
71
-
72
- BUNDLED WITH
73
- 2.3.18