chewy 0.8.1 → 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +10 -8
- data/Appraisals +8 -0
- data/CHANGELOG.md +33 -6
- data/Gemfile +6 -4
- data/README.md +240 -111
- data/gemfiles/rails.4.2.activerecord.gemfile +1 -0
- data/gemfiles/rails.4.2.activerecord.kaminari.gemfile +1 -0
- data/gemfiles/rails.4.2.activerecord.will_paginate.gemfile +1 -0
- data/gemfiles/sequel.4.23.gemfile +13 -0
- data/lib/chewy.rb +24 -11
- data/lib/chewy/config.rb +4 -4
- data/lib/chewy/index.rb +1 -1
- data/lib/chewy/index/settings.rb +1 -1
- data/lib/chewy/query.rb +43 -4
- data/lib/chewy/railtie.rb +2 -2
- data/lib/chewy/rake_helper.rb +62 -0
- data/lib/chewy/rspec/update_index.rb +3 -3
- data/lib/chewy/strategy.rb +6 -0
- data/lib/chewy/strategy/active_job.rb +28 -0
- data/lib/chewy/strategy/atomic.rb +1 -1
- data/lib/chewy/strategy/sidekiq.rb +3 -1
- data/lib/chewy/type.rb +2 -1
- data/lib/chewy/type/adapter/active_record.rb +9 -2
- data/lib/chewy/type/adapter/base.rb +6 -0
- data/lib/chewy/type/adapter/mongoid.rb +7 -1
- data/lib/chewy/type/adapter/orm.rb +1 -1
- data/lib/chewy/type/adapter/sequel.rb +125 -0
- data/lib/chewy/type/mapping.rb +26 -1
- data/lib/chewy/type/observe.rb +40 -17
- data/lib/chewy/version.rb +1 -1
- data/lib/sequel/plugins/chewy_observe.rb +71 -0
- data/lib/tasks/chewy.rake +19 -61
- data/spec/chewy/config_spec.rb +9 -5
- data/spec/chewy/fields/base_spec.rb +21 -7
- data/spec/chewy/index/actions_spec.rb +5 -5
- data/spec/chewy/query_spec.rb +69 -0
- data/spec/chewy/runtime_spec.rb +1 -1
- data/spec/chewy/strategy/active_job_spec.rb +49 -0
- data/spec/chewy/strategy_spec.rb +2 -2
- data/spec/chewy/type/adapter/sequel_spec.rb +46 -0
- data/spec/chewy/type/import_spec.rb +4 -2
- data/spec/chewy/type/mapping_spec.rb +19 -0
- data/spec/chewy/type/observe_spec.rb +43 -14
- data/spec/chewy_spec.rb +2 -3
- data/spec/spec_helper.rb +6 -3
- data/spec/support/active_record.rb +5 -8
- data/spec/support/mongoid.rb +5 -8
- data/spec/support/sequel.rb +69 -0
- metadata +14 -3
data/lib/chewy.rb
CHANGED
@@ -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 =
|
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 }
|
data/lib/chewy/config.rb
CHANGED
@@ -19,8 +19,8 @@ module Chewy
|
|
19
19
|
#
|
20
20
|
:post_filter_mode,
|
21
21
|
|
22
|
-
# The first
|
23
|
-
# If you
|
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
|
|
data/lib/chewy/index.rb
CHANGED
@@ -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
|
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)
|
data/lib/chewy/index/settings.rb
CHANGED
@@ -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
|
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)
|
data/lib/chewy/query.rb
CHANGED
@@ -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
|
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
|
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
|
-
#
|
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.
|
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
|
data/lib/chewy/railtie.rb
CHANGED
@@ -10,7 +10,7 @@ module Chewy
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def call(env)
|
13
|
-
if env['PATH_INFO']
|
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::
|
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
|
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-
|
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
|
data/lib/chewy/strategy.rb
CHANGED
@@ -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
|
@@ -19,7 +19,9 @@ module Chewy
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def leave
|
22
|
-
@stash.
|
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
|
data/lib/chewy/type.rb
CHANGED
@@ -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
|
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
|
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)
|