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
@@ -0,0 +1,174 @@
1
+ require 'chewy/query/nodes/base'
2
+ require 'chewy/query/nodes/expr'
3
+ require 'chewy/query/nodes/field'
4
+ require 'chewy/query/nodes/bool'
5
+ require 'chewy/query/nodes/and'
6
+ require 'chewy/query/nodes/or'
7
+ require 'chewy/query/nodes/not'
8
+ require 'chewy/query/nodes/raw'
9
+ require 'chewy/query/nodes/exists'
10
+ require 'chewy/query/nodes/missing'
11
+ require 'chewy/query/nodes/range'
12
+ require 'chewy/query/nodes/prefix'
13
+ require 'chewy/query/nodes/regexp'
14
+ require 'chewy/query/nodes/equal'
15
+ require 'chewy/query/nodes/query'
16
+ require 'chewy/query/nodes/script'
17
+
18
+ module Chewy
19
+ class Query
20
+ # Context provides simplified DSL functionality for filters declaring.
21
+ # You can use logic operations <tt>&</tt> and <tt>|</tt> to concat
22
+ # expressions.
23
+ #
24
+ # UsersIndex.filter{ (article.title =~ /Honey/) & (age < 42) & !rate }
25
+ #
26
+ #
27
+ class Context
28
+ def initialize &block
29
+ @block = block
30
+ @outer = eval('self', block.binding)
31
+ end
32
+
33
+ # Outer scope call
34
+ # Block evaluates in the external context
35
+ #
36
+ # def name
37
+ # 'Friend'
38
+ # end
39
+ #
40
+ # UsersIndex.filter{ name == o{ name } } # => {filter: {term: {name: 'Friend'}}}
41
+ #
42
+ def o &block
43
+ @outer.instance_exec(&block)
44
+ end
45
+
46
+ # Returns field node
47
+ # Used if method_missing is not working by some reason.
48
+ # Additional expression options might be passed as second argument hash.
49
+ #
50
+ # UsersIndex.filter{ f(:name) == 'Name' } == UsersIndex.filter{ name == 'Name' } # => true
51
+ # UsersIndex.filter{ f(:name, execution: :bool) == ['Name1', 'Name2'] } ==
52
+ # UsersIndex.filter{ name(execution: :bool) == ['Name1', 'Name2'] } # => true
53
+ #
54
+ # Supports block for getting field name from the outer scope
55
+ #
56
+ # def field
57
+ # :name
58
+ # end
59
+ #
60
+ # UsersIndex.filter{ f{ field } == 'Name' } == UsersIndex.filter{ name == 'Name' } # => true
61
+ #
62
+ def f name = nil, *args, &block
63
+ name = block ? o(&block) : name
64
+ Nodes::Field.new name, *args
65
+ end
66
+
67
+ # Returns script filter
68
+ # Just script filter. Supports additional params.
69
+ #
70
+ # UsersIndex.filter{ s('doc["num1"].value > 1') }
71
+ # UsersIndex.filter{ s('doc["num1"].value > param1', param1: 42) }
72
+ #
73
+ # Supports block for getting script from the outer scope
74
+ #
75
+ # def script
76
+ # 'doc["num1"].value > param1 || 1'
77
+ # end
78
+ #
79
+ # UsersIndex.filter{ s{ script } } == UsersIndex.filter{ s('doc["num1"].value > 1') } # => true
80
+ # UsersIndex.filter{ s(param1: 42) { script } } == UsersIndex.filter{ s('doc["num1"].value > 1', param1: 42) } # => true
81
+ #
82
+ def s *args, &block
83
+ params = args.extract_options!
84
+ script = block ? o(&block) : args.first
85
+ Nodes::Script.new script, params
86
+ end
87
+
88
+ # Returns query filter
89
+ #
90
+ # UsersIndex.filter{ q(query_string: {query: 'name: hello'}) }
91
+ #
92
+ # Supports block for getting query from the outer scope
93
+ #
94
+ # def query
95
+ # {query_string: {query: 'name: hello'}}
96
+ # end
97
+ #
98
+ # UsersIndex.filter{ q{ query } } == UsersIndex.filter{ q(query_string: {query: 'name: hello'}) } # => true
99
+ #
100
+ def q query = nil, &block
101
+ Nodes::Query.new block ? o(&block) : query
102
+ end
103
+
104
+ # Returns raw expression
105
+ # Same as filter with arguments instead of block, but can participate in expressions
106
+ #
107
+ # UsersIndex.filter{ r(term: {name: 'Name'}) }
108
+ # UsersIndex.filter{ r(term: {name: 'Name'}) & (age < 42) }
109
+ #
110
+ # Supports block for getting raw filter from the outer scope
111
+ #
112
+ # def filter
113
+ # {term: {name: 'Name'}}
114
+ # end
115
+ #
116
+ # UsersIndex.filter{ r{ filter } } == UsersIndex.filter{ r(term: {name: 'Name'}) } # => true
117
+ # UsersIndex.filter{ r{ filter } } == UsersIndex.filter(term: {name: 'Name'}) # => true
118
+ #
119
+ def r raw = nil, &block
120
+ Nodes::Raw.new block ? o(&block) : raw
121
+ end
122
+
123
+ # Bool filter chainable methods
124
+ # Used to create bool query. Nodes are passed as arguments.
125
+ #
126
+ # UsersIndex.filter{ must(age < 42, name == 'Name') }
127
+ # UsersIndex.filter{ should(age < 42, name == 'Name') }
128
+ # UsersIndex.filter{ must(age < 42).should(name == 'Name1', name == 'Name2') }
129
+ # UsersIndex.filter{ should_not(age >= 42).must(name == 'Name1') }
130
+ #
131
+ %w(must must_not should).each do |method|
132
+ define_method method do |*exprs|
133
+ Nodes::Bool.new.send(method, *exprs)
134
+ end
135
+ end
136
+
137
+ # Creates field or exists node
138
+ # Additional options for further expression might be passed as hash
139
+ #
140
+ # UsersIndex.filter{ name == 'Name' } == UsersIndex.filter(term: {name: 'Name'}) # => true
141
+ # UsersIndex.filter{ name? } == UsersIndex.filter(exists: {term: 'name'}) # => true
142
+ # UsersIndex.filter{ name(execution: :bool) == ['Name1', 'Name2'] } ==
143
+ # UsersIndex.filter(terms: {name: ['Name1', 'Name2'], execution: :bool}) # => true
144
+ #
145
+ # Also field names might be chained to use dot-notation for ES field names
146
+ #
147
+ # UsersIndex.filter{ article.title =~ 'Hello' }
148
+ # UsersIndex.filter{ article.tags? }
149
+ #
150
+ def method_missing method, *args, &block
151
+ method = method.to_s
152
+ if method =~ /\?\Z/
153
+ Nodes::Exists.new method.gsub(/\?\Z/, '')
154
+ else
155
+ f method, *args
156
+ end
157
+ end
158
+
159
+ # Evaluates context block, returns top node.
160
+ # For internal usage.
161
+ #
162
+ def __result__
163
+ instance_exec(&@block)
164
+ end
165
+
166
+ # Renders evaluated filters.
167
+ # For internal usage.
168
+ #
169
+ def __render__
170
+ __result__.__render__ # haha, wtf?
171
+ end
172
+ end
173
+ end
174
+ end
@@ -1,53 +1,44 @@
1
1
  module Chewy
2
2
  class Query
3
3
  class Criteria
4
- STORAGES = [:search, :query, :facets, :filters, :sort, :fields]
4
+ STORAGES = [:options, :queries, :facets, :filters, :sort, :fields, :types]
5
5
 
6
- def ==(other)
7
- storages == other.storages
6
+ def initialize options = {}
7
+ @options = options.merge(query_mode: Chewy.query_mode, filter_mode: Chewy.filter_mode)
8
8
  end
9
9
 
10
- def storages
11
- STORAGES.map { |storage| send(storage) }
10
+ def == other
11
+ other.is_a?(self.class) && storages == other.storages
12
12
  end
13
13
 
14
- [:search, :query, :facets].each do |storage|
15
- class_eval <<-METHODS, __FILE__, __LINE__ + 1
16
- def #{storage}
17
- @#{storage} ||= {}
18
- end
19
- METHODS
20
- end
21
-
22
- [:filters, :sort, :fields].each do |storage|
23
- class_eval <<-METHODS, __FILE__, __LINE__ + 1
24
- def #{storage}
25
- @#{storage} ||= []
26
- end
27
- METHODS
14
+ { (STORAGES - [:options, :facets]) => '[]', [:options, :facets] => '{}' }.each do |storages, default|
15
+ storages.each do |storage|
16
+ class_eval <<-METHODS, __FILE__, __LINE__ + 1
17
+ def #{storage}
18
+ @#{storage} ||= #{default}
19
+ end
20
+ METHODS
21
+ end
28
22
  end
29
23
 
30
24
  STORAGES.each do |storage|
31
- class_eval <<-METHODS, __FILE__, __LINE__ + 1
32
- def #{storage}?
33
- #{storage}.any?
34
- end
35
- METHODS
36
- end
37
-
38
- def update_search(modifer)
39
- search.merge!(modifer)
25
+ define_method "#{storage}?" do
26
+ send(storage).any?
27
+ end
40
28
  end
41
29
 
42
- def update_query(modifer)
43
- query.merge!(modifer)
30
+ def update_options(modifer)
31
+ options.merge!(modifer)
44
32
  end
45
33
 
46
-
47
34
  def update_facets(modifer)
48
35
  facets.merge!(modifer)
49
36
  end
50
37
 
38
+ def update_queries(modifer)
39
+ @queries = queries + Array.wrap(modifer).delete_if(&:blank?)
40
+ end
41
+
51
42
  def update_filters(modifer)
52
43
  @filters = filters + Array.wrap(modifer).delete_if(&:blank?)
53
44
  end
@@ -60,13 +51,42 @@ module Chewy
60
51
  @sort = sort + modifer
61
52
  end
62
53
 
63
- def update_fields(modifer, options = {})
64
- @fields = nil if options[:purge]
65
- @fields = (fields + Array.wrap(modifer).flatten.map(&:to_s).delete_if(&:blank?)).uniq
54
+ %w(fields types).each do |storage|
55
+ define_method "update_#{storage}" do |modifer, options = {}|
56
+ variable = "@#{storage}"
57
+ instance_variable_set(variable, nil) if options[:purge]
58
+ modifer = send(storage) | Array.wrap(modifer).flatten.map(&:to_s).delete_if(&:blank?)
59
+ instance_variable_set(variable, modifer)
60
+ end
61
+ end
62
+
63
+ def merge! other
64
+ STORAGES.each do |storage|
65
+ send("update_#{storage}", other.send(storage))
66
+ end
67
+ self
68
+ end
69
+
70
+ def merge other
71
+ clone.merge!(other)
72
+ end
73
+
74
+ def request_body
75
+ body = (_request_query || {}).tap do |body|
76
+ body.merge!(facets: facets) if facets?
77
+ body.merge!(sort: sort) if sort?
78
+ body.merge!(fields: fields) if fields?
79
+ end
80
+
81
+ {body: body.merge!(_request_options)}
66
82
  end
67
83
 
68
84
  protected
69
85
 
86
+ def storages
87
+ STORAGES.map { |storage| send(storage) }
88
+ end
89
+
70
90
  def initialize_clone(other)
71
91
  STORAGES.each do |storage|
72
92
  value = other.send(storage)
@@ -76,6 +96,79 @@ module Chewy
76
96
  end
77
97
  end
78
98
  end
99
+
100
+ def _request_options
101
+ options.slice(:size, :from, :explain)
102
+ end
103
+
104
+ def _request_query
105
+ request_filter = _request_filter
106
+ request_query = _queries_join(queries, options[:query_mode])
107
+
108
+ if request_filter
109
+ {query: {
110
+ filtered: {
111
+ query: request_query ? request_query : {match_all: {}},
112
+ filter: request_filter
113
+ }
114
+ }}
115
+ elsif request_query
116
+ {query: request_query}
117
+ end
118
+ end
119
+
120
+ def _request_filter
121
+ filter_mode = options[:filter_mode]
122
+ request_filter = if filter_mode == :and
123
+ filters
124
+ else
125
+ [_filters_join(filters, filter_mode)]
126
+ end
127
+
128
+ _filters_join([_request_types, *request_filter], :and)
129
+ end
130
+
131
+ def _request_types
132
+ _filters_join(types.map { |type| {type: {value: type}} }, :or)
133
+ end
134
+
135
+ def _queries_join queries, logic
136
+ queries = queries.compact
137
+
138
+ if queries.many?
139
+ case logic
140
+ when :dis_max
141
+ {dis_max: {queries: queries}}
142
+ when :must, :should
143
+ {bool: {logic => queries}}
144
+ else
145
+ if logic.is_a?(Float)
146
+ {dis_max: {queries: queries, tie_breaker: logic}}
147
+ else
148
+ {bool: {should: queries, minimum_should_match: logic}}
149
+ end
150
+ end
151
+ else
152
+ queries.first
153
+ end
154
+ end
155
+
156
+ def _filters_join filters, logic
157
+ filters = filters.compact
158
+
159
+ if filters.many?
160
+ case logic
161
+ when :and, :or
162
+ {logic => filters}
163
+ when :must, :should
164
+ {bool: {logic => filters}}
165
+ else
166
+ {bool: {should: filters, minimum_should_match: logic}}
167
+ end
168
+ else
169
+ filters.first
170
+ end
171
+ end
79
172
  end
80
173
  end
81
174
  end
@@ -4,23 +4,23 @@ module Chewy
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  def load(options = {})
7
- ::Kaminari.paginate_array(_load_objects(options),
8
- limit: limit_value, offset: offset_value, total_count: total_count)
7
+ if defined?(::Kaminari)
8
+ ::Kaminari.paginate_array(_load_objects(options),
9
+ limit: limit_value, offset: offset_value, total_count: total_count)
10
+ else
11
+ _load_objects(options)
12
+ end
9
13
  end
10
14
 
11
15
  private
12
16
 
13
17
  def _load_objects(options)
14
18
  loaded_objects = Hash[_results.group_by(&:class).map do |type, objects|
15
- model = type.adapter.model
16
- scope = model.where(id: objects.map(&:id))
17
- additional_scope = options[:scopes][type.type_name.to_sym] if options[:scopes]
18
- scope = scope.instance_eval(&additional_scope) if additional_scope
19
-
20
- [type, scope.index_by(&:id)]
19
+ loaded = type.adapter.load(objects, options[type.type_name.to_sym] || {})
20
+ [type, loaded.index_by.with_index { |loaded, i| objects[i] }]
21
21
  end]
22
22
 
23
- _results.map { |result| loaded_objects[result.class][result.id.to_i] }.compact
23
+ _results.map { |result| loaded_objects[result.class][result] }
24
24
  end
25
25
  end
26
26
  end
@@ -0,0 +1,25 @@
1
+ module Chewy
2
+ class Query
3
+ module Nodes
4
+ class And < Expr
5
+ def initialize *nodes
6
+ @options = nodes.extract_options!
7
+ @nodes = nodes.flatten.map { |node| node.is_a?(self.class) ? node.__nodes__ : node }.flatten
8
+ end
9
+
10
+ def __nodes__
11
+ @nodes
12
+ end
13
+
14
+ def __render__
15
+ nodes = @nodes.map(&:__render__)
16
+ if @options.key?(:cache)
17
+ {and: {filters: nodes, _cache: !!@options[:cache]}}
18
+ else
19
+ {and: nodes}
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ module Chewy
2
+ class Query
3
+ module Nodes
4
+ class Base
5
+ def render
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def eql? other
10
+ other.is_a?(self.class) && instance_variables.all? do |ivar|
11
+ instance_variable_get(ivar).eql? other.instance_variable_get(ivar)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end