elasticsearch-model-extensions 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +2 -0
- data/elasticsearch-model-extensions.gemspec +23 -0
- data/lib/elasticsearch/model/extensions/batch_updating.rb +139 -0
- data/lib/elasticsearch/model/extensions/callback.rb +18 -0
- data/lib/elasticsearch/model/extensions/configuration.rb +72 -0
- data/lib/elasticsearch/model/extensions/dependency_tracking.rb +45 -0
- data/lib/elasticsearch/model/extensions/destroy_callback.rb +30 -0
- data/lib/elasticsearch/model/extensions/index_operations.rb +86 -0
- data/lib/elasticsearch/model/extensions/mapping_node.rb +65 -0
- data/lib/elasticsearch/model/extensions/mapping_reflection.rb +95 -0
- data/lib/elasticsearch/model/extensions/outer_document_updating.rb +175 -0
- data/lib/elasticsearch/model/extensions/partial_updating.rb +148 -0
- data/lib/elasticsearch/model/extensions/update_callback.rb +43 -0
- data/lib/elasticsearch/model/extensions/version.rb +7 -0
- data/lib/elasticsearch/model/extensions.rb +9 -0
- metadata +94 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 91592da0d915c072cdaa65f2a805d94edab055e9
|
4
|
+
data.tar.gz: c9b8c32829eb32ca15d9971fe5dc967b2947b740
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0900f09019a5451ba338c7b619a3ad23cda124624f9aa902865289f27164c6f7245d2235b77b9ffce6beda2a927cc02e7a26426d2235339bd6ddefb3a0fa44bb
|
7
|
+
data.tar.gz: 523697b1b2836f2033941899db7077edc6ea2c7302b8d317f54745d3339e86bf3286e96065ae9155f1522e1c95ca4d5e095997954b94b7c86e337def1686a54b
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Yusuke KUOKA
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Elasticsearch::Model::Extensions
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'elasticsearch-model-extensions'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install elasticsearch-model-extensions
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
1. Fork it ( https://github.com/[my-github-username]/elasticsearch-model-extensions/fork )
|
28
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
31
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'elasticsearch/model/extensions/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "elasticsearch-model-extensions"
|
8
|
+
spec.version = Elasticsearch::Model::Extensions::VERSION
|
9
|
+
spec.authors = ["Yusuke KUOKA"]
|
10
|
+
spec.email = ["yusuke.kuoka@crowdworks.co.jp"]
|
11
|
+
spec.summary = %q{A set of extensions for elasticsearch-model which aims to ease the burden of things like re-indexing, verbose/complex mapping that you may face once you started using elasticsearch seriously.}
|
12
|
+
spec.description = %q{A set of extensions for elasticsearch-model which aims to ease the burden of things like re-indexing, verbose/complex mapping.}
|
13
|
+
spec.homepage = "https://github.com/crowdworks/elasticsearch-model-extensions"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require_relative 'mapping_reflection'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Model
|
5
|
+
module Extensions
|
6
|
+
module BatchUpdating
|
7
|
+
DEFAULT_BATCH_SIZE = 100
|
8
|
+
|
9
|
+
def self.included(klass)
|
10
|
+
klass.extend ClassMethods
|
11
|
+
|
12
|
+
unless klass.respond_to? :with_indexed_tables_included
|
13
|
+
class << klass
|
14
|
+
def with_indexed_tables_included
|
15
|
+
raise "#{self}.with_indexed_tables_included is not implemented."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
unless klass.respond_to? :elasticsearch_hosts
|
21
|
+
class << klass
|
22
|
+
def elasticsearch_hosts
|
23
|
+
raise "#{self}.elasticsearch_hosts is not implemented."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def split_ids_into(chunk_num, min:nil, max:nil)
|
31
|
+
min ||= minimum(:id)
|
32
|
+
max ||= maximum(:id)
|
33
|
+
chunk_num.times.inject([]) do |r,i|
|
34
|
+
chunk_size = ((max-min+1)/chunk_num.to_f).ceil
|
35
|
+
first = chunk_size * i
|
36
|
+
|
37
|
+
last = if i == chunk_num - 1
|
38
|
+
max
|
39
|
+
else
|
40
|
+
chunk_size * (i + 1) - 1
|
41
|
+
end
|
42
|
+
|
43
|
+
r << (first..last)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def update_index_in_parallel(parallelism:, index: nil, type: nil, min: nil, max: nil, batch_size:DEFAULT_BATCH_SIZE)
|
48
|
+
klass = self
|
49
|
+
|
50
|
+
Parallel.each(klass.split_ids_into(parallelism, min: min, max: max), in_processes: parallelism) do |id_range|
|
51
|
+
@rdb_reconnected ||= klass.connection.reconnect! || true
|
52
|
+
@elasticsearch_reconnected ||= klass.__elasticsearch__.client = Elasticsearch::Client.new(host: klass.elasticsearch_hosts)
|
53
|
+
klass.for_indexing.update_index_for_ids_in_range id_range, index: index, type: type, batch_size: batch_size
|
54
|
+
end
|
55
|
+
|
56
|
+
klass.connection.reconnect!
|
57
|
+
end
|
58
|
+
|
59
|
+
def for_indexing
|
60
|
+
for_batch_indexing
|
61
|
+
end
|
62
|
+
|
63
|
+
def for_batch_indexing
|
64
|
+
with_indexed_tables_included.extending(::Elasticsearch::Model::Extensions::BatchUpdating::Association::Extension)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param [Fixnum] batch_size
|
68
|
+
def update_index_in_batches(batch_size: DEFAULT_BATCH_SIZE, where: nil, index: nil, type: nil)
|
69
|
+
records_in_scope = if where.nil?
|
70
|
+
for_batch_indexing
|
71
|
+
else
|
72
|
+
for_batch_indexing.where(where)
|
73
|
+
end
|
74
|
+
|
75
|
+
records_in_scope.update_index_in_batches(batch_size: batch_size, index: index, type: type)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param [Array] records
|
79
|
+
def update_index_in_batch(records, index: nil, type: nil, client: nil)
|
80
|
+
klass = self
|
81
|
+
|
82
|
+
client ||= klass.__elasticsearch__.client
|
83
|
+
index ||= klass.index_name
|
84
|
+
type ||= klass.document_type
|
85
|
+
|
86
|
+
if records.size > 1
|
87
|
+
response = client.bulk \
|
88
|
+
index: index,
|
89
|
+
type: type,
|
90
|
+
body: records.map { |r| { index: { _id: r.id, data: r.as_indexed_json } } }
|
91
|
+
|
92
|
+
one_or_more_errors_occurred = response["errors"]
|
93
|
+
|
94
|
+
if one_or_more_errors_occurred
|
95
|
+
Rails.logger.warn "One or more error(s) occurred while updating the index #{records} for the type #{type}\n#{JSON.pretty_generate(response)}"
|
96
|
+
end
|
97
|
+
else
|
98
|
+
records.each do |r|
|
99
|
+
client.index index: index, type: type, id: r.id, body: r.as_indexed_json
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
module Association
|
106
|
+
module Extension
|
107
|
+
def update_index_in_chunks(num, index: index)
|
108
|
+
klass.split_ids_into(num).map do |r|
|
109
|
+
if block_given?
|
110
|
+
yield -> { update_index_for_ids_in_range(r, index: index) }
|
111
|
+
else
|
112
|
+
update_index_for_ids_in_range(r, index: index)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def update_index_for_ids_from(from, to:, index: nil, type: nil, batch_size: DEFAULT_BATCH_SIZE)
|
118
|
+
record_id = arel_table[:id]
|
119
|
+
|
120
|
+
conditions = record_id.gteq(from).and(record_id.lteq(to))
|
121
|
+
|
122
|
+
update_index_in_batches(batch_size: batch_size, index: index, type: type, conditions: conditions)
|
123
|
+
end
|
124
|
+
|
125
|
+
def update_index_for_ids_in_range(range, index: nil, type: nil, batch_size: DEFAULT_BATCH_SIZE)
|
126
|
+
update_index_for_ids_from(range.first, to: range.last, type: type, index: index, batch_size: batch_size)
|
127
|
+
end
|
128
|
+
|
129
|
+
def update_index_in_batches(batch_size: DEFAULT_BATCH_SIZE, conditions:nil, index: nil, type: nil)
|
130
|
+
find_in_batches(batch_size: batch_size, conditions: conditions) do |records|
|
131
|
+
klass.update_index_in_batch(records, index: index, type: type)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'configuration'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Model
|
5
|
+
module Extensions
|
6
|
+
class Callback
|
7
|
+
# @param [Configuration] config
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def config
|
13
|
+
@config
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Extensions
|
4
|
+
class Configuration
|
5
|
+
attr_reader :delayed
|
6
|
+
|
7
|
+
def initialize(active_record_class, parent_class: parent_class, delayed:, if: -> r { true }, records_to_update_documents: nil)
|
8
|
+
@delayed = @delayed
|
9
|
+
|
10
|
+
@active_record_class = active_record_class
|
11
|
+
@parent_class = parent_class
|
12
|
+
@if = binding.local_variable_get(:if)
|
13
|
+
@records_to_update_documents = records_to_update_documents
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_hash
|
17
|
+
@cached_hash ||= build_hash
|
18
|
+
end
|
19
|
+
|
20
|
+
def field_to_update
|
21
|
+
to_hash[:field_to_update]
|
22
|
+
end
|
23
|
+
|
24
|
+
def only_if
|
25
|
+
to_hash[:only_if]
|
26
|
+
end
|
27
|
+
|
28
|
+
def records_to_update_documents
|
29
|
+
to_hash[:records_to_update_documents]
|
30
|
+
end
|
31
|
+
|
32
|
+
def optionally_delayed
|
33
|
+
-> t { delayed ? t.delay : t }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def build_hash
|
39
|
+
child_class = @active_record_class
|
40
|
+
|
41
|
+
path = child_class.path_from(@parent_class)
|
42
|
+
parent_to_child_path = path.map(&:name)
|
43
|
+
|
44
|
+
# a has_a b has_a cという関係のとき、cが更新されたらaのフィールドbをupdateする必要がある。
|
45
|
+
# そのとき、
|
46
|
+
# 親aから子cへのパスが[:b, :c]だったら、bだけをupdateすればよいので
|
47
|
+
field_to_update = parent_to_child_path.first
|
48
|
+
|
49
|
+
puts "#{child_class.name} updates #{@parent_class.name}'s #{field_to_update}"
|
50
|
+
|
51
|
+
# TODO 勝手にインスタンスの状態を書き換えていて、相当いまいち。インスタンス外に出す。
|
52
|
+
child_class.instance_variable_set :@nested_object_fields, @parent_class.nested_object_fields_for(parent_to_child_path).map(&:to_s)
|
53
|
+
child_class.instance_variable_set :@has_dependent_fields, @parent_class.has_dependent_fields?(field_to_update) ||
|
54
|
+
(path.first.destination.through_class == child_class && @parent_class.has_association_named?(field_to_update) && @parent_class.has_document_field_named?(field_to_update))
|
55
|
+
|
56
|
+
custom_if = @if
|
57
|
+
|
58
|
+
update_strategy_class = Elasticsearch::Model::Extensions::OuterDocumentUpdating.strategy_for child_class
|
59
|
+
update_strategy = update_strategy_class.new(from: @parent_class, to: child_class)
|
60
|
+
|
61
|
+
only_if, records_to_update_documents = update_strategy.apply
|
62
|
+
|
63
|
+
{
|
64
|
+
field_to_update: field_to_update,
|
65
|
+
records_to_update_documents: @records_to_update_documents || records_to_update_documents,
|
66
|
+
only_if: -> r { custom_if.call(r) && only_if.call(r) }
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Extensions
|
4
|
+
module DependencyTracking
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
|
8
|
+
base.class_eval do
|
9
|
+
before_validation do
|
10
|
+
self.class.each_dependent_attribute_for(changes) do |a|
|
11
|
+
attribute_will_change! a
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def tracks_attributes_dependencies(dependencies)
|
19
|
+
const_set 'DEPENDENT_CUSTOM_ATTRIBUTES', dependencies.dup.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
def each_dependent_attribute_for(changed_attributes)
|
23
|
+
const_get('DEPENDENT_CUSTOM_ATTRIBUTES').each do |attributes, dependent_attributes|
|
24
|
+
dependent_attributes.each do |dependent_attribute|
|
25
|
+
attributes.each do |a|
|
26
|
+
yield dependent_attribute if changed_attributes.include? a
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_dependent_fields?(field)
|
33
|
+
const_get('DEPENDENT_CUSTOM_ATTRIBUTES').any? do |from, to|
|
34
|
+
from.include? field.to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def has_association_named?(table_name)
|
39
|
+
reflect_on_all_associations.any? { |a| a.name == table_name }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'callback'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Model
|
5
|
+
module Extensions
|
6
|
+
class DestroyCallback < Callback
|
7
|
+
def after_commit(record)
|
8
|
+
field_to_update = config.field_to_update
|
9
|
+
records_to_update_documents = config.records_to_update_documents
|
10
|
+
optionally_delayed = config.optionally_delayed
|
11
|
+
only_if = config.only_if
|
12
|
+
|
13
|
+
record.instance_eval do
|
14
|
+
return unless only_if.call(self)
|
15
|
+
|
16
|
+
target = records_to_update_documents.call(self)
|
17
|
+
|
18
|
+
if target.respond_to? :each
|
19
|
+
target.map(&:reload).map(&optionally_delayed).each do |t|
|
20
|
+
t.partially_update_document(field_to_update)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
optionally_delayed.call(target.reload).partially_update_document(field_to_update)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Extensions
|
4
|
+
module IndexOperations
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def create_index(name:, force:true)
|
11
|
+
klass = self
|
12
|
+
|
13
|
+
client = __elasticsearch__.client
|
14
|
+
|
15
|
+
indices = client.indices
|
16
|
+
|
17
|
+
if indices.exists(index: name) && force
|
18
|
+
indices.delete index: name
|
19
|
+
end
|
20
|
+
|
21
|
+
indices.create index: name, body: { settings: klass.settings.to_hash, mappings: klass.mappings.to_hash }
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete_index(name:)
|
25
|
+
client = __elasticsearch__.client
|
26
|
+
|
27
|
+
indices = client.indices
|
28
|
+
|
29
|
+
indices.delete index: name
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_alias(name: nil)
|
33
|
+
name ||= index_name
|
34
|
+
|
35
|
+
client = __elasticsearch__.client
|
36
|
+
|
37
|
+
indices = client.indices
|
38
|
+
|
39
|
+
indices_aliased = indices.get_alias(name: name).keys
|
40
|
+
|
41
|
+
indices_aliased.each do |index|
|
42
|
+
indices.delete name: name, index: index
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def prepare_alias(name:, force: true)
|
47
|
+
client = __elasticsearch__.client
|
48
|
+
|
49
|
+
indices = client.indices
|
50
|
+
|
51
|
+
if indices.exists(index: name) && force
|
52
|
+
indices.delete index: name
|
53
|
+
end
|
54
|
+
|
55
|
+
unless indices.exists_alias(name: name)
|
56
|
+
aliased_index_name = "#{index_name}_#{Time.now.to_i}"
|
57
|
+
|
58
|
+
create_index(name: aliased_index_name, force: force)
|
59
|
+
|
60
|
+
indices.put_alias index: aliased_index_name, name: name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def replace_index_for_alias(name:, to:)
|
65
|
+
client = __elasticsearch__.client
|
66
|
+
|
67
|
+
indices = client.indices
|
68
|
+
|
69
|
+
if indices.exists_alias name: name
|
70
|
+
old_index_name = indices.get_alias(name: name).keys.first
|
71
|
+
|
72
|
+
indices.update_aliases body: {
|
73
|
+
actions: [
|
74
|
+
{ remove: { index: old_index_name, alias: name } },
|
75
|
+
{ add: { index: to, alias: name } }
|
76
|
+
]
|
77
|
+
}
|
78
|
+
else
|
79
|
+
indices.put_alias index: to, name: name
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'shortest_path'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Model
|
5
|
+
module Extensions
|
6
|
+
class MappingNode < ShortestPath::Node
|
7
|
+
def self.from_class(klass)
|
8
|
+
name = klass.document_type.intern
|
9
|
+
|
10
|
+
new(klass: klass, name: name, mapping: klass.mapping.to_hash[name])
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(klass:, name:, mapping:, through_class:nil)
|
14
|
+
@klass = klass
|
15
|
+
@name = name
|
16
|
+
@mapping = mapping
|
17
|
+
@through_class = through_class
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
@name
|
22
|
+
end
|
23
|
+
|
24
|
+
def relates_to_class?(klass)
|
25
|
+
@klass == klass || @through_class == klass
|
26
|
+
end
|
27
|
+
|
28
|
+
def klass
|
29
|
+
@klass
|
30
|
+
end
|
31
|
+
|
32
|
+
def through_class
|
33
|
+
@through_class
|
34
|
+
end
|
35
|
+
|
36
|
+
def each(&block)
|
37
|
+
associations = @klass.reflect_on_all_associations
|
38
|
+
|
39
|
+
props = @mapping[:properties]
|
40
|
+
fields = props.keys
|
41
|
+
|
42
|
+
edges = fields.map { |f|
|
43
|
+
a = associations.find { |a| a.name == f }
|
44
|
+
|
45
|
+
if a && a.options[:polymorphic] != true
|
46
|
+
through_class = if a.options[:through]
|
47
|
+
a.options[:through].to_s.classify.constantize
|
48
|
+
end
|
49
|
+
|
50
|
+
dest = MappingNode.new(klass: a.class_name.constantize, name: f.to_s.pluralize.intern, mapping: props[f], through_class: through_class)
|
51
|
+
|
52
|
+
edge_class.new(name: f, destination: dest)
|
53
|
+
end
|
54
|
+
}.reject(&:nil?)
|
55
|
+
|
56
|
+
if block.nil?
|
57
|
+
edges
|
58
|
+
else
|
59
|
+
edges.each(&block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Extensions
|
4
|
+
module MappingReflection
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# @param [Class] destination_class
|
11
|
+
def path_in_mapping_to_class(destination_class, current_properties: nil, current_class: nil, visited_classes: nil)
|
12
|
+
current_properties ||= mappings.to_hash[:"#{self.document_type}"][:properties]
|
13
|
+
visited_classes ||= []
|
14
|
+
current_class ||= self
|
15
|
+
|
16
|
+
# Recurse only on associations
|
17
|
+
current_properties.keys.each do |key|
|
18
|
+
association_found = current_class.reflect_on_all_associations.find { |a| a.name == key }
|
19
|
+
|
20
|
+
next unless association_found
|
21
|
+
next if visited_classes.include? association_found.klass
|
22
|
+
|
23
|
+
if association_found.klass == destination_class
|
24
|
+
return [key]
|
25
|
+
else
|
26
|
+
suffix_found = path_in_mapping_to_class(
|
27
|
+
destination_class,
|
28
|
+
current_properties: current_properties[key][:properties],
|
29
|
+
current_class: association_found.klass,
|
30
|
+
visited_classes: visited_classes.dup.append(association_found.klass)
|
31
|
+
)
|
32
|
+
|
33
|
+
if suffix_found
|
34
|
+
return [key] + suffix_found
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param [Symbol] nested_object_name
|
43
|
+
# @return [Array<Symbol>]
|
44
|
+
def path_in_mapping_to(nested_object_name, root_properties: nil)
|
45
|
+
root_properties ||= mappings.to_hash[:"#{self.document_type}"][:properties]
|
46
|
+
|
47
|
+
keys = root_properties.keys
|
48
|
+
|
49
|
+
keys.each do |key|
|
50
|
+
if key == nested_object_name
|
51
|
+
return [key]
|
52
|
+
end
|
53
|
+
|
54
|
+
next if root_properties[key][:type] != 'object'
|
55
|
+
|
56
|
+
suffix = path_in_mapping_to(nested_object_name, root_properties: root_properties[key][:properties])
|
57
|
+
|
58
|
+
if suffix.include? nested_object_name
|
59
|
+
return [key] + suffix
|
60
|
+
end
|
61
|
+
end
|
62
|
+
[]
|
63
|
+
end
|
64
|
+
|
65
|
+
def document_field_named(field_name)
|
66
|
+
root_properties ||= mappings.to_hash[:"#{self.document_type}"][:properties]
|
67
|
+
root_properties[field_name]
|
68
|
+
end
|
69
|
+
|
70
|
+
def has_document_field_named?(field_name)
|
71
|
+
!! document_field_named(field_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param [Array<Symbol>] path
|
75
|
+
def nested_object_fields_for(path, root_properties: nil)
|
76
|
+
root_properties ||= mappings.to_hash[:"#{self.document_type}"][:properties]
|
77
|
+
|
78
|
+
keys = root_properties.keys
|
79
|
+
|
80
|
+
suffix, *postfix = path
|
81
|
+
|
82
|
+
return root_properties.keys if suffix.nil?
|
83
|
+
|
84
|
+
keys.each do |key|
|
85
|
+
if key == suffix
|
86
|
+
result = nested_object_fields_for(postfix, root_properties: root_properties[key][:properties])
|
87
|
+
return result if result
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require_relative 'mapping_node'
|
2
|
+
require_relative 'update_callback'
|
3
|
+
require_relative 'destroy_callback'
|
4
|
+
|
5
|
+
module Elasticsearch
|
6
|
+
module Model
|
7
|
+
module Extensions
|
8
|
+
module OuterDocumentUpdating
|
9
|
+
def self.included(klass)
|
10
|
+
klass.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
def index_update_required?
|
14
|
+
(previous_changes.keys & self.class.nested_object_fields).size > 0 ||
|
15
|
+
(previous_changes.size > 0 && self.class.has_dependent_fields?)
|
16
|
+
end
|
17
|
+
|
18
|
+
class Update
|
19
|
+
def initialize(from:, to:)
|
20
|
+
@parent_class = from
|
21
|
+
@child_class = to
|
22
|
+
end
|
23
|
+
|
24
|
+
class Default < Update
|
25
|
+
def self.applicable_to?(klass)
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply
|
30
|
+
parent_class = @parent_class
|
31
|
+
child_class = @child_class
|
32
|
+
|
33
|
+
only_if = -> r { true }
|
34
|
+
|
35
|
+
puts "Parent: #{@parent_class.name}"
|
36
|
+
puts "Child: #{@child_class.name}"
|
37
|
+
|
38
|
+
# 子cから親aへのパスが[:b, :a]のようなパスだったら、c.b.aのようにaを辿れるはずなので
|
39
|
+
records_to_update_documents = begin
|
40
|
+
child_to_parent_path = Elasticsearch::Model::Extensions::OuterDocumentUpdating::ClassMethods::AssociationTraversal.shortest_path(from: child_class, to: parent_class)
|
41
|
+
|
42
|
+
-> updated_record { child_to_parent_path.inject(updated_record) { |d, parent_association| d.send parent_association } }
|
43
|
+
end
|
44
|
+
|
45
|
+
[only_if, records_to_update_documents]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Configures callbacks to update the index of the model associated through a polymorphic association
|
50
|
+
# @see http://guides.rubyonrails.org/association_basics.html#polymorphic-associations
|
51
|
+
class ThroughPolymorphicAssociation < Update
|
52
|
+
def self.applicable_to?(klass)
|
53
|
+
!! polymorphic_assoc_for(klass)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.polymorphic_assoc_for(klass)
|
57
|
+
klass.reflect_on_all_associations.find { |a|
|
58
|
+
a.macro == :belongs_to && a.options[:polymorphic]
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply
|
63
|
+
parent_class = @parent_class
|
64
|
+
child_class = @child_class
|
65
|
+
|
66
|
+
polymorphic_assoc = self.class.polymorphic_assoc_for(child_class)
|
67
|
+
polymorphic_assoc_name = polymorphic_assoc.name
|
68
|
+
|
69
|
+
parent_type_attribute_name = :"#{polymorphic_assoc_name}_type"
|
70
|
+
parent_id_attribute_name = :"#{polymorphic_assoc_name}_id"
|
71
|
+
|
72
|
+
only_if = -> updated_record {
|
73
|
+
updated_record.send(parent_type_attribute_name) == parent_class.name
|
74
|
+
}
|
75
|
+
|
76
|
+
records_to_update_documents = -> updated_record {
|
77
|
+
parent_class.where(id: updated_record.send(parent_id_attribute_name))
|
78
|
+
}
|
79
|
+
|
80
|
+
[only_if, records_to_update_documents]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
STRATEGIES = [Update::ThroughPolymorphicAssociation, Update::Default]
|
86
|
+
|
87
|
+
def self.strategy_for(klass)
|
88
|
+
STRATEGIES.find { |s| s.applicable_to? klass }
|
89
|
+
end
|
90
|
+
|
91
|
+
module ClassMethods
|
92
|
+
def nested_object_fields
|
93
|
+
@nested_object_fields
|
94
|
+
end
|
95
|
+
|
96
|
+
def has_dependent_fields?
|
97
|
+
@has_dependent_fields
|
98
|
+
end
|
99
|
+
|
100
|
+
def path_from(from)
|
101
|
+
Elasticsearch::Model::Extensions::MappingNode.
|
102
|
+
from_class(from).
|
103
|
+
breadth_first_search { |e| e.destination.relates_to_class?(self) }.
|
104
|
+
first
|
105
|
+
end
|
106
|
+
|
107
|
+
def initialize_active_record!(active_record_class, parent_class: parent_class, delayed:, if: -> r { true }, records_to_update_documents: nil)
|
108
|
+
config = Elasticsearch::Model::Extensions::Configuration.new(active_record_class, parent_class: parent_class, delayed: delayed, if: binding.local_variable_get(:if), records_to_update_documents: records_to_update_documents)
|
109
|
+
|
110
|
+
active_record_class.after_commit Elasticsearch::Model::Extensions::UpdateCallback.new(config)
|
111
|
+
active_record_class.after_commit Elasticsearch::Model::Extensions::DestroyCallback.new(config), on: :destroy
|
112
|
+
end
|
113
|
+
|
114
|
+
def partially_updates_document_of(parent_class, delayed:, if: -> r { true }, records_to_update_documents: nil)
|
115
|
+
initialize_active_record!(
|
116
|
+
self,
|
117
|
+
parent_class: parent_class,
|
118
|
+
delayed: delayed,
|
119
|
+
if: binding.local_variable_get(:if),
|
120
|
+
records_to_update_documents: records_to_update_documents
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
module AssociationTraversal
|
125
|
+
class << self
|
126
|
+
def shortest_path(from:, to:, visited_classes: nil)
|
127
|
+
visited_classes ||= []
|
128
|
+
current_class = from
|
129
|
+
destination_class = to
|
130
|
+
|
131
|
+
paths = []
|
132
|
+
|
133
|
+
current_class.reflect_on_all_associations.each do |association_found|
|
134
|
+
|
135
|
+
next if association_found.options[:polymorphic]
|
136
|
+
|
137
|
+
key = association_found.name
|
138
|
+
|
139
|
+
begin
|
140
|
+
klass = association_found.class_name.constantize
|
141
|
+
rescue => e
|
142
|
+
warn "#{e.message} while reflecting #{current_class.name}\##{key}\n#{e.backtrace[0...1].join("\n")}"
|
143
|
+
next
|
144
|
+
end
|
145
|
+
|
146
|
+
next if visited_classes.include? association_found.class_name
|
147
|
+
|
148
|
+
if klass == destination_class
|
149
|
+
return [key]
|
150
|
+
else
|
151
|
+
suffix_found = shortest_path(
|
152
|
+
from: klass,
|
153
|
+
to: destination_class,
|
154
|
+
visited_classes: visited_classes.append(association_found.class_name)
|
155
|
+
)
|
156
|
+
|
157
|
+
if suffix_found
|
158
|
+
paths << [key] + suffix_found
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
if paths.empty?
|
164
|
+
nil
|
165
|
+
else
|
166
|
+
paths.min_by { |path| path.size }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Extensions
|
4
|
+
module PartialUpdating
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.build_as_json_options(klass:, props: )
|
10
|
+
indexed_attributes = props.keys
|
11
|
+
associations = klass.reflect_on_all_associations.select { |a| %i| has_one has_many belongs_to |.include? a.macro }
|
12
|
+
association_names = associations.map(&:name)
|
13
|
+
persisted_attributes = klass.attribute_names.map(&:intern)
|
14
|
+
|
15
|
+
nested_attributes = indexed_attributes & association_names
|
16
|
+
method_attributes = indexed_attributes - persisted_attributes - nested_attributes
|
17
|
+
only_attributes = indexed_attributes - nested_attributes
|
18
|
+
|
19
|
+
options = {}
|
20
|
+
|
21
|
+
if only_attributes.size > 1
|
22
|
+
options[:only] = only_attributes
|
23
|
+
elsif only_attributes.size == 1
|
24
|
+
options[:only] = only_attributes.first
|
25
|
+
end
|
26
|
+
|
27
|
+
if method_attributes.size > 1
|
28
|
+
options[:methods] = method_attributes
|
29
|
+
elsif method_attributes.size == 1
|
30
|
+
options[:methods] = method_attributes.first
|
31
|
+
end
|
32
|
+
|
33
|
+
nested_attributes.each do |n|
|
34
|
+
a = associations.find { |a| a.name == n.intern }
|
35
|
+
nested_klass = a.class_name.constantize
|
36
|
+
nested_prop = props[n]
|
37
|
+
if nested_prop.present?
|
38
|
+
options[:include] ||= {}
|
39
|
+
options[:include][n] = build_as_json_options(
|
40
|
+
klass: nested_klass,
|
41
|
+
props: nested_prop[:properties]
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
options
|
47
|
+
end
|
48
|
+
|
49
|
+
def as_indexed_json(options={})
|
50
|
+
as_json(options.merge(self.class.as_json_options))
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [Array<Symbol>] changed_attributes
|
54
|
+
# @param [Proc<Symbol, Hash>] json_options
|
55
|
+
def build_partial_document_for_update(*changed_attributes, json_options: nil)
|
56
|
+
changes = {}
|
57
|
+
|
58
|
+
json_options ||= -> field { self.class.partial_as_json_options(field) || {} }
|
59
|
+
|
60
|
+
self.class.each_field_to_update_according_to_changed_fields(changed_attributes) do |field_to_update|
|
61
|
+
options = json_options.call field_to_update
|
62
|
+
|
63
|
+
json = __send__(:"#{field_to_update}").as_json(options)
|
64
|
+
|
65
|
+
changes[field_to_update] = json
|
66
|
+
end
|
67
|
+
|
68
|
+
changes
|
69
|
+
end
|
70
|
+
|
71
|
+
def partially_update_document(*changed_attributes)
|
72
|
+
if changed_attributes.empty?
|
73
|
+
__elasticsearch__.index_document
|
74
|
+
else
|
75
|
+
begin
|
76
|
+
partial_document = build_partial_document_for_update(*changed_attributes)
|
77
|
+
rescue => e
|
78
|
+
Rails.logger.error "Error in #partially_update_document: #{e.message}\n#{e.backtrace.join("\n")}"
|
79
|
+
end
|
80
|
+
|
81
|
+
update_document(partial_document)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [Hash] partial_document
|
86
|
+
def update_document(partial_document)
|
87
|
+
klass = self.class
|
88
|
+
|
89
|
+
__elasticsearch__.client.update(
|
90
|
+
{ index: klass.index_name,
|
91
|
+
type: klass.document_type,
|
92
|
+
id: self.id,
|
93
|
+
body: { doc: partial_document } }
|
94
|
+
) if partial_document.size > 0
|
95
|
+
end
|
96
|
+
|
97
|
+
module ClassMethods
|
98
|
+
def as_json_options
|
99
|
+
@as_json_options ||= Elasticsearch::Model::Extensions::PartialUpdating.build_as_json_options(
|
100
|
+
klass: self,
|
101
|
+
props: self.mappings.to_hash[self.document_type.intern][:properties]
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
def partial_as_json_options(field)
|
106
|
+
as_json_options[:include][field]
|
107
|
+
end
|
108
|
+
|
109
|
+
def each_field_to_update_according_to_changed_fields(changed_fields)
|
110
|
+
root_mapping_properties = mappings.to_hash[:"#{document_type}"][:properties]
|
111
|
+
|
112
|
+
changed_fields.each do |changed_field|
|
113
|
+
field_mapping = root_mapping_properties[:"#{changed_field}"]
|
114
|
+
|
115
|
+
next unless field_mapping
|
116
|
+
|
117
|
+
yield changed_field
|
118
|
+
end
|
119
|
+
|
120
|
+
each_dependent_attribute_for(changed_fields.map(&:to_s)) do |a|
|
121
|
+
a_sym = a.intern
|
122
|
+
|
123
|
+
yield a_sym
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def fields_to_update_according_to_changed_fields(changed_fields)
|
128
|
+
fields = []
|
129
|
+
each_field_to_update_according_to_changed_fields changed_fields do |field_to_update|
|
130
|
+
fields << field_to_update
|
131
|
+
end
|
132
|
+
fields
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
module Callbacks
|
137
|
+
def self.included(base)
|
138
|
+
base.class_eval do
|
139
|
+
after_commit lambda { __elasticsearch__.index_document }, on: :create
|
140
|
+
after_commit lambda { partially_update_document(*previous_changes.keys.map(&:intern)) }, on: :update, if: -> { previous_changes.size != 0 }
|
141
|
+
after_commit lambda { __elasticsearch__.delete_document }, on: :destroy
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative 'callback'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Model
|
5
|
+
module Extensions
|
6
|
+
class UpdateCallback < Callback
|
7
|
+
def after_commit(record)
|
8
|
+
field_to_update = config.field_to_update
|
9
|
+
records_to_update_documents = config.records_to_update_documents
|
10
|
+
optionally_delayed = config.optionally_delayed
|
11
|
+
only_if = config.only_if
|
12
|
+
|
13
|
+
record.instance_eval do
|
14
|
+
return unless only_if.call(self) && index_update_required?
|
15
|
+
|
16
|
+
target = records_to_update_documents.call(self)
|
17
|
+
|
18
|
+
if target.respond_to? :each
|
19
|
+
# `reload` required to ensure that the outer record is up-to-date with changes
|
20
|
+
# when `self` is an instance of a `through` model.
|
21
|
+
#
|
22
|
+
# Imagine the case where we have an association containing:
|
23
|
+
#
|
24
|
+
# `article has_many comments through article_comments`
|
25
|
+
#
|
26
|
+
# and:
|
27
|
+
#
|
28
|
+
# `article_comments belongs_to article`
|
29
|
+
#
|
30
|
+
# Here, `article_comment.article` may contain outdated `comments` because `article_comment.article`
|
31
|
+
# won't be notified with changes in `article_comments` thus won't reload `comments` automatically.
|
32
|
+
target.map(&:reload).map(&optionally_delayed).each do |t|
|
33
|
+
t.partially_update_document(field_to_update)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
optionally_delayed.call(target.reload).partially_update_document(field_to_update)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: elasticsearch-model-extensions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yusuke KUOKA
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
description: A set of extensions for elasticsearch-model which aims to ease the burden
|
42
|
+
of things like re-indexing, verbose/complex mapping.
|
43
|
+
email:
|
44
|
+
- yusuke.kuoka@crowdworks.co.jp
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".gitignore"
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- elasticsearch-model-extensions.gemspec
|
55
|
+
- lib/elasticsearch/model/extensions.rb
|
56
|
+
- lib/elasticsearch/model/extensions/batch_updating.rb
|
57
|
+
- lib/elasticsearch/model/extensions/callback.rb
|
58
|
+
- lib/elasticsearch/model/extensions/configuration.rb
|
59
|
+
- lib/elasticsearch/model/extensions/dependency_tracking.rb
|
60
|
+
- lib/elasticsearch/model/extensions/destroy_callback.rb
|
61
|
+
- lib/elasticsearch/model/extensions/index_operations.rb
|
62
|
+
- lib/elasticsearch/model/extensions/mapping_node.rb
|
63
|
+
- lib/elasticsearch/model/extensions/mapping_reflection.rb
|
64
|
+
- lib/elasticsearch/model/extensions/outer_document_updating.rb
|
65
|
+
- lib/elasticsearch/model/extensions/partial_updating.rb
|
66
|
+
- lib/elasticsearch/model/extensions/update_callback.rb
|
67
|
+
- lib/elasticsearch/model/extensions/version.rb
|
68
|
+
homepage: https://github.com/crowdworks/elasticsearch-model-extensions
|
69
|
+
licenses:
|
70
|
+
- MIT
|
71
|
+
metadata: {}
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 2.2.2
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: A set of extensions for elasticsearch-model which aims to ease the burden
|
92
|
+
of things like re-indexing, verbose/complex mapping that you may face once you started
|
93
|
+
using elasticsearch seriously.
|
94
|
+
test_files: []
|