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