chewy 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +13 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +5 -3
  4. data/CHANGELOG.md +75 -0
  5. data/README.md +487 -92
  6. data/Rakefile +3 -2
  7. data/chewy.gemspec +2 -2
  8. data/filters +76 -0
  9. data/lib/chewy.rb +5 -3
  10. data/lib/chewy/config.rb +36 -19
  11. data/lib/chewy/fields/base.rb +5 -1
  12. data/lib/chewy/index.rb +22 -10
  13. data/lib/chewy/index/actions.rb +13 -13
  14. data/lib/chewy/index/search.rb +7 -2
  15. data/lib/chewy/query.rb +382 -64
  16. data/lib/chewy/query/context.rb +174 -0
  17. data/lib/chewy/query/criteria.rb +127 -34
  18. data/lib/chewy/query/loading.rb +9 -9
  19. data/lib/chewy/query/nodes/and.rb +25 -0
  20. data/lib/chewy/query/nodes/base.rb +17 -0
  21. data/lib/chewy/query/nodes/bool.rb +32 -0
  22. data/lib/chewy/query/nodes/equal.rb +34 -0
  23. data/lib/chewy/query/nodes/exists.rb +20 -0
  24. data/lib/chewy/query/nodes/expr.rb +28 -0
  25. data/lib/chewy/query/nodes/field.rb +106 -0
  26. data/lib/chewy/query/nodes/missing.rb +20 -0
  27. data/lib/chewy/query/nodes/not.rb +25 -0
  28. data/lib/chewy/query/nodes/or.rb +25 -0
  29. data/lib/chewy/query/nodes/prefix.rb +18 -0
  30. data/lib/chewy/query/nodes/query.rb +20 -0
  31. data/lib/chewy/query/nodes/range.rb +63 -0
  32. data/lib/chewy/query/nodes/raw.rb +15 -0
  33. data/lib/chewy/query/nodes/regexp.rb +31 -0
  34. data/lib/chewy/query/nodes/script.rb +20 -0
  35. data/lib/chewy/query/pagination.rb +28 -22
  36. data/lib/chewy/railtie.rb +23 -0
  37. data/lib/chewy/rspec/update_index.rb +20 -3
  38. data/lib/chewy/type/adapter/active_record.rb +78 -5
  39. data/lib/chewy/type/adapter/base.rb +46 -0
  40. data/lib/chewy/type/adapter/object.rb +40 -8
  41. data/lib/chewy/type/base.rb +1 -1
  42. data/lib/chewy/type/import.rb +18 -44
  43. data/lib/chewy/type/observe.rb +24 -14
  44. data/lib/chewy/version.rb +1 -1
  45. data/lib/tasks/chewy.rake +27 -0
  46. data/spec/chewy/config_spec.rb +30 -12
  47. data/spec/chewy/fields/base_spec.rb +11 -5
  48. data/spec/chewy/index/actions_spec.rb +20 -20
  49. data/spec/chewy/index/search_spec.rb +5 -5
  50. data/spec/chewy/index_spec.rb +28 -8
  51. data/spec/chewy/query/context_spec.rb +173 -0
  52. data/spec/chewy/query/criteria_spec.rb +219 -12
  53. data/spec/chewy/query/loading_spec.rb +6 -4
  54. data/spec/chewy/query/nodes/and_spec.rb +16 -0
  55. data/spec/chewy/query/nodes/bool_spec.rb +22 -0
  56. data/spec/chewy/query/nodes/equal_spec.rb +32 -0
  57. data/spec/chewy/query/nodes/exists_spec.rb +18 -0
  58. data/spec/chewy/query/nodes/missing_spec.rb +15 -0
  59. data/spec/chewy/query/nodes/not_spec.rb +16 -0
  60. data/spec/chewy/query/nodes/or_spec.rb +16 -0
  61. data/spec/chewy/query/nodes/prefix_spec.rb +16 -0
  62. data/spec/chewy/query/nodes/query_spec.rb +12 -0
  63. data/spec/chewy/query/nodes/range_spec.rb +32 -0
  64. data/spec/chewy/query/nodes/raw_spec.rb +11 -0
  65. data/spec/chewy/query/nodes/regexp_spec.rb +31 -0
  66. data/spec/chewy/query/nodes/script_spec.rb +15 -0
  67. data/spec/chewy/query/pagination_spec.rb +3 -2
  68. data/spec/chewy/query_spec.rb +83 -26
  69. data/spec/chewy/rspec/update_index_spec.rb +20 -0
  70. data/spec/chewy/type/adapter/active_record_spec.rb +102 -0
  71. data/spec/chewy/type/adapter/object_spec.rb +82 -0
  72. data/spec/chewy/type/import_spec.rb +30 -1
  73. data/spec/chewy/type/mapping_spec.rb +1 -1
  74. data/spec/chewy/type/observe_spec.rb +46 -12
  75. data/spec/spec_helper.rb +7 -6
  76. data/spec/support/class_helpers.rb +2 -2
  77. metadata +98 -48
  78. data/.rvmrc +0 -1
  79. data/lib/chewy/index/client.rb +0 -13
  80. data/spec/chewy/index/client_spec.rb +0 -18
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'elasticsearch/extensions/test/cluster/tasks'
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
data/chewy.gemspec CHANGED
@@ -18,13 +18,13 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_development_dependency 'bundler'
22
21
  spec.add_development_dependency 'rake'
23
- spec.add_development_dependency 'rspec'
22
+ spec.add_development_dependency 'rspec', '~> 2.14'
24
23
  spec.add_development_dependency 'sqlite3'
25
24
  spec.add_development_dependency 'kaminari'
26
25
  spec.add_development_dependency 'activerecord', '>= 3.2'
27
26
  spec.add_development_dependency 'database_cleaner'
27
+ spec.add_development_dependency 'elasticsearch-extensions'
28
28
  spec.add_development_dependency 'rubysl', '~> 2.0' if RUBY_ENGINE == 'rbx'
29
29
 
30
30
  spec.add_dependency 'activesupport', '>= 3.2'
data/filters ADDED
@@ -0,0 +1,76 @@
1
+ term
2
+ name == 'value'
3
+ name != 'value'
4
+ terms
5
+ name == ['value1', 'value2'] plain
6
+ name != ['value1', 'value2']
7
+
8
+ name(:&) == ['value1', 'value2'] or
9
+ name(:|) == ['value1', 'value2'] and
10
+ name(:b) == ['value1', 'value2'] bool
11
+ name(:f) == ['value1', 'value2'] fielddata
12
+ regexp
13
+ name == /regexp/
14
+ name =~ /regexp/
15
+ name != /regexp/
16
+ name !~ /regexp/
17
+ name(:anystring, :intersection) == /regexp/
18
+ prefix
19
+ name =~ 'pref'
20
+ name !~ 'pref'
21
+
22
+ exists
23
+ name?
24
+ missing
25
+ !name
26
+ !name?
27
+ name == nil
28
+
29
+ numeric_range Numeric
30
+ range Other
31
+ date >= Date.today
32
+ date > Date.today
33
+ date <= Date.today
34
+ date < Date.today
35
+
36
+ date == (2.days.ago..3.days.since) ()
37
+ date == [2.days.ago..3.days.since] []
38
+
39
+ bool
40
+ must(name == 'name', email == 'email')
41
+ .should(name == 'name', email == 'email')
42
+ .must_not(name == 'name', email == 'email')
43
+ and
44
+ (name == 'name') & (email == 'email')
45
+ or
46
+ (name == 'name') | (email == 'email')
47
+ not
48
+ !(name == 'name')
49
+ email != 'email'
50
+
51
+ script s()
52
+ query q()
53
+
54
+ has child
55
+ has_child('type').query()
56
+ has_child('type').filter()
57
+ has parent
58
+ has_parent('type').query()
59
+ has_parent('type').filter()
60
+ nested
61
+ name.nested()
62
+ match all
63
+ match_all
64
+
65
+ geo bounding box
66
+ geo distance
67
+ geo distance range
68
+ geo polygon
69
+ geoshape
70
+ geohash cell
71
+
72
+ indices
73
+ type
74
+ ids
75
+
76
+ limit
data/lib/chewy.rb CHANGED
@@ -14,6 +14,8 @@ require 'chewy/fields/base'
14
14
  require 'chewy/fields/default'
15
15
  require 'chewy/fields/root'
16
16
 
17
+ require 'chewy/railtie' if defined?(::Rails)
18
+
17
19
  ActiveSupport.on_load(:active_record) do
18
20
  extend Chewy::Type::Observe::ActiveRecordMethods
19
21
  end
@@ -39,9 +41,9 @@ module Chewy
39
41
  index = class_name.safe_constantize
40
42
  raise Chewy::UnderivableType.new("Can not find index named `#{class_name}`") unless index && index < Chewy::Index
41
43
  type = if type_name.present?
42
- index.types[type_name] or raise Chewy::UnderivableType.new("Index `#{class_name}` doesn`t have type named `#{type_name}`")
43
- elsif index.types.values.one?
44
- index.types.values.first
44
+ index.type_hash[type_name] or raise Chewy::UnderivableType.new("Index `#{class_name}` doesn`t have type named `#{type_name}`")
45
+ elsif index.types.one?
46
+ index.types.first
45
47
  else
46
48
  raise Chewy::UnderivableType.new("Index `#{class_name}` has more than one type, please specify type via `#{index_name}#type_name`")
47
49
  end
data/lib/chewy/config.rb CHANGED
@@ -2,46 +2,63 @@ module Chewy
2
2
  class Config
3
3
  include Singleton
4
4
 
5
- attr_accessor :observing_enabled, :client_options
5
+ attr_accessor :client_options, :urgent_update, :query_mode, :filter_mode, :logger
6
6
 
7
7
  def self.delegated
8
8
  public_instance_methods - self.superclass.public_instance_methods - Singleton.public_instance_methods
9
9
  end
10
10
 
11
11
  def initialize
12
- @observing_enabled = true
12
+ @urgent_update = false
13
13
  @client_options = {}
14
+ @query_mode = :must
15
+ @filter_mode = :and
14
16
  end
15
17
 
16
18
  def client_options
17
- yaml_options = if defined? Rails
18
- file = Rails.root.join(*%w(config chewy.yml))
19
- YAML.load_file(file)[Rails.env].try(:deep_symbolize_keys) if File.exists?(file)
20
- end
21
- @client_options.merge(yaml_options || {})
19
+ options = @client_options.merge(yaml_options)
20
+ options.merge!(logger: logger) if logger
21
+ options
22
+ end
23
+
24
+ def client?
25
+ !!Thread.current[:chewy_client]
26
+ end
27
+
28
+ def client
29
+ Thread.current[:chewy_client] ||= ::Elasticsearch::Client.new client_options
22
30
  end
23
31
 
24
32
  def atomic?
25
- atomic_stash.any?
33
+ stash.any?
26
34
  end
27
35
 
28
36
  def atomic
29
- atomic_stash.push({})
30
- result = yield
31
- atomic_stash.last.each { |type, ids| type.import(ids) }
32
- result
37
+ stash.push({})
38
+ yield
33
39
  ensure
34
- atomic_stash.pop
40
+ stash.pop.each { |type, ids| type.import(ids) }
35
41
  end
36
42
 
37
- def atomic_stash(type = nil, *ids)
38
- if type
43
+ def stash *args
44
+ if args.any?
45
+ type, ids = *args
39
46
  raise ArgumentError.new('Only Chewy::Type::Base accepted as the first argument') unless type < Chewy::Type::Base
40
- atomic_stash.push({}) unless atomic_stash.last
41
- atomic_stash.last[type] ||= []
42
- atomic_stash.last[type] |= ids.flatten
47
+ stash.last[type] ||= []
48
+ stash.last[type] |= ids
43
49
  else
44
- Thread.current[:chewy_atomic] ||= []
50
+ Thread.current[:chewy_cache] ||= []
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def yaml_options
57
+ @yaml_options ||= begin
58
+ if defined?(Rails)
59
+ file = Rails.root.join(*%w(config chewy.yml))
60
+ YAML.load_file(file)[Rails.env].try(:deep_symbolize_keys) if File.exists?(file)
61
+ end || {}
45
62
  end
46
63
  end
47
64
  end
@@ -13,7 +13,11 @@ module Chewy
13
13
  end
14
14
 
15
15
  def compose(object)
16
- result = value ? value.call(object) : object.send(name)
16
+ result = if value && value.is_a?(Proc)
17
+ value.arity == 0 ? object.instance_exec(&value) : value.call(object)
18
+ else
19
+ object.send(name)
20
+ end
17
21
 
18
22
  result = if result.is_a?(Enumerable)
19
23
  result.map { |object| nested_compose(object) }
data/lib/chewy/index.rb CHANGED
@@ -1,30 +1,42 @@
1
1
  require 'chewy/index/actions'
2
- require 'chewy/index/client'
3
2
  require 'chewy/index/search'
4
3
 
5
4
  module Chewy
6
5
  class Index
7
6
  include Actions
8
- include Client
9
7
  include Search
10
8
 
11
- class_attribute :types
12
- self.types = {}
9
+ singleton_class.delegate :client, to: 'Chewy'
10
+
11
+ class_attribute :type_hash
12
+ self.type_hash = {}
13
13
 
14
14
  class_attribute :_settings
15
15
  self._settings = {}
16
16
 
17
17
  def self.define_type(name_or_scope, &block)
18
18
  type_class = Chewy::Type.new(self, name_or_scope, &block)
19
- self.types = types.merge(type_class.type_name => type_class)
19
+ self.type_hash = type_hash.merge(type_class.type_name => type_class)
20
20
 
21
21
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
22
22
  def self.#{type_class.type_name}
23
- types['#{type_class.type_name}']
23
+ type_hash['#{type_class.type_name}']
24
24
  end
25
25
  RUBY
26
26
  end
27
27
 
28
+ def self.types *args
29
+ if args.any?
30
+ all.types *args
31
+ else
32
+ type_hash.values
33
+ end
34
+ end
35
+
36
+ def self.type_names
37
+ type_hash.keys
38
+ end
39
+
28
40
  def self.settings(params)
29
41
  self._settings = params
30
42
  end
@@ -43,7 +55,7 @@ module Chewy
43
55
  end
44
56
 
45
57
  def self.mappings_hash
46
- mappings = types.values.map(&:mappings_hash).inject(:merge)
58
+ mappings = types.map(&:mappings_hash).inject(:merge)
47
59
  mappings.present? ? {mappings: mappings} : {}
48
60
  end
49
61
 
@@ -56,15 +68,15 @@ module Chewy
56
68
  end
57
69
 
58
70
  def self.search_type
59
- types.keys
71
+ type_names
60
72
  end
61
73
 
62
74
  def self.import
63
- types.values.all? { |t| t.import }
75
+ types.all? { |t| t.import }
64
76
  end
65
77
 
66
78
  def self.reset
67
- index_purge!
79
+ purge!
68
80
  import
69
81
  end
70
82
  end
@@ -4,38 +4,38 @@ module Chewy
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  module ClassMethods
7
- def index_exists?
7
+ def exists?
8
8
  client.indices.exists(index: index_name)
9
9
  end
10
10
 
11
- def index_create
12
- index_create!
11
+ def create
12
+ create!
13
13
  rescue Elasticsearch::Transport::Transport::Errors::BadRequest
14
14
  false
15
15
  end
16
16
 
17
- def index_create!
17
+ def create!
18
18
  client.indices.create(index: index_name, body: index_params)
19
19
  end
20
20
 
21
- def index_delete
22
- index_delete!
21
+ def delete
22
+ delete!
23
23
  rescue Elasticsearch::Transport::Transport::Errors::NotFound
24
24
  false
25
25
  end
26
26
 
27
- def index_delete!
27
+ def delete!
28
28
  client.indices.delete(index: index_name)
29
29
  end
30
30
 
31
- def index_purge
32
- index_delete
33
- index_create
31
+ def purge
32
+ delete
33
+ create
34
34
  end
35
35
 
36
- def index_purge!
37
- index_delete
38
- index_create!
36
+ def purge!
37
+ delete
38
+ create!
39
39
  end
40
40
  end
41
41
  end
@@ -3,9 +3,14 @@ module Chewy
3
3
  module Search
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ included do
7
+ singleton_class.delegate :explain, :limit, :offset, :facets, :query,
8
+ :filter, :order, :reorder, :only, :types, to: :all
9
+ end
10
+
6
11
  module ClassMethods
7
- def search
8
- Chewy::Query.new(search_index, type: search_type)
12
+ def all
13
+ Chewy::Query.new(search_index, types: search_type)
9
14
  end
10
15
 
11
16
  def search_string query, options = {}
data/lib/chewy/query.rb CHANGED
@@ -1,132 +1,450 @@
1
+ begin
2
+ require 'kaminari'
3
+ rescue LoadError
4
+ end
5
+
1
6
  require 'chewy/query/criteria'
7
+ require 'chewy/query/context'
2
8
  require 'chewy/query/loading'
3
9
  require 'chewy/query/pagination'
4
10
 
5
11
  module Chewy
12
+ # Query allows you to create ES search requests with convenient
13
+ # chainable DSL. Queries are lazy evaluated and might be merged.
14
+ # The same DSL is used for whole index or individual types query build.
15
+ #
16
+ # UsersIndex.filter{ age < 42 }.query(text: {name: 'Alex'}).limit(20)
17
+ # UsersIndex::User.filter{ age < 42 }.query(text: {name: 'Alex'}).limit(20)
18
+ #
6
19
  class Query
7
20
  include Enumerable
8
21
  include Loading
9
22
  include Pagination
10
23
 
11
- DEFAULT_OPTIONS = {}
12
-
13
- delegate :each, to: :_results
24
+ delegate :each, :count, :size, to: :_results
14
25
  alias_method :to_ary, :to_a
15
26
 
16
27
  attr_reader :index, :options, :criteria
17
28
 
18
- def initialize(index, options = {})
19
- @index, @options = index, DEFAULT_OPTIONS.merge(options)
29
+ def initialize index, options = {}
30
+ @index, @options = index, options
31
+ @types = Array.wrap(options.delete(:types))
20
32
  @criteria = Criteria.new
21
33
  reset
22
34
  end
23
35
 
24
- def ==(other)
25
- if other.is_a?(self.class)
36
+ # Comparation with other query or collection
37
+ # If other is collection - search request is executed and
38
+ # result is used for comparation
39
+ #
40
+ # UsersIndex.filter(term: {name: 'Johny'}) == UsersIndex.filter(term: {name: 'Johny'}) # => true
41
+ # UsersIndex.filter(term: {name: 'Johny'}) == UsersIndex.filter(term: {name: 'Johny'}).to_a # => true
42
+ # UsersIndex.filter(term: {name: 'Johny'}) == UsersIndex.filter(term: {name: 'Winnie'}) # => false
43
+ #
44
+ def == other
45
+ super || if other.is_a?(self.class)
26
46
  other.criteria == criteria
27
47
  else
28
48
  to_a == other
29
49
  end
30
50
  end
31
51
 
32
- def explain(value = nil)
33
- chain { criteria.update_search explain: (value.nil? ? true : value) }
52
+ # Adds <tt>explain</tt> parameter to search request.
53
+ #
54
+ # UsersIndex.filter(term: {name: 'Johny'}).explain
55
+ # UsersIndex.filter(term: {name: 'Johny'}).explain(true)
56
+ # UsersIndex.filter(term: {name: 'Johny'}).explain(false)
57
+ #
58
+ # Calling explain without any arguments sets explanation flag to true.
59
+ # With <tt>explain: true</tt>, every result object has <tt>_explanation</tt>
60
+ # method
61
+ #
62
+ # UsersIndex::User.filter(term: {name: 'Johny'}).explain.first._explanation # => {...}
63
+ #
64
+ def explain value = nil
65
+ chain { criteria.update_options explain: (value.nil? ? true : value) }
66
+ end
67
+
68
+ # Sets query compilation mode for search request.
69
+ # Not used if only one filter for search is specified.
70
+ # Possible values:
71
+ #
72
+ # * <tt>:must</tt>
73
+ # Default value. Query compiles into a bool <tt>must</tt> query.
74
+ #
75
+ # Ex:
76
+ #
77
+ # UsersIndex.query(text: {name: 'Johny'}).query(range: {age: {lte: 42}})
78
+ # # => {body: {
79
+ # query: {bool: {must: [{text: {name: 'Johny'}}, {range: {age: {lte: 42}}}]}}
80
+ # }}
81
+ #
82
+ # * <tt>:should</tt>
83
+ # Query compiles into a bool <tt>should</tt> query.
84
+ #
85
+ # Ex:
86
+ #
87
+ # UsersIndex.query(text: {name: 'Johny'}).query(range: {age: {lte: 42}}).query_mode(:should)
88
+ # # => {body: {
89
+ # query: {bool: {should: [{text: {name: 'Johny'}}, {range: {age: {lte: 42}}}]}}
90
+ # }}
91
+ #
92
+ # * Any acceptable <tt>minimum_should_match</tt> value (1, '2', '75%')
93
+ # Query compiles into a bool <tt>should</tt> query with <tt>minimum_should_match</tt> set.
94
+ #
95
+ # Ex:
96
+ #
97
+ # UsersIndex.query(text: {name: 'Johny'}).query(range: {age: {lte: 42}}).query_mode('50%')
98
+ # # => {body: {
99
+ # query: {bool: {
100
+ # should: [{text: {name: 'Johny'}}, {range: {age: {lte: 42}}}],
101
+ # minimum_should_match: '50%'
102
+ # }}
103
+ # }}
104
+ #
105
+ # * <tt>:dis_max</tt>
106
+ # Query compiles into a <tt>dis_max</tt> query.
107
+ #
108
+ # Ex:
109
+ #
110
+ # UsersIndex.query(text: {name: 'Johny'}).query(range: {age: {lte: 42}}).query_mode(:dis_max)
111
+ # # => {body: {
112
+ # query: {dis_max: {queries: [{text: {name: 'Johny'}}, {range: {age: {lte: 42}}}]}}
113
+ # }}
114
+ #
115
+ # * Any Float value (0.0, 0.7, 1.0)
116
+ # Query compiles into a <tt>dis_max</tt> query with <tt>tie_breaker</tt> option set.
117
+ #
118
+ # Ex:
119
+ #
120
+ # UsersIndex.query(text: {name: 'Johny'}).query(range: {age: {lte: 42}}).query_mode(0.7)
121
+ # # => {body: {
122
+ # query: {dis_max: {
123
+ # queries: [{text: {name: 'Johny'}}, {range: {age: {lte: 42}}}],
124
+ # tie_breaker: 0.7
125
+ # }}
126
+ # }}
127
+ #
128
+ # Default value for <tt>:query_mode</tt> might be changed
129
+ # with <tt>Chewy.query_mode</tt> config option.
130
+ #
131
+ # Chewy.query_mode = :dis_max
132
+ # Chewy.query_mode = '50%'
133
+ #
134
+ def query_mode value
135
+ chain { criteria.update_options query_mode: value }
34
136
  end
35
137
 
36
- def limit(value)
37
- chain { criteria.update_search size: Integer(value) }
138
+ # Sets query compilation mode for search request.
139
+ # Not used if only one filter for search is specified.
140
+ # Possible values:
141
+ #
142
+ # * <tt>:and</tt>
143
+ # Default value. Filter compiles into an <tt>and</tt> filter.
144
+ #
145
+ # Ex:
146
+ #
147
+ # UsersIndex.filter{ name == 'Johny' }.filter{ age <= 42 }
148
+ # # => {body: {query: {filtered: {
149
+ # query: {...},
150
+ # filter: {and: [{term: {name: 'Johny'}}, {range: {age: {lte: 42}}}]}
151
+ # }}}}
152
+ #
153
+ # * <tt>:or</tt>
154
+ # Filter compiles into an <tt>or</tt> filter.
155
+ #
156
+ # Ex:
157
+ #
158
+ # UsersIndex.filter{ name == 'Johny' }.filter{ age <= 42 }.filter_mode(:or)
159
+ # # => {body: {query: {filtered: {
160
+ # query: {...},
161
+ # filter: {or: [{term: {name: 'Johny'}}, {range: {age: {lte: 42}}}]}
162
+ # }}}}
163
+ #
164
+ # * <tt>:must</tt>
165
+ # Filter compiles into a bool <tt>must</tt> filter.
166
+ #
167
+ # Ex:
168
+ #
169
+ # UsersIndex.filter{ name == 'Johny' }.filter{ age <= 42 }.filter_mode(:must)
170
+ # # => {body: {query: {filtered: {
171
+ # query: {...},
172
+ # filter: {bool: {must: [{term: {name: 'Johny'}}, {range: {age: {lte: 42}}}]}}
173
+ # }}}}
174
+ #
175
+ # * <tt>:should</tt>
176
+ # Filter compiles into a bool <tt>should</tt> filter.
177
+ #
178
+ # Ex:
179
+ #
180
+ # UsersIndex.filter{ name == 'Johny' }.filter{ age <= 42 }.filter_mode(:should)
181
+ # # => {body: {query: {filtered: {
182
+ # query: {...},
183
+ # filter: {bool: {should: [{term: {name: 'Johny'}}, {range: {age: {lte: 42}}}]}}
184
+ # }}}}
185
+ #
186
+ # * Any acceptable <tt>minimum_should_match</tt> value (1, '2', '75%')
187
+ # Filter compiles into bool <tt>should</tt> filter with <tt>minimum_should_match</tt> set.
188
+ #
189
+ # Ex:
190
+ #
191
+ # UsersIndex.filter{ name == 'Johny' }.filter{ age <= 42 }.filter_mode('50%')
192
+ # # => {body: {query: {filtered: {
193
+ # query: {...},
194
+ # filter: {bool: {
195
+ # should: [{term: {name: 'Johny'}}, {range: {age: {lte: 42}}}],
196
+ # minimum_should_match: '50%'
197
+ # }}
198
+ # }}}}
199
+ #
200
+ # Default value for <tt>:filter_mode</tt> might be changed
201
+ # with <tt>Chewy.filter_mode</tt> config option.
202
+ #
203
+ # Chewy.filter_mode = :should
204
+ # Chewy.filter_mode = '50%'
205
+ #
206
+ def filter_mode value
207
+ chain { criteria.update_options filter_mode: value }
38
208
  end
39
209
 
40
- def offset(value)
41
- chain { criteria.update_search from: Integer(value) }
210
+ # Sets elasticsearch <tt>size</tt> search request param
211
+ # Default value is set in the elasticsearch and is 10.
212
+ #
213
+ # UsersIndex.filter{ name == 'Johny' }.limit(100)
214
+ # # => {body: {
215
+ # query: {...},
216
+ # size: 100
217
+ # }}
218
+ #
219
+ def limit value
220
+ chain { criteria.update_options size: Integer(value) }
42
221
  end
43
222
 
44
- def query(params)
45
- chain { criteria.update_query params }
223
+ # Sets elasticsearch <tt>from</tt> search request param
224
+ #
225
+ # UsersIndex.filter{ name == 'Johny' }.offset(300)
226
+ # # => {body: {
227
+ # query: {...},
228
+ # from: 300
229
+ # }}
230
+ #
231
+ def offset value
232
+ chain { criteria.update_options from: Integer(value) }
46
233
  end
47
234
 
48
- def facets(params)
235
+ # Adds facets section to the search request.
236
+ # All the chained facets a merged and added to the
237
+ # search request
238
+ #
239
+ # UsersIndex.facets(tags: {terms: {field: 'tags'}}).facets(ages: {terms: {field: 'age'}})
240
+ # # => {body: {
241
+ # query: {...},
242
+ # facets: {tags: {terms: {field: 'tags'}}, ages: {terms: {field: 'age'}}}
243
+ # }}
244
+ #
245
+ def facets params
49
246
  chain { criteria.update_facets params }
50
247
  end
51
248
 
52
- def filter(params)
249
+ # Adds one or more query to the search request
250
+ # Internally queries are stored as an array
251
+ # While the full query compilation this array compiles
252
+ # according to <tt>:query_mode</tt> option value
253
+ #
254
+ # By default it joines inside <tt>must</tt> query
255
+ # See <tt>#query_mode</tt> chainable method for more info.
256
+ #
257
+ # UsersIndex.query(text: {name: 'Johny'}).query(range: {age: {lte: 42}})
258
+ # UsersIndex::User.query(text: {name: 'Johny'}).query(range: {age: {lte: 42}})
259
+ # # => {body: {
260
+ # query: {bool: {must: [{text: {name: 'Johny'}}, {range: {age: {lte: 42}}}]}}
261
+ # }}
262
+ #
263
+ # If only one query was specified, it will become a result
264
+ # query as is, without joining.
265
+ #
266
+ # UsersIndex.query(text: {name: 'Johny'})
267
+ # # => {body: {
268
+ # query: {text: {name: 'Johny'}}
269
+ # }}
270
+ #
271
+ def query params
272
+ chain { criteria.update_queries params }
273
+ end
274
+
275
+ # Adds one or more filter to the search request
276
+ # Internally filters are stored as an array
277
+ # While the full query compilation this array compiles
278
+ # according to <tt>:filter_mode</tt> option value
279
+ #
280
+ # By default it joines inside <tt>and</tt> filter
281
+ # See <tt>#filter_mode</tt> chainable method for more info.
282
+ #
283
+ # Also this method supports block DSL.
284
+ # See <tt>Chewy::Query::Context</tt> for more info.
285
+ #
286
+ # UsersIndex.filter(term: {name: 'Johny'}).filter(range: {age: {lte: 42}})
287
+ # UsersIndex::User.filter(term: {name: 'Johny'}).filter(range: {age: {lte: 42}})
288
+ # UsersIndex.filter{ name == 'Johny' }.filter{ age <= 42 }
289
+ # # => {body: {query: {filtered: {
290
+ # query: {...},
291
+ # filter: {and: [{term: {name: 'Johny'}}, {range: {age: {lte: 42}}}]}
292
+ # }}}}
293
+ #
294
+ # If only one filter was specified, it will become a result
295
+ # filter as is, without joining.
296
+ #
297
+ # UsersIndex.filter(term: {name: 'Johny'})
298
+ # # => {body: {query: {filtered: {
299
+ # query: {...},
300
+ # filter: {term: {name: 'Johny'}}
301
+ # }}}}
302
+ #
303
+ def filter params = nil, &block
304
+ params = Context.new(&block).__render__ if block
53
305
  chain { criteria.update_filters params }
54
306
  end
55
307
 
56
- def order(*params)
308
+ # Sets search request sorting
309
+ #
310
+ # UsersIndex.order(:first_name, :last_name).order(age: :desc).order(price: {order: :asc, mode: :avg})
311
+ # # => {body: {
312
+ # query: {...},
313
+ # sort: ['first_name', 'last_name', {age: 'desc'}, {price: {order: 'asc', mode: 'avg'}}]
314
+ # }}
315
+ #
316
+ def order *params
57
317
  chain { criteria.update_sort params }
58
318
  end
59
319
 
60
- def reorder(*params)
320
+ # Cleans up previous search sorting and sets the new one
321
+ #
322
+ # UsersIndex.order(:first_name, :last_name).order(age: :desc).reorder(price: {order: :asc, mode: :avg})
323
+ # # => {body: {
324
+ # query: {...},
325
+ # sort: [{price: {order: 'asc', mode: 'avg'}}]
326
+ # }}
327
+ #
328
+ def reorder *params
61
329
  chain { criteria.update_sort params, purge: true }
62
330
  end
63
331
 
64
- def only(*params)
332
+ # Sets search request field list
333
+ #
334
+ # UsersIndex.only(:first_name, :last_name).only(:age)
335
+ # # => {body: {
336
+ # query: {...},
337
+ # fields: ['first_name', 'last_name', 'age']
338
+ # }}
339
+ #
340
+ def only *params
65
341
  chain { criteria.update_fields params }
66
342
  end
67
343
 
68
- protected
69
-
70
- def initialize_clone(other)
71
- @criteria = other.criteria.clone
72
- reset
344
+ # Cleans up previous search field list and sets the new one
345
+ #
346
+ # UsersIndex.only(:first_name, :last_name).only!(:age)
347
+ # # => {body: {
348
+ # query: {...},
349
+ # fields: ['age']
350
+ # }}
351
+ #
352
+ def only! *params
353
+ chain { criteria.update_fields params, purge: true }
73
354
  end
74
355
 
75
- private
76
-
77
- def chain &block
78
- clone.tap { |q| q.instance_eval(&block) }
356
+ # Specify types participating in the search result
357
+ # Works via <tt>types</tt> filter. Always merged with another filters
358
+ # with the <tt>and</tt> filter.
359
+ #
360
+ # UsersIndex.types(:admin, :manager).filters{ name == 'Johny' }.filters{ age <= 42 }
361
+ # # => {body: {query: {filtered: {
362
+ # query: {...},
363
+ # filter: {and: [
364
+ # {or: [
365
+ # {type: {value: 'admin'}},
366
+ # {type: {value: 'manager'}}
367
+ # ]},
368
+ # {term: {name: 'Johny'}},
369
+ # {range: {age: {lte: 42}}}
370
+ # ]}
371
+ # }}}}
372
+ #
373
+ # UsersIndex.types(:admin, :manager).filters{ name == 'Johny' }.filters{ age <= 42 }.filter_mode(:or)
374
+ # # => {body: {query: {filtered: {
375
+ # query: {...},
376
+ # filter: {and: [
377
+ # {or: [
378
+ # {type: {value: 'admin'}},
379
+ # {type: {value: 'manager'}}
380
+ # ]},
381
+ # {or: [
382
+ # {term: {name: 'Johny'}},
383
+ # {range: {age: {lte: 42}}}
384
+ # ]}
385
+ # ]}
386
+ # }}}}
387
+ #
388
+ def types *params
389
+ if params.any?
390
+ chain { criteria.update_types params }
391
+ else
392
+ @types
393
+ end
79
394
  end
80
395
 
81
- def reset
82
- @_response, @_results = nil
396
+ # Acts the same way as <tt>types</tt>, but cleans up previously set types
397
+ #
398
+ # UsersIndex.types(:admin).types!(:manager)
399
+ # # => {body: {query: {filtered: {
400
+ # query: {...},
401
+ # filter: {type: {value: 'manager'}}
402
+ # }}}}
403
+ #
404
+ def types! *params
405
+ chain { criteria.update_types params, purge: true }
83
406
  end
84
407
 
85
- def types
86
- @types ||= Array.wrap(options[:type] || options[:types])
408
+ # Merges two queries.
409
+ # Merges all the values in criteria with the same rules as values added manually.
410
+ #
411
+ # scope1 = UsersIndex.filter{ name == 'Johny' }
412
+ # scope2 = UsersIndex.filter{ age <= 42 }
413
+ # scope3 = UsersIndex.filter{ name == 'Johny' }.filter{ age <= 42 }
414
+ #
415
+ # scope1.merge(scope2) == scope3 # => true
416
+ #
417
+ def merge other
418
+ chain { criteria.merge!(other.criteria) }
87
419
  end
88
420
 
89
- def _filters
90
- if criteria.filters.many?
91
- {and: criteria.filters}
92
- else
93
- criteria.filters.first
94
- end
95
- end
421
+ protected
96
422
 
97
- def _request_query
98
- if criteria.filters?
99
- {query: {
100
- filtered: {
101
- query: criteria.query? ? criteria.query : {match_all: {}},
102
- filter: _filters
103
- }
104
- }}
105
- elsif criteria.query?
106
- {query: criteria.query}
107
- else
108
- {}
109
- end
423
+ def initialize_clone other
424
+ @criteria = other.criteria.clone
425
+ reset
110
426
  end
111
427
 
112
- def _request_body
113
- body = _request_query
114
- body = body.merge!(facets: criteria.facets) if criteria.facets?
115
- body = body.merge!(sort: criteria.sort) if criteria.sort?
116
- body = body.merge!(fields: criteria.fields) if criteria.fields?
117
- {body: body}
428
+ private
429
+
430
+ def chain &block
431
+ clone.tap { |q| q.instance_eval(&block) }
118
432
  end
119
433
 
120
- def _request_target
121
- {index: index.index_name, type: types}
434
+ def reset
435
+ @_request, @_response, @_results = nil
122
436
  end
123
437
 
124
438
  def _request
125
- [criteria.search, _request_target, _request_body].inject(:merge)
439
+ @_request ||= criteria.request_body.merge(index: index.index_name, type: types)
126
440
  end
127
441
 
128
442
  def _response
129
- @_response ||= index.client.search(_request)
443
+ @_response ||= begin
444
+ ActiveSupport::Notifications.instrument 'search_query.chewy', request: _request, index: index do
445
+ index.client.search(_request)
446
+ end
447
+ end
130
448
  end
131
449
 
132
450
  def _results
@@ -134,7 +452,7 @@ module Chewy
134
452
  attributes = hit['_source'] || hit['fields'] || {}
135
453
  attributes.reverse_merge!(id: hit['_id']).merge!(_score: hit['_score'])
136
454
  attributes.merge!(_explain: hit['_explanation']) if hit['_explanation']
137
- index.types[hit['_type']].new attributes
455
+ index.type_hash[hit['_type']].new attributes
138
456
  end
139
457
  end
140
458
  end