chewy 0.0.1 → 0.1.0

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 (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