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