chewy 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 +17 -0
- data/.rspec +3 -0
- data/.rvmrc +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +12 -0
- data/Guardfile +24 -0
- data/LICENSE.txt +22 -0
- data/README.md +208 -0
- data/Rakefile +6 -0
- data/chewy.gemspec +32 -0
- data/lib/chewy.rb +55 -0
- data/lib/chewy/config.rb +48 -0
- data/lib/chewy/fields/base.rb +49 -0
- data/lib/chewy/fields/default.rb +10 -0
- data/lib/chewy/fields/root.rb +10 -0
- data/lib/chewy/index.rb +71 -0
- data/lib/chewy/index/actions.rb +43 -0
- data/lib/chewy/index/client.rb +13 -0
- data/lib/chewy/index/search.rb +26 -0
- data/lib/chewy/query.rb +141 -0
- data/lib/chewy/query/criteria.rb +81 -0
- data/lib/chewy/query/loading.rb +27 -0
- data/lib/chewy/query/pagination.rb +39 -0
- data/lib/chewy/rspec.rb +1 -0
- data/lib/chewy/rspec/update_index.rb +121 -0
- data/lib/chewy/type.rb +22 -0
- data/lib/chewy/type/adapter/active_record.rb +27 -0
- data/lib/chewy/type/adapter/object.rb +22 -0
- data/lib/chewy/type/base.rb +41 -0
- data/lib/chewy/type/import.rb +67 -0
- data/lib/chewy/type/mapping.rb +50 -0
- data/lib/chewy/type/observe.rb +37 -0
- data/lib/chewy/type/wrapper.rb +35 -0
- data/lib/chewy/version.rb +3 -0
- data/spec/chewy/config_spec.rb +50 -0
- data/spec/chewy/fields/base_spec.rb +70 -0
- data/spec/chewy/fields/default_spec.rb +6 -0
- data/spec/chewy/fields/root_spec.rb +6 -0
- data/spec/chewy/index/actions_spec.rb +53 -0
- data/spec/chewy/index/client_spec.rb +18 -0
- data/spec/chewy/index/search_spec.rb +54 -0
- data/spec/chewy/index_spec.rb +65 -0
- data/spec/chewy/query/criteria_spec.rb +73 -0
- data/spec/chewy/query/loading_spec.rb +37 -0
- data/spec/chewy/query/pagination_spec.rb +40 -0
- data/spec/chewy/query_spec.rb +110 -0
- data/spec/chewy/rspec/update_index_spec.rb +149 -0
- data/spec/chewy/type/import_spec.rb +68 -0
- data/spec/chewy/type/mapping_spec.rb +54 -0
- data/spec/chewy/type/observe_spec.rb +55 -0
- data/spec/chewy/type/wrapper_spec.rb +35 -0
- data/spec/chewy/type_spec.rb +43 -0
- data/spec/chewy_spec.rb +36 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/support/class_helpers.rb +16 -0
- data/spec/support/fail_helpers.rb +13 -0
- metadata +249 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
module Chewy
|
2
|
+
class Query
|
3
|
+
module Loading
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def load(options = {})
|
7
|
+
::Kaminari.paginate_array(_load_objects(options),
|
8
|
+
limit: limit_value, offset: offset_value, total_count: total_count)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def _load_objects(options)
|
14
|
+
loaded_objects = Hash[_results.group_by(&:class).map do |type, objects|
|
15
|
+
model = type.adapter.model
|
16
|
+
scope = model.where(id: objects.map(&:id))
|
17
|
+
additional_scope = options[:scopes][type.type_name.to_sym] if options[:scopes]
|
18
|
+
scope = scope.instance_eval(&additional_scope) if additional_scope
|
19
|
+
|
20
|
+
[type, scope.index_by(&:id)]
|
21
|
+
end]
|
22
|
+
|
23
|
+
_results.map { |result| loaded_objects[result.class][result.id.to_i] }.compact
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'kaminari'
|
2
|
+
|
3
|
+
module Chewy
|
4
|
+
class Query
|
5
|
+
module Pagination
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
include Kaminari::PageScopeMethods
|
10
|
+
|
11
|
+
delegate :default_per_page, :max_per_page, :max_pages, to: :_kaminari_config
|
12
|
+
|
13
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
14
|
+
def #{Kaminari.config.page_method_name}(num = 1)
|
15
|
+
limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1))
|
16
|
+
end
|
17
|
+
RUBY
|
18
|
+
end
|
19
|
+
|
20
|
+
def total_count
|
21
|
+
_response['hits']['total']
|
22
|
+
end
|
23
|
+
|
24
|
+
def limit_value
|
25
|
+
(criteria.search[:size].presence || default_per_page).to_i
|
26
|
+
end
|
27
|
+
|
28
|
+
def offset_value
|
29
|
+
criteria.search[:from].to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def _kaminari_config
|
35
|
+
Kaminari.config
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/chewy/rspec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'chewy/rspec/update_index'
|
@@ -0,0 +1,121 @@
|
|
1
|
+
RSpec::Matchers.define :update_index do |type_name|
|
2
|
+
chain(:and_reindex) do |*args|
|
3
|
+
@reindex ||= {}
|
4
|
+
@reindex.merge!(extract_documents(*args))
|
5
|
+
end
|
6
|
+
|
7
|
+
chain(:and_delete) do |*args|
|
8
|
+
@delete ||= {}
|
9
|
+
@delete.merge!(extract_documents(*args))
|
10
|
+
end
|
11
|
+
|
12
|
+
match do |block|
|
13
|
+
@reindex ||= {}
|
14
|
+
@delete ||= {}
|
15
|
+
|
16
|
+
type = Chewy.derive_type(type_name)
|
17
|
+
updated = []
|
18
|
+
type.stub(:bulk) do |options|
|
19
|
+
updated += options[:body].map do |updated_document|
|
20
|
+
updated_document = updated_document.symbolize_keys
|
21
|
+
body = updated_document[:index] || updated_document[:delete]
|
22
|
+
body[:data] = body[:data].symbolize_keys if body[:data]
|
23
|
+
updated_document
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
block.call
|
28
|
+
|
29
|
+
@updated = updated
|
30
|
+
@updated.each do |updated_document|
|
31
|
+
if body = updated_document[:index]
|
32
|
+
if document = @reindex[body[:_id].to_s]
|
33
|
+
document[:real_count] += 1
|
34
|
+
document[:real_attributes].merge!(body[:data])
|
35
|
+
end
|
36
|
+
elsif body = updated_document[:delete]
|
37
|
+
if document = @delete[body[:_id].to_s]
|
38
|
+
document[:real_count] += 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
@reindex.each do |_, document|
|
44
|
+
document[:match_count] = (!document[:expected_count] && document[:real_count] > 0) ||
|
45
|
+
(document[:expected_count] && document[:expected_count] == document[:real_count])
|
46
|
+
document[:match_attributes] = document[:expected_attributes].blank? ||
|
47
|
+
document[:real_attributes].slice(*document[:expected_attributes].keys) == document[:expected_attributes]
|
48
|
+
end
|
49
|
+
@delete.each do |_, document|
|
50
|
+
document[:match_count] = (!document[:expected_count] && document[:real_count] > 0) ||
|
51
|
+
(document[:expected_count] && document[:expected_count] == document[:real_count])
|
52
|
+
end
|
53
|
+
|
54
|
+
@updated.any? &&
|
55
|
+
@reindex.all? { |_, document| document[:match_count] && document[:match_attributes] } &&
|
56
|
+
@delete.all? { |_, document| document[:match_count] }
|
57
|
+
end
|
58
|
+
|
59
|
+
failure_message_for_should do
|
60
|
+
output = ''
|
61
|
+
|
62
|
+
if @updated.none?
|
63
|
+
output << "Expected index `#{type_name}` to be updated, but it was not\n"
|
64
|
+
end
|
65
|
+
|
66
|
+
output << @reindex.each.with_object('') do |(id, document), output|
|
67
|
+
unless document[:match_count] && document[:match_attributes]
|
68
|
+
output << "Expected document with id `#{id}` to be reindexed"
|
69
|
+
if document[:real_count] > 0
|
70
|
+
output << "\n #{document[:expected_count]} times, but was reindexed #{document[:real_count]} times" if document[:expected_count] && !document[:match_count]
|
71
|
+
output << "\n with #{document[:expected_attributes]}, but it was reindexed with #{document[:real_attributes]}" if document[:expected_attributes].present? && !document[:match_attributes]
|
72
|
+
else
|
73
|
+
output << ", but it was not"
|
74
|
+
end
|
75
|
+
output << "\n"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
output << @delete.each.with_object('') do |(id, document), output|
|
80
|
+
unless document[:match_count]
|
81
|
+
output << "Expected document with id `#{id}` to be deleted"
|
82
|
+
if document[:real_count] > 0 && document[:expected_count] && !document[:match_count]
|
83
|
+
output << "\n #{document[:expected_count]} times, but was deleted #{document[:real_count]} times"
|
84
|
+
else
|
85
|
+
output << ", but it was not"
|
86
|
+
end
|
87
|
+
output << "\n"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
output
|
92
|
+
end
|
93
|
+
|
94
|
+
failure_message_for_should_not do
|
95
|
+
if @updated.any?
|
96
|
+
"Expected index `#{type_name}` not to be updated, but it was with #{
|
97
|
+
@updated.map(&:values).flatten.group_by { |documents| documents[:_id] }.map do |id, documents|
|
98
|
+
"\n document id `#{id}` (#{documents.count} times)"
|
99
|
+
end.join
|
100
|
+
}\n"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def extract_documents *args
|
105
|
+
options = args.extract_options!
|
106
|
+
|
107
|
+
expected_count = options[:times] || options[:count]
|
108
|
+
expected_attributes = (options[:with] || options[:attributes] || {}).symbolize_keys!
|
109
|
+
|
110
|
+
Hash[args.flatten.map do |document|
|
111
|
+
id = document.respond_to?(:id) ? document.id.to_s : document.to_s
|
112
|
+
[id, {
|
113
|
+
document: document,
|
114
|
+
expected_count: expected_count,
|
115
|
+
expected_attributes: expected_attributes,
|
116
|
+
real_count: 0,
|
117
|
+
real_attributes: {}
|
118
|
+
}]
|
119
|
+
end]
|
120
|
+
end
|
121
|
+
end
|
data/lib/chewy/type.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'chewy/type/base'
|
2
|
+
|
3
|
+
module Chewy
|
4
|
+
module Type
|
5
|
+
def self.new(index, target, options = {}, &block)
|
6
|
+
type = Class.new(Chewy::Type::Base)
|
7
|
+
|
8
|
+
adapter = if (target.is_a?(Class) && target < ActiveRecord::Base) || target.is_a?(::ActiveRecord::Relation)
|
9
|
+
Chewy::Type::Adapter::ActiveRecord.new(target, options)
|
10
|
+
else
|
11
|
+
Chewy::Type::Adapter::Object.new(target)
|
12
|
+
end
|
13
|
+
|
14
|
+
index.const_set(adapter.name, type)
|
15
|
+
type.send(:define_singleton_method, :index) { index }
|
16
|
+
type.send(:define_singleton_method, :adapter) { adapter }
|
17
|
+
|
18
|
+
type.class_eval &block if block
|
19
|
+
type
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Chewy
|
2
|
+
module Type
|
3
|
+
module Adapter
|
4
|
+
class ActiveRecord
|
5
|
+
attr_reader :model, :scope, :options
|
6
|
+
|
7
|
+
def initialize subject, options = {}
|
8
|
+
@options = options
|
9
|
+
if subject.is_a?(::ActiveRecord::Relation)
|
10
|
+
@model = subject.klass
|
11
|
+
@scope = subject
|
12
|
+
else
|
13
|
+
@model = subject
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
@name ||= options[:name].present? ? options[:name].to_s.camelize : model.model_name.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def type_name
|
22
|
+
@type_name ||= (options[:name].presence || model.model_name).to_s.underscore
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Chewy
|
2
|
+
module Type
|
3
|
+
module Adapter
|
4
|
+
class Object
|
5
|
+
attr_reader :subject, :options
|
6
|
+
|
7
|
+
def initialize subject, options = {}
|
8
|
+
@options = options
|
9
|
+
@subject = subject
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
@name ||= subject.to_s.camelize
|
14
|
+
end
|
15
|
+
|
16
|
+
def type_name
|
17
|
+
@type_name ||= subject.to_s.underscore
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'chewy/index/search'
|
2
|
+
require 'chewy/type/mapping'
|
3
|
+
require 'chewy/type/wrapper'
|
4
|
+
require 'chewy/type/observe'
|
5
|
+
require 'chewy/type/import'
|
6
|
+
require 'chewy/type/adapter/object'
|
7
|
+
require 'chewy/type/adapter/active_record'
|
8
|
+
|
9
|
+
module Chewy
|
10
|
+
module Type
|
11
|
+
class Base
|
12
|
+
include Chewy::Index::Search
|
13
|
+
include Mapping
|
14
|
+
include Wrapper
|
15
|
+
include Observe
|
16
|
+
include Import
|
17
|
+
|
18
|
+
singleton_class.delegate :client, to: :index
|
19
|
+
|
20
|
+
def self.index
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.adapter
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.type_name(suggest = nil)
|
29
|
+
adapter.type_name
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.search_index
|
33
|
+
index
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.search_type
|
37
|
+
type_name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Chewy
|
2
|
+
module Type
|
3
|
+
module Import
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def bulk(options = {})
|
11
|
+
client.bulk options.merge(index: index.index_name, type: type_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def import(*args)
|
15
|
+
options = args.extract_options!
|
16
|
+
collection = args.first || adapter.model.all
|
17
|
+
|
18
|
+
if collection.is_a? ::ActiveRecord::Relation
|
19
|
+
scoped_relation(collection)
|
20
|
+
.find_in_batches(options.slice(:batch_size)) { |objects| import_objects objects }
|
21
|
+
else
|
22
|
+
collection = Array.wrap(collection)
|
23
|
+
if collection.all? { |entity| entity.respond_to?(:id) }
|
24
|
+
import_objects collection
|
25
|
+
else
|
26
|
+
import_ids collection, options
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def scoped_relation(relation)
|
34
|
+
adapter.scope ? relation.merge(adapter.scope) : relation
|
35
|
+
end
|
36
|
+
|
37
|
+
def import_ids(ids, options = {})
|
38
|
+
ids = ids.map(&:to_i).uniq
|
39
|
+
scoped_relation(adapter.model.where(id: ids))
|
40
|
+
.find_in_batches(options.slice(:batch_size)) do |objects|
|
41
|
+
ids -= objects.map(&:id)
|
42
|
+
import_objects objects
|
43
|
+
end
|
44
|
+
|
45
|
+
body = ids.map { |id| {delete: {_index: index.index_name, _type: type_name, _id: id}} }
|
46
|
+
bulk refresh: true, body: body if body.any?
|
47
|
+
end
|
48
|
+
|
49
|
+
def import_objects(objects)
|
50
|
+
body = objects.map do |object|
|
51
|
+
identify = {_index: index.index_name, _type: type_name, _id: object.id}
|
52
|
+
if object.respond_to?(:destroyed?) && object.destroyed?
|
53
|
+
{delete: identify}
|
54
|
+
else
|
55
|
+
{index: identify.merge!(data: object_to_data(object))}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
bulk refresh: true, body: body if body.any?
|
59
|
+
end
|
60
|
+
|
61
|
+
def object_to_data(object)
|
62
|
+
(self.root_object ||= build_root).compose(object)[type_name.to_sym]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Chewy
|
2
|
+
module Type
|
3
|
+
module Mapping
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :root_object, instance_reader: false, instance_writer: false
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def root(options = {}, &block)
|
12
|
+
raise "Root is already defined" if self.root_object
|
13
|
+
build_root(options, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def field(*args, &block)
|
17
|
+
options = args.extract_options!
|
18
|
+
build_root unless self.root_object
|
19
|
+
|
20
|
+
if args.size > 1
|
21
|
+
args.map { |name| field(name, options) }
|
22
|
+
else
|
23
|
+
expand_nested(Chewy::Fields::Default.new(args.first, options), &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def mappings_hash
|
28
|
+
root_object ? root_object.mappings_hash : {}
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def expand_nested(field, &block)
|
34
|
+
@_current_field.nested(field) if @_current_field
|
35
|
+
if block
|
36
|
+
previous_field, @_current_field = @_current_field, field
|
37
|
+
block.call
|
38
|
+
@_current_field = previous_field
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_root(options = {}, &block)
|
43
|
+
self.root_object = Chewy::Fields::Root.new(type_name, options)
|
44
|
+
expand_nested(self.root_object, &block)
|
45
|
+
@_current_field = self.root_object
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Chewy
|
2
|
+
module Type
|
3
|
+
module Observe
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ActiveRecordMethods
|
7
|
+
def update_elasticsearch(type_name, &block)
|
8
|
+
update = Proc.new do
|
9
|
+
Chewy.derive_type(type_name).update_index(instance_eval(&block))
|
10
|
+
end
|
11
|
+
|
12
|
+
after_save &update
|
13
|
+
after_destroy &update
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def update_index(objects)
|
19
|
+
if Chewy.observing_enabled
|
20
|
+
if Chewy.atomic?
|
21
|
+
ids = if objects.is_a?(::ActiveRecord::Relation)
|
22
|
+
objects.pluck(:id)
|
23
|
+
else
|
24
|
+
Array.wrap(objects).map { |object| object.respond_to?(:id) ? object.id : object.to_i }
|
25
|
+
end
|
26
|
+
Chewy.atomic_stash self, ids
|
27
|
+
else
|
28
|
+
import objects
|
29
|
+
end if objects
|
30
|
+
end
|
31
|
+
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|