chewy 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +13 -5
- data/.gitignore +1 -0
- data/.travis.yml +5 -3
- data/CHANGELOG.md +75 -0
- data/README.md +487 -92
- data/Rakefile +3 -2
- data/chewy.gemspec +2 -2
- data/filters +76 -0
- data/lib/chewy.rb +5 -3
- data/lib/chewy/config.rb +36 -19
- data/lib/chewy/fields/base.rb +5 -1
- data/lib/chewy/index.rb +22 -10
- data/lib/chewy/index/actions.rb +13 -13
- data/lib/chewy/index/search.rb +7 -2
- data/lib/chewy/query.rb +382 -64
- data/lib/chewy/query/context.rb +174 -0
- data/lib/chewy/query/criteria.rb +127 -34
- data/lib/chewy/query/loading.rb +9 -9
- data/lib/chewy/query/nodes/and.rb +25 -0
- data/lib/chewy/query/nodes/base.rb +17 -0
- data/lib/chewy/query/nodes/bool.rb +32 -0
- data/lib/chewy/query/nodes/equal.rb +34 -0
- data/lib/chewy/query/nodes/exists.rb +20 -0
- data/lib/chewy/query/nodes/expr.rb +28 -0
- data/lib/chewy/query/nodes/field.rb +106 -0
- data/lib/chewy/query/nodes/missing.rb +20 -0
- data/lib/chewy/query/nodes/not.rb +25 -0
- data/lib/chewy/query/nodes/or.rb +25 -0
- data/lib/chewy/query/nodes/prefix.rb +18 -0
- data/lib/chewy/query/nodes/query.rb +20 -0
- data/lib/chewy/query/nodes/range.rb +63 -0
- data/lib/chewy/query/nodes/raw.rb +15 -0
- data/lib/chewy/query/nodes/regexp.rb +31 -0
- data/lib/chewy/query/nodes/script.rb +20 -0
- data/lib/chewy/query/pagination.rb +28 -22
- data/lib/chewy/railtie.rb +23 -0
- data/lib/chewy/rspec/update_index.rb +20 -3
- data/lib/chewy/type/adapter/active_record.rb +78 -5
- data/lib/chewy/type/adapter/base.rb +46 -0
- data/lib/chewy/type/adapter/object.rb +40 -8
- data/lib/chewy/type/base.rb +1 -1
- data/lib/chewy/type/import.rb +18 -44
- data/lib/chewy/type/observe.rb +24 -14
- data/lib/chewy/version.rb +1 -1
- data/lib/tasks/chewy.rake +27 -0
- data/spec/chewy/config_spec.rb +30 -12
- data/spec/chewy/fields/base_spec.rb +11 -5
- data/spec/chewy/index/actions_spec.rb +20 -20
- data/spec/chewy/index/search_spec.rb +5 -5
- data/spec/chewy/index_spec.rb +28 -8
- data/spec/chewy/query/context_spec.rb +173 -0
- data/spec/chewy/query/criteria_spec.rb +219 -12
- data/spec/chewy/query/loading_spec.rb +6 -4
- data/spec/chewy/query/nodes/and_spec.rb +16 -0
- data/spec/chewy/query/nodes/bool_spec.rb +22 -0
- data/spec/chewy/query/nodes/equal_spec.rb +32 -0
- data/spec/chewy/query/nodes/exists_spec.rb +18 -0
- data/spec/chewy/query/nodes/missing_spec.rb +15 -0
- data/spec/chewy/query/nodes/not_spec.rb +16 -0
- data/spec/chewy/query/nodes/or_spec.rb +16 -0
- data/spec/chewy/query/nodes/prefix_spec.rb +16 -0
- data/spec/chewy/query/nodes/query_spec.rb +12 -0
- data/spec/chewy/query/nodes/range_spec.rb +32 -0
- data/spec/chewy/query/nodes/raw_spec.rb +11 -0
- data/spec/chewy/query/nodes/regexp_spec.rb +31 -0
- data/spec/chewy/query/nodes/script_spec.rb +15 -0
- data/spec/chewy/query/pagination_spec.rb +3 -2
- data/spec/chewy/query_spec.rb +83 -26
- data/spec/chewy/rspec/update_index_spec.rb +20 -0
- data/spec/chewy/type/adapter/active_record_spec.rb +102 -0
- data/spec/chewy/type/adapter/object_spec.rb +82 -0
- data/spec/chewy/type/import_spec.rb +30 -1
- data/spec/chewy/type/mapping_spec.rb +1 -1
- data/spec/chewy/type/observe_spec.rb +46 -12
- data/spec/spec_helper.rb +7 -6
- data/spec/support/class_helpers.rb +2 -2
- metadata +98 -48
- data/.rvmrc +0 -1
- data/lib/chewy/index/client.rb +0 -13
- data/spec/chewy/index/client_spec.rb +0 -18
@@ -0,0 +1,20 @@
|
|
1
|
+
module Chewy
|
2
|
+
class Query
|
3
|
+
module Nodes
|
4
|
+
class Script < Expr
|
5
|
+
def initialize script, params = {}
|
6
|
+
@script = script
|
7
|
+
@params = params
|
8
|
+
@options = params.extract!(:cache)
|
9
|
+
end
|
10
|
+
|
11
|
+
def __render__
|
12
|
+
script = {script: @script}
|
13
|
+
script.merge!(params: @params) if @params.present?
|
14
|
+
script.merge!(_cache: !!@options[:cache]) if @options.key?(:cache)
|
15
|
+
{script: script}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,38 +1,44 @@
|
|
1
|
-
require 'kaminari'
|
2
|
-
|
3
1
|
module Chewy
|
4
2
|
class Query
|
5
3
|
module Pagination
|
6
4
|
extend ActiveSupport::Concern
|
7
5
|
|
8
6
|
included do
|
9
|
-
include Kaminari::
|
7
|
+
include Kaminari if defined?(::Kaminari)
|
8
|
+
end
|
10
9
|
|
11
|
-
|
10
|
+
module Kaminari
|
11
|
+
extend ActiveSupport::Concern
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1))
|
16
|
-
end
|
17
|
-
RUBY
|
18
|
-
end
|
13
|
+
included do
|
14
|
+
include ::Kaminari::PageScopeMethods
|
19
15
|
|
20
|
-
|
21
|
-
_response['hits']['total']
|
22
|
-
end
|
16
|
+
delegate :default_per_page, :max_per_page, :max_pages, to: :_kaminari_config
|
23
17
|
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
19
|
+
def #{::Kaminari.config.page_method_name}(num = 1)
|
20
|
+
limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1))
|
21
|
+
end
|
22
|
+
RUBY
|
23
|
+
end
|
27
24
|
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
def total_count
|
26
|
+
_response['hits']['total']
|
27
|
+
end
|
28
|
+
|
29
|
+
def limit_value
|
30
|
+
(criteria.options[:size].presence || default_per_page).to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def offset_value
|
34
|
+
criteria.options[:from].to_i
|
35
|
+
end
|
31
36
|
|
32
|
-
|
37
|
+
private
|
33
38
|
|
34
|
-
|
35
|
-
|
39
|
+
def _kaminari_config
|
40
|
+
::Kaminari.config
|
41
|
+
end
|
36
42
|
end
|
37
43
|
end
|
38
44
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Chewy
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
rake_tasks do
|
4
|
+
load 'tasks/chewy.rake'
|
5
|
+
end
|
6
|
+
|
7
|
+
initializer 'chewy.add_app_chewy_path' do |app|
|
8
|
+
app.config.paths.add 'app/chewy'
|
9
|
+
end
|
10
|
+
|
11
|
+
initializer 'chewy.add_requests_logging' do |app|
|
12
|
+
ActiveSupport::Notifications.subscribe('import_objects.chewy') do |name, start, finish, id, payload|
|
13
|
+
duration = ((finish - start).to_f * 10000).round / 10.0
|
14
|
+
Rails.logger.debug(" \e[1m\e[33m#{payload[:type]} Import (#{duration}ms)\e[0m #{payload[:import]}")
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveSupport::Notifications.subscribe('search_query.chewy') do |name, start, finish, id, payload|
|
18
|
+
duration = ((finish - start).to_f * 10000).round / 10.0
|
19
|
+
Rails.logger.debug(" \e[1m\e[33m#{payload[:index]} Search (#{duration}ms)\e[0m #{payload[:request]}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
RSpec::Matchers.define :update_index do |type_name|
|
1
|
+
RSpec::Matchers.define :update_index do |type_name, options = {}|
|
2
2
|
chain(:and_reindex) do |*args|
|
3
3
|
@reindex ||= {}
|
4
4
|
@reindex.merge!(extract_documents(*args))
|
@@ -9,9 +9,15 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
9
9
|
@delete.merge!(extract_documents(*args))
|
10
10
|
end
|
11
11
|
|
12
|
+
chain(:only) do |*args|
|
13
|
+
@only = true
|
14
|
+
end
|
15
|
+
|
12
16
|
match do |block|
|
13
17
|
@reindex ||= {}
|
14
18
|
@delete ||= {}
|
19
|
+
@missed_reindex = []
|
20
|
+
@missed_delete = []
|
15
21
|
|
16
22
|
type = Chewy.derive_type(type_name)
|
17
23
|
updated = []
|
@@ -24,7 +30,11 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
27
|
-
|
33
|
+
if options[:atomic] == false
|
34
|
+
block.call
|
35
|
+
else
|
36
|
+
Chewy.atomic { block.call }
|
37
|
+
end
|
28
38
|
|
29
39
|
@updated = updated
|
30
40
|
@updated.each do |updated_document|
|
@@ -32,10 +42,14 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
32
42
|
if document = @reindex[body[:_id].to_s]
|
33
43
|
document[:real_count] += 1
|
34
44
|
document[:real_attributes].merge!(body[:data])
|
45
|
+
else
|
46
|
+
@missed_reindex.push(body[:_id].to_s) if @only
|
35
47
|
end
|
36
48
|
elsif body = updated_document[:delete]
|
37
49
|
if document = @delete[body[:_id].to_s]
|
38
50
|
document[:real_count] += 1
|
51
|
+
else
|
52
|
+
@missed_delete.push(body[:_id].to_s) if @only
|
39
53
|
end
|
40
54
|
end
|
41
55
|
end
|
@@ -51,7 +65,7 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
51
65
|
(document[:expected_count] && document[:expected_count] == document[:real_count])
|
52
66
|
end
|
53
67
|
|
54
|
-
@updated.any? &&
|
68
|
+
@updated.any? && @missed_reindex.none? && @missed_delete.none? &&
|
55
69
|
@reindex.all? { |_, document| document[:match_count] && document[:match_attributes] } &&
|
56
70
|
@delete.all? { |_, document| document[:match_count] }
|
57
71
|
end
|
@@ -61,6 +75,9 @@ RSpec::Matchers.define :update_index do |type_name|
|
|
61
75
|
|
62
76
|
if @updated.none?
|
63
77
|
output << "Expected index `#{type_name}` to be updated, but it was not\n"
|
78
|
+
else
|
79
|
+
output << "Expected index `#{type_name}` to update documents #{@reindex.keys} only, but #{@missed_reindex} was updated also\n" if @missed_reindex.any?
|
80
|
+
output << "Expected index `#{type_name}` to delete documents #{@delete.keys} only, but #{@missed_delete} was deleted also\n" if @missed_delete.any?
|
64
81
|
end
|
65
82
|
|
66
83
|
output << @reindex.each.with_object('') do |(id, document), output|
|
@@ -1,11 +1,12 @@
|
|
1
|
+
require 'chewy/type/adapter/base'
|
2
|
+
|
1
3
|
module Chewy
|
2
4
|
module Type
|
3
5
|
module Adapter
|
4
|
-
class ActiveRecord
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@options = options
|
6
|
+
class ActiveRecord < Base
|
7
|
+
def initialize *args
|
8
|
+
@options = args.extract_options!
|
9
|
+
subject = args.first
|
9
10
|
if subject.is_a?(::ActiveRecord::Relation)
|
10
11
|
@model = subject.klass
|
11
12
|
@scope = subject
|
@@ -21,6 +22,78 @@ module Chewy
|
|
21
22
|
def type_name
|
22
23
|
@type_name ||= (options[:name].presence || model.model_name).to_s.underscore
|
23
24
|
end
|
25
|
+
|
26
|
+
def import *args, &block
|
27
|
+
import_options = args.extract_options!
|
28
|
+
import_options[:batch_size] ||= BATCH_SIZE
|
29
|
+
collection = args.none? ? model_all :
|
30
|
+
(args.one? && args.first.is_a?(::ActiveRecord::Relation) ? args.first : args.flatten)
|
31
|
+
if collection.is_a?(::ActiveRecord::Relation)
|
32
|
+
result = false
|
33
|
+
merged_scope(collection).find_in_batches(import_options.slice(:batch_size)) do |group|
|
34
|
+
result = block.call grouped_objects(group)
|
35
|
+
end
|
36
|
+
result
|
37
|
+
else
|
38
|
+
if collection.all? { |object| object.respond_to?(:id) }
|
39
|
+
collection.in_groups_of(import_options[:batch_size], false).all? do |group|
|
40
|
+
block.call grouped_objects(group)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
import_ids(collection, import_options, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def load *args
|
49
|
+
load_options = args.extract_options!
|
50
|
+
objects = args.flatten
|
51
|
+
|
52
|
+
scope = model.where(id: objects.map(&:id))
|
53
|
+
loaded_objects = if load_options[:scope].is_a?(Proc)
|
54
|
+
scope.instance_eval(&load_options[:scope])
|
55
|
+
elsif load_options[:scope].is_a?(::ActiveRecord::Relation)
|
56
|
+
scope.merge(load_options[:scope])
|
57
|
+
else
|
58
|
+
scope
|
59
|
+
end.index_by { |object| object.id.to_s }
|
60
|
+
|
61
|
+
objects.map { |object| loaded_objects[object.id.to_s] }
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :model, :scope, :options
|
67
|
+
|
68
|
+
def import_ids(ids, import_options = {}, &block)
|
69
|
+
ids = ids.map(&:to_i).uniq
|
70
|
+
|
71
|
+
indexed = false
|
72
|
+
merged_scope(model.where(id: ids)).find_in_batches(import_options.slice(:batch_size)) do |objects|
|
73
|
+
ids -= objects.map(&:id)
|
74
|
+
indexed = block.call index: objects
|
75
|
+
end
|
76
|
+
|
77
|
+
deleted = ids.in_groups_of(import_options[:batch_size], false).all? do |group|
|
78
|
+
block.call(delete: group)
|
79
|
+
end
|
80
|
+
|
81
|
+
indexed && deleted
|
82
|
+
end
|
83
|
+
|
84
|
+
def grouped_objects(objects)
|
85
|
+
objects.group_by do |object|
|
86
|
+
object.destroyed? ? :delete : :index
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def merged_scope(target)
|
91
|
+
scope ? scope.clone.merge(target) : target
|
92
|
+
end
|
93
|
+
|
94
|
+
def model_all
|
95
|
+
::ActiveRecord::VERSION::MAJOR < 4 ? model.scoped : model.all
|
96
|
+
end
|
24
97
|
end
|
25
98
|
end
|
26
99
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Chewy
|
2
|
+
module Type
|
3
|
+
module Adapter
|
4
|
+
# Basic adapter class. Contains interface, need to implement to add any classes support
|
5
|
+
class Base
|
6
|
+
BATCH_SIZE = 1000
|
7
|
+
|
8
|
+
# Camelcased name, used as type class constant name.
|
9
|
+
# For returned value 'Product' will be generated class name `ProductsIndex::Product`
|
10
|
+
#
|
11
|
+
def name
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
# Underscored type name, user for elasticsearch type creation
|
16
|
+
# and for type class access with ProductsIndex.type_hash hash or method.
|
17
|
+
# `ProductsIndex.type_hash['product']` or `ProductsIndex.product`
|
18
|
+
#
|
19
|
+
def type_name
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
# Splits passed objects to groups according to `:batch_size` options.
|
24
|
+
# For every group crates hash with action keys. Example:
|
25
|
+
#
|
26
|
+
# { delete: [object1, object2], index: [object3, object4, object5] }
|
27
|
+
#
|
28
|
+
# Returns true id all the block call returns true and false otherwise
|
29
|
+
#
|
30
|
+
def import *args, &block
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns array of loaded objects for passed objects array. If some object
|
35
|
+
# was not loaded, it returns `nil` in the place of this object
|
36
|
+
#
|
37
|
+
# load(double(id: 1), double(id: 2), double(id: 3)) #=>
|
38
|
+
# # [<Product id: 1>, nil, <Product id: 3>], assuming, #2 was not found
|
39
|
+
#
|
40
|
+
def load *args
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,20 +1,52 @@
|
|
1
|
+
require 'chewy/type/adapter/base'
|
2
|
+
|
1
3
|
module Chewy
|
2
4
|
module Type
|
3
5
|
module Adapter
|
4
|
-
class Object
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@options = options
|
9
|
-
@subject = subject
|
6
|
+
class Object < Base
|
7
|
+
def initialize *args
|
8
|
+
@options = args.extract_options!
|
9
|
+
@target = args.first
|
10
10
|
end
|
11
11
|
|
12
12
|
def name
|
13
|
-
@name ||=
|
13
|
+
@name ||= (options[:name] || target).to_s.camelize
|
14
14
|
end
|
15
15
|
|
16
16
|
def type_name
|
17
|
-
@type_name ||=
|
17
|
+
@type_name ||= (options[:name] || target).to_s.underscore
|
18
|
+
end
|
19
|
+
|
20
|
+
def import *args, &block
|
21
|
+
import_options = args.extract_options!
|
22
|
+
batch_size = import_options.delete(:batch_size) || BATCH_SIZE
|
23
|
+
objects = args.flatten
|
24
|
+
|
25
|
+
objects.in_groups_of(batch_size, false).all? do |group|
|
26
|
+
action_groups = group.group_by do |object|
|
27
|
+
raise "Object is not a `#{target}`" if class_target? && !object.is_a?(target)
|
28
|
+
object.respond_to?(:destroyed?) && object.destroyed? ? :delete : :index
|
29
|
+
end
|
30
|
+
block.call action_groups
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def load *args
|
35
|
+
load_options = args.extract_options!
|
36
|
+
objects = args.flatten
|
37
|
+
if class_target?
|
38
|
+
objects.map { |object| target.wrap(object) }
|
39
|
+
else
|
40
|
+
objects
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :target, :options
|
47
|
+
|
48
|
+
def class_target?
|
49
|
+
@class_target ||= @target.is_a?(Class)
|
18
50
|
end
|
19
51
|
end
|
20
52
|
end
|
data/lib/chewy/type/base.rb
CHANGED
data/lib/chewy/type/import.rb
CHANGED
@@ -3,62 +3,36 @@ module Chewy
|
|
3
3
|
module Import
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
included do
|
7
|
-
end
|
8
|
-
|
9
6
|
module ClassMethods
|
10
7
|
def bulk(options = {})
|
11
8
|
client.bulk options.merge(index: index.index_name, type: type_name)
|
12
9
|
end
|
13
10
|
|
14
11
|
def import(*args)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
12
|
+
import_options = args.extract_options!
|
13
|
+
bulk_options = import_options.extract!(:refresh).reverse_merge!(refresh: true)
|
14
|
+
identify = {_index: index.index_name, _type: type_name}
|
15
|
+
|
16
|
+
adapter.import(*args, import_options) do |action_objects|
|
17
|
+
payload = {type: self}
|
18
|
+
payload.merge! import: Hash[action_objects.map { |action, objects| [action, objects.count] }]
|
19
|
+
|
20
|
+
ActiveSupport::Notifications.instrument 'import_objects.chewy', payload do
|
21
|
+
body = action_objects.each.with_object([]) do |(action, objects), result|
|
22
|
+
result.concat(if action == :delete
|
23
|
+
objects.map { |object| { action => identify.merge(_id: object.respond_to?(:id) ? object.id : object) } }
|
24
|
+
else
|
25
|
+
objects.map { |object| { action => identify.merge(_id: object.id, data: object_data(object)) } }
|
26
|
+
end)
|
27
|
+
end
|
28
|
+
body.any? ? !!bulk(bulk_options.merge(body: body)) : true
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|
30
32
|
|
31
33
|
private
|
32
34
|
|
33
|
-
def
|
34
|
-
adapter.scope ? relation.merge(adapter.scope) : relation
|
35
|
-
end
|
36
|
-
|
37
|
-
def import_ids(ids, options = {})
|
38
|
-
ids = ids.map(&:to_i).uniq
|
39
|
-
scoped_relation(adapter.model.where(id: ids))
|
40
|
-
.find_in_batches(options.slice(:batch_size)) do |objects|
|
41
|
-
ids -= objects.map(&:id)
|
42
|
-
import_objects objects
|
43
|
-
end
|
44
|
-
|
45
|
-
body = ids.map { |id| {delete: {_index: index.index_name, _type: type_name, _id: id}} }
|
46
|
-
bulk refresh: true, body: body if body.any?
|
47
|
-
end
|
48
|
-
|
49
|
-
def import_objects(objects)
|
50
|
-
body = objects.map do |object|
|
51
|
-
identify = {_index: index.index_name, _type: type_name, _id: object.id}
|
52
|
-
if object.respond_to?(:destroyed?) && object.destroyed?
|
53
|
-
{delete: identify}
|
54
|
-
else
|
55
|
-
{index: identify.merge!(data: object_to_data(object))}
|
56
|
-
end
|
57
|
-
end
|
58
|
-
bulk refresh: true, body: body if body.any?
|
59
|
-
end
|
60
|
-
|
61
|
-
def object_to_data(object)
|
35
|
+
def object_data(object)
|
62
36
|
(self.root_object ||= build_root).compose(object)[type_name.to_sym]
|
63
37
|
end
|
64
38
|
end
|