stretchy 0.4.6 → 0.4.7

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.
@@ -3,7 +3,7 @@ module Stretchy
3
3
  # A Clause is the basic unit of Stretchy's chainable query syntax.
4
4
  # Think of it as a state machine, with transitions between states
5
5
  # being handled by methods that return another Clause. When you
6
- # call the `where` method, it stores the params passed as an
6
+ # call the `where` method, it stores the params passed as an
7
7
  # internal representation, to be compiled down to the Elastic query
8
8
  # syntax, and returns a WhereClause. The WhereClause reflects the
9
9
  # current state of the query, and gives you access to methods like
@@ -21,18 +21,18 @@ module Stretchy
21
21
  delegate [:request, :response, :results, :ids, :hits, :query,
22
22
  :took, :shards, :total, :max_score, :total_pages] => :query_results
23
23
  delegate [:to_search] => :base
24
- delegate [:where, :range, :geo, :terms, :not] => :build_where
25
- delegate [:match, :fulltext, :more_like] => :build_match
24
+ delegate [:where, :range, :geo, :terms, :not, :filter] => :build_where
25
+ delegate [:match, :fulltext, :more_like, :query] => :build_match
26
26
 
27
27
  #
28
28
  # Generates a chainable query. The only required option for the
29
29
  # first initialization is `:type` , which specifies what type
30
30
  # to query on your index.
31
- #
31
+ #
32
32
  # @overload initialize(base_or_opts, params)
33
33
  # @param base [Base] another clause to copy attributes from
34
34
  # @param params [Hash] params to set on the new state
35
- #
35
+ #
36
36
  # @overload initialize(base_or_opts)
37
37
  # @option base_or_opts [String] :index The Elastic index to query
38
38
  # @option base_or_opts [String] :type The Lucene type to query on
@@ -49,7 +49,7 @@ module Stretchy
49
49
  end
50
50
  end
51
51
 
52
- #
52
+ #
53
53
  # Exits any state the query is in (boost, inverse, should, etc)
54
54
  # and returns to the root query state. You can use this before
55
55
  # calling `.where` or other overridden methods to ensure they
@@ -59,44 +59,44 @@ module Stretchy
59
59
  # End-of-chain methods (such as `.boost.where.not()`) should
60
60
  # always return to the root state, and state is not
61
61
  # something you should have to think about.
62
- #
62
+ #
63
63
  # @return [Base] Continue the query chain from the root state
64
64
  #
65
65
  def root
66
66
  Base.new(base)
67
67
  end
68
68
 
69
- #
69
+ #
70
70
  # Sets how many results to return, similar to
71
71
  # ActiveRecord's limit method.
72
- #
72
+ #
73
73
  # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html Elastic Docs - Request Body Search
74
- #
74
+ #
75
75
  # @param num [Integer] How many results to return
76
- #
76
+ #
77
77
  # @return [self]
78
78
  def limit(num)
79
79
  base.limit = num
80
80
  self
81
81
  end
82
82
 
83
- #
83
+ #
84
84
  # Accessor for `@limit`
85
- #
85
+ #
86
86
  # @return [Integer] Value of `@limit`
87
87
  def get_limit
88
88
  base.limit
89
89
  end
90
90
  alias :limit_value :get_limit
91
91
 
92
- #
92
+ #
93
93
  # Sets the offset to start returning results at.
94
94
  # Corresponds to Elastic's "from" parameter
95
- #
95
+ #
96
96
  # @param num [Integer] Offset for query
97
- #
97
+ #
98
98
  # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html Elastic Docs - Request Body Search
99
- #
99
+ #
100
100
  # @return [self]
101
101
  def offset(num)
102
102
  base.offset = num
@@ -104,19 +104,19 @@ module Stretchy
104
104
  end
105
105
  alias :per_page :offset
106
106
 
107
- #
107
+ #
108
108
  # Accessor for `@offset`
109
- #
109
+ #
110
110
  # @return [Integer] Offset for query
111
111
  def get_offset
112
112
  base.offset
113
113
  end
114
114
 
115
- #
115
+ #
116
116
  # Allows pagination via Kaminari-like accessor
117
117
  # @param num [Integer] Page number. Natural numbers only, **this is not zero-indexed**
118
118
  # @option per_page [Integer] :per_page (DEFAULT_LIMIT) Number of results per page
119
- #
119
+ #
120
120
  # @return [self] Allows continuing the query chain
121
121
  def page(num, params = {})
122
122
  base.limit = params[:limit] || params[:per_page] || get_limit
@@ -124,28 +124,28 @@ module Stretchy
124
124
  self
125
125
  end
126
126
 
127
- #
127
+ #
128
128
  # Accessor for current page
129
- #
129
+ #
130
130
  # @return [Integer] (offset / limit).ceil
131
131
  def get_page
132
132
  base.page
133
133
  end
134
134
  alias :current_page :get_page
135
135
 
136
- #
136
+ #
137
137
  # Select fields for Elasticsearch to return
138
- #
139
- # By default, Stretchy will return the entire _source
138
+ #
139
+ # By default, Stretchy will return the entire _source
140
140
  # for each document. If you call `.fields` with no
141
141
  # arguments or an empty array, Stretchy will pass
142
142
  # an empty array and only the "_type" and "_id"
143
143
  # fields will be returned.
144
- #
144
+ #
145
145
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-fields.html Elastic Docs - Fields
146
- #
146
+ #
147
147
  # @param new_fields [Array] Fields elasticsearch should return
148
- #
148
+ #
149
149
  # @return [self] Allows continuing the query chain
150
150
  def fields(*args)
151
151
  base.fields ||= []
@@ -153,20 +153,20 @@ module Stretchy
153
153
  self
154
154
  end
155
155
 
156
- #
156
+ #
157
157
  # Accessor for fields Elasticsearch will return
158
- #
158
+ #
159
159
  # @return [Array] List of fields in the current query
160
160
  def get_fields
161
161
  base.fields
162
162
  end
163
163
 
164
- #
164
+ #
165
165
  # Tells the search to explain the scoring
166
166
  # mechanism for each document.
167
- #
167
+ #
168
168
  # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-explain.html Elastic Docs - Request Body Search (explain)
169
- #
169
+ #
170
170
  # @return [self] Allows continuing the query chain
171
171
  def explain
172
172
  base.explain = true
@@ -177,19 +177,19 @@ module Stretchy
177
177
  !!base.explain
178
178
  end
179
179
 
180
- #
180
+ #
181
181
  # Filter for documents that do not match the specified fields and values
182
- #
182
+ #
183
183
  # @overload not(params)
184
184
  # @param [String] A string that must not be matched anywhere in the document
185
185
  # @overload not(params)
186
186
  # @param [Hash] A hash of fields and strings or terms that must not be matched in those fields
187
- #
187
+ #
188
188
  # @return [MatchClause, WhereClause] inverted query state with match filters applied
189
189
  #
190
190
  # @see {MatchClause#not}
191
191
  # @see {WhereClause#not}
192
- #
192
+ #
193
193
  def not(params = {}, options = {})
194
194
  if params.is_a?(String)
195
195
  build_match.not(params, options)
@@ -198,42 +198,42 @@ module Stretchy
198
198
  end
199
199
  end
200
200
 
201
- #
201
+ #
202
202
  # Used for boosting the relevance score of
203
203
  # search results. `match` and `where` clauses
204
204
  # added after `boost` will be applied as
205
205
  # boosting functions instead of filters
206
- #
206
+ #
207
207
  # @example Boost documents that match a filter
208
208
  # query.boost.where('post.user_id' => current_user.id)
209
- #
209
+ #
210
210
  # @example Boost documents that match fulltext search
211
211
  # query.boost.match('user search terms')
212
- #
212
+ #
213
213
  # @return [BoostClause] query in boost context
214
- #
214
+ #
215
215
  # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html Elastic Docs - Function Score Query
216
216
  def boost
217
217
  BoostClause.new(base)
218
218
  end
219
219
 
220
- #
220
+ #
221
221
  # Adds filters in the `should` context. Operates just like
222
- # {#where}, but these filters only serve to add to the
222
+ # {#where}, but these filters only serve to add to the
223
223
  # relevance score of the returned documents, rather than
224
224
  # being required to match.
225
- #
225
+ #
226
226
  # @overload should(params)
227
- # @param [String] A string to match via full-text search
227
+ # @param [String] A string to match via full-text search
228
228
  # anywhere in the document.
229
- #
229
+ #
230
230
  # @overload should(params)
231
231
  # @param [Hash] Options to generate filters.
232
- #
232
+ #
233
233
  # @return [WhereClause] current query state with should clauses applied
234
- #
234
+ #
235
235
  # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html Elastic Docs - Bool Query
236
- #
236
+ #
237
237
  # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-filter.html Elastic Docs - Bool Filter
238
238
  def should(params = {}, options = {})
239
239
  if params.is_a?(Hash)
@@ -243,11 +243,11 @@ module Stretchy
243
243
  end
244
244
  end
245
245
 
246
- #
246
+ #
247
247
  # Allows adding raw aggregation JSON to your
248
248
  # query
249
249
  # @param params = {} [Hash] JSON to aggregate on
250
- #
250
+ #
251
251
  # @return [self] Allows continuing the query chain
252
252
  def aggregations(params = {})
253
253
  base.aggregate_builder = base.aggregate_builder.merge(params)
@@ -260,19 +260,19 @@ module Stretchy
260
260
  end
261
261
  alias :get_aggs :get_aggregations
262
262
 
263
- #
263
+ #
264
264
  # Accessor for `@inverse`
265
- #
265
+ #
266
266
  # @return [true, false] If current context is inverse
267
267
  def inverse?
268
268
  !!@inverse
269
269
  end
270
270
 
271
- #
271
+ #
272
272
  # The Results object for this query, which handles
273
273
  # sending the search request and providing convienent
274
274
  # accessors for the response.
275
- #
275
+ #
276
276
  # @return [Results::Base] The results returned from Elastic
277
277
  def query_results
278
278
  @query_results ||= Stretchy::Results::Base.new(base)
@@ -292,6 +292,12 @@ module Stretchy
292
292
  params.is_a?(String) ? { '_all' => params } : params
293
293
  end
294
294
 
295
+ def merge_state(options = {})
296
+ options[:should] = true if options[:should].nil? && should?
297
+ options[:inverse] = true if options[:inverse].nil? && inverse?
298
+ options
299
+ end
300
+
295
301
  end
296
302
  end
297
303
  end
@@ -2,35 +2,35 @@ require 'stretchy/clauses/base'
2
2
 
3
3
  module Stretchy
4
4
  module Clauses
5
- #
5
+ #
6
6
  # A Boost clause encapsulates the boost query state. It
7
7
  # basically says "the next where / range / match filter
8
8
  # will be used to boost a document's score instead of
9
9
  # selecting documents to return."
10
- #
10
+ #
11
11
  # Calling `.boost` by itself doesn't do anything, but
12
12
  # the next method (`.near`, `.match`, etc) will specify
13
13
  # a boost using the same syntax as other clauses. These
14
- # methods take a `:weight` parameter specifying the weight
14
+ # methods take a `:weight` parameter specifying the weight
15
15
  # to assign that boost.
16
- #
16
+ #
17
17
  # @author [atevans]
18
- #
18
+ #
19
19
  class BoostClause < Base
20
20
 
21
21
  extend Forwardable
22
22
 
23
- delegate [:geo, :range] => :where
24
- delegate [:fulltext] => :match
23
+ delegate [:geo, :range, :filter] => :where
24
+ delegate [:fulltext, :query] => :match
25
25
 
26
- #
26
+ #
27
27
  # Changes query state to "match" in the context
28
28
  # of boosting. Options here work the same way as
29
29
  # {MatchClause#initialize}, but the combined query
30
30
  # will be applied as a boost function.
31
- #
31
+ #
32
32
  # @param params = {} [Hash] params for full text matching
33
- #
33
+ #
34
34
  # @return [BoostMatchClause] query with boost match state
35
35
  def match(params = {}, options = {})
36
36
  clause = BoostMatchClause.new(base)
@@ -38,23 +38,21 @@ module Stretchy
38
38
  clause
39
39
  end
40
40
 
41
- #
41
+ #
42
42
  # Changes query state to "where" in the context
43
43
  # of boosting. Works the same way as {WhereClause},
44
44
  # but applies the generated filters as a boost
45
- # function.
46
- #
45
+ # function.
46
+ #
47
47
  # @param params = {} [Hash] Filters to use in this boost.
48
- #
48
+ #
49
49
  # @return [BoostWhereClause] Query state with boost filters applied
50
- #
50
+ #
51
51
  def where(params = {}, options = {})
52
52
  BoostWhereClause.new(base).boost_where(params, options)
53
53
  end
54
- alias :filter :where
55
54
 
56
-
57
- #
55
+ #
58
56
  # Adds a boost based on the value in the specified field.
59
57
  # You can pass more than one field as arguments, and
60
58
  # you can also pass the `factor` and `modifier` options
@@ -66,16 +64,16 @@ module Stretchy
66
64
  #
67
65
  # @example Adding two fields with options
68
66
  # query = query.boost.field(:numeric_field, :other_field, factor: 7, modifier: :log2p)
69
- #
67
+ #
70
68
  # @param *args [Arguments] Fields to add to the document score
71
69
  # @param options = {} [Hash] Options to pass to the field_value_factor boost
72
- #
70
+ #
73
71
  # @return [self] Query state with field boosts applied
74
72
  #
75
73
  # @see https://www.elastic.co/guide/en/elasticsearch/guide/current/boosting-by-popularity.html Elasticsearch guide on boosting by popularity
76
74
  #
77
75
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#_field_value_factor Elasticsearch field value factor reference
78
- #
76
+ #
79
77
  def field(*args)
80
78
  options = args.last.is_a?(Hash) ? args.pop : {}
81
79
  args.each do |field|
@@ -88,18 +86,18 @@ module Stretchy
88
86
  raise Errors::InvalidQueryError.new("Cannot call .not directly after boost - use .where.not or .match.not instead")
89
87
  end
90
88
 
91
- #
89
+ #
92
90
  # Adds a {Boosts::FieldDecayBoost}, which boosts
93
- # a search result based on how close it is to a
91
+ # a search result based on how close it is to a
94
92
  # specified value. That value can be a date, time,
95
93
  # number, or {Types::GeoPoint}
96
- #
94
+ #
97
95
  # Required:
98
- #
96
+ #
99
97
  # * `:field`
100
98
  # * `:origin` or `:lat` & `:lng` combo
101
99
  # * `:scale`
102
- #
100
+ #
103
101
  # @option params [Numeric] :field What field to check with this boost
104
102
  # @option params [Date, Time, Numeric, Types::GeoPoint] :origin Boost score based on how close the field is to this value. Required unless {Types::GeoPoint} is present (:lat, :lng, etc)
105
103
  # @option params [Numeric] :lat Latitude, for a geo point
@@ -109,10 +107,10 @@ module Stretchy
109
107
  # @option params [Numeric] :longitude Longitude, for a geo point
110
108
  # @option params [String] :scale When the field is this distance from origin, the boost will be multiplied by `:decay` . Default is 0.5, so when `:origin` is a geo point and `:scale` is '10mi', then this boost will be twice as much for a point at the origin as for one 10 miles away
111
109
  # @option params [String] :offset Anything within this distance of the origin is boosted as if it were at the origin
112
- # @option params [Symbol] :type (:gauss) What type of decay to use. One of `:linear`, `:exp`, or `:gauss`
110
+ # @option params [Symbol] :type (:gauss) What type of decay to use. One of `:linear`, `:exp`, or `:gauss`
113
111
  # @option params [Numeric] :decay_amount (0.5) How much the boost falls off when it is `:scale` distance from `:origin`
114
112
  # @option params [Numeric] :weight (1.2) How strongly to weight this boost compared to others
115
- #
113
+ #
116
114
  # @example Boost near a geo point
117
115
  # query.boost.near(
118
116
  # field: :coords,
@@ -121,14 +119,14 @@ module Stretchy
121
119
  # lat: 33.3,
122
120
  # lng: 28.2
123
121
  # )
124
- #
122
+ #
125
123
  # @example Boost near a date
126
124
  # query.boost.near(
127
125
  # field: :published_at,
128
126
  # origin: Time.now,
129
127
  # scale: '3d'
130
128
  # )
131
- #
129
+ #
132
130
  # @example Boost near a number (with params)
133
131
  # query.boost.near(
134
132
  # field: :followers,
@@ -139,9 +137,9 @@ module Stretchy
139
137
  # decay: 0.75,
140
138
  # weight: 10
141
139
  # )
142
- #
140
+ #
143
141
  # @return [Base] Query with field decay filter added
144
- #
142
+ #
145
143
  # @see http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html Elastic Docs - Function Score Query
146
144
  def near(params = {}, options = {})
147
145
  if params[:lat] || params[:latitude] ||
@@ -154,61 +152,61 @@ module Stretchy
154
152
  end
155
153
  alias :geo :near
156
154
 
157
- #
155
+ #
158
156
  # Adds a {Boosts::RandomBoost} to the query, for slightly
159
157
  # randomizing search results.
160
- #
158
+ #
161
159
  # @param seed [Numeric] The seed for the random value
162
160
  # @param weight [Numeric] The weight for this random value
163
- #
161
+ #
164
162
  # @return [Base] Query with random boost applied
165
- #
163
+ #
166
164
  # @see http://www.elastic.co/guide/en/elasticsearch/guide/master/random-scoring.html Elastic Docs - Random Scoring
167
165
  def random(*args)
168
166
  base.boost_builder.functions << Stretchy::Boosts::RandomBoost.new(*args)
169
167
  Base.new(base)
170
168
  end
171
169
 
172
- #
170
+ #
173
171
  # Defines a global boost for all documents in the query
174
- #
172
+ #
175
173
  # @param num [Numeric] Boost to apply to the whole query
176
- #
174
+ #
177
175
  # @return [self] Boost context with overall boost applied
178
176
  def all(num)
179
177
  base.boost_builder.overall_boost = num
180
178
  self
181
179
  end
182
180
 
183
- #
181
+ #
184
182
  # The maximum boost that any document can have
185
- #
183
+ #
186
184
  # @param num [Numeric] Maximum score a document can have
187
- #
185
+ #
188
186
  # @return [self] Boost context with maximum score applied
189
187
  def max(num)
190
188
  base.boost_builder.max_boost = num
191
189
  self
192
190
  end
193
191
 
194
- #
192
+ #
195
193
  # Set scoring mode for when a document matches multiple
196
194
  # boost functions.
197
- #
195
+ #
198
196
  # @param mode [Symbol] Score mode. Can be one of `multiply sum avg first max min`
199
- #
197
+ #
200
198
  # @return [self] Boost context with score mode applied
201
199
  def score_mode(mode)
202
200
  base.boost_builder.score_mode = mode
203
201
  self
204
202
  end
205
203
 
206
- #
204
+ #
207
205
  # Set boost mode for when a document matches multiple
208
206
  # boost functions.
209
- #
207
+ #
210
208
  # @param mode [Symbol] Boost mode. Can be one of `multiply replace sum avg max min`
211
- #
209
+ #
212
210
  # @return [self] Boost context with boost mode applied
213
211
  def boost_mode(mode)
214
212
  base.boost_builder.boost_mode = mode
@@ -217,4 +215,4 @@ module Stretchy
217
215
 
218
216
  end
219
217
  end
220
- end
218
+ end
@@ -2,40 +2,57 @@ require 'stretchy/clauses/boost_clause'
2
2
 
3
3
  module Stretchy
4
4
  module Clauses
5
- #
5
+ #
6
6
  # Boost documents that match a free-text query. Most
7
- # options will be passed into {#initialize}, but you
7
+ # options will be passed into {#initialize}, but you
8
8
  # can also chain `.not` onto it. Calling `.where` or
9
9
  # `.match` from here will apply filters (*not boosts*)
10
10
  # and return to the base state
11
- #
11
+ #
12
12
  # @author [atevans]
13
- #
13
+ #
14
14
  class BoostMatchClause < BoostClause
15
15
 
16
16
  delegate [:range, :geo] => :where
17
17
 
18
- #
18
+ #
19
19
  # Switches to inverse context, and applies filters as inverse
20
20
  # options (ie, documents that *do not* match the query will
21
21
  # be boosted)
22
- #
22
+ #
23
23
  # @overload not(params)
24
24
  # @param [String] String that must not match anywhere in the document
25
- #
25
+ #
26
26
  # @overload not(params)
27
27
  # @param params [Hash] Fields and values that should not match in the document
28
- #
28
+ #
29
29
  # @return [BoostMatchClause] Query with inverse matching boost function applied
30
30
  def not(params = {})
31
31
  @inverse = true
32
32
  match_function(hashify_params(params))
33
33
  end
34
34
 
35
+ #
36
+ # Boosts documents that are returned by a Match query.
37
+ #
38
+ # @param [Hash] Parameters for the match query. See {MatchClause#match}
39
+ # @param [Hash] Options for Stretchy to determine behavior of this boost
40
+ #
41
+ # @return [BoostMatchClause] Query with boost match applied
35
42
  def boost_match(params = {}, options = {})
36
43
  match_function(hashify_params(params), options)
37
44
  end
38
45
 
46
+ #
47
+ # Boosts documents according to how closely they match a given phrase.
48
+ # This acts similarly to {MatchClause#fulltext}, but only adds a boost,
49
+ # so it will not interfere with the query. For example, it will not
50
+ # filter out documents that don't match at least one term.
51
+ #
52
+ # @param [Hash] Parameters for the match query. See {MatchClause#match}
53
+ # @param [Hash] Options for Stretchy to determine behavior of this boost
54
+ #
55
+ # @return [Base] Query with boost match applied
39
56
  def fulltext(params = {}, options = {})
40
57
  _params = hashify_params(params)
41
58
  weight = _params.delete(:weight) || options[:weight]
@@ -48,35 +65,51 @@ module Stretchy
48
65
  Base.new(base)
49
66
  end
50
67
 
51
- #
68
+ #
69
+ # Boosts documents using a query with arbitrary json passed to the
70
+ # method. See {MatchClause#query}.
71
+ #
72
+ # @param [Hash] Arbitrary json to use as a query
73
+ # @param [Hash] Options for Stretchy to determine behavior of this boost
74
+ #
75
+ # @return [Base] Query with arbitrary json query boost added
76
+ def query(params = {}, options = {})
77
+ weight = params.delete(:weight) || options[:weight]
78
+ clause = MatchClause.new.query(params, options)
79
+ boost = clause.to_boost(weight)
80
+ base.boost_builder.add_boost(boost) if boost
81
+ Base.new(base)
82
+ end
83
+
84
+ #
52
85
  # Returns to the base context; filters passed here
53
86
  # will be used to filter documents.
54
- #
87
+ #
55
88
  # @example Returning to base context
56
89
  # query.boost.match('string').where(other_field: 64)
57
- #
90
+ #
58
91
  # @example Staying in boost context
59
92
  # query.boost.match('string').boost.where(other_field: 99)
60
- #
93
+ #
61
94
  # @see {WhereClause#initialize}
62
- #
95
+ #
63
96
  # @return [WhereClause] Query with where clause applied
64
97
  def where(*args)
65
98
  WhereClause.new(base).where(*args)
66
99
  end
67
100
 
68
- #
101
+ #
69
102
  # Returns to the base context. Queries passed here
70
103
  # will be used to filter documents.
71
- #
104
+ #
72
105
  # @example Returning to base context
73
106
  # query.boost.match(message: 'curse word').match('username')
74
- #
107
+ #
75
108
  # @example Staying in boost context
76
109
  # query.boost.match(message: 'happy word').boost.match('love')
77
- #
110
+ #
78
111
  # @see {MatchClause#initialize}
79
- #
112
+ #
80
113
  # @return [MatchClause] Base context with match queries applied
81
114
  def match(*args)
82
115
  MatchClause.new(base).match(*args)
@@ -94,4 +127,4 @@ module Stretchy
94
127
 
95
128
  end
96
129
  end
97
- end
130
+ end