chewy 0.8.1 → 0.8.2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -8
  3. data/Appraisals +8 -0
  4. data/CHANGELOG.md +33 -6
  5. data/Gemfile +6 -4
  6. data/README.md +240 -111
  7. data/gemfiles/rails.4.2.activerecord.gemfile +1 -0
  8. data/gemfiles/rails.4.2.activerecord.kaminari.gemfile +1 -0
  9. data/gemfiles/rails.4.2.activerecord.will_paginate.gemfile +1 -0
  10. data/gemfiles/sequel.4.23.gemfile +13 -0
  11. data/lib/chewy.rb +24 -11
  12. data/lib/chewy/config.rb +4 -4
  13. data/lib/chewy/index.rb +1 -1
  14. data/lib/chewy/index/settings.rb +1 -1
  15. data/lib/chewy/query.rb +43 -4
  16. data/lib/chewy/railtie.rb +2 -2
  17. data/lib/chewy/rake_helper.rb +62 -0
  18. data/lib/chewy/rspec/update_index.rb +3 -3
  19. data/lib/chewy/strategy.rb +6 -0
  20. data/lib/chewy/strategy/active_job.rb +28 -0
  21. data/lib/chewy/strategy/atomic.rb +1 -1
  22. data/lib/chewy/strategy/sidekiq.rb +3 -1
  23. data/lib/chewy/type.rb +2 -1
  24. data/lib/chewy/type/adapter/active_record.rb +9 -2
  25. data/lib/chewy/type/adapter/base.rb +6 -0
  26. data/lib/chewy/type/adapter/mongoid.rb +7 -1
  27. data/lib/chewy/type/adapter/orm.rb +1 -1
  28. data/lib/chewy/type/adapter/sequel.rb +125 -0
  29. data/lib/chewy/type/mapping.rb +26 -1
  30. data/lib/chewy/type/observe.rb +40 -17
  31. data/lib/chewy/version.rb +1 -1
  32. data/lib/sequel/plugins/chewy_observe.rb +71 -0
  33. data/lib/tasks/chewy.rake +19 -61
  34. data/spec/chewy/config_spec.rb +9 -5
  35. data/spec/chewy/fields/base_spec.rb +21 -7
  36. data/spec/chewy/index/actions_spec.rb +5 -5
  37. data/spec/chewy/query_spec.rb +69 -0
  38. data/spec/chewy/runtime_spec.rb +1 -1
  39. data/spec/chewy/strategy/active_job_spec.rb +49 -0
  40. data/spec/chewy/strategy_spec.rb +2 -2
  41. data/spec/chewy/type/adapter/sequel_spec.rb +46 -0
  42. data/spec/chewy/type/import_spec.rb +4 -2
  43. data/spec/chewy/type/mapping_spec.rb +19 -0
  44. data/spec/chewy/type/observe_spec.rb +43 -14
  45. data/spec/chewy_spec.rb +2 -3
  46. data/spec/spec_helper.rb +6 -3
  47. data/spec/support/active_record.rb +5 -8
  48. data/spec/support/mongoid.rb +5 -8
  49. data/spec/support/sequel.rb +69 -0
  50. metadata +14 -3
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "activerecord", "~> 4.2.0"
6
6
  gem "activesupport", "~> 4.2.0"
7
+ gem "activejob", "~> 4.2.0"
7
8
  gem "resque", :require => false
8
9
  gem "sidekiq", :require => false
9
10
 
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "activerecord", "~> 4.2.0"
6
6
  gem "activesupport", "~> 4.2.0"
7
+ gem "activejob", "~> 4.2.0"
7
8
  gem "kaminari", "0.16.3", :require => false
8
9
 
9
10
  group :test do
@@ -4,6 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "activerecord", "~> 4.2.0"
6
6
  gem "activesupport", "~> 4.2.0"
7
+ gem "activejob", "~> 4.2.0"
7
8
  gem "will_paginate", :require => false
8
9
 
9
10
  group :test do
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sequel", "~> 4.23.0"
6
+ gem "activesupport", "~> 4.2.0"
7
+
8
+ group :test do
9
+ gem "guard"
10
+ gem "guard-rspec"
11
+ end
12
+
13
+ gemspec :path => "../"
@@ -1,9 +1,17 @@
1
- require 'active_support'
2
- require 'active_support/log_subscriber'
3
- require 'active_support/deprecation'
4
- require 'active_support/core_ext'
5
1
  require 'active_support/concern'
2
+ require 'active_support/deprecation'
6
3
  require 'active_support/json'
4
+ require 'active_support/log_subscriber'
5
+
6
+ require 'active_support/core_ext/array/access'
7
+ require 'active_support/core_ext/array/wrap'
8
+ require 'active_support/core_ext/enumerable'
9
+ require 'active_support/core_ext/hash/reverse_merge'
10
+ require 'active_support/core_ext/numeric/time'
11
+ require 'active_support/core_ext/object/blank'
12
+ require 'active_support/core_ext/object/inclusion'
13
+ require 'active_support/core_ext/string/inflections'
14
+
7
15
  require 'i18n/core_ext/hash'
8
16
  require 'chewy/backports/deep_dup' unless Object.respond_to?(:deep_dup)
9
17
  require 'singleton'
@@ -13,6 +21,7 @@ require 'elasticsearch'
13
21
  require 'chewy/version'
14
22
  require 'chewy/errors'
15
23
  require 'chewy/config'
24
+ require 'chewy/rake_helper'
16
25
  require 'chewy/repository'
17
26
  require 'chewy/runtime'
18
27
  require 'chewy/log_subscriber'
@@ -58,7 +67,17 @@ ActiveSupport.on_load(:mongoid) do
58
67
  end
59
68
 
60
69
  module Chewy
70
+
71
+ @adapters = [
72
+ Chewy::Type::Adapter::ActiveRecord,
73
+ Chewy::Type::Adapter::Mongoid,
74
+ Chewy::Type::Adapter::Sequel,
75
+ Chewy::Type::Adapter::Object
76
+ ]
77
+
61
78
  class << self
79
+ attr_accessor :adapters
80
+
62
81
  # Derives type from string `index#type` representation:
63
82
  #
64
83
  # Chewy.derive_type('users#user') # => UsersIndex::User
@@ -90,13 +109,7 @@ module Chewy
90
109
  def create_type index, target, options = {}, &block
91
110
  type = Class.new(Chewy::Type)
92
111
 
93
- adapter = if defined?(::ActiveRecord::Base) && ((target.is_a?(Class) && target < ::ActiveRecord::Base) || target.is_a?(::ActiveRecord::Relation))
94
- Chewy::Type::Adapter::ActiveRecord.new(target, options)
95
- elsif defined?(::Mongoid::Document) && ((target.is_a?(Class) && target.ancestors.include?(::Mongoid::Document)) || target.is_a?(::Mongoid::Criteria))
96
- Chewy::Type::Adapter::Mongoid.new(target, options)
97
- else
98
- Chewy::Type::Adapter::Object.new(target, options)
99
- end
112
+ adapter = adapters.find { |adapter| adapter.accepts?(target) }.new(target, options)
100
113
 
101
114
  index.const_set(adapter.name, type)
102
115
  type.send(:define_singleton_method, :index) { index }
@@ -19,8 +19,8 @@ module Chewy
19
19
  #
20
20
  :post_filter_mode,
21
21
 
22
- # The first trategy in stack. `:base` by default.
23
- # If you neet to return to the previous chewy behavior -
22
+ # The first strategy in stack. `:base` by default.
23
+ # If you need to return to the previous chewy behavior -
24
24
  # just set it to `:bypass`
25
25
  #
26
26
  :root_strategy,
@@ -106,8 +106,8 @@ module Chewy
106
106
  #
107
107
  def configuration
108
108
  yaml_settings.merge(settings.deep_symbolize_keys).tap do |configuration|
109
- configuration.merge(logger: transport_logger) if transport_logger
110
- configuration.merge(tracer: transport_tracer) if transport_tracer
109
+ configuration.merge!(logger: transport_logger) if transport_logger
110
+ configuration.merge!(tracer: transport_tracer) if transport_tracer
111
111
  end
112
112
  end
113
113
 
@@ -92,7 +92,7 @@ module Chewy
92
92
  #
93
93
  # UsersIndex.types # => [UsersIndex::Admin, UsersIndex::Manager, UsersIndex::User]
94
94
  #
95
- # If arguments are passed it treats like a part of chainable query dsl and
95
+ # If arguments are passed it treats like a part of chainable query DSL and
96
96
  # adds types array for index to select.
97
97
  #
98
98
  # UsersIndex.filters { name =~ 'ro' }.types(:admin, :manager)
@@ -2,7 +2,7 @@ module Chewy
2
2
  class Index
3
3
 
4
4
  # Stores ElasticSearch index settings and resolves `analysis`
5
- # hash. At first, you need to store sone analyzers or other
5
+ # hash. At first, you need to store some analyzers or other
6
6
  # analysis options to the corresponding repository:
7
7
  #
8
8
  # Chewy.analyzer :title_analyzer, type: 'custom', filter: %w(lowercase icu_folding title_nysiis)
@@ -386,7 +386,7 @@ module Chewy
386
386
  # added to the search request and combinded according to
387
387
  # <tt>boost_mode</tt> and <tt>score_mode</tt>
388
388
  #
389
- # This probably only makes sense if you specifiy a filter
389
+ # This probably only makes sense if you specify a filter
390
390
  # for the boost factor as well
391
391
  #
392
392
  # UsersIndex.boost_factor(23, filter: { term: { foo: :bar} })
@@ -408,7 +408,7 @@ module Chewy
408
408
  # added to the search request and combinded according to
409
409
  # <tt>boost_mode</tt> and <tt>score_mode</tt>
410
410
  #
411
- # This probably only makes sense if you specifiy a filter
411
+ # This probably only makes sense if you specify a filter
412
412
  # for the random score as well.
413
413
  #
414
414
  # If you do not pass in a seed value, Time.now will be used
@@ -511,7 +511,11 @@ module Chewy
511
511
  # }}
512
512
  #
513
513
  def aggregations params = nil
514
+ @_named_aggs ||= _build_named_aggs
515
+ @_fully_qualified_named_aggs ||= _build_fqn_aggs
514
516
  if params
517
+ params = { params => @_named_aggs[params] } if params.is_a?(Symbol)
518
+ params = { params => _get_fully_qualified_named_agg(params) } if params.is_a?(String) && params =~ /\A\S+#\S+\.\S+\z/
515
519
  chain { criteria.update_aggregations params }
516
520
  else
517
521
  _response['aggregations'] || {}
@@ -519,6 +523,41 @@ module Chewy
519
523
  end
520
524
  alias :aggs :aggregations
521
525
 
526
+ # In this simplest of implementations each named aggregation must be uniquely named
527
+ def _build_named_aggs
528
+ named_aggs = {}
529
+ @_indexes.each do |index|
530
+ index.types.each do |type|
531
+ type._agg_defs.each do |agg_name, prc|
532
+ named_aggs[agg_name] = prc.call
533
+ end
534
+ end
535
+ end
536
+ named_aggs
537
+ end
538
+
539
+ def _build_fqn_aggs
540
+ named_aggs = {}
541
+ @_indexes.each do |index|
542
+ named_aggs[index.to_s.downcase] ||= {}
543
+ index.types.each do |type|
544
+ named_aggs[index.to_s.downcase][type.to_s.downcase] ||= {}
545
+ type._agg_defs.each do |agg_name, prc|
546
+ named_aggs[index.to_s.downcase][type.to_s.downcase][agg_name.to_s.downcase] = prc.call
547
+ end
548
+ end
549
+ end
550
+ named_aggs
551
+ end
552
+
553
+ def _get_fully_qualified_named_agg(str)
554
+ parts = str.scan(/\A(\S+)#(\S+)\.(\S+)\z/).first
555
+ idx = "#{parts[0]}index"
556
+ type = "#{idx}::#{parts[1]}"
557
+ agg_name = parts[2]
558
+ @_fully_qualified_named_aggs[idx][type][agg_name]
559
+ end
560
+
522
561
  # Sets elasticsearch <tt>suggest</tt> search request param
523
562
  #
524
563
  # UsersIndex.suggest(name: {text: 'Joh', term: {field: 'name'}})
@@ -876,7 +915,7 @@ module Chewy
876
915
  end
877
916
  end
878
917
 
879
- # Deletes all records matching a query.
918
+ # Find all records matching a query.
880
919
  #
881
920
  # UsersIndex.find(42)
882
921
  # UsersIndex.filter{ age <= 42 }.find(42)
@@ -982,7 +1021,7 @@ module Chewy
982
1021
 
983
1022
  def _derive_index index_name
984
1023
  (@derive_index ||= {})[index_name] ||= _indexes_hash[index_name] ||
985
- _indexes_hash[_indexes_hash.keys.sort_by(&:length).reverse.detect { |name| index_name.starts_with?(name) }]
1024
+ _indexes_hash[_indexes_hash.keys.sort_by(&:length).reverse.detect { |name| index_name.start_with?(name) }]
986
1025
  end
987
1026
 
988
1027
  def _indexes_hash
@@ -10,7 +10,7 @@ module Chewy
10
10
  end
11
11
 
12
12
  def call(env)
13
- if env['PATH_INFO'] =~ /^\/assets\//
13
+ if env['PATH_INFO'].start_with?('/assets/')
14
14
  @app.call(env)
15
15
  else
16
16
  Chewy.strategy(Chewy.request_strategy) { @app.call(env) }
@@ -55,7 +55,7 @@ module Chewy
55
55
  end
56
56
 
57
57
  initializer 'chewy.request_strategy' do |app|
58
- app.config.middleware.insert_after(Rack::Runtime, RequestStrategy)
58
+ app.config.middleware.insert_after(Rails::Rack::Logger, RequestStrategy)
59
59
  end
60
60
 
61
61
  initializer 'chewy.add_app_chewy_path' do |app|
@@ -0,0 +1,62 @@
1
+ module Chewy
2
+ module RakeHelper
3
+ class << self
4
+
5
+ def subscribe_task_stats!
6
+ ActiveSupport::Notifications.subscribe('import_objects.chewy') do |name, start, finish, id, payload|
7
+ duration = (finish - start).round(2)
8
+ puts " Imported #{payload[:type]} for #{duration}s, documents total: #{payload[:import].try(:[], :index).to_i}"
9
+ payload[:errors].each do |action, errors|
10
+ puts " #{action.to_s.humanize} errors:"
11
+ errors.each do |error, documents|
12
+ puts " `#{error}`"
13
+ puts " on #{documents.count} documents: #{documents}"
14
+ end
15
+ end if payload[:errors]
16
+ end
17
+ end
18
+
19
+ def eager_load_chewy!
20
+ dirs = Chewy::Railtie.all_engines.map { |engine| engine.paths['app/chewy'] }.compact.map(&:existent).flatten.uniq
21
+
22
+ dirs.each do |dir|
23
+ Dir.glob(File.join(dir, '**/*.rb')).each do |file|
24
+ require_dependency file
25
+ end
26
+ end
27
+ end
28
+
29
+ def normalize_index index
30
+ "#{index.to_s.gsub(/index\z/i, '').camelize}Index".constantize
31
+ end
32
+
33
+ # Performs zero downtime reindexing of all documents in the specified index.
34
+ def reset_index index
35
+ index = normalize_index(index)
36
+ puts "Resetting #{index}"
37
+ index.reset! (Time.now.to_f * 1000).round
38
+ end
39
+
40
+ # Performs zero downtime reindexing of all documents across all indices.
41
+ def reset_all
42
+ eager_load_chewy!
43
+ Chewy::Index.descendants.each { |index| reset_index index }
44
+ end
45
+
46
+ def update_index index
47
+ index = normalize_index(index)
48
+ puts "Updating #{index}"
49
+ if index.exists?
50
+ index.import
51
+ else
52
+ puts "Index `#{index.index_name}` does not exists. Use rake chewy:reset[#{index.index_name}] to create and update it."
53
+ end
54
+ end
55
+
56
+ def update_all
57
+ eager_load_chewy!
58
+ Chewy::Index.descendants.each { |index| update_index index }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -18,7 +18,7 @@ require 'i18n/core_ext/hash'
18
18
  # Combined matcher chain methods:
19
19
  #
20
20
  # specify { expect { user1.destroy!; user2.save! } }
21
- # .to update_index(UsersIndex:User).and_reindex(user2).and_delete(user1) }
21
+ # .to update_index(UsersIndex::User).and_reindex(user2).and_delete(user1) }
22
22
  #
23
23
  RSpec::Matchers.define :update_index do |type_name, options = {}|
24
24
 
@@ -43,7 +43,7 @@ RSpec::Matchers.define :update_index do |type_name, options = {}|
43
43
  # .to update_index(UsersIndex::User).and_reindex(user, times: 2) }
44
44
  #
45
45
  # Specify reindexed attributes. Note that arrays are
46
- # compared position-independantly.
46
+ # compared position-independently.
47
47
  #
48
48
  # specify { expect { user.update_attributes!(name: 'Duke') }
49
49
  # .to update_index(UsersIndex.user).and_reindex(user, with: {name: 'Duke'}) }
@@ -241,4 +241,4 @@ RSpec::Matchers.define :update_index do |type_name, options = {}|
241
241
  end
242
242
  difference.none?
243
243
  end
244
- end
244
+ end
@@ -15,6 +15,12 @@ begin
15
15
  rescue LoadError
16
16
  end
17
17
 
18
+ begin
19
+ require 'active_job'
20
+ require 'chewy/strategy/active_job'
21
+ rescue LoadError
22
+ end
23
+
18
24
  module Chewy
19
25
  # This class represents strategies stack with `:base`
20
26
  # Strategy on top of it. This causes raising exceptions
@@ -0,0 +1,28 @@
1
+ module Chewy
2
+ class Strategy
3
+ # The strategy works the same way as atomic, but performs
4
+ # async index update driven by active_job
5
+ #
6
+ # Chewy.strategy(:active_job) do
7
+ # User.all.map(&:save) # Does nothing here
8
+ # Post.all.map(&:save) # And here
9
+ # # It imports all the changed users and posts right here
10
+ # end
11
+ #
12
+ class ActiveJob < Atomic
13
+ class Worker < ::ActiveJob::Base
14
+ queue_as :chewy
15
+
16
+ def perform(type, ids)
17
+ type.constantize.import!(ids)
18
+ end
19
+ end
20
+
21
+ def leave
22
+ @stash.each do |type, ids|
23
+ Chewy::Strategy::ActiveJob::Worker.perform_later(type.name, ids) unless ids.empty?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,6 +1,6 @@
1
1
  module Chewy
2
2
  class Strategy
3
- # This strategy accomulates all the objects prepared for
3
+ # This strategy accumulates all the objects prepared for
4
4
  # indexing and fires index process when strategy is popped
5
5
  # from the strategies stack.
6
6
  #
@@ -19,7 +19,9 @@ module Chewy
19
19
  end
20
20
 
21
21
  def leave
22
- @stash.all? { |type, ids| Chewy::Strategy::Sidekiq::Worker.perform_async(type.name, ids) }
22
+ @stash.each do |type, ids|
23
+ Chewy::Strategy::Sidekiq::Worker.perform_async(type.name, ids) unless ids.empty?
24
+ end
23
25
  end
24
26
  end
25
27
  end
@@ -8,6 +8,7 @@ require 'chewy/type/import'
8
8
  require 'chewy/type/adapter/object'
9
9
  require 'chewy/type/adapter/active_record'
10
10
  require 'chewy/type/adapter/mongoid'
11
+ require 'chewy/type/adapter/sequel'
11
12
 
12
13
  module Chewy
13
14
  class Type
@@ -21,7 +22,7 @@ module Chewy
21
22
 
22
23
  singleton_class.delegate :index_name, :client, to: :index
23
24
 
24
- # Chewy index current type blongs to. Defined inside `Chewy.create_type`
25
+ # Chewy index current type belongs to. Defined inside `Chewy.create_type`
25
26
  #
26
27
  def self.index
27
28
  raise NotImplementedError
@@ -4,12 +4,19 @@ module Chewy
4
4
  class Type
5
5
  module Adapter
6
6
  class ActiveRecord < Orm
7
+
8
+ def self.accepts?(target)
9
+ defined?(::ActiveRecord::Base) && (
10
+ target.is_a?(Class) && target < ::ActiveRecord::Base ||
11
+ target.is_a?(::ActiveRecord::Relation))
12
+ end
13
+
7
14
  private
8
15
 
9
16
  def cleanup_default_scope!
10
17
  if Chewy.logger && (@default_scope.arel.orders.present? ||
11
18
  @default_scope.arel.limit.present? || @default_scope.arel.offset.present?)
12
- Chewy.logger.warn('Default type scope order, limit and offest are ignored and will be nullified')
19
+ Chewy.logger.warn('Default type scope order, limit and offset are ignored and will be nullified')
13
20
  end
14
21
 
15
22
  @default_scope = @default_scope.reorder(nil).limit(nil).offset(nil)
@@ -31,7 +38,7 @@ module Chewy
31
38
  end
32
39
 
33
40
  def pluck_ids(scope)
34
- scope.pluck(target.primary_key.to_sym)
41
+ scope.except(:includes).uniq.pluck(target.primary_key.to_sym)
35
42
  end
36
43
 
37
44
  def scope_where_ids_in(scope, ids)