chewy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.rvmrc +1 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +12 -0
  7. data/Guardfile +24 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +208 -0
  10. data/Rakefile +6 -0
  11. data/chewy.gemspec +32 -0
  12. data/lib/chewy.rb +55 -0
  13. data/lib/chewy/config.rb +48 -0
  14. data/lib/chewy/fields/base.rb +49 -0
  15. data/lib/chewy/fields/default.rb +10 -0
  16. data/lib/chewy/fields/root.rb +10 -0
  17. data/lib/chewy/index.rb +71 -0
  18. data/lib/chewy/index/actions.rb +43 -0
  19. data/lib/chewy/index/client.rb +13 -0
  20. data/lib/chewy/index/search.rb +26 -0
  21. data/lib/chewy/query.rb +141 -0
  22. data/lib/chewy/query/criteria.rb +81 -0
  23. data/lib/chewy/query/loading.rb +27 -0
  24. data/lib/chewy/query/pagination.rb +39 -0
  25. data/lib/chewy/rspec.rb +1 -0
  26. data/lib/chewy/rspec/update_index.rb +121 -0
  27. data/lib/chewy/type.rb +22 -0
  28. data/lib/chewy/type/adapter/active_record.rb +27 -0
  29. data/lib/chewy/type/adapter/object.rb +22 -0
  30. data/lib/chewy/type/base.rb +41 -0
  31. data/lib/chewy/type/import.rb +67 -0
  32. data/lib/chewy/type/mapping.rb +50 -0
  33. data/lib/chewy/type/observe.rb +37 -0
  34. data/lib/chewy/type/wrapper.rb +35 -0
  35. data/lib/chewy/version.rb +3 -0
  36. data/spec/chewy/config_spec.rb +50 -0
  37. data/spec/chewy/fields/base_spec.rb +70 -0
  38. data/spec/chewy/fields/default_spec.rb +6 -0
  39. data/spec/chewy/fields/root_spec.rb +6 -0
  40. data/spec/chewy/index/actions_spec.rb +53 -0
  41. data/spec/chewy/index/client_spec.rb +18 -0
  42. data/spec/chewy/index/search_spec.rb +54 -0
  43. data/spec/chewy/index_spec.rb +65 -0
  44. data/spec/chewy/query/criteria_spec.rb +73 -0
  45. data/spec/chewy/query/loading_spec.rb +37 -0
  46. data/spec/chewy/query/pagination_spec.rb +40 -0
  47. data/spec/chewy/query_spec.rb +110 -0
  48. data/spec/chewy/rspec/update_index_spec.rb +149 -0
  49. data/spec/chewy/type/import_spec.rb +68 -0
  50. data/spec/chewy/type/mapping_spec.rb +54 -0
  51. data/spec/chewy/type/observe_spec.rb +55 -0
  52. data/spec/chewy/type/wrapper_spec.rb +35 -0
  53. data/spec/chewy/type_spec.rb +43 -0
  54. data/spec/chewy_spec.rb +36 -0
  55. data/spec/spec_helper.rb +48 -0
  56. data/spec/support/class_helpers.rb +16 -0
  57. data/spec/support/fail_helpers.rb +13 -0
  58. 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
@@ -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
@@ -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