chewy 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +35 -29
  4. data/Appraisals +37 -0
  5. data/CHANGELOG.md +115 -4
  6. data/Gemfile +2 -3
  7. data/README.md +135 -40
  8. data/chewy.gemspec +4 -3
  9. data/gemfiles/rails.3.2.activerecord.gemfile +13 -0
  10. data/gemfiles/rails.3.2.activerecord.kaminari.gemfile +14 -0
  11. data/gemfiles/rails.3.2.activerecord.will_paginate.gemfile +14 -0
  12. data/gemfiles/rails.4.0.activerecord.gemfile +13 -0
  13. data/gemfiles/rails.4.0.activerecord.kaminari.gemfile +14 -0
  14. data/gemfiles/rails.4.0.activerecord.will_paginate.gemfile +14 -0
  15. data/gemfiles/rails.4.0.mongoid.gemfile +13 -0
  16. data/gemfiles/rails.4.0.mongoid.kaminari.gemfile +14 -0
  17. data/gemfiles/rails.4.0.mongoid.will_paginate.gemfile +14 -0
  18. data/gemfiles/rails.4.1.activerecord.gemfile +13 -0
  19. data/gemfiles/rails.4.1.activerecord.kaminari.gemfile +14 -0
  20. data/gemfiles/rails.4.1.activerecord.will_paginate.gemfile +14 -0
  21. data/gemfiles/rails.4.1.mongoid.gemfile +13 -0
  22. data/gemfiles/rails.4.1.mongoid.kaminari.gemfile +14 -0
  23. data/gemfiles/rails.4.1.mongoid.will_paginate.gemfile +14 -0
  24. data/gemfiles/rails.4.2.activerecord.gemfile +13 -0
  25. data/gemfiles/rails.4.2.activerecord.kaminari.gemfile +14 -0
  26. data/gemfiles/rails.4.2.activerecord.will_paginate.gemfile +14 -0
  27. data/gemfiles/rails.4.2.mongoid.gemfile +13 -0
  28. data/gemfiles/rails.4.2.mongoid.kaminari.gemfile +14 -0
  29. data/gemfiles/rails.4.2.mongoid.will_paginate.gemfile +14 -0
  30. data/lib/chewy.rb +65 -0
  31. data/lib/chewy/config.rb +44 -93
  32. data/lib/chewy/errors.rb +14 -5
  33. data/lib/chewy/fields/base.rb +8 -7
  34. data/lib/chewy/fields/root.rb +2 -2
  35. data/lib/chewy/index.rb +7 -9
  36. data/lib/chewy/log_subscriber.rb +34 -0
  37. data/lib/chewy/query.rb +41 -27
  38. data/lib/chewy/query/criteria.rb +28 -23
  39. data/lib/chewy/query/scoping.rb +20 -0
  40. data/lib/chewy/railtie.rb +51 -13
  41. data/lib/chewy/repository.rb +61 -0
  42. data/lib/chewy/rspec/update_index.rb +3 -6
  43. data/lib/chewy/search.rb +28 -7
  44. data/lib/chewy/strategy.rb +60 -0
  45. data/lib/chewy/strategy/atomic.rb +31 -0
  46. data/lib/chewy/strategy/base.rb +27 -0
  47. data/lib/chewy/strategy/bypass.rb +15 -0
  48. data/lib/chewy/strategy/urgent.rb +17 -0
  49. data/lib/chewy/type.rb +19 -5
  50. data/lib/chewy/type/adapter/active_record.rb +28 -117
  51. data/lib/chewy/type/adapter/base.rb +35 -0
  52. data/lib/chewy/type/adapter/mongoid.rb +23 -123
  53. data/lib/chewy/type/adapter/object.rb +41 -19
  54. data/lib/chewy/type/adapter/orm.rb +142 -0
  55. data/lib/chewy/type/import.rb +43 -16
  56. data/lib/chewy/type/observe.rb +8 -21
  57. data/lib/chewy/version.rb +1 -1
  58. data/lib/tasks/chewy.rake +8 -4
  59. data/spec/chewy/config_spec.rb +20 -97
  60. data/spec/chewy/fields/base_spec.rb +24 -11
  61. data/spec/chewy/fields/time_fields_spec.rb +27 -0
  62. data/spec/chewy/index/settings_spec.rb +2 -1
  63. data/spec/chewy/index_spec.rb +98 -79
  64. data/spec/chewy/query/criteria_spec.rb +14 -0
  65. data/spec/chewy/query_spec.rb +1 -1
  66. data/spec/chewy/repository_spec.rb +50 -0
  67. data/spec/chewy/search_spec.rb +100 -0
  68. data/spec/chewy/strategy_spec.rb +109 -0
  69. data/spec/chewy/type/adapter/active_record_spec.rb +110 -46
  70. data/spec/chewy/type/adapter/mongoid_spec.rb +123 -74
  71. data/spec/chewy/type/adapter/object_spec.rb +51 -34
  72. data/spec/chewy/type/import_spec.rb +21 -21
  73. data/spec/chewy/type/observe_spec.rb +26 -29
  74. data/spec/chewy/type_spec.rb +19 -0
  75. data/spec/chewy_spec.rb +19 -3
  76. data/spec/spec_helper.rb +1 -1
  77. data/spec/support/active_record.rb +2 -1
  78. data/spec/support/mongoid.rb +29 -38
  79. metadata +85 -55
  80. data/gemfiles/Gemfile.rails-3.2.active_record +0 -6
  81. data/gemfiles/Gemfile.rails-3.2.active_record.kaminari +0 -7
  82. data/gemfiles/Gemfile.rails-3.2.active_record.will_paginate +0 -7
  83. data/gemfiles/Gemfile.rails-4.0.active_record +0 -6
  84. data/gemfiles/Gemfile.rails-4.0.active_record.kaminari +0 -7
  85. data/gemfiles/Gemfile.rails-4.0.active_record.will_paginate +0 -7
  86. data/gemfiles/Gemfile.rails-4.0.mongoid +0 -6
  87. data/gemfiles/Gemfile.rails-4.0.mongoid.kaminari +0 -7
  88. data/gemfiles/Gemfile.rails-4.0.mongoid.will_paginate +0 -7
  89. data/gemfiles/Gemfile.rails-4.1.active_record +0 -6
  90. data/gemfiles/Gemfile.rails-4.1.active_record.kaminari +0 -7
  91. data/gemfiles/Gemfile.rails-4.1.active_record.will_paginate +0 -7
  92. data/gemfiles/Gemfile.rails-4.1.mongoid +0 -6
  93. data/gemfiles/Gemfile.rails-4.1.mongoid.kaminari +0 -7
  94. data/gemfiles/Gemfile.rails-4.1.mongoid.will_paginate +0 -7
  95. data/gemfiles/Gemfile.rails-4.2.active_record +0 -6
  96. data/gemfiles/Gemfile.rails-4.2.active_record.kaminari +0 -7
  97. data/gemfiles/Gemfile.rails-4.2.active_record.will_paginate +0 -7
  98. data/gemfiles/Gemfile.rails-4.2.mongoid +0 -6
  99. data/gemfiles/Gemfile.rails-4.2.mongoid.kaminari +0 -7
  100. data/gemfiles/Gemfile.rails-4.2.mongoid.will_paginate +0 -7
  101. data/spec/chewy/index/search_spec.rb +0 -46
@@ -11,20 +11,29 @@ module Chewy
11
11
  class UnderivableType < Error
12
12
  end
13
13
 
14
+ class UndefinedUpdateStrategy < Error
15
+ def initialize type
16
+ super <<-MESSAGE
17
+ Index update strategy is undefined in current context.
18
+ Please wrap your code with `Chewy.strategy(:strategy_name) block.`
19
+ MESSAGE
20
+ end
21
+ end
22
+
14
23
  class DocumentNotFound < Error
15
24
  end
16
25
 
17
26
  class ImportFailed < Error
18
27
  def initialize type, errors
19
- output = "Import failed for `#{type}` with:\n"
28
+ message = "Import failed for `#{type}` with:\n"
20
29
  errors.each do |action, errors|
21
- output << " #{action.to_s.humanize} errors:\n"
30
+ message << " #{action.to_s.humanize} errors:\n"
22
31
  errors.each do |error, documents|
23
- output << " `#{error}`\n"
24
- output << " on #{documents.count} documents: #{documents}\n"
32
+ message << " `#{error}`\n"
33
+ message << " on #{documents.count} documents: #{documents}\n"
25
34
  end
26
35
  end
27
- super output
36
+ super message
28
37
  end
29
38
  end
30
39
  end
@@ -20,9 +20,10 @@ module Chewy
20
20
  false
21
21
  end
22
22
 
23
- def compose(object)
23
+ def compose(object, *parent_objects)
24
24
  result = if value && value.is_a?(Proc)
25
- value.arity == 0 ? object.instance_exec(&value) : value.call(object)
25
+ value.arity.zero? ? object.instance_exec(&value) :
26
+ value.call(object, *parent_objects.first(value.arity - 1))
26
27
  elsif object.is_a?(Hash)
27
28
  object[name] || object[name.to_s]
28
29
  else
@@ -30,12 +31,12 @@ module Chewy
30
31
  end
31
32
 
32
33
  result = if result.respond_to?(:to_ary)
33
- result.to_ary.map { |object| nested_compose(object) }
34
+ result.to_ary.map { |result| nested_compose(result, object, *parent_objects) }
34
35
  else
35
- nested_compose(result)
36
+ nested_compose(result, object, *parent_objects)
36
37
  end if nested.any? && !multi_field?
37
38
 
38
- {name => result.as_json}
39
+ {name => result.as_json(root: false)}
39
40
  end
40
41
 
41
42
  def nested(field = nil)
@@ -57,8 +58,8 @@ module Chewy
57
58
 
58
59
  private
59
60
 
60
- def nested_compose(value)
61
- nested.values.map { |field| field.compose(value) if value }.compact.inject(:merge)
61
+ def nested_compose(value, *parent_objects)
62
+ nested.values.map { |field| field.compose(value, *parent_objects) if value }.compact.inject(:merge)
62
63
  end
63
64
  end
64
65
  end
@@ -10,7 +10,7 @@ module Chewy
10
10
  @id = options.delete(:id) || options.delete(:_id)
11
11
  @parent = options.delete(:parent) || options.delete(:_parent)
12
12
  @parent_id = options.delete(:parent_id)
13
- options.reverse_merge!(value: ->(_){_})
13
+ options.reverse_merge!(value: ->(_) { _ })
14
14
  super(name, options)
15
15
  options.delete(:type)
16
16
  @dynamic_templates = []
@@ -66,7 +66,7 @@ module Chewy
66
66
  parent_id.arity == 0 ? object.instance_exec(&parent_id) : parent_id.call(object)
67
67
  end
68
68
  end
69
-
69
+
70
70
  def compose_id(object)
71
71
  if id
72
72
  id.arity == 0 ? object.instance_exec(&id) : id.call(object)
@@ -34,7 +34,7 @@ module Chewy
34
34
  else
35
35
  @index_name ||= begin
36
36
  build_index_name(
37
- name.gsub(/Index\Z/, '').demodulize.underscore,
37
+ name.sub(/Index\Z/, '').demodulize.underscore,
38
38
  prefix: Chewy.configuration[:prefix]
39
39
  ) if name
40
40
  end
@@ -137,6 +137,12 @@ module Chewy
137
137
  self._settings = Chewy::Index::Settings.new params
138
138
  end
139
139
 
140
+ # Returns list of public class methods defined in current index
141
+ #
142
+ def self.scopes
143
+ public_methods - Chewy::Index.public_methods - type_names.map(&:to_sym)
144
+ end
145
+
140
146
  private
141
147
 
142
148
  def self.build_index_name *args
@@ -156,13 +162,5 @@ module Chewy
156
162
  def self.index_params
157
163
  [settings_hash, mappings_hash].inject(:merge)
158
164
  end
159
-
160
- def self.search_index
161
- self
162
- end
163
-
164
- def self.search_type
165
- type_names
166
- end
167
165
  end
168
166
  end
@@ -0,0 +1,34 @@
1
+ module Chewy
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def logger
4
+ Chewy.logger
5
+ end
6
+
7
+ def import_objects(event)
8
+ render_action('Import', event) { |payload| payload[:import] }
9
+ end
10
+
11
+ def search_query(event)
12
+ render_action('Search', event) { |payload| payload[:request] }
13
+ end
14
+
15
+ def delete_query(event)
16
+ render_action('Delete by Query', event) { |payload| payload[:request] }
17
+ end
18
+
19
+ def render_action action, event, &block
20
+ payload = event.payload
21
+ description = block.call(payload)
22
+
23
+ if description.present?
24
+ subject = payload[:type].presence || payload[:index]
25
+ action = "#{subject} #{action} (#{event.duration.round(1)}ms)"
26
+ action = color(action, GREEN, true)
27
+
28
+ debug(" #{action} #{description}")
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ Chewy::LogSubscriber.attach_to :chewy
@@ -1,5 +1,6 @@
1
1
  require 'chewy/query/criteria'
2
2
  require 'chewy/query/filters'
3
+ require 'chewy/query/scoping'
3
4
  require 'chewy/query/loading'
4
5
  require 'chewy/query/pagination'
5
6
 
@@ -13,6 +14,7 @@ module Chewy
13
14
  #
14
15
  class Query
15
16
  include Enumerable
17
+ include Scoping
16
18
  include Loading
17
19
  include Pagination
18
20
 
@@ -29,13 +31,14 @@ module Chewy
29
31
  delegate :each, :count, :size, to: :_collection
30
32
  alias_method :to_ary, :to_a
31
33
 
32
- attr_reader :index, :options, :criteria
34
+ attr_reader :_indexes, :_types, :options, :criteria
33
35
 
34
- def initialize index, options = {}
35
- @index, @options = index, options
36
- @types = Array.wrap(options.delete(:types))
36
+ def initialize *indexes_or_types_and_options
37
+ @options = indexes_or_types_and_options.extract_options!
38
+ @_types = indexes_or_types_and_options.select { |klass| klass < Chewy::Type }
39
+ @_indexes = indexes_or_types_and_options.select { |klass| klass < Chewy::Index }
40
+ @_indexes |= @_types.map(&:index)
37
41
  @criteria = Criteria.new
38
- reset
39
42
  end
40
43
 
41
44
  # Comparation with other query or collection
@@ -347,21 +350,21 @@ module Chewy
347
350
  # added to the search request and combinded according to
348
351
  # <tt>boost_mode</tt> and <tt>score_mode</tt>
349
352
  #
350
- # UsersIndex.script_score("doc['boost'].value", filter: { term: {foo: :bar} })
353
+ # UsersIndex.script_score("doc['boost'].value", params: { modifier: 2 })
351
354
  # # => {body:
352
355
  # query: {
353
356
  # function_score: {
354
357
  # query: { ...},
355
358
  # functions: [{
356
359
  # script_score: {
357
- # script: "doc['boost'].value"
358
- # },
359
- # filter: { term: { foo: :bar } }
360
+ # script: "doc['boost'].value * modifier",
361
+ # params: { modifier: 2 }
362
+ # }
360
363
  # }
361
364
  # }]
362
365
  # } } }
363
366
  def script_score(script, options = {})
364
- scoring = options.merge(script_score: { script: script })
367
+ scoring = { script_score: { script: script }.merge(options) }
365
368
  chain { criteria.update_scores scoring }
366
369
  end
367
370
 
@@ -800,11 +803,7 @@ module Chewy
800
803
  # }}}}
801
804
  #
802
805
  def types *params
803
- if params.any?
804
- chain { criteria.update_types params }
805
- else
806
- @types
807
- end
806
+ chain { criteria.update_types params }
808
807
  end
809
808
 
810
809
  # Acts the same way as <tt>types</tt>, but cleans up previously set types
@@ -841,8 +840,11 @@ module Chewy
841
840
  #
842
841
  def delete_all
843
842
  request = chain { criteria.update_options simple: true }.send(:_request)
844
- ActiveSupport::Notifications.instrument 'delete_query.chewy', request: request, index: index do
845
- index.client.delete_by_query(request)
843
+ ActiveSupport::Notifications.instrument 'delete_query.chewy',
844
+ request: request, indexes: _indexes, types: _types,
845
+ index: _indexes.one? ? _indexes.first : _indexes,
846
+ type: _types.one? ? _types.first : _types do
847
+ Chewy.client.delete_by_query(request)
846
848
  end
847
849
  end
848
850
 
@@ -899,7 +901,7 @@ module Chewy
899
901
  private
900
902
 
901
903
  def chain &block
902
- clone.tap { |q| q.instance_eval(&block) }
904
+ clone.tap { |q| q.instance_exec(&block) }
903
905
  end
904
906
 
905
907
  def reset
@@ -907,17 +909,20 @@ module Chewy
907
909
  end
908
910
 
909
911
  def _request
910
- @_request ||= criteria.request_body.merge(index: index.index_name, type: types)
912
+ @_request ||= criteria.request_body.merge(index: _indexes.map(&:index_name), type: _types.map(&:type_name))
911
913
  end
912
914
 
913
915
  def _response
914
- @_response ||= ActiveSupport::Notifications.instrument 'search_query.chewy', request: _request, index: index do
915
- begin
916
- index.client.search(_request)
917
- rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
918
- raise e if e.message !~ /IndexMissingException/
919
- {}
920
- end
916
+ @_response ||= ActiveSupport::Notifications.instrument 'search_query.chewy',
917
+ request: _request, indexes: _indexes, types: _types,
918
+ index: _indexes.one? ? _indexes.first : _indexes,
919
+ type: _types.one? ? _types.first : _types do
920
+ begin
921
+ Chewy.client.search(_request)
922
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
923
+ raise e if e.message !~ /IndexMissingException/
924
+ {}
925
+ end
921
926
  end
922
927
  end
923
928
 
@@ -928,7 +933,7 @@ module Chewy
928
933
  .merge!(_score: hit['_score'])
929
934
  .merge!(_explanation: hit['_explanation'])
930
935
 
931
- wrapper = index.type_hash[hit['_type']].new attributes
936
+ wrapper = _derive_index(hit['_index']).type_hash[hit['_type']].new attributes
932
937
  wrapper._data = hit
933
938
  wrapper
934
939
  end
@@ -941,5 +946,14 @@ module Chewy
941
946
  _results.map(&:_object) : _results
942
947
  end
943
948
  end
949
+
950
+ def _derive_index index_name
951
+ (@derive_index ||= {})[index_name] ||= _indexes_hash[index_name] ||
952
+ _indexes_hash[_indexes_hash.keys.sort_by(&:length).reverse.detect { |name| index_name.starts_with?(name) }]
953
+ end
954
+
955
+ def _indexes_hash
956
+ @_indexes_hash ||= _indexes.index_by(&:index_name)
957
+ end
944
958
  end
945
959
  end
@@ -5,14 +5,14 @@ module Chewy
5
5
  class Criteria
6
6
  include Compose
7
7
  ARRAY_STORAGES = [:queries, :filters, :post_filters, :sort, :fields, :types, :scores]
8
- HASH_STORAGES = [:options, :request_options, :facets, :aggregations, :suggest]
8
+ HASH_STORAGES = [:options, :request_options, :facets, :aggregations, :suggest, :script_fields]
9
9
  STORAGES = ARRAY_STORAGES + HASH_STORAGES
10
10
 
11
11
  def initialize options = {}
12
12
  @options = options.merge(
13
13
  query_mode: Chewy.query_mode,
14
14
  filter_mode: Chewy.filter_mode,
15
- post_filter_mode: Chewy.filter_mode || Chewy.post_filter_mode
15
+ post_filter_mode: Chewy.post_filter_mode || Chewy.filter_mode
16
16
  )
17
17
  end
18
18
 
@@ -40,52 +40,56 @@ module Chewy
40
40
  !!options[:none]
41
41
  end
42
42
 
43
- def update_options(modifer)
44
- options.merge!(modifer)
43
+ def update_options(modifier)
44
+ options.merge!(modifier)
45
45
  end
46
46
 
47
- def update_request_options(modifer)
48
- request_options.merge!(modifer)
47
+ def update_request_options(modifier)
48
+ request_options.merge!(modifier)
49
49
  end
50
50
 
51
- def update_facets(modifer)
52
- facets.merge!(modifer)
51
+ def update_facets(modifier)
52
+ facets.merge!(modifier)
53
53
  end
54
54
 
55
- def update_scores(modifer)
56
- @scores = scores + Array.wrap(modifer).reject(&:blank?)
55
+ def update_scores(modifier)
56
+ @scores = scores + Array.wrap(modifier).reject(&:blank?)
57
57
  end
58
58
 
59
- def update_aggregations(modifer)
60
- aggregations.merge!(modifer)
59
+ def update_aggregations(modifier)
60
+ aggregations.merge!(modifier)
61
61
  end
62
62
 
63
63
  def update_suggest(modifier)
64
64
  suggest.merge!(modifier)
65
65
  end
66
66
 
67
+ def update_script_fields(modifier)
68
+ script_fields.merge!(modifier)
69
+ end
70
+
67
71
  [:filters, :queries, :post_filters].each do |storage|
68
72
  class_eval <<-RUBY
69
- def update_#{storage}(modifer)
70
- @#{storage} = #{storage} + Array.wrap(modifer).reject(&:blank?)
73
+ def update_#{storage}(modifier)
74
+ @#{storage} = #{storage} + Array.wrap(modifier).reject(&:blank?)
71
75
  end
72
76
  RUBY
73
77
  end
74
78
 
75
- def update_sort(modifer, options = {})
79
+ def update_sort(modifier, options = {})
76
80
  @sort = nil if options[:purge]
77
- modifer = Array.wrap(modifer).flatten.map do |element|
81
+ modifier = Array.wrap(modifier).flatten.map do |element|
78
82
  element.is_a?(Hash) ? element.map { |k, v| {k => v} } : element
79
83
  end.flatten
80
- @sort = sort + modifer
84
+ @sort = sort + modifier
81
85
  end
82
86
 
83
87
  %w(fields types).each do |storage|
84
- define_method "update_#{storage}" do |modifer, options = {}|
88
+ define_method "update_#{storage}" do |modifier, options = {}|
85
89
  variable = "@#{storage}"
86
90
  instance_variable_set(variable, nil) if options[:purge]
87
- modifer = send(storage) | Array.wrap(modifer).flatten.map(&:to_s).reject(&:blank?)
88
- instance_variable_set(variable, modifer)
91
+ modifier = send(storage) | Array.wrap(modifier).flatten.map(&:to_s).reject(&:blank?)
92
+ instance_variable_set(variable, modifier)
89
93
  end
90
94
  end
91
95
 
@@ -112,6 +116,7 @@ module Chewy
112
116
  body.merge!(suggest: suggest) if suggest?
113
117
  body.merge!(sort: sort) if sort?
114
118
  body.merge!(_source: fields) if fields?
119
+ body.merge!(script_fields: script_fields) if script_fields?
115
120
 
116
121
  body = _boost_query(body)
117
122
 
@@ -133,14 +138,14 @@ module Chewy
133
138
  end
134
139
 
135
140
  def _boost_query(body)
136
- scores? or return body
141
+ return body unless scores?
137
142
  query = body.delete :query
138
143
  filter = body.delete :filter
139
144
  if query && filter
140
145
  query = { filtered: { query: query, filter: filter } }
141
146
  filter = nil
142
147
  end
143
- score = { }
148
+ score = {}
144
149
  score[:functions] = scores
145
150
  score[:boost_mode] = options[:boost_mode] if options[:boost_mode]
146
151
  score[:score_mode] = options[:score_mode] if options[:score_mode]
@@ -165,7 +170,7 @@ module Chewy
165
170
  end
166
171
 
167
172
  def _request_types
168
- _filters_join(types.map { |type| {type: {value: type}} }, :or)
173
+ _filters_join(types.map { |type| { type: { value: type } } }, :or)
169
174
  end
170
175
 
171
176
  def _request_post_filter
@@ -0,0 +1,20 @@
1
+ module Chewy
2
+ class Query
3
+ module Scoping
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def scopes
8
+ Thread.current[:chewy_scopes] ||= []
9
+ end
10
+ end
11
+
12
+ def scoping
13
+ self.class.scopes.push(self)
14
+ yield
15
+ ensure
16
+ self.class.scopes.pop
17
+ end
18
+ end
19
+ end
20
+ end