elasticsearch_record 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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