sunspot 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +10 -0
- data/Gemfile.lock +32 -0
- data/History.txt +24 -0
- data/README.rdoc +18 -5
- data/lib/sunspot.rb +40 -0
- data/lib/sunspot/dsl.rb +2 -2
- data/lib/sunspot/dsl/field_query.rb +2 -2
- data/lib/sunspot/dsl/fields.rb +0 -10
- data/lib/sunspot/dsl/restriction.rb +4 -4
- data/lib/sunspot/dsl/restriction_with_near.rb +121 -0
- data/lib/sunspot/dsl/scope.rb +55 -67
- data/lib/sunspot/dsl/standard_query.rb +11 -15
- data/lib/sunspot/field.rb +30 -29
- data/lib/sunspot/field_factory.rb +0 -18
- data/lib/sunspot/installer/solrconfig_updater.rb +0 -30
- data/lib/sunspot/query.rb +4 -3
- data/lib/sunspot/query/common_query.rb +2 -2
- data/lib/sunspot/query/composite_fulltext.rb +7 -2
- data/lib/sunspot/query/connective.rb +21 -6
- data/lib/sunspot/query/dismax.rb +1 -0
- data/lib/sunspot/query/geo.rb +53 -0
- data/lib/sunspot/query/more_like_this.rb +1 -0
- data/lib/sunspot/query/restriction.rb +5 -5
- data/lib/sunspot/query/standard_query.rb +0 -4
- data/lib/sunspot/search/abstract_search.rb +1 -7
- data/lib/sunspot/search/hit.rb +10 -10
- data/lib/sunspot/search/query_facet.rb +8 -3
- data/lib/sunspot/session.rb +10 -2
- data/lib/sunspot/session_proxy.rb +16 -0
- data/lib/sunspot/session_proxy/master_slave_session_proxy.rb +1 -1
- data/lib/sunspot/session_proxy/sharding_session_proxy.rb +7 -0
- data/lib/sunspot/session_proxy/silent_fail_session_proxy.rb +42 -0
- data/lib/sunspot/session_proxy/thread_local_session_proxy.rb +1 -1
- data/lib/sunspot/setup.rb +1 -17
- data/lib/sunspot/type.rb +38 -6
- data/lib/sunspot/util.rb +21 -31
- data/lib/sunspot/version.rb +1 -1
- data/solr/solr/conf/solrconfig.xml +0 -4
- data/spec/api/binding_spec.rb +12 -0
- data/spec/api/indexer/attributes_spec.rb +22 -22
- data/spec/api/query/connectives_examples.rb +14 -1
- data/spec/api/query/fulltext_examples.rb +3 -3
- data/spec/api/query/geo_examples.rb +69 -0
- data/spec/api/query/scope_examples.rb +32 -13
- data/spec/api/query/standard_spec.rb +1 -1
- data/spec/api/search/faceting_spec.rb +5 -1
- data/spec/api/search/hits_spec.rb +14 -12
- data/spec/api/session_proxy/class_sharding_session_proxy_spec.rb +1 -1
- data/spec/api/session_proxy/sharding_session_proxy_spec.rb +1 -1
- data/spec/api/session_proxy/silent_fail_session_proxy_spec.rb +24 -0
- data/spec/api/session_spec.rb +22 -0
- data/spec/integration/local_search_spec.rb +42 -69
- data/spec/integration/scoped_search_spec.rb +30 -0
- data/spec/mocks/connection.rb +6 -2
- data/spec/mocks/photo.rb +0 -1
- data/spec/mocks/post.rb +11 -2
- data/spec/mocks/user.rb +6 -1
- data/spec/spec_helper.rb +2 -12
- metadata +209 -177
- data/lib/sunspot/query/local.rb +0 -26
- data/solr/solr/lib/lucene-spatial-2.9.1.jar +0 -0
- data/solr/solr/lib/solr-spatial-light-0.0.6.jar +0 -0
- data/spec/api/query/local_examples.rb +0 -38
- data/tasks/gemspec.rake +0 -33
- data/tasks/rcov.rake +0 -28
- data/tasks/spec.rake +0 -24
@@ -81,7 +81,7 @@ module Sunspot
|
|
81
81
|
begin
|
82
82
|
hits = if solr_response && solr_response['docs']
|
83
83
|
solr_response['docs'].map do |doc|
|
84
|
-
Hit.new(doc, highlights_for(doc),
|
84
|
+
Hit.new(doc, highlights_for(doc), self)
|
85
85
|
end
|
86
86
|
end
|
87
87
|
maybe_will_paginate(hits || [])
|
@@ -268,12 +268,6 @@ module Sunspot
|
|
268
268
|
end
|
269
269
|
end
|
270
270
|
|
271
|
-
def distance_for(doc)
|
272
|
-
if @solr_result['distances']
|
273
|
-
@solr_result['distances'][doc['id']]
|
274
|
-
end
|
275
|
-
end
|
276
|
-
|
277
271
|
def verified_hits
|
278
272
|
@verified_hits ||= maybe_will_paginate(hits.select { |hit| hit.instance })
|
279
273
|
end
|
data/lib/sunspot/search/hit.rb
CHANGED
@@ -3,8 +3,8 @@ module Sunspot
|
|
3
3
|
#
|
4
4
|
# Hit objects represent the raw information returned by Solr for a single
|
5
5
|
# document. As well as the primary key and class name, hit objects give
|
6
|
-
# access to stored field values, keyword relevance score, and
|
7
|
-
#
|
6
|
+
# access to stored field values, keyword relevance score, and keyword
|
7
|
+
# highlighting.
|
8
8
|
#
|
9
9
|
class Hit
|
10
10
|
SPECIAL_KEYS = Set.new(%w(id type score)) #:nodoc:
|
@@ -23,17 +23,12 @@ module Sunspot
|
|
23
23
|
#
|
24
24
|
attr_reader :score
|
25
25
|
#
|
26
|
-
# For geographical searches, this is the distance between the search
|
27
|
-
# centerpoint and the document's location. Otherwise, it's nil.
|
28
|
-
#
|
29
|
-
attr_reader :distance
|
30
26
|
|
31
27
|
attr_writer :result #:nodoc:
|
32
28
|
|
33
|
-
def initialize(raw_hit, highlights,
|
29
|
+
def initialize(raw_hit, highlights, search) #:nodoc:
|
34
30
|
@class_name, @primary_key = *raw_hit['id'].match(/([^ ]+) (.+)/)[1..2]
|
35
31
|
@score = raw_hit['score']
|
36
|
-
@distance = distance
|
37
32
|
@search = search
|
38
33
|
@stored_values = raw_hit
|
39
34
|
@stored_cache = {}
|
@@ -104,7 +99,7 @@ module Sunspot
|
|
104
99
|
private
|
105
100
|
|
106
101
|
def setup
|
107
|
-
@setup ||= Sunspot::Setup.for(@class_name)
|
102
|
+
@setup ||= Sunspot::Setup.for(Util.full_const_get(@class_name))
|
108
103
|
end
|
109
104
|
|
110
105
|
def highlights_cache
|
@@ -126,7 +121,12 @@ module Sunspot
|
|
126
121
|
def stored_value(field_name, dynamic_field_name)
|
127
122
|
setup.stored_fields(field_name, dynamic_field_name).each do |field|
|
128
123
|
if value = @stored_values[field.indexed_name]
|
129
|
-
|
124
|
+
case value
|
125
|
+
when Array
|
126
|
+
return value.map { |item| field.cast(item) }
|
127
|
+
else
|
128
|
+
return field.cast(value)
|
129
|
+
end
|
130
130
|
end
|
131
131
|
end
|
132
132
|
nil
|
@@ -38,7 +38,7 @@ module Sunspot
|
|
38
38
|
private
|
39
39
|
|
40
40
|
def sort_rows!(rows)
|
41
|
-
case @options[:sort] || (:count if
|
41
|
+
case @options[:sort] || (:count if limit)
|
42
42
|
when :count
|
43
43
|
rows.sort! { |lrow, rrow| rrow.count <=> lrow.count }
|
44
44
|
when :index
|
@@ -52,11 +52,16 @@ module Sunspot
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
55
|
-
if
|
56
|
-
rows.replace(rows.first(
|
55
|
+
if limit
|
56
|
+
rows.replace(rows.first(limit))
|
57
57
|
end
|
58
58
|
rows
|
59
59
|
end
|
60
|
+
|
61
|
+
def limit
|
62
|
+
return @limit if defined?(@limit)
|
63
|
+
@limit = (@options[:limit].to_i if @options[:limit].to_i > 0)
|
64
|
+
end
|
60
65
|
end
|
61
66
|
end
|
62
67
|
end
|
data/lib/sunspot/session.rb
CHANGED
@@ -107,6 +107,14 @@ module Sunspot
|
|
107
107
|
connection.commit
|
108
108
|
end
|
109
109
|
|
110
|
+
#
|
111
|
+
# See Sunspot.optimize
|
112
|
+
#
|
113
|
+
def optimize
|
114
|
+
@adds = @deletes = 0
|
115
|
+
connection.optimize
|
116
|
+
end
|
117
|
+
|
110
118
|
#
|
111
119
|
# See Sunspot.remove
|
112
120
|
#
|
@@ -115,9 +123,9 @@ module Sunspot
|
|
115
123
|
types = objects
|
116
124
|
conjunction = Query::Connective::Conjunction.new
|
117
125
|
if types.length == 1
|
118
|
-
conjunction.
|
126
|
+
conjunction.add_positive_restriction(TypeField.instance, Query::Restriction::EqualTo, types.first)
|
119
127
|
else
|
120
|
-
conjunction.
|
128
|
+
conjunction.add_positive_restriction(TypeField.instance, Query::Restriction::AnyOf, types)
|
121
129
|
end
|
122
130
|
dsl = DSL::Scope.new(conjunction, setup_for_types(types))
|
123
131
|
Util.instance_eval_or_call(dsl, &block)
|
@@ -27,6 +27,14 @@ module Sunspot
|
|
27
27
|
module SessionProxy
|
28
28
|
NotSupportedError = Class.new(StandardError)
|
29
29
|
|
30
|
+
autoload(
|
31
|
+
:AbstractSessionProxy,
|
32
|
+
File.join(
|
33
|
+
File.dirname(__FILE__),
|
34
|
+
'session_proxy',
|
35
|
+
'abstract_session_proxy'
|
36
|
+
)
|
37
|
+
)
|
30
38
|
autoload(
|
31
39
|
:ThreadLocalSessionProxy,
|
32
40
|
File.join(
|
@@ -67,5 +75,13 @@ module Sunspot
|
|
67
75
|
'id_sharding_session_proxy'
|
68
76
|
)
|
69
77
|
)
|
78
|
+
autoload(
|
79
|
+
:SilentFailSessionProxy,
|
80
|
+
File.join(
|
81
|
+
File.dirname(__FILE__),
|
82
|
+
'session_proxy',
|
83
|
+
'silent_fail_session_proxy'
|
84
|
+
)
|
85
|
+
)
|
70
86
|
end
|
71
87
|
end
|
@@ -18,7 +18,7 @@ module Sunspot
|
|
18
18
|
attr_reader :slave_session
|
19
19
|
|
20
20
|
delegate :batch, :commit, :commit_if_delete_dirty, :commit_if_dirty,
|
21
|
-
:config, :delete_dirty?, :dirty?, :index, :index!, :remove,
|
21
|
+
:config, :delete_dirty?, :dirty?, :index, :index!, :optimize, :remove,
|
22
22
|
:remove!, :remove_all, :remove_all!, :remove_by_id,
|
23
23
|
:remove_by_id!, :to => :master_session
|
24
24
|
delegate :new_search, :search, :new_more_like_this, :more_like_this, :to => :slave_session
|
@@ -119,6 +119,13 @@ module Sunspot
|
|
119
119
|
all_sessions.each { |session| session.commit }
|
120
120
|
end
|
121
121
|
|
122
|
+
#
|
123
|
+
# Optimize all shards. See Sunspot.optimize
|
124
|
+
#
|
125
|
+
def optimize
|
126
|
+
all_sessions.each { |session| session.optimize }
|
127
|
+
end
|
128
|
+
|
122
129
|
#
|
123
130
|
# Commit all dirty sessions. Only dirty sessions will be committed.
|
124
131
|
#
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'abstract_session_proxy')
|
2
|
+
|
3
|
+
module Sunspot
|
4
|
+
module SessionProxy
|
5
|
+
class SilentFailSessionProxy < AbstractSessionProxy
|
6
|
+
|
7
|
+
attr_reader :search_session
|
8
|
+
|
9
|
+
delegate :new_search, :search, :config,
|
10
|
+
:new_more_like_this, :more_like_this,
|
11
|
+
:delete_dirty, :delete_dirty?,
|
12
|
+
:to => :search_session
|
13
|
+
|
14
|
+
def initialize(search_session = Sunspot.session)
|
15
|
+
@search_session = search_session
|
16
|
+
end
|
17
|
+
|
18
|
+
def rescued_exception(method, e)
|
19
|
+
$stderr.puts("Exception in #{method}: #{e.message}")
|
20
|
+
end
|
21
|
+
|
22
|
+
SUPPORTED_METHODS = [
|
23
|
+
:batch, :commit, :commit_if_dirty, :commit_if_delete_dirty, :dirty?,
|
24
|
+
:index!, :index, :optimize, :remove!, :remove, :remove_all!, :remove_all,
|
25
|
+
:remove_by_id!, :remove_by_id
|
26
|
+
]
|
27
|
+
|
28
|
+
SUPPORTED_METHODS.each do |method|
|
29
|
+
module_eval(<<-RUBY)
|
30
|
+
def #{method}(*args, &block)
|
31
|
+
begin
|
32
|
+
search_session.#{method}(*args, &block)
|
33
|
+
rescue => e
|
34
|
+
self.rescued_exception(:#{method}, e)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
RUBY
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -17,7 +17,7 @@ module Sunspot
|
|
17
17
|
attr_reader :config
|
18
18
|
@@next_id = 0
|
19
19
|
|
20
|
-
delegate :batch, :commit, :commit_if_delete_dirty, :commit_if_dirty, :delete_dirty?, :dirty?, :index, :index!, :new_search, :remove, :remove!, :remove_all, :remove_all!, :remove_by_id, :remove_by_id!, :search, :more_like_this, :new_more_like_this, :to => :session
|
20
|
+
delegate :batch, :commit, :commit_if_delete_dirty, :commit_if_dirty, :delete_dirty?, :dirty?, :index, :index!, :new_search, :optimize, :remove, :remove!, :remove_all, :remove_all!, :remove_by_id, :remove_by_id!, :search, :more_like_this, :new_more_like_this, :to => :session
|
21
21
|
|
22
22
|
#
|
23
23
|
# Optionally pass an existing Sunspot::Configuration object. If none is
|
data/lib/sunspot/setup.rb
CHANGED
@@ -77,15 +77,6 @@ module Sunspot
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
-
#
|
81
|
-
# The coordinates field factory is used for populating the coordinate fields
|
82
|
-
# of documents during index, but does not actually generate fields (since
|
83
|
-
# the field names used in search are static).
|
84
|
-
#
|
85
|
-
def set_coordinates_field(name = nil, &block)
|
86
|
-
@coordinates_field_factory = FieldFactory::Coordinates.new(name, &block)
|
87
|
-
end
|
88
|
-
|
89
80
|
#
|
90
81
|
# Add a document boost to documents at index time. Document boost can be
|
91
82
|
# static (the same for all documents of this class), or extracted on a per-
|
@@ -234,7 +225,6 @@ module Sunspot
|
|
234
225
|
def all_field_factories
|
235
226
|
all_field_factories = []
|
236
227
|
all_field_factories.concat(field_factories).concat(text_field_factories).concat(dynamic_field_factories)
|
237
|
-
all_field_factories << @coordinates_field_factory if @coordinates_field_factory
|
238
228
|
all_field_factories
|
239
229
|
end
|
240
230
|
|
@@ -319,13 +309,7 @@ module Sunspot
|
|
319
309
|
# Setup instance associated with the given class or its nearest ancestor
|
320
310
|
#
|
321
311
|
def for(clazz) #:nodoc:
|
322
|
-
|
323
|
-
if clazz.respond_to?(:name)
|
324
|
-
clazz.name
|
325
|
-
else
|
326
|
-
clazz
|
327
|
-
end
|
328
|
-
setups[class_name.to_sym] || self.for(clazz.superclass) if clazz
|
312
|
+
setups[clazz.name.to_sym] || self.for(clazz.superclass) if clazz
|
329
313
|
end
|
330
314
|
|
331
315
|
protected
|
data/lib/sunspot/type.rb
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
begin
|
3
|
+
require 'geohash'
|
4
|
+
rescue LoadError => e
|
5
|
+
require 'pr_geohash'
|
6
|
+
end
|
7
|
+
|
1
8
|
module Sunspot
|
2
9
|
#
|
3
10
|
# This module contains singleton objects that represent the types that can be
|
@@ -58,12 +65,7 @@ module Sunspot
|
|
58
65
|
end
|
59
66
|
|
60
67
|
class AbstractType #:nodoc:
|
61
|
-
|
62
|
-
def instance
|
63
|
-
@instance ||= new
|
64
|
-
end
|
65
|
-
private :new
|
66
|
-
end
|
68
|
+
include Singleton
|
67
69
|
|
68
70
|
def accepts_dynamic?
|
69
71
|
true
|
@@ -317,11 +319,41 @@ module Sunspot
|
|
317
319
|
true
|
318
320
|
when 'false'
|
319
321
|
false
|
322
|
+
when true, false
|
323
|
+
string
|
320
324
|
end
|
321
325
|
end
|
322
326
|
end
|
323
327
|
register BooleanType, TrueClass, FalseClass
|
324
328
|
|
329
|
+
#
|
330
|
+
# The Location type encodes geographical coordinates as a GeoHash.
|
331
|
+
# The data for this type must respond to the `lat` and `lng` methods; you
|
332
|
+
# can use Sunspot::Util::Coordinates as a wrapper if your source data does
|
333
|
+
# not follow this API.
|
334
|
+
#
|
335
|
+
# Location fields are most usefully searched using the
|
336
|
+
# Sunspot::DSL::RestrictionWithType#near method; see that method for more
|
337
|
+
# information on geographical search.
|
338
|
+
#
|
339
|
+
# ==== Example
|
340
|
+
#
|
341
|
+
# Sunspot.setup(Post) do
|
342
|
+
# location :coordinates do
|
343
|
+
# Sunspot::Util::Coordinates.new(coordinates[0], coordinates[1])
|
344
|
+
# end
|
345
|
+
# end
|
346
|
+
#
|
347
|
+
class LocationType < AbstractType
|
348
|
+
def indexed_name(name)
|
349
|
+
"#{name}_s"
|
350
|
+
end
|
351
|
+
|
352
|
+
def to_indexed(value)
|
353
|
+
GeoHash.encode(value.lat.to_f, value.lng.to_f, 12)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
325
357
|
class ClassType < AbstractType
|
326
358
|
def indexed_name(name) #:nodoc:
|
327
359
|
'class_name'
|
data/lib/sunspot/util.rb
CHANGED
@@ -108,6 +108,20 @@ module Sunspot
|
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
111
|
+
#
|
112
|
+
# When generating boosts, Solr requires that the values be in standard
|
113
|
+
# (not scientific) notation. We would like to ensure a minimum number of
|
114
|
+
# significant digits (i.e., digits that are not prefix zeros) for small
|
115
|
+
# float values.
|
116
|
+
#
|
117
|
+
def format_float(f, digits)
|
118
|
+
if f < 1
|
119
|
+
sprintf('%.*f', digits - Math.log10(f), f)
|
120
|
+
else
|
121
|
+
f.to_s
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
111
125
|
#
|
112
126
|
# Perform a deep merge of hashes, returning the result as a new hash.
|
113
127
|
# See #deep_merge_into for rules used to merge the hashes
|
@@ -182,35 +196,7 @@ module Sunspot
|
|
182
196
|
end
|
183
197
|
end
|
184
198
|
|
185
|
-
|
186
|
-
def initialize(coords)
|
187
|
-
@coords = coords
|
188
|
-
end
|
189
|
-
|
190
|
-
def lat
|
191
|
-
if @coords.respond_to?(:first)
|
192
|
-
@coords.first
|
193
|
-
elsif @coords.respond_to?(:lat)
|
194
|
-
@coords.lat
|
195
|
-
else
|
196
|
-
@coords.latitude
|
197
|
-
end.to_f
|
198
|
-
end
|
199
|
-
|
200
|
-
def lng
|
201
|
-
if @coords.respond_to?(:last)
|
202
|
-
@coords.last
|
203
|
-
elsif @coords.respond_to?(:lng)
|
204
|
-
@coords.lng
|
205
|
-
elsif @coords.respond_to?(:lon)
|
206
|
-
@coords.lon
|
207
|
-
elsif @coords.respond_to?(:long)
|
208
|
-
@coords.long
|
209
|
-
elsif @coords.respond_to?(:longitude)
|
210
|
-
@coords.longitude
|
211
|
-
end.to_f
|
212
|
-
end
|
213
|
-
end
|
199
|
+
Coordinates = Struct.new(:lat, :lng)
|
214
200
|
|
215
201
|
class ContextBoundDelegate
|
216
202
|
class <<self
|
@@ -237,12 +223,16 @@ module Sunspot
|
|
237
223
|
@__receiver__, @__calling_context__ = receiver, calling_context
|
238
224
|
end
|
239
225
|
|
226
|
+
def id
|
227
|
+
@__calling_context__.__send__(:id)
|
228
|
+
end
|
229
|
+
|
240
230
|
def method_missing(method, *args, &block)
|
241
231
|
begin
|
242
|
-
@__receiver__.
|
232
|
+
@__receiver__.__send__(method.to_sym, *args, &block)
|
243
233
|
rescue ::NoMethodError => e
|
244
234
|
begin
|
245
|
-
@__calling_context__.
|
235
|
+
@__calling_context__.__send__(method.to_sym, *args, &block)
|
246
236
|
rescue ::NoMethodError
|
247
237
|
raise(e)
|
248
238
|
end
|
data/lib/sunspot/version.rb
CHANGED
@@ -442,9 +442,6 @@
|
|
442
442
|
<str name="version">2.1</str>
|
443
443
|
-->
|
444
444
|
</lst>
|
445
|
-
<arr name="last-components">
|
446
|
-
<str>spatial</str>
|
447
|
-
</arr>
|
448
445
|
</requestHandler>
|
449
446
|
<!-- Please refer to http://wiki.apache.org/solr/SolrReplication for details on configuring replication -->
|
450
447
|
<!-- remove the <lst name="master"> section if this is just a slave -->
|
@@ -928,7 +925,6 @@
|
|
928
925
|
<healthcheck type="file">server-enabled</healthcheck>
|
929
926
|
-->
|
930
927
|
</admin>
|
931
|
-
<searchComponent name="spatial" class="me.outofti.solrspatiallight.SpatialQueryComponent"/>
|
932
928
|
<requestHandler class="solr.MoreLikeThisHandler" name="/mlt">
|
933
929
|
<lst name="defaults">
|
934
930
|
<str name="mlt.mintf">1</str>
|
data/spec/api/binding_spec.rb
CHANGED
@@ -9,6 +9,14 @@ describe "DSL bindings" do
|
|
9
9
|
value.should == 'value'
|
10
10
|
end
|
11
11
|
|
12
|
+
it 'should give access to calling context\'s id method in search DSL' do
|
13
|
+
value = nil
|
14
|
+
session.search(Post) do
|
15
|
+
value = id
|
16
|
+
end
|
17
|
+
value.should == 16
|
18
|
+
end
|
19
|
+
|
12
20
|
it 'should give access to calling context\'s methods in nested DSL block' do
|
13
21
|
value = nil
|
14
22
|
session.search(Post) do
|
@@ -35,4 +43,8 @@ describe "DSL bindings" do
|
|
35
43
|
def test_method
|
36
44
|
'value'
|
37
45
|
end
|
46
|
+
|
47
|
+
def id
|
48
|
+
16
|
49
|
+
end
|
38
50
|
end
|