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.
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