chewy 0.8.4 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (179) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +34 -0
  3. data/.rubocop_todo.yml +44 -0
  4. data/.travis.yml +20 -60
  5. data/Appraisals +15 -40
  6. data/CHANGELOG.md +42 -0
  7. data/Gemfile +1 -0
  8. data/Guardfile +5 -5
  9. data/README.md +155 -6
  10. data/Rakefile +11 -1
  11. data/chewy.gemspec +5 -7
  12. data/gemfiles/rails.3.2.activerecord.gemfile +1 -0
  13. data/gemfiles/rails.3.2.activerecord.kaminari.gemfile +1 -0
  14. data/gemfiles/rails.3.2.activerecord.will_paginate.gemfile +1 -0
  15. data/gemfiles/rails.4.2.activerecord.gemfile +1 -0
  16. data/gemfiles/rails.4.2.activerecord.kaminari.gemfile +1 -0
  17. data/gemfiles/rails.4.2.activerecord.will_paginate.gemfile +1 -0
  18. data/gemfiles/{rails.4.2.mongoid.4.0.0.gemfile → rails.4.2.mongoid.4.0.gemfile} +2 -1
  19. data/gemfiles/{rails.4.2.mongoid.4.0.0.kaminari.gemfile → rails.4.2.mongoid.4.0.kaminari.gemfile} +2 -1
  20. data/gemfiles/{rails.4.2.mongoid.4.0.0.will_paginate.gemfile → rails.4.2.mongoid.4.0.will_paginate.gemfile} +2 -1
  21. data/gemfiles/{rails.4.2.mongoid.5.1.0.gemfile → rails.4.2.mongoid.5.1.gemfile} +2 -1
  22. data/gemfiles/{rails.4.2.mongoid.5.1.0.kaminari.gemfile → rails.4.2.mongoid.5.1.kaminari.gemfile} +2 -1
  23. data/gemfiles/{rails.4.2.mongoid.5.1.0.will_paginate.gemfile → rails.4.2.mongoid.5.1.will_paginate.gemfile} +2 -1
  24. data/gemfiles/{rails.4.0.activerecord.gemfile → rails.5.0.activerecord.gemfile} +4 -2
  25. data/gemfiles/{rails.4.0.mongoid.4.0.0.kaminari.gemfile → rails.5.0.activerecord.kaminari.gemfile} +4 -2
  26. data/gemfiles/{rails.4.0.mongoid.4.0.0.will_paginate.gemfile → rails.5.0.activerecord.will_paginate.gemfile} +4 -2
  27. data/gemfiles/{sequel.4.31.gemfile → sequel.4.38.gemfile} +3 -2
  28. data/lib/chewy.rb +24 -16
  29. data/lib/chewy/backports/deep_dup.rb +1 -1
  30. data/lib/chewy/backports/duplicable.rb +1 -0
  31. data/lib/chewy/config.rb +13 -7
  32. data/lib/chewy/errors.rb +4 -4
  33. data/lib/chewy/fields/base.rb +19 -14
  34. data/lib/chewy/fields/root.rb +11 -9
  35. data/lib/chewy/index.rb +38 -25
  36. data/lib/chewy/index/actions.rb +17 -17
  37. data/lib/chewy/index/settings.rb +3 -4
  38. data/lib/chewy/journal.rb +107 -0
  39. data/lib/chewy/journal/apply.rb +31 -0
  40. data/lib/chewy/journal/clean.rb +24 -0
  41. data/lib/chewy/journal/entry.rb +83 -0
  42. data/lib/chewy/journal/query.rb +87 -0
  43. data/lib/chewy/log_subscriber.rb +8 -8
  44. data/lib/chewy/minitest.rb +1 -0
  45. data/lib/chewy/minitest/helpers.rb +77 -0
  46. data/lib/chewy/minitest/search_index_receiver.rb +80 -0
  47. data/lib/chewy/query.rb +116 -60
  48. data/lib/chewy/query/compose.rb +5 -6
  49. data/lib/chewy/query/criteria.rb +26 -16
  50. data/lib/chewy/query/filters.rb +9 -9
  51. data/lib/chewy/query/loading.rb +2 -2
  52. data/lib/chewy/query/nodes/and.rb +3 -3
  53. data/lib/chewy/query/nodes/base.rb +1 -1
  54. data/lib/chewy/query/nodes/bool.rb +6 -4
  55. data/lib/chewy/query/nodes/equal.rb +6 -6
  56. data/lib/chewy/query/nodes/exists.rb +2 -2
  57. data/lib/chewy/query/nodes/expr.rb +2 -2
  58. data/lib/chewy/query/nodes/field.rb +35 -31
  59. data/lib/chewy/query/nodes/has_child.rb +1 -0
  60. data/lib/chewy/query/nodes/has_parent.rb +1 -0
  61. data/lib/chewy/query/nodes/has_relation.rb +11 -13
  62. data/lib/chewy/query/nodes/match_all.rb +1 -1
  63. data/lib/chewy/query/nodes/missing.rb +2 -2
  64. data/lib/chewy/query/nodes/not.rb +3 -3
  65. data/lib/chewy/query/nodes/or.rb +3 -3
  66. data/lib/chewy/query/nodes/prefix.rb +4 -3
  67. data/lib/chewy/query/nodes/query.rb +3 -3
  68. data/lib/chewy/query/nodes/range.rb +11 -11
  69. data/lib/chewy/query/nodes/raw.rb +1 -1
  70. data/lib/chewy/query/nodes/regexp.rb +15 -11
  71. data/lib/chewy/query/nodes/script.rb +6 -6
  72. data/lib/chewy/query/pagination/will_paginate.rb +2 -2
  73. data/lib/chewy/railtie.rb +3 -3
  74. data/lib/chewy/rake_helper.rb +51 -30
  75. data/lib/chewy/repository.rb +2 -2
  76. data/lib/chewy/rspec.rb +1 -1
  77. data/lib/chewy/rspec/update_index.rb +46 -47
  78. data/lib/chewy/runtime/version.rb +4 -4
  79. data/lib/chewy/search.rb +7 -5
  80. data/lib/chewy/strategy.rb +10 -8
  81. data/lib/chewy/strategy/atomic.rb +2 -2
  82. data/lib/chewy/strategy/base.rb +4 -4
  83. data/lib/chewy/strategy/bypass.rb +1 -2
  84. data/lib/chewy/strategy/sidekiq.rb +2 -0
  85. data/lib/chewy/strategy/urgent.rb +1 -1
  86. data/lib/chewy/type.rb +51 -45
  87. data/lib/chewy/type/adapter/active_record.rb +23 -12
  88. data/lib/chewy/type/adapter/base.rb +4 -4
  89. data/lib/chewy/type/adapter/mongoid.rb +6 -6
  90. data/lib/chewy/type/adapter/object.rb +15 -12
  91. data/lib/chewy/type/adapter/orm.rb +24 -15
  92. data/lib/chewy/type/adapter/sequel.rb +11 -7
  93. data/lib/chewy/type/crutch.rb +4 -3
  94. data/lib/chewy/type/import.rb +51 -32
  95. data/lib/chewy/type/mapping.rb +17 -17
  96. data/lib/chewy/type/observe.rb +9 -7
  97. data/lib/chewy/type/witchcraft.rb +62 -23
  98. data/lib/chewy/type/wrapper.rb +20 -14
  99. data/lib/chewy/version.rb +1 -1
  100. data/lib/generators/chewy/install_generator.rb +3 -3
  101. data/lib/tasks/chewy.rake +28 -23
  102. data/spec/chewy/config_spec.rb +33 -12
  103. data/spec/chewy/fields/base_spec.rb +143 -154
  104. data/spec/chewy/fields/root_spec.rb +22 -20
  105. data/spec/chewy/fields/time_fields_spec.rb +11 -9
  106. data/spec/chewy/index/actions_spec.rb +27 -4
  107. data/spec/chewy/index/aliases_spec.rb +2 -2
  108. data/spec/chewy/index/settings_spec.rb +72 -50
  109. data/spec/chewy/index_spec.rb +95 -43
  110. data/spec/chewy/journal/apply_spec.rb +120 -0
  111. data/spec/chewy/journal/entry_spec.rb +237 -0
  112. data/spec/chewy/journal_spec.rb +173 -0
  113. data/spec/chewy/minitest/helpers_spec.rb +90 -0
  114. data/spec/chewy/minitest/search_index_receiver_spec.rb +120 -0
  115. data/spec/chewy/query/criteria_spec.rb +504 -237
  116. data/spec/chewy/query/filters_spec.rb +94 -66
  117. data/spec/chewy/query/loading_spec.rb +76 -40
  118. data/spec/chewy/query/nodes/and_spec.rb +3 -7
  119. data/spec/chewy/query/nodes/bool_spec.rb +5 -13
  120. data/spec/chewy/query/nodes/equal_spec.rb +20 -20
  121. data/spec/chewy/query/nodes/exists_spec.rb +7 -7
  122. data/spec/chewy/query/nodes/has_child_spec.rb +42 -23
  123. data/spec/chewy/query/nodes/has_parent_spec.rb +42 -23
  124. data/spec/chewy/query/nodes/match_all_spec.rb +2 -2
  125. data/spec/chewy/query/nodes/missing_spec.rb +6 -5
  126. data/spec/chewy/query/nodes/not_spec.rb +3 -7
  127. data/spec/chewy/query/nodes/or_spec.rb +3 -7
  128. data/spec/chewy/query/nodes/prefix_spec.rb +6 -6
  129. data/spec/chewy/query/nodes/query_spec.rb +3 -3
  130. data/spec/chewy/query/nodes/range_spec.rb +19 -19
  131. data/spec/chewy/query/nodes/raw_spec.rb +2 -2
  132. data/spec/chewy/query/nodes/regexp_spec.rb +31 -19
  133. data/spec/chewy/query/nodes/script_spec.rb +5 -5
  134. data/spec/chewy/query/pagination/kaminari_spec.rb +2 -2
  135. data/spec/chewy/query/pagination/will_paginage_spec.rb +6 -7
  136. data/spec/chewy/query/pagination_spec.rb +2 -3
  137. data/spec/chewy/query_spec.rb +208 -145
  138. data/spec/chewy/repository_spec.rb +8 -8
  139. data/spec/chewy/rspec/update_index_spec.rb +180 -111
  140. data/spec/chewy/search_spec.rb +8 -8
  141. data/spec/chewy/strategy/active_job_spec.rb +2 -2
  142. data/spec/chewy/strategy/atomic_spec.rb +4 -1
  143. data/spec/chewy/strategy/resque_spec.rb +2 -2
  144. data/spec/chewy/strategy/sidekiq_spec.rb +2 -2
  145. data/spec/chewy/type/actions_spec.rb +1 -1
  146. data/spec/chewy/type/adapter/active_record_spec.rb +255 -149
  147. data/spec/chewy/type/adapter/mongoid_spec.rb +169 -108
  148. data/spec/chewy/type/adapter/object_spec.rb +56 -40
  149. data/spec/chewy/type/adapter/sequel_spec.rb +248 -163
  150. data/spec/chewy/type/import_spec.rb +78 -47
  151. data/spec/chewy/type/mapping_spec.rb +6 -6
  152. data/spec/chewy/type/observe_spec.rb +20 -14
  153. data/spec/chewy/type/witchcraft_spec.rb +89 -43
  154. data/spec/chewy/type_spec.rb +4 -3
  155. data/spec/chewy_spec.rb +10 -8
  156. data/spec/spec_helper.rb +3 -0
  157. data/spec/support/active_record.rb +1 -1
  158. data/spec/support/class_helpers.rb +10 -11
  159. data/spec/support/mongoid.rb +2 -2
  160. data/spec/support/sequel.rb +1 -1
  161. metadata +65 -35
  162. data/gemfiles/rails.4.0.activerecord.kaminari.gemfile +0 -14
  163. data/gemfiles/rails.4.0.activerecord.will_paginate.gemfile +0 -14
  164. data/gemfiles/rails.4.0.mongoid.4.0.0.gemfile +0 -15
  165. data/gemfiles/rails.4.0.mongoid.5.1.0.gemfile +0 -15
  166. data/gemfiles/rails.4.0.mongoid.5.1.0.kaminari.gemfile +0 -14
  167. data/gemfiles/rails.4.0.mongoid.5.1.0.will_paginate.gemfile +0 -14
  168. data/gemfiles/rails.4.1.activerecord.gemfile +0 -15
  169. data/gemfiles/rails.4.1.activerecord.kaminari.gemfile +0 -14
  170. data/gemfiles/rails.4.1.activerecord.will_paginate.gemfile +0 -14
  171. data/gemfiles/rails.4.1.mongoid.4.0.0.gemfile +0 -15
  172. data/gemfiles/rails.4.1.mongoid.4.0.0.kaminari.gemfile +0 -14
  173. data/gemfiles/rails.4.1.mongoid.4.0.0.will_paginate.gemfile +0 -14
  174. data/gemfiles/rails.4.1.mongoid.5.1.0.gemfile +0 -15
  175. data/gemfiles/rails.4.1.mongoid.5.1.0.kaminari.gemfile +0 -14
  176. data/gemfiles/rails.4.1.mongoid.5.1.0.will_paginate.gemfile +0 -14
  177. data/gemfiles/rails.5.0.0.beta3.activerecord.gemfile +0 -16
  178. data/gemfiles/rails.5.0.0.beta3.activerecord.kaminari.gemfile +0 -16
  179. data/gemfiles/rails.5.0.0.beta3.activerecord.will_paginate.gemfile +0 -15
@@ -0,0 +1,31 @@
1
+ module Chewy
2
+ class Journal
3
+ module Apply
4
+ # Applies all changes that were done since some moment
5
+ #
6
+ # @param time [Integer] timestamp from which changes will be applied
7
+ # @param options [Hash]
8
+ # @option options [Integer] :retries maximum number of attempts to make journal "empty". By default is set to 10
9
+ # @option options [Boolean] :once shows whether we should try until the journal is clean. If set to true, :retries is ignored
10
+ # @option options [Array<Chewy::Index>] :only filters the resulting set of records by index name
11
+ def since(time, options = {})
12
+ previous_entries = []
13
+ retries = options[:retries] || 10
14
+ stage = 0
15
+ while stage < retries
16
+ stage += 1
17
+ previous_entries.select { |entry| entry.created_at.to_i >= time }
18
+ entries = Entry.group(Entry.since(time, options[:only]))
19
+ Entry.subtract(entries, previous_entries)
20
+ break if entries.length.zero?
21
+ ActiveSupport::Notifications.instrument 'apply_journal.chewy', stage: stage
22
+ entries.each { |entry| entry.index.import(entry.object_ids, journal: false) }
23
+ break if options[:once]
24
+ time = Entry.recent_timestamp(entries)
25
+ previous_entries = entries
26
+ end
27
+ end
28
+ module_function :since
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ module Chewy
2
+ class Journal
3
+ module Clean
4
+ DELETE_BATCH_SIZE = 10_000
5
+
6
+ def until(time)
7
+ query = Query.new(time, :lte, nil, false).to_h
8
+ search_query = query.merge(fields: ['_id'], size: DELETE_BATCH_SIZE)
9
+ index_name = Journal.index_name
10
+
11
+ count = Chewy.client.count(index: index_name, body: query)['count']
12
+
13
+ (count.to_f / DELETE_BATCH_SIZE).ceil.times do
14
+ ids = Chewy.client.search(index: index_name, body: search_query)['hits']['hits'].map { |doc| doc['_id'] }
15
+ Chewy.client.bulk(body: ids.map { |id| { delete: { _index: index_name, _type: Journal.type_name, _id: id } } }, refresh: true)
16
+ end
17
+
18
+ Chewy.wait_for_status
19
+ count
20
+ end
21
+ module_function :until
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,83 @@
1
+ module Chewy
2
+ class Journal
3
+ # Describes a journal entry and provides necessary assisting methods
4
+ class Entry
5
+ ATTRIBUTES = %w(index_name type_name action object_ids created_at).freeze
6
+
7
+ attr_accessor(*ATTRIBUTES)
8
+
9
+ def initialize(attributes = {})
10
+ attributes.slice(*ATTRIBUTES).each do |attr, value|
11
+ public_send("#{attr}=", value)
12
+ end
13
+ end
14
+
15
+ # Loads all entries since some time
16
+ # @param time [Integer] a timestamp from which we load a journal
17
+ # @param indices [Array<Chewy::Index>] journal records related to these indices will be loaded only
18
+ def self.since(time, indices = [])
19
+ query = Query.new(time, :gte, indices).to_h
20
+ parameters = { index: Journal.index_name, type: Journal.type_name, body: query }
21
+ size = Chewy.client.search(search_type: 'count', **parameters)['hits']['total']
22
+ if size > 0
23
+ Chewy.client
24
+ .search(size: size, sort: 'created_at', **parameters)['hits']['hits']
25
+ .map { |r| new(r['_source']) }
26
+ else
27
+ []
28
+ end
29
+ end
30
+
31
+ # Groups a list of entries by full type name to decrease
32
+ # a number of calls to Elasticsearch during journal apply
33
+ # @param entries [Array<Chewy::Journal::Entry>]
34
+ def self.group(entries)
35
+ entries.group_by(&:full_type_name)
36
+ .map { |_, grouped_entries| grouped_entries.reduce(:merge) }
37
+ end
38
+
39
+ # Allows to filter one list of entries from another
40
+ # If any records with the same full type name are found then their object_ids will be subtracted
41
+ # @param from [Array<Chewy::Journal::Entry>] from which list we subtract another
42
+ # @param what [Array<Chewy::Journal::Entry>] what we subtract
43
+ def self.subtract(from, what)
44
+ return from if what.empty?
45
+ from.each do |from_entry|
46
+ what.each do |what_entry|
47
+ from_entry.object_ids -= what_entry.object_ids if from_entry == what_entry
48
+ end
49
+ end
50
+ from.delete_if(&:empty?)
51
+ end
52
+
53
+ # Get the most recent timestamp from a list of entries
54
+ # @param entries [Array<Chewy::Journal::Entry>]
55
+ def self.recent_timestamp(entries)
56
+ entries.map { |entry| entry.created_at.to_i }.max
57
+ end
58
+
59
+ def index
60
+ @index ||= Chewy.derive_type(full_type_name)
61
+ end
62
+
63
+ def full_type_name
64
+ "#{index_name}##{type_name}"
65
+ end
66
+
67
+ def merge(other)
68
+ return self if other.nil? || full_type_name != other.full_type_name
69
+ self.object_ids |= other.object_ids
70
+ self.created_at = [created_at, other.created_at].compact.max
71
+ self
72
+ end
73
+
74
+ def ==(other)
75
+ full_type_name == other.try(:full_type_name)
76
+ end
77
+
78
+ def empty?
79
+ !object_ids || object_ids.empty?
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,87 @@
1
+ module Chewy
2
+ class Journal
3
+ class Query
4
+ # @param time [Integer]
5
+ # @param comparator [Symbol, String] lt, lte, gt, gte
6
+ # @param indices [Array<Chewy::Index>] which indices should only be selected in the resulting set
7
+ # @param use_filter [Boolean] should we use filter or query
8
+ def initialize(time, comparator, indices, use_filter = true)
9
+ @time = time
10
+ @comparator = comparator
11
+ @indices = indices || []
12
+ @use_filter = use_filter
13
+ end
14
+
15
+ # @return [Hash] ElasicSearch query
16
+ def to_h
17
+ @query ||= { query: { filtered: filtered } }
18
+ end
19
+
20
+ private
21
+
22
+ def filtered
23
+ if @use_filter
24
+ using_filter_query
25
+ else
26
+ using_query_query
27
+ end
28
+ end
29
+
30
+ def using_filter_query
31
+ if @indices.any?
32
+ {
33
+ filter: {
34
+ bool: {
35
+ must: [
36
+ range_filter,
37
+ bool: {
38
+ should: @indices.collect { |i| index_filter(i) }
39
+ }
40
+ ]
41
+ }
42
+ }
43
+ }
44
+ else
45
+ {
46
+ filter: range_filter
47
+ }
48
+ end
49
+ end
50
+
51
+ def using_query_query
52
+ if @indices.any?
53
+ {
54
+ query: range_filter,
55
+ filter: {
56
+ bool: {
57
+ should: @indices.collect { |i| index_filter(i) }
58
+ }
59
+ }
60
+ }
61
+ else
62
+ {
63
+ query: range_filter
64
+ }
65
+ end
66
+ end
67
+
68
+ def range_filter
69
+ {
70
+ range: {
71
+ created_at: {
72
+ @comparator => @time.to_i
73
+ }
74
+ }
75
+ }
76
+ end
77
+
78
+ def index_filter(index)
79
+ {
80
+ term: {
81
+ index_name: index.derivable_index_name
82
+ }
83
+ }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -16,17 +16,17 @@ module Chewy
16
16
  render_action('Delete by Query', event) { |payload| payload[:request] }
17
17
  end
18
18
 
19
- def render_action action, event, &block
19
+ def render_action(action, event)
20
20
  payload = event.payload
21
- description = block.call(payload)
21
+ description = yield(payload)
22
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)
23
+ return if description.blank?
27
24
 
28
- debug(" #{action} #{description}")
29
- end
25
+ subject = payload[:type].presence || payload[:index]
26
+ action = "#{subject} #{action} (#{event.duration.round(1)}ms)"
27
+ action = color(action, GREEN, true)
28
+
29
+ debug(" #{action} #{description}")
30
30
  end
31
31
  end
32
32
  end
@@ -0,0 +1 @@
1
+ require 'chewy/minitest/helpers'
@@ -0,0 +1,77 @@
1
+ require_relative 'search_index_receiver'
2
+
3
+ module Chewy
4
+ module Minitest
5
+ module Helpers
6
+ extend ActiveSupport::Concern
7
+
8
+ # Assert that an index *changes* during a block.
9
+ # @param (Chewy::Type) index the index / type to watch, eg EntitiesIndex::Entity.
10
+ # @param (Symbol) strategy the Chewy strategy to use around the block. See Chewy docs.
11
+ # @param (boolean) assert the index changes
12
+ # @param (boolean) bypass_actual_index
13
+ # True to preempt the http call to Elastic, false otherwise.
14
+ # Should be set to true unless actually testing search functionality.
15
+ #
16
+ # @return (SearchIndexReceiver) for optional further assertions on the nature of the index changes.
17
+ def assert_indexes(index, strategy: :atomic, bypass_actual_index: true)
18
+ type = Chewy.derive_type index
19
+ receiver = SearchIndexReceiver.new
20
+
21
+ bulk_method = type.method :bulk
22
+ # Manually mocking #bulk because we need to properly capture `self`
23
+ bulk_mock = lambda do |*bulk_args|
24
+ receiver.catch bulk_args, self
25
+
26
+ bulk_method.call(*bulk_args) unless bypass_actual_index
27
+
28
+ {}
29
+ end
30
+
31
+ type.define_singleton_method :bulk, bulk_mock
32
+
33
+ Chewy.strategy(strategy) do
34
+ yield
35
+ end
36
+
37
+ type.define_singleton_method :bulk, bulk_method
38
+
39
+ assert_includes receiver.updated_indexes, index, "Expected #{index} to be updated but it wasn't"
40
+
41
+ receiver
42
+ end
43
+
44
+ # Run indexing for the database changes during the block provided.
45
+ # By default, indexing is run at the end of the block.
46
+ # @param (Symbol) strategy the Chewy index update strategy see Chewy docs.
47
+ def run_indexing(strategy: :atomic)
48
+ Chewy.strategy strategy do
49
+ yield
50
+ end
51
+ end
52
+
53
+ module ClassMethods
54
+ # Declare that all tests in this file require real indexing, always.
55
+ # In my completely unscientific experiments, this roughly doubled test runtime.
56
+ # Use with trepidation.
57
+ def index_everything!
58
+ setup do
59
+ Chewy.strategy :urgent
60
+ end
61
+
62
+ teardown do
63
+ Chewy.strategy.pop
64
+ end
65
+ end
66
+ end
67
+
68
+ included do
69
+ teardown do
70
+ # always destroy indexes between tests
71
+ # Prevent croll pollution of test cases due to indexing
72
+ Chewy.massacre
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,80 @@
1
+ # Test helper class to provide minitest hooks for Chewy::Index testing.
2
+ #
3
+ # @note Intended to be used in conjunction with a test helper which mocks over the #bulk
4
+ # method on a Chewy::Type class. (See SearchTestHelper)
5
+ #
6
+ # The class will capture the data from the *param on the Chewy::Type#bulk method and
7
+ # aggregate the data for test analysis.
8
+ class SearchIndexReceiver
9
+ def initialize
10
+ @mutations = {}
11
+ end
12
+
13
+ # @param bulk_params the bulk_params that should be sent to the Chewy::Type#bulk method.
14
+ # @param (Chewy::Type) type the Index::Type executing this query.
15
+ def catch(bulk_params, type)
16
+ Array.wrap(bulk_params).map { |y| y[:body] }.flatten.each do |update|
17
+ if update[:delete]
18
+ mutation_for(type).deletes << update[:delete][:_id]
19
+ elsif update[:index]
20
+ mutation_for(type).indexes << update[:index]
21
+ end
22
+ end
23
+ end
24
+
25
+ # @param index return only index requests to the specified Chewy::Type index.
26
+ # @return the index changes captured by the mock.
27
+ def indexes_for(index = nil)
28
+ if index
29
+ mutation_for(index).indexes
30
+ else
31
+ Hash[
32
+ @mutations.map { |a, b| [a, b.indexes] }
33
+ ]
34
+ end
35
+ end
36
+ alias_method :indexes, :indexes_for
37
+
38
+ # @param index return only delete requests to the specified Chewy::Type index.
39
+ # @return the index deletes captured by the mock.
40
+ def deletes_for(index = nil)
41
+ if index
42
+ mutation_for(index).deletes
43
+ else
44
+ Hash[
45
+ @mutations.map { |a, b| [a, b.deletes] }
46
+ ]
47
+ end
48
+ end
49
+ alias_method :deletes, :deletes_for
50
+
51
+ # Check to see if a given object has been indexed.
52
+ # @param (#id) obj the object to look for.
53
+ # @param Chewy::Type what type the object should be indexed as.
54
+ # @return bool if the object was indexed.
55
+ def indexed?(obj, type)
56
+ indexes_for(type).map { |i| i[:_id] }.include? obj.id
57
+ end
58
+
59
+ # Check to see if a given object has been deleted.
60
+ # @param (#id) obj the object to look for.
61
+ # @param Chewy::Type what type the object should have been deleted from.
62
+ # @return bool if the object was deleted.
63
+ def deleted?(obj, type)
64
+ deletes_for(type).include? obj.id
65
+ end
66
+
67
+ # @return a list of Chewy::Type indexes changed.
68
+ def updated_indexes
69
+ @mutations.keys
70
+ end
71
+
72
+ private
73
+
74
+ # Get the mutation object for a given type.
75
+ # @param (Chewy::Type) type the index type to fetch.
76
+ # @return (#indexes, #deletes) an object with a list of indexes and a list of deletes.
77
+ def mutation_for(type)
78
+ @mutations[type] ||= OpenStruct.new(indexes: [], deletes: [])
79
+ end
80
+ end
@@ -23,7 +23,7 @@ module Chewy
23
23
 
24
24
  attr_reader :_indexes, :_types, :options, :criteria
25
25
 
26
- def initialize *indexes_or_types_and_options
26
+ def initialize(*indexes_or_types_and_options)
27
27
  @options = indexes_or_types_and_options.extract_options!
28
28
  @_types = indexes_or_types_and_options.select { |klass| klass < Chewy::Type }
29
29
  @_indexes = indexes_or_types_and_options.select { |klass| klass < Chewy::Index }
@@ -39,12 +39,8 @@ module Chewy
39
39
  # UsersIndex.filter(term: {name: 'Johny'}) == UsersIndex.filter(term: {name: 'Johny'}).to_a # => true
40
40
  # UsersIndex.filter(term: {name: 'Johny'}) == UsersIndex.filter(term: {name: 'Winnie'}) # => false
41
41
  #
42
- def == other
43
- super || if other.is_a?(self.class)
44
- other.criteria == criteria
45
- else
46
- to_a == other
47
- end
42
+ def ==(other)
43
+ super || other.is_a?(self.class) ? other.criteria == criteria : other == to_a
48
44
  end
49
45
 
50
46
  # Adds <tt>explain</tt> parameter to search request.
@@ -59,7 +55,7 @@ module Chewy
59
55
  #
60
56
  # UsersIndex::User.filter(term: {name: 'Johny'}).explain.first._explanation # => {...}
61
57
  #
62
- def explain value = nil
58
+ def explain(value = nil)
63
59
  chain { criteria.update_request_options explain: (value.nil? ? true : value) }
64
60
  end
65
61
 
@@ -73,7 +69,7 @@ module Chewy
73
69
  # script: "doc['coordinates'].distanceInMiles(lat, lon)"
74
70
  # }
75
71
  # )
76
- def script_fields value
72
+ def script_fields(value)
77
73
  chain { criteria.update_script_fields(value) }
78
74
  end
79
75
 
@@ -143,7 +139,7 @@ module Chewy
143
139
  # Chewy.query_mode = :dis_max
144
140
  # Chewy.query_mode = '50%'
145
141
  #
146
- def query_mode value
142
+ def query_mode(value)
147
143
  chain { criteria.update_options query_mode: value }
148
144
  end
149
145
 
@@ -215,7 +211,7 @@ module Chewy
215
211
  # Chewy.filter_mode = :should
216
212
  # Chewy.filter_mode = '50%'
217
213
  #
218
- def filter_mode value
214
+ def filter_mode(value)
219
215
  chain { criteria.update_options filter_mode: value }
220
216
  end
221
217
 
@@ -227,7 +223,7 @@ module Chewy
227
223
  # UsersIndex.post_filter{ name == 'Johny' }.post_filter{ age <= 42 }.post_filter_mode(:should)
228
224
  # UsersIndex.post_filter{ name == 'Johny' }.post_filter{ age <= 42 }.post_filter_mode('50%')
229
225
  #
230
- def post_filter_mode value
226
+ def post_filter_mode(value)
231
227
  chain { criteria.update_options post_filter_mode: value }
232
228
  end
233
229
 
@@ -276,7 +272,7 @@ module Chewy
276
272
  # Use the timeout because it is important to your SLA, not because you want
277
273
  # to abort the execution of long running queries.
278
274
  #
279
- def timeout value
275
+ def timeout(value)
280
276
  chain { criteria.update_request_options timeout: value }
281
277
  end
282
278
 
@@ -289,8 +285,8 @@ module Chewy
289
285
  # size: 100
290
286
  # }}
291
287
  #
292
- def limit value
293
- chain { criteria.update_request_options size: Integer(value) }
288
+ def limit(value = nil, &block)
289
+ chain { criteria.update_request_options size: block || Integer(value) }
294
290
  end
295
291
 
296
292
  # Sets elasticsearch <tt>from</tt> search request param
@@ -301,15 +297,15 @@ module Chewy
301
297
  # from: 300
302
298
  # }}
303
299
  #
304
- def offset value
305
- chain { criteria.update_request_options from: Integer(value) }
300
+ def offset(value = nil, &block)
301
+ chain { criteria.update_request_options from: block || Integer(value) }
306
302
  end
307
303
 
308
304
  # Elasticsearch highlight query option support
309
305
  #
310
306
  # UsersIndex.query(...).highlight(fields: { ... })
311
307
  #
312
- def highlight value
308
+ def highlight(value)
313
309
  chain { criteria.update_request_options highlight: value }
314
310
  end
315
311
 
@@ -317,7 +313,7 @@ module Chewy
317
313
  #
318
314
  # UsersIndex.query(...).rescore(query: { ... })
319
315
  #
320
- def rescore value
316
+ def rescore(value)
321
317
  chain { criteria.update_request_options rescore: value }
322
318
  end
323
319
 
@@ -325,10 +321,18 @@ module Chewy
325
321
  #
326
322
  # UsersIndex.query(...).min_score(0.5)
327
323
  #
328
- def min_score value
324
+ def min_score(value)
329
325
  chain { criteria.update_request_options min_score: value }
330
326
  end
331
327
 
328
+ # Elasticsearch track_scores option support
329
+ #
330
+ # UsersIndex.query(...).track_scores(true)
331
+ #
332
+ def track_scores(value)
333
+ chain { criteria.update_request_options track_scores: value }
334
+ end
335
+
332
336
  # Adds facets section to the search request.
333
337
  # All the chained facets a merged and added to the
334
338
  # search request
@@ -342,7 +346,7 @@ module Chewy
342
346
  # If called parameterless - returns result facets from ES performing request.
343
347
  # Returns empty hash if no facets was requested or resulted.
344
348
  #
345
- def facets params = nil
349
+ def facets(params = nil)
346
350
  raise RemovedFeature, 'removed in elasticsearch 2.0' if Runtime.version >= '2.0'
347
351
  if params
348
352
  chain { criteria.update_facets params }
@@ -395,6 +399,28 @@ module Chewy
395
399
  chain { criteria.update_scores scoring }
396
400
  end
397
401
 
402
+ # Add a weight scoring function to the search. All scores are
403
+ # added to the search request and combinded according to
404
+ # <tt>boost_mode</tt> and <tt>score_mode</tt>
405
+ #
406
+ # This probably only makes sense if you specify a filter
407
+ # for the weight as well.
408
+ #
409
+ # UsersIndex.weight(23, filter: { term: { foo: :bar} })
410
+ # # => {body:
411
+ # query: {
412
+ # function_score: {
413
+ # query: { ...},
414
+ # functions: [{
415
+ # weight: 23,
416
+ # filter: { term: { foo: :bar } }
417
+ # }]
418
+ # } } }
419
+ def weight(factor, options = {})
420
+ scoring = options.merge(weight: factor.to_i)
421
+ chain { criteria.update_scores scoring }
422
+ end
423
+
398
424
  # Adds a random score to the search request. All scores are
399
425
  # added to the search request and combinded according to
400
426
  # <tt>boost_mode</tt> and <tt>score_mode</tt>
@@ -484,11 +510,20 @@ module Chewy
484
510
  def decay(function, field, options = {})
485
511
  field_options = options.extract!(:origin, :scale, :offset, :decay).delete_if { |_, v| v.nil? }
486
512
  scoring = options.merge(function => {
487
- field => field_options
488
- })
513
+ field => field_options
514
+ })
489
515
  chain { criteria.update_scores scoring }
490
516
  end
491
517
 
518
+ # Sets <tt>preference</tt> for request.
519
+ # For instance, one can use <tt>preference=_primary</tt> to execute only on the primary shards.
520
+ #
521
+ # scope = UsersIndex.preference(:_primary)
522
+ #
523
+ def preference(value)
524
+ chain { criteria.update_search_options preference: value }
525
+ end
526
+
492
527
  # Sets elasticsearch <tt>aggregations</tt> search request param
493
528
  #
494
529
  # UsersIndex.filter{ name == 'Johny' }.aggregations(category_id: {terms: {field: 'category_ids'}})
@@ -501,7 +536,7 @@ module Chewy
501
536
  # }
502
537
  # }}
503
538
  #
504
- def aggregations params = nil
539
+ def aggregations(params = nil)
505
540
  @_named_aggs ||= _build_named_aggs
506
541
  @_fully_qualified_named_aggs ||= _build_fqn_aggs
507
542
  if params
@@ -512,7 +547,7 @@ module Chewy
512
547
  _response['aggregations'] || {}
513
548
  end
514
549
  end
515
- alias :aggs :aggregations
550
+ alias_method :aggs, :aggregations
516
551
 
517
552
  # In this simplest of implementations each named aggregation must be uniquely named
518
553
  def _build_named_aggs
@@ -562,7 +597,7 @@ module Chewy
562
597
  # }
563
598
  # }}
564
599
  #
565
- def suggest params = nil
600
+ def suggest(params = nil)
566
601
  if params
567
602
  chain { criteria.update_suggest params }
568
603
  else
@@ -595,7 +630,7 @@ module Chewy
595
630
  # } }
596
631
  # }}
597
632
  #
598
- def strategy value = nil
633
+ def strategy(value = nil)
599
634
  chain { criteria.update_options strategy: value }
600
635
  end
601
636
 
@@ -621,7 +656,7 @@ module Chewy
621
656
  # query: {text: {name: 'Johny'}}
622
657
  # }}
623
658
  #
624
- def query params
659
+ def query(params)
625
660
  chain { criteria.update_queries params }
626
661
  end
627
662
 
@@ -653,7 +688,7 @@ module Chewy
653
688
  # filter: {term: {name: 'Johny'}}
654
689
  # }}}}
655
690
  #
656
- def filter params = nil, &block
691
+ def filter(params = nil, &block)
657
692
  params = Filters.new(&block).__render__ if block
658
693
  chain { criteria.update_filters params }
659
694
  end
@@ -684,7 +719,7 @@ module Chewy
684
719
  # post_filter: {term: {name: 'Johny'}}
685
720
  # }}
686
721
  #
687
- def post_filter params = nil, &block
722
+ def post_filter(params = nil, &block)
688
723
  params = Filters.new(&block).__render__ if block
689
724
  chain { criteria.update_post_filters params }
690
725
  end
@@ -722,7 +757,7 @@ module Chewy
722
757
  #
723
758
  # Default value for <tt>:boost_mode</tt> might be changed
724
759
  # with <tt>Chewy.score_mode</tt> config option.
725
- def boost_mode value
760
+ def boost_mode(value)
726
761
  chain { criteria.update_options boost_mode: value }
727
762
  end
728
763
 
@@ -762,7 +797,7 @@ module Chewy
762
797
  #
763
798
  # Chewy.score_mode = :first
764
799
  #
765
- def score_mode value
800
+ def score_mode(value)
766
801
  chain { criteria.update_options score_mode: value }
767
802
  end
768
803
 
@@ -774,7 +809,7 @@ module Chewy
774
809
  # sort: ['first_name', 'last_name', {age: 'desc'}, {price: {order: 'asc', mode: 'avg'}}]
775
810
  # }}
776
811
  #
777
- def order *params
812
+ def order(*params)
778
813
  chain { criteria.update_sort params }
779
814
  end
780
815
 
@@ -786,7 +821,7 @@ module Chewy
786
821
  # sort: [{price: {order: 'asc', mode: 'avg'}}]
787
822
  # }}
788
823
  #
789
- def reorder *params
824
+ def reorder(*params)
790
825
  chain { criteria.update_sort params, purge: true }
791
826
  end
792
827
 
@@ -798,7 +833,7 @@ module Chewy
798
833
  # fields: ['first_name', 'last_name', 'age']
799
834
  # }}
800
835
  #
801
- def only *params
836
+ def only(*params)
802
837
  chain { criteria.update_fields params }
803
838
  end
804
839
 
@@ -810,7 +845,7 @@ module Chewy
810
845
  # fields: ['age']
811
846
  # }}
812
847
  #
813
- def only! *params
848
+ def only!(*params)
814
849
  chain { criteria.update_fields params, purge: true }
815
850
  end
816
851
 
@@ -846,7 +881,7 @@ module Chewy
846
881
  # ]}
847
882
  # }}}}
848
883
  #
849
- def types *params
884
+ def types(*params)
850
885
  chain { criteria.update_types params }
851
886
  end
852
887
 
@@ -858,7 +893,7 @@ module Chewy
858
893
  # filter: {type: {value: 'manager'}}
859
894
  # }}}}
860
895
  #
861
- def types! *params
896
+ def types!(*params)
862
897
  chain { criteria.update_types params, purge: true }
863
898
  end
864
899
 
@@ -872,8 +907,8 @@ module Chewy
872
907
  # scope = UsersIndex.aggs(max_age: { max: { field: 'age' } }).search_type(:count)
873
908
  # max_age = scope.aggs['max_age']['value']
874
909
  #
875
- def search_type val
876
- chain { options.merge!(search_type: val) }
910
+ def search_type(value)
911
+ chain { criteria.update_search_options search_type: value }
877
912
  end
878
913
 
879
914
  # Merges two queries.
@@ -885,7 +920,7 @@ module Chewy
885
920
  #
886
921
  # scope1.merge(scope2) == scope3 # => true
887
922
  #
888
- def merge other
923
+ def merge(other)
889
924
  chain { criteria.merge!(other.criteria) }
890
925
  end
891
926
 
@@ -898,15 +933,15 @@ module Chewy
898
933
  #
899
934
  def delete_all
900
935
  if Runtime.version > '2.0'
901
- plugins = Chewy.client.nodes.info(plugins: true)["nodes"].values.map { |item| item["plugins"] }.flatten
902
- raise PluginMissing, "install delete-by-query plugin" unless plugins.find { |item| item["name"] == 'delete-by-query' }
936
+ plugins = Chewy.client.nodes.info(plugins: true)['nodes'].values.map { |item| item['plugins'] }.flatten
937
+ raise PluginMissing, 'install delete-by-query plugin' unless plugins.find { |item| item['name'] == 'delete-by-query' }
903
938
  end
904
939
  request = chain { criteria.update_options simple: true }.send(:_request)
905
940
  ActiveSupport::Notifications.instrument 'delete_query.chewy',
906
941
  request: request, indexes: _indexes, types: _types,
907
942
  index: _indexes.one? ? _indexes.first : _indexes,
908
943
  type: _types.one? ? _types.first : _types do
909
- Chewy.client.delete_by_query(request)
944
+ Chewy.client.delete_by_query(request)
910
945
  end
911
946
  end
912
947
 
@@ -924,13 +959,31 @@ module Chewy
924
959
  # UsersIndex::User.find([8, 13]) # array of objects with ids in [8, 13]
925
960
  # UsersIndex::User.find([42]) # array of the object with id == 42
926
961
  #
927
- def find *ids
962
+ def find(*ids)
928
963
  results = chain { criteria.update_options simple: true }.filter { _id == ids.flatten }.to_a
929
964
 
930
- raise Chewy::DocumentNotFound.new("Could not find documents for ids #{ids.flatten}") if results.empty?
965
+ raise Chewy::DocumentNotFound, "Could not find documents for ids #{ids.flatten}" if results.empty?
931
966
  ids.one? && !ids.first.is_a?(Array) ? results.first : results
932
967
  end
933
968
 
969
+ # Returns true if there are at least one document that matches the query
970
+ #
971
+ # PlacesIndex.query(...).filter(...).exists?
972
+ #
973
+ def exists?
974
+ search_type(:count).total > 0
975
+ end
976
+
977
+ # Sets limit to be equal to total documents count
978
+ #
979
+ # PlacesIndex.query(...).filter(...).unlimited
980
+ #
981
+
982
+ def unlimited
983
+ count_query = search_type(:count)
984
+ offset(0).limit { count_query.total }
985
+ end
986
+
934
987
  # Returns request total time elapsed as reported by elasticsearch
935
988
  #
936
989
  # UsersIndex.query(...).filter(...).took
@@ -955,14 +1008,14 @@ module Chewy
955
1008
 
956
1009
  protected
957
1010
 
958
- def initialize_clone other
1011
+ def initialize_clone(other)
959
1012
  @criteria = other.criteria.clone
960
1013
  reset
961
1014
  end
962
1015
 
963
1016
  private
964
1017
 
965
- def chain &block
1018
+ def chain(&block)
966
1019
  clone.tap { |q| q.instance_exec(&block) }
967
1020
  end
968
1021
 
@@ -973,8 +1026,8 @@ module Chewy
973
1026
  def _request
974
1027
  @_request ||= begin
975
1028
  request = criteria.request_body
976
- request.merge!(index: _indexes.map(&:index_name), type: _types.map(&:type_name))
977
- request.merge!(search_type: options[:search_type]) if options[:search_type]
1029
+ request[:index] = _indexes.map(&:index_name)
1030
+ request[:type] = _types.map(&:type_name)
978
1031
  request
979
1032
  end
980
1033
  end
@@ -984,12 +1037,12 @@ module Chewy
984
1037
  request: _request, indexes: _indexes, types: _types,
985
1038
  index: _indexes.one? ? _indexes.first : _indexes,
986
1039
  type: _types.one? ? _types.first : _types do
987
- begin
988
- Chewy.client.search(_request)
989
- rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
990
- raise e if e.message !~ /IndexMissingException/ && e.message !~ /index_not_found_exception/
991
- {}
992
- end
1040
+ begin
1041
+ Chewy.client.search(_request)
1042
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
1043
+ raise e if e.message !~ /IndexMissingException/ && e.message !~ /index_not_found_exception/
1044
+ {}
1045
+ end
993
1046
  end
994
1047
  end
995
1048
 
@@ -1000,7 +1053,7 @@ module Chewy
1000
1053
  .merge!(_score: hit['_score'])
1001
1054
  .merge!(_explanation: hit['_explanation'])
1002
1055
 
1003
- wrapper = _derive_index(hit['_index']).type_hash[hit['_type']].new attributes
1056
+ wrapper = _derive_index(hit['_index']).type(hit['_type']).new(attributes)
1004
1057
  wrapper._data = hit
1005
1058
  wrapper
1006
1059
  end
@@ -1009,12 +1062,15 @@ module Chewy
1009
1062
  def _collection
1010
1063
  @_collection ||= begin
1011
1064
  _load_objects! if criteria.options[:preload]
1012
- criteria.options[:preload] && criteria.options[:loaded_objects] ?
1013
- _results.map(&:_object) : _results
1065
+ if criteria.options[:preload] && criteria.options[:loaded_objects]
1066
+ _results.map(&:_object)
1067
+ else
1068
+ _results
1069
+ end
1014
1070
  end
1015
1071
  end
1016
1072
 
1017
- def _derive_index index_name
1073
+ def _derive_index(index_name)
1018
1074
  (@derive_index ||= {})[index_name] ||= _indexes_hash[index_name] ||
1019
1075
  _indexes_hash[_indexes_hash.keys.sort_by(&:length).reverse.detect { |name| index_name.start_with?(name) }]
1020
1076
  end