chewy 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -3
- data/CHANGELOG.md +43 -1
- data/Gemfile +3 -0
- data/README.md +49 -11
- data/chewy.gemspec +1 -2
- data/gemfiles/Gemfile.rails-3.2 +1 -0
- data/gemfiles/Gemfile.rails-4.0 +1 -0
- data/lib/chewy.rb +8 -2
- data/lib/chewy/backports/deep_dup.rb +46 -0
- data/lib/chewy/backports/duplicable.rb +90 -0
- data/lib/chewy/config.rb +33 -6
- data/lib/chewy/errors.rb +1 -1
- data/lib/chewy/fields/base.rb +19 -7
- data/lib/chewy/fields/root.rb +13 -0
- data/lib/chewy/index/actions.rb +14 -6
- data/lib/chewy/index/search.rb +3 -2
- data/lib/chewy/query.rb +131 -17
- data/lib/chewy/query/compose.rb +27 -17
- data/lib/chewy/query/criteria.rb +34 -22
- data/lib/chewy/query/loading.rb +94 -10
- data/lib/chewy/query/nodes/exists.rb +1 -1
- data/lib/chewy/query/nodes/has_relation.rb +1 -1
- data/lib/chewy/query/nodes/missing.rb +1 -1
- data/lib/chewy/query/pagination.rb +8 -38
- data/lib/chewy/query/pagination/kaminari.rb +37 -0
- data/lib/chewy/runtime.rb +9 -0
- data/lib/chewy/runtime/version.rb +25 -0
- data/lib/chewy/type/adapter/active_record.rb +21 -7
- data/lib/chewy/type/adapter/base.rb +1 -1
- data/lib/chewy/type/adapter/object.rb +9 -6
- data/lib/chewy/type/import.rb +7 -4
- data/lib/chewy/type/mapping.rb +9 -9
- data/lib/chewy/type/wrapper.rb +1 -1
- data/lib/chewy/version.rb +1 -1
- data/lib/tasks/chewy.rake +40 -21
- data/spec/chewy/config_spec.rb +1 -1
- data/spec/chewy/fields/base_spec.rb +273 -8
- data/spec/chewy/index/actions_spec.rb +1 -2
- data/spec/chewy/index/aliases_spec.rb +0 -1
- data/spec/chewy/index/search_spec.rb +0 -8
- data/spec/chewy/index/settings_spec.rb +0 -2
- data/spec/chewy/index_spec.rb +0 -2
- data/spec/chewy/query/criteria_spec.rb +85 -18
- data/spec/chewy/query/loading_spec.rb +26 -9
- data/spec/chewy/query/nodes/and_spec.rb +2 -2
- data/spec/chewy/query/nodes/exists_spec.rb +6 -6
- data/spec/chewy/query/nodes/missing_spec.rb +4 -4
- data/spec/chewy/query/nodes/or_spec.rb +2 -2
- data/spec/chewy/query/pagination/kaminari_spec.rb +55 -0
- data/spec/chewy/query/pagination_spec.rb +15 -22
- data/spec/chewy/query_spec.rb +121 -52
- data/spec/chewy/rspec/update_index_spec.rb +0 -1
- data/spec/chewy/runtime/version_spec.rb +48 -0
- data/spec/chewy/runtime_spec.rb +9 -0
- data/spec/chewy/type/adapter/active_record_spec.rb +52 -0
- data/spec/chewy/type/adapter/object_spec.rb +33 -0
- data/spec/chewy/type/import_spec.rb +1 -3
- data/spec/chewy/type/mapping_spec.rb +4 -6
- data/spec/chewy/type/observe_spec.rb +0 -2
- data/spec/chewy/type/wrapper_spec.rb +0 -2
- data/spec/chewy/type_spec.rb +26 -5
- data/spec/chewy_spec.rb +0 -2
- data/spec/spec_helper.rb +2 -2
- metadata +15 -21
- data/lib/chewy/fields/default.rb +0 -10
- data/spec/chewy/fields/default_spec.rb +0 -6
data/lib/chewy/query/loading.rb
CHANGED
@@ -3,24 +3,108 @@ module Chewy
|
|
3
3
|
module Loading
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
+
# Lazily loads actual ORM/ODM objects for search result.
|
7
|
+
# Returns scope marked to return loaded objects array instead of
|
8
|
+
# chewy wrappers. In case when object can not be loaded because it
|
9
|
+
# was deleted or don't satisfy given scope or options - the
|
10
|
+
# result collection will contain nil value in the place of this
|
11
|
+
# object. Use `compact` method to avoid this if necessary.
|
12
|
+
#
|
13
|
+
# UsersIndex.query(...).load #=> [#<User id: 42...>, ...]
|
14
|
+
# UsersIndex.query(...).load.filter(...) #=> [#<User id: 42...>, ...]
|
15
|
+
#
|
16
|
+
# Possible options:
|
17
|
+
#
|
18
|
+
# <tt>:scope</tt> - used to give a scope for _every_ loaded type.
|
19
|
+
#
|
20
|
+
# PlacesIndex.query(...).load(scope: ->{ includes(:testimonials) })
|
21
|
+
#
|
22
|
+
# If places here contain cities and countries then preload will be
|
23
|
+
# done like this:
|
24
|
+
#
|
25
|
+
# City.where(id: [...]).includes(:testimonials)
|
26
|
+
# Country.where(id: [...]).includes(:testimonials)
|
27
|
+
#
|
28
|
+
# It is also possible to pass own scope for every loaded type:
|
29
|
+
#
|
30
|
+
# PlacesIndex.query(...).load(
|
31
|
+
# city: { scope: ->{ includes(:testimonials, :country) }}
|
32
|
+
# country: { scope: ->{ includes(:testimonials, :cities) }}
|
33
|
+
# )
|
34
|
+
#
|
35
|
+
# And loading will be performed as:
|
36
|
+
#
|
37
|
+
# City.where(id: [...]).includes(:testimonials, :country)
|
38
|
+
# Country.where(id: [...]).includes(:testimonials, :cities)
|
39
|
+
#
|
40
|
+
# In case of ActiveRecord objects loading the same result
|
41
|
+
# will be reached using ActiveRecord scopes instead of
|
42
|
+
# lambdas. But it works only with per-type scopes,
|
43
|
+
# and doesn't work with the common scope.
|
44
|
+
#
|
45
|
+
# PlacesIndex.query(...).load(
|
46
|
+
# city: { scope: City.includes(:testimonials, :country) }
|
47
|
+
# country: { scope: Country.includes(:testimonials, :cities) }
|
48
|
+
# )
|
49
|
+
#
|
50
|
+
# <tt>:only</tt> - loads objects for the specified types
|
51
|
+
#
|
52
|
+
# PlacesIndex.query(...).load(only: :city)
|
53
|
+
# PlacesIndex.query(...).load(only: [:city])
|
54
|
+
# PlacesIndex.query(...).load(only: [:city, :country])
|
55
|
+
#
|
56
|
+
# <tt>:except</tt> - doesn't load listed types
|
57
|
+
#
|
58
|
+
# PlacesIndex.query(...).load(except: :city)
|
59
|
+
# PlacesIndex.query(...).load(except: [:city])
|
60
|
+
# PlacesIndex.query(...).load(except: [:city, :country])
|
61
|
+
#
|
6
62
|
def load(options = {})
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
63
|
+
chain { criteria.update_options preload: options, loaded_objects: true }
|
64
|
+
end
|
65
|
+
|
66
|
+
# This methods is just convenient way to preload some ORM/ODM
|
67
|
+
# objects and continue to work with Chewy wrappers. Returns
|
68
|
+
# Chewy query scope. Note that `load` method performs ES request
|
69
|
+
# so preload method should also be the last in scope methods chain.
|
70
|
+
# Takes the same options as the `load` method
|
71
|
+
#
|
72
|
+
# PlacesIndex.query(...).preload(only: :city)
|
73
|
+
#
|
74
|
+
# Loaded objects are also attached to corresponding Chewy
|
75
|
+
# type wrapper objects and available with `_object` accessor.
|
76
|
+
#
|
77
|
+
# scope = PlacesIndex.query(...)
|
78
|
+
# preload_scope = scope.preload
|
79
|
+
# preload_scope.first #=> PlacesIndex::City wrapper instance
|
80
|
+
# preload_scope.first._object #=> City model instance
|
81
|
+
# scope.load == preload_scope.map(&:_object) #=> true
|
82
|
+
#
|
83
|
+
def preload(options = {})
|
84
|
+
chain { criteria.update_options preload: options, loaded_objects: false }
|
13
85
|
end
|
14
86
|
|
15
87
|
private
|
16
88
|
|
17
|
-
def _load_objects
|
89
|
+
def _load_objects!
|
90
|
+
options = criteria.options[:preload]
|
91
|
+
only = Array.wrap(options[:only]).map(&:to_s)
|
92
|
+
except = Array.wrap(options[:except]).map(&:to_s)
|
93
|
+
|
18
94
|
loaded_objects = Hash[_results.group_by(&:class).map do |type, objects|
|
95
|
+
next if except.include?(type.type_name)
|
96
|
+
next if only.any? && !only.include?(type.type_name)
|
97
|
+
|
19
98
|
loaded = type.adapter.load(objects, options.merge(_type: type))
|
20
|
-
[type, loaded.index_by.with_index
|
21
|
-
|
99
|
+
[type, loaded.index_by.with_index do |loaded, i|
|
100
|
+
objects[i]._object = loaded
|
101
|
+
objects[i]
|
102
|
+
end]
|
103
|
+
end.compact]
|
22
104
|
|
23
|
-
_results.map
|
105
|
+
_results.map do |result|
|
106
|
+
loaded_objects[result.class][result] if loaded_objects[result.class]
|
107
|
+
end
|
24
108
|
end
|
25
109
|
end
|
26
110
|
end
|
@@ -1,45 +1,15 @@
|
|
1
1
|
module Chewy
|
2
2
|
class Query
|
3
3
|
module Pagination
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
module Kaminari
|
11
|
-
extend ActiveSupport::Concern
|
12
|
-
|
13
|
-
included do
|
14
|
-
include ::Kaminari::PageScopeMethods
|
15
|
-
|
16
|
-
delegate :default_per_page, :max_per_page, :max_pages, to: :_kaminari_config
|
17
|
-
|
18
|
-
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
19
|
-
def #{::Kaminari.config.page_method_name}(num = 1)
|
20
|
-
limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1))
|
21
|
-
end
|
22
|
-
METHOD
|
23
|
-
end
|
24
|
-
|
25
|
-
def total_count
|
26
|
-
_response['hits']['total']
|
27
|
-
end
|
28
|
-
|
29
|
-
def limit_value
|
30
|
-
(criteria.options[:size].presence || default_per_page).to_i
|
31
|
-
end
|
32
|
-
|
33
|
-
def offset_value
|
34
|
-
criteria.options[:from].to_i
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def _kaminari_config
|
40
|
-
::Kaminari.config
|
41
|
-
end
|
4
|
+
# Returns request total found documents count
|
5
|
+
#
|
6
|
+
# PlacesIndex.query(...).filter(...).total_count
|
7
|
+
#
|
8
|
+
def total_count
|
9
|
+
_response['hits'].try(:[], 'total') || 0
|
42
10
|
end
|
43
11
|
end
|
44
12
|
end
|
45
13
|
end
|
14
|
+
|
15
|
+
require 'chewy/query/pagination/kaminari' if defined?(::Kaminari)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Chewy
|
2
|
+
class Query
|
3
|
+
module Pagination
|
4
|
+
module Kaminari
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include ::Kaminari::PageScopeMethods
|
9
|
+
|
10
|
+
delegate :default_per_page, :max_per_page, :max_pages, to: :_kaminari_config
|
11
|
+
|
12
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
13
|
+
def #{::Kaminari.config.page_method_name}(num = 1)
|
14
|
+
limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1))
|
15
|
+
end
|
16
|
+
METHOD
|
17
|
+
end
|
18
|
+
|
19
|
+
def limit_value
|
20
|
+
(criteria.request_options[:size].presence || default_per_page).to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
def offset_value
|
24
|
+
criteria.request_options[:from].to_i
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def _kaminari_config
|
30
|
+
::Kaminari.config
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Chewy::Query::Pagination.send :include, Chewy::Query::Pagination::Kaminari
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Chewy
|
2
|
+
module Runtime
|
3
|
+
class Version
|
4
|
+
include Comparable
|
5
|
+
attr_reader :major, :minor, :patch
|
6
|
+
|
7
|
+
def initialize version
|
8
|
+
@major, @minor, @patch = *(version.to_s.split('.', 3) + [0]*3).first(3).map(&:to_i)
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
[major, minor, patch].join('.')
|
13
|
+
end
|
14
|
+
|
15
|
+
def <=> other
|
16
|
+
other = self.class.new(other) unless other.is_a?(self.class)
|
17
|
+
[
|
18
|
+
major <=> other.major,
|
19
|
+
minor <=> other.minor,
|
20
|
+
patch <=> other.patch
|
21
|
+
].detect { |c| c != 0 } || 0
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -16,11 +16,7 @@ module Chewy
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def name
|
19
|
-
@name ||= options[:name].present? ? options[:name].to_s.camelize : model.model_name.to_s
|
20
|
-
end
|
21
|
-
|
22
|
-
def type_name
|
23
|
-
@type_name ||= (options[:name].presence || model.model_name).to_s.underscore
|
19
|
+
@name ||= (options[:name].present? ? options[:name].to_s.camelize : model.model_name.to_s).demodulize
|
24
20
|
end
|
25
21
|
|
26
22
|
# Import method fo ActiveRecord takes import data and import options
|
@@ -44,8 +40,24 @@ module Chewy
|
|
44
40
|
# users = User.all
|
45
41
|
# users.each { |user| user.destroy if user.incative? }
|
46
42
|
# UsersIndex::User.import users # inactive users will be deleted from index
|
43
|
+
# # or
|
47
44
|
# UsersIndex::User.import users.map(&:id) # deleted user ids will be deleted from index
|
48
45
|
#
|
46
|
+
# Also there is custom API method `delete_from_index?`. It it returns `true`
|
47
|
+
# object will be deleted from index. Note that if this method is defined and
|
48
|
+
# return `false` Chewy will still check `destroyed?` method. This is useful
|
49
|
+
# for paranoid objects sdeleting implementation.
|
50
|
+
#
|
51
|
+
# class User
|
52
|
+
# alias_method :delete_from_index?, :deleted_at?
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# users = User.all
|
56
|
+
# users.each { |user| user.deleted_at = Time.now }
|
57
|
+
# UsersIndex::User.import users # paranoid deleted users will be deleted from index
|
58
|
+
# # or
|
59
|
+
# UsersIndex::User.import users.map(&:id) # user ids will be deleted from index
|
60
|
+
#
|
49
61
|
def import *args, &block
|
50
62
|
import_options = args.extract_options!
|
51
63
|
import_options[:batch_size] ||= BATCH_SIZE
|
@@ -97,7 +109,7 @@ module Chewy
|
|
97
109
|
indexed = true
|
98
110
|
merged_scope(scoped_model(ids)).find_in_batches(import_options.slice(:batch_size)) do |objects|
|
99
111
|
ids -= objects.map(&:id)
|
100
|
-
indexed &= block.call
|
112
|
+
indexed &= block.call(grouped_objects(objects))
|
101
113
|
end
|
102
114
|
|
103
115
|
deleted = ids.in_groups_of(import_options[:batch_size], false).map do |group|
|
@@ -109,7 +121,9 @@ module Chewy
|
|
109
121
|
|
110
122
|
def grouped_objects(objects)
|
111
123
|
objects.group_by do |object|
|
112
|
-
object.
|
124
|
+
delete = object.delete_from_index? if object.respond_to?(:delete_from_index?)
|
125
|
+
delete ||= object.destroyed?
|
126
|
+
delete ? :delete : :index
|
113
127
|
end
|
114
128
|
end
|
115
129
|
|
@@ -10,11 +10,7 @@ module Chewy
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def name
|
13
|
-
@name ||= (options[:name] || target).to_s.camelize
|
14
|
-
end
|
15
|
-
|
16
|
-
def type_name
|
17
|
-
@type_name ||= (options[:name] || target).to_s.underscore
|
13
|
+
@name ||= (options[:name] || target).to_s.camelize.demodulize
|
18
14
|
end
|
19
15
|
|
20
16
|
# Imports passed data with options
|
@@ -27,6 +23,11 @@ module Chewy
|
|
27
23
|
#
|
28
24
|
# <tt>:batch_size</tt> - import batch size, 1000 objects by default
|
29
25
|
#
|
26
|
+
# If methods `delete_from_index?` or `destroyed?` are defined for object
|
27
|
+
# and any return true then object will be deleted from index. But to be
|
28
|
+
# destroyed objects need to respond to `id` method as well, so ElasticSearch
|
29
|
+
# could know which one to delete.
|
30
|
+
#
|
30
31
|
def import *args, &block
|
31
32
|
import_options = args.extract_options!
|
32
33
|
batch_size = import_options.delete(:batch_size) || BATCH_SIZE
|
@@ -35,7 +36,9 @@ module Chewy
|
|
35
36
|
objects.in_groups_of(batch_size, false).map do |group|
|
36
37
|
action_groups = group.group_by do |object|
|
37
38
|
raise "Object is not a `#{target}`" if class_target? && !object.is_a?(target)
|
38
|
-
object.
|
39
|
+
delete = object.delete_from_index? if object.respond_to?(:delete_from_index?)
|
40
|
+
delete ||= object.destroyed? if object.respond_to?(:destroyed?)
|
41
|
+
delete ? :delete : :index
|
39
42
|
end
|
40
43
|
block.call action_groups
|
41
44
|
end.all?
|
data/lib/chewy/type/import.rb
CHANGED
@@ -15,6 +15,8 @@ module Chewy
|
|
15
15
|
# UsersIndex::User.import suffix: Time.now.to_i # imports data to index with specified suffix if such is exists
|
16
16
|
# UsersIndex::User.import batch_size: 300 # import batch size
|
17
17
|
#
|
18
|
+
# See adapters documentation for more details.
|
19
|
+
#
|
18
20
|
def import *args
|
19
21
|
import_options = args.extract_options!
|
20
22
|
bulk_options = import_options.reject { |k, v| ![:refresh, :suffix].include?(k) }.reverse_merge!(refresh: true)
|
@@ -34,7 +36,7 @@ module Chewy
|
|
34
36
|
end
|
35
37
|
|
36
38
|
# Perform import operation for specified documents.
|
37
|
-
# Raises Chewy::
|
39
|
+
# Raises Chewy::ImportFailed exception in case of import errors.
|
38
40
|
#
|
39
41
|
# UsersIndex::User.import! # imports default data set
|
40
42
|
# UsersIndex::User.import! User.active # imports active users
|
@@ -44,6 +46,8 @@ module Chewy
|
|
44
46
|
# UsersIndex::User.import! suffix: Time.now.to_i # imports data to index with specified suffix if such is exists
|
45
47
|
# UsersIndex::User.import! batch_size: 300 # import batch size
|
46
48
|
#
|
49
|
+
# See adapters documentation for more details.
|
50
|
+
#
|
47
51
|
def import! *args
|
48
52
|
errors = nil
|
49
53
|
subscriber = ActiveSupport::Notifications.subscribe('import_objects.chewy') do |*args|
|
@@ -51,7 +55,7 @@ module Chewy
|
|
51
55
|
end
|
52
56
|
import *args
|
53
57
|
ActiveSupport::Notifications.unsubscribe(subscriber)
|
54
|
-
raise Chewy::
|
58
|
+
raise Chewy::ImportFailed.new(self, errors) if errors.present?
|
55
59
|
true
|
56
60
|
end
|
57
61
|
|
@@ -60,9 +64,8 @@ module Chewy
|
|
60
64
|
def bulk options = {}
|
61
65
|
suffix = options.delete(:suffix)
|
62
66
|
|
63
|
-
Chewy.wait_for_status
|
64
|
-
|
65
67
|
result = client.bulk options.merge(index: index.build_index_name(suffix: suffix), type: type_name)
|
68
|
+
Chewy.wait_for_status
|
66
69
|
|
67
70
|
extract_errors result
|
68
71
|
end
|
data/lib/chewy/type/mapping.rb
CHANGED
@@ -82,14 +82,14 @@ module Chewy
|
|
82
82
|
# will be an array of hashes, if `user.quiz` is not a collection association
|
83
83
|
# then just values hash will be put in the index.
|
84
84
|
#
|
85
|
-
# field :quiz
|
85
|
+
# field :quiz do
|
86
86
|
# field :question, :answer
|
87
87
|
# field :score, type: 'integer'
|
88
88
|
# end
|
89
89
|
#
|
90
90
|
# Nested fields are composed from nested objects:
|
91
91
|
#
|
92
|
-
# field :name,
|
92
|
+
# field :name, value: -> { name_translations } do
|
93
93
|
# field :ru, value: ->(name) { name['ru'] }
|
94
94
|
# field :en, value: ->(name) { name['en'] }
|
95
95
|
# end
|
@@ -99,12 +99,12 @@ module Chewy
|
|
99
99
|
#
|
100
100
|
# field :name, type: 'object', value: -> { name_translations }
|
101
101
|
#
|
102
|
-
# The special case is
|
103
|
-
#
|
102
|
+
# The special case is multi_field. If type options and block are
|
103
|
+
# both present field is treated as a multi-field. In that case field
|
104
|
+
# composition changes satisfy elasticsearch rules:
|
104
105
|
#
|
105
|
-
# field :full_name, type: '
|
106
|
-
# field :
|
107
|
-
# field :sorted, index: 'analyzed', analyzer: 'sorted'
|
106
|
+
# field :full_name, type: 'string', analyzer: 'name', value: ->{ full_name.try(:strip) } do
|
107
|
+
# field :sorted, analyzer: 'sorted'
|
108
108
|
# end
|
109
109
|
#
|
110
110
|
def field *args, &block
|
@@ -114,7 +114,7 @@ module Chewy
|
|
114
114
|
if args.size > 1
|
115
115
|
args.map { |name| field(name, options) }
|
116
116
|
else
|
117
|
-
expand_nested(Chewy::Fields::
|
117
|
+
expand_nested(Chewy::Fields::Base.new(args.first, options), &block)
|
118
118
|
end
|
119
119
|
end
|
120
120
|
|
@@ -135,7 +135,7 @@ module Chewy
|
|
135
135
|
# template 'title.*', mapping_hash # dot in template causes "path_match" using
|
136
136
|
# template /tit.+/, mapping_hash # using "match_pattern": "regexp"
|
137
137
|
# template /title\..+/, mapping_hash # "\." - escaped dot causes "path_match" using
|
138
|
-
# template /tit.+/, 'string' mapping_hash # "match_mapping_type" as the optionsl second argument
|
138
|
+
# template /tit.+/, 'string', mapping_hash # "match_mapping_type" as the optionsl second argument
|
139
139
|
# template template42: {match: 'hello*', mapping: {type: 'object'}} # or even pass a template as is
|
140
140
|
#
|
141
141
|
def template *args
|