elasticsearch-model-extensions 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|