chewy 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
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)