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 +4 -4
- data/README.md +204 -27
- data/docs/CHANGELOG.md +18 -0
- data/elasticsearch_record.gemspec +2 -2
- data/lib/active_record/connection_adapters/elasticsearch/schema_statements.rb +8 -0
- data/lib/active_record/connection_adapters/elasticsearch_adapter.rb +21 -12
- data/lib/elasticsearch_record/core.rb +20 -15
- data/lib/elasticsearch_record/gem_version.rb +2 -2
- data/lib/elasticsearch_record/instrumentation/log_subscriber.rb +5 -4
- data/lib/elasticsearch_record/model_api.rb +155 -0
- data/lib/elasticsearch_record/relation/core_methods.rb +14 -6
- data/lib/elasticsearch_record/relation/result_methods.rb +10 -5
- data/lib/elasticsearch_record/relation/value_methods.rb +9 -0
- data/lib/elasticsearch_record/result.rb +1 -1
- data/lib/elasticsearch_record.rb +1 -0
- metadata +14 -14
- data/Gemfile.lock +0 -73
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f50568400141462093f5d91bf5b26efb44a52b9529577d5e72a4d3699a57717
|
4
|
+
data.tar.gz: 89536ffacc4ddd5fb4d0334eda843d206c25fc867664f6f9454ee07d921ec712
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
-
|
18
|
-
-
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
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
|
393
|
+
Access these methods through the model class method `.connection`.
|
260
394
|
|
261
395
|
```ruby
|
262
|
-
|
263
|
-
|
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
|
-
|
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
|
-
|
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
|
36
|
-
spec.add_dependency 'elasticsearch', '
|
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
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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 +
|
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 :
|
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 +
|
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
|
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 +
|
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
|
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 +
|
49
|
+
# with enabled +delegate_id_attribute+.
|
46
50
|
def id_was
|
47
|
-
|
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 +
|
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 +
|
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
|
-
#
|
88
|
-
#
|
89
|
-
|
90
|
-
|
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
|
|
@@ -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: ').
|
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
|
-
#
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
110
|
+
primary_key
|
111
|
+
else
|
112
|
+
nil
|
113
|
+
end
|
108
114
|
|
109
115
|
# slightly changed original methods content
|
110
|
-
if
|
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
|
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
|
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
|
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 -
|
154
|
-
|
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
|
|
data/lib/elasticsearch_record.rb
CHANGED
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.
|
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-
|
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
|
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
|
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: '
|
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: '
|
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.
|
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
|