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