outoftime-sunspot 0.8.9 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/README.rdoc +13 -21
  2. data/Rakefile +0 -2
  3. data/TODO +2 -15
  4. data/VERSION.yml +2 -2
  5. data/bin/sunspot-configure-solr +46 -0
  6. data/bin/sunspot-solr +15 -7
  7. data/lib/sunspot/adapters.rb +5 -1
  8. data/lib/sunspot/composite_setup.rb +186 -0
  9. data/lib/sunspot/configuration.rb +7 -1
  10. data/lib/sunspot/data_extractor.rb +10 -0
  11. data/lib/sunspot/date_facet.rb +36 -0
  12. data/lib/sunspot/date_facet_row.rb +17 -0
  13. data/lib/sunspot/dsl/field_query.rb +72 -0
  14. data/lib/sunspot/dsl/fields.rb +30 -3
  15. data/lib/sunspot/dsl/query.rb +16 -35
  16. data/lib/sunspot/dsl/query_facet.rb +31 -0
  17. data/lib/sunspot/dsl/scope.rb +76 -20
  18. data/lib/sunspot/dsl/search.rb +30 -0
  19. data/lib/sunspot/dsl.rb +1 -1
  20. data/lib/sunspot/facet.rb +17 -3
  21. data/lib/sunspot/facet_row.rb +4 -4
  22. data/lib/sunspot/field.rb +130 -207
  23. data/lib/sunspot/field_factory.rb +126 -0
  24. data/lib/sunspot/indexer.rb +61 -14
  25. data/lib/sunspot/instantiated_facet.rb +38 -0
  26. data/lib/sunspot/instantiated_facet_row.rb +12 -0
  27. data/lib/sunspot/query/base_query.rb +90 -0
  28. data/lib/sunspot/query/connective.rb +77 -0
  29. data/lib/sunspot/query/dynamic_query.rb +39 -56
  30. data/lib/sunspot/query/field_facet.rb +132 -4
  31. data/lib/sunspot/query/field_query.rb +57 -0
  32. data/lib/sunspot/query/pagination.rb +1 -1
  33. data/lib/sunspot/query/query_facet.rb +72 -0
  34. data/lib/sunspot/query/query_facet_row.rb +19 -0
  35. data/lib/sunspot/query/restriction.rb +9 -7
  36. data/lib/sunspot/query/scope.rb +165 -0
  37. data/lib/sunspot/query/sort.rb +17 -14
  38. data/lib/sunspot/query/sort_composite.rb +33 -0
  39. data/lib/sunspot/query.rb +162 -351
  40. data/lib/sunspot/query_facet.rb +33 -0
  41. data/lib/sunspot/query_facet_row.rb +21 -0
  42. data/lib/sunspot/schema.rb +165 -0
  43. data/lib/sunspot/search/hit.rb +62 -0
  44. data/lib/sunspot/search.rb +104 -41
  45. data/lib/sunspot/session.rb +64 -32
  46. data/lib/sunspot/setup.rb +119 -48
  47. data/lib/sunspot/type.rb +48 -2
  48. data/lib/sunspot.rb +74 -8
  49. data/solr/solr/conf/schema.xml +44 -225
  50. data/spec/api/build_search_spec.rb +557 -63
  51. data/spec/api/indexer_spec.rb +156 -74
  52. data/spec/api/query_spec.rb +55 -31
  53. data/spec/api/search_retrieval_spec.rb +210 -33
  54. data/spec/api/session_spec.rb +81 -26
  55. data/spec/api/sunspot_spec.rb +5 -7
  56. data/spec/integration/faceting_spec.rb +130 -0
  57. data/spec/integration/keyword_search_spec.rb +72 -31
  58. data/spec/integration/scoped_search_spec.rb +13 -0
  59. data/spec/integration/stored_fields_spec.rb +10 -0
  60. data/spec/mocks/blog.rb +3 -0
  61. data/spec/mocks/comment.rb +12 -23
  62. data/spec/mocks/connection.rb +84 -0
  63. data/spec/mocks/mock_adapter.rb +11 -3
  64. data/spec/mocks/mock_record.rb +41 -0
  65. data/spec/mocks/photo.rb +8 -0
  66. data/spec/mocks/post.rb +18 -23
  67. data/spec/spec_helper.rb +29 -14
  68. data/tasks/gemspec.rake +4 -3
  69. data/tasks/rdoc.rake +2 -2
  70. data/tasks/schema.rake +19 -0
  71. data/templates/schema.xml.haml +24 -0
  72. metadata +48 -7
  73. data/spec/mocks/base_class.rb +0 -2
@@ -1,12 +1,41 @@
1
+ require 'set'
2
+
1
3
  module Sunspot
2
- class Query
4
+ module Query
3
5
  #
4
6
  # Encapsulates a query component representing a field facet. Users create
5
7
  # instances using DSL::Query#facet
6
8
  #
7
9
  class FieldFacet #:nodoc:
8
- def initialize(field)
9
- @field = field
10
+ class <<self
11
+ protected :new
12
+
13
+ #
14
+ # Return the appropriate FieldFacet instance for the field and options.
15
+ # If a :time_range option is specified, and the field type is TimeType,
16
+ # build a DateFieldFacet. Otherwise, build a normal FieldFacet.
17
+ #
18
+ # ==== Returns
19
+ #
20
+ # FieldFacet:: FieldFacet instance of appropriate class.
21
+ #
22
+ def build(field, options)
23
+ if options.has_key?(:time_range)
24
+ unless field.type == Type::TimeType
25
+ raise(
26
+ ArgumentError,
27
+ ":time_range key can only be specified for time fields"
28
+ )
29
+ end
30
+ DateFieldFacet.new(field, options)
31
+ else
32
+ FieldFacet.new(field, options)
33
+ end
34
+ end
35
+ end
36
+
37
+ def initialize(field, options)
38
+ @field, @options = field, options
10
39
  end
11
40
 
12
41
  # ==== Returns
@@ -14,7 +43,106 @@ module Sunspot
14
43
  # Hash:: solr-ruby params for this field facet
15
44
  #
16
45
  def to_params
17
- { :facets => { :fields => [@field.indexed_name] }}
46
+ params = { :"facet.field" => [@field.indexed_name], :facet => 'true' }
47
+ params[param_key(:sort)] =
48
+ case @options[:sort]
49
+ when :count then 'true'
50
+ when :index then 'false'
51
+ when nil
52
+ else raise(ArgumentError, 'Allowed facet sort options are :count and :index')
53
+ end
54
+ params[param_key(:limit)] = @options[:limit]
55
+ params[param_key(:mincount)] =
56
+ if @options[:minimum_count] then @options[:minimum_count]
57
+ elsif @options[:zeros] then 0
58
+ else 1
59
+ end
60
+ params
61
+ end
62
+
63
+ private
64
+
65
+ #
66
+ # Given a facet parameter name, return the appropriate Solr parameter for
67
+ # this facet.
68
+ #
69
+ # ==== Returns
70
+ #
71
+ # Symbol:: Solr query parameter key
72
+ #
73
+ def param_key(name)
74
+ :"f.#{@field.indexed_name}.facet.#{name}"
75
+ end
76
+ end
77
+
78
+ class DateFieldFacet < FieldFacet #:nodoc:
79
+ ALLOWED_OTHER = Set.new(%w(before after between none all))
80
+
81
+ #
82
+ # Convert the facet to date params.
83
+ #
84
+ def to_params
85
+ super.merge(
86
+ :"facet.date" => [@field.indexed_name],
87
+ param_key('date.start') => start_time.utc.xmlschema,
88
+ param_key('date.end') => end_time.utc.xmlschema,
89
+ param_key('date.gap') => "+#{interval}SECONDS",
90
+ param_key('date.other') => others
91
+ )
92
+ end
93
+
94
+ private
95
+
96
+ #
97
+ # Start time for facet range
98
+ #
99
+ # ==== Returns
100
+ #
101
+ # Time:: Start time
102
+ #
103
+ def start_time
104
+ @options[:time_range].first
105
+ end
106
+
107
+ #
108
+ # End time for facet range
109
+ #
110
+ # ==== Returns
111
+ #
112
+ # Time:: End time
113
+ #
114
+ def end_time
115
+ @options[:time_range].last
116
+ end
117
+
118
+ #
119
+ # Time interval that each facet row should cover. Default is 1 day.
120
+ #
121
+ # ===== Returns
122
+ #
123
+ # Integer:: Time interval in seconds
124
+ #
125
+ def interval
126
+ @options[:time_interval] || 86400
127
+ end
128
+
129
+ #
130
+ # Other time ranges to create facet rows for. Allowed values are defined
131
+ # in ALLOWED_OTHER constant.
132
+ #
133
+ def others
134
+ if others = @options[:time_other]
135
+ Array(others).map do |other|
136
+ other = other.to_s
137
+ unless ALLOWED_OTHER.include?(other)
138
+ raise(
139
+ ArgumentError,
140
+ "#{other.inspect} is not a valid argument for :time_other"
141
+ )
142
+ end
143
+ other
144
+ end
145
+ end
18
146
  end
19
147
  end
20
148
  end
@@ -0,0 +1,57 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # This class acts as a base class for query components that encapsulate
5
+ # operations on fields. It is subclassed by the Query::Query class and the
6
+ # Query::DynamicQuery class.
7
+ #
8
+ class FieldQuery < Scope
9
+ #
10
+ # Add a field facet. See Sunspot::Facet for more information.
11
+ #
12
+ # ==== Parameters
13
+ #
14
+ # field_name<Symbol>:: Name of the field on which to get a facet
15
+ #
16
+ # ==== Returns
17
+ #
18
+ # FieldFacet:: The field facet object
19
+ #
20
+ def add_field_facet(field_name, options = nil)
21
+ add_component(FieldFacet.build(build_field(field_name), options || {}))
22
+ end
23
+
24
+ #
25
+ # Add a query facet.
26
+ #
27
+ # ==== Parameters
28
+ #
29
+ # name<Symbol>::
30
+ # The name associated with the query facet. This is not passed to Solr,
31
+ # but allows the user to retrieve the facet result by passing the name
32
+ # to the Search#facet method.
33
+ #
34
+ # ==== Returns
35
+ #
36
+ # QueryFacet:: The query facet object
37
+ #
38
+ def add_query_facet(name)
39
+ add_component(facet = QueryFacet.new(name, setup))
40
+ query_facets[name.to_sym] = facet
41
+ facet
42
+ end
43
+
44
+ #
45
+ # Set result ordering.
46
+ #
47
+ # ==== Parameters
48
+ #
49
+ # field_name<Symbol>:: Name of the field on which to order
50
+ # direction<Symbol>:: :asc or :desc (default :asc)
51
+ #
52
+ def order_by(field_name, direction = nil)
53
+ add_sort(Sort.new(build_field(field_name), direction))
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,5 +1,5 @@
1
1
  module Sunspot
2
- class Query
2
+ module Query
3
3
  #
4
4
  # A query component that holds information about pagination. Unlike other
5
5
  # query components, this one is mutable, because the query itself holds a
@@ -0,0 +1,72 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # QueryFacets encapsulate requests for Sunspot's query faceting capability.
5
+ # They are created by the FieldQuery#add_query_facet method.
6
+ #
7
+ #--
8
+ #
9
+ # The actual concept of a QueryFacet is somewhat artificial - it provides a
10
+ # grouping for the facet at the Sunspot level, which provides a nicer and
11
+ # more consistent API in Sunspot; Solr does not provide any grouping for
12
+ # query facet rows, instead returning each requested row individually, keyed
13
+ # by the boolean phrase used in the facet query.
14
+ #
15
+ class QueryFacet
16
+ attr_reader :name #:nodoc:
17
+
18
+ def initialize(name, setup) #:nodoc:
19
+ @name = name
20
+ @setup = setup
21
+ @components = []
22
+ end
23
+
24
+ #
25
+ # Add a QueryFacetRow to this facet. The label argument becomes the value
26
+ # of the Sunspot::QueryFacetRow object corresponding to this query facet
27
+ # row.
28
+ #
29
+ # ==== Parameters
30
+ #
31
+ # label<Object>::
32
+ # An object that will become the value of the result row. Use whatever
33
+ # type is most intuitive.
34
+ #
35
+ # ==== Returns
36
+ #
37
+ # QueryFacetRow:: QueryFacetRow object containing scope for this row
38
+ #
39
+ def add_row(label)
40
+ @components << row = QueryFacetRow.new(label, @setup)
41
+ row
42
+ end
43
+
44
+ #
45
+ # Express this query facet as Solr parameters
46
+ #
47
+ # ==== Returns
48
+ #
49
+ # Hash:: Solr params hash
50
+ #
51
+ def to_params #:nodoc:
52
+ components = @components.map { |component| component.to_boolean_phrase }
53
+ components = components.first if components.length == 1
54
+ {
55
+ :facet => 'true',
56
+ :"facet.query" => components
57
+ }
58
+ end
59
+
60
+ #
61
+ # Get query facet rows (used when constructing results)
62
+ #
63
+ # ==== Returns
64
+ #
65
+ # Array:: Array of QueryFacetRow objects.
66
+ #
67
+ def rows #:nodoc:
68
+ @components
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # QueryFacetRow objects encapsulate restrictions for a particular
5
+ # QueryFacet. They also contain a label attribute, which is used as the
6
+ # value for the search result's corresponding facet row object.
7
+ #
8
+ # See Query::Scope for the API provided.
9
+ #
10
+ class QueryFacetRow < Connective::Conjunction
11
+ attr_reader :label #:nodoc:
12
+
13
+ def initialize(label, setup) #:nodoc:
14
+ super(setup)
15
+ @label = label
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,5 @@
1
1
  module Sunspot
2
- class Query
2
+ module Query
3
3
  module Restriction #:nodoc:
4
4
  class <<self
5
5
  #
@@ -33,6 +33,8 @@ module Sunspot
33
33
  # * #to_solr_conditional
34
34
  #
35
35
  class Base #:nodoc:
36
+ include RSolr::Char
37
+
36
38
  def initialize(field, value, negative = false)
37
39
  @field, @value, @negative = field, value, negative
38
40
  end
@@ -49,7 +51,7 @@ module Sunspot
49
51
  # Hash:: Representation of this restriction as solr-ruby parameters
50
52
  #
51
53
  def to_params
52
- { :filter_queries => [to_boolean_phrase] }
54
+ { :fq => [to_boolean_phrase] }
53
55
  end
54
56
 
55
57
  #
@@ -79,7 +81,7 @@ module Sunspot
79
81
  # String:: Boolean phrase for restriction in the positive
80
82
  #
81
83
  def to_positive_boolean_phrase
82
- "#{Solr::Util.query_parser_escape(@field.indexed_name)}:#{to_solr_conditional}"
84
+ "#{escape(@field.indexed_name)}:#{to_solr_conditional}"
83
85
  end
84
86
 
85
87
  #
@@ -117,7 +119,7 @@ module Sunspot
117
119
  # String:: Solr API representation of given value
118
120
  #
119
121
  def solr_value(value = @value)
120
- Solr::Util.query_parser_escape(@field.to_indexed(value))
122
+ escape(@field.to_indexed(value))
121
123
  end
122
124
  end
123
125
 
@@ -130,7 +132,7 @@ module Sunspot
130
132
  unless @value.nil?
131
133
  super
132
134
  else
133
- "-#{Solr::Util.query_parser_escape(@field.indexed_name)}:[* TO *]"
135
+ "-#{escape(@field.indexed_name)}:[* TO *]"
134
136
  end
135
137
  end
136
138
 
@@ -138,7 +140,7 @@ module Sunspot
138
140
  unless @value.nil?
139
141
  super
140
142
  else
141
- "#{Solr::Util.query_parser_escape(@field.indexed_name)}:[* TO *]"
143
+ "#{escape(@field.indexed_name)}:[* TO *]"
142
144
  end
143
145
  end
144
146
 
@@ -215,7 +217,7 @@ module Sunspot
215
217
 
216
218
  def to_positive_boolean_phrase
217
219
  adapter = Adapters::InstanceAdapter.adapt(@object)
218
- "id:#{Solr::Util.query_parser_escape(adapter.index_id)}"
220
+ "id:#{escape(adapter.index_id)}"
219
221
  end
220
222
  end
221
223
  end
@@ -0,0 +1,165 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # The Scope class encapsulates a set of restrictions that scope search
5
+ # results (as well as query facets rows). This class's API is exposed by
6
+ # Query::Query and Query::QueryFacetRow.
7
+ #
8
+ class Scope
9
+ #
10
+ # Add a restriction to the query.
11
+ #
12
+ # ==== Parameters
13
+ #
14
+ # field_name<Symbol>:: Name of the field to which the restriction applies
15
+ # restriction_type<Class,Symbol>::
16
+ # Subclass of Sunspot::Query::Restriction::Base, or snake_cased name as symbol
17
+ # (e.g., +:equal_to+)
18
+ # value<Object>::
19
+ # Value against which the restriction applies (e.g. less_than(2) has a
20
+ # value of 2)
21
+ # negated::
22
+ # Whether this restriction should be negated (use add_negated_restriction)
23
+ #
24
+ def add_restriction(field_name, restriction_type, value, negated = false)
25
+ if restriction_type.is_a?(Symbol)
26
+ restriction_type = Restriction[restriction_type]
27
+ end
28
+ add_component(
29
+ restriction = restriction_type.new(
30
+ build_field(field_name), value, negated
31
+ )
32
+ )
33
+ restriction
34
+ end
35
+
36
+ #
37
+ # Add a negated restriction to the query. The restriction will be taken as
38
+ # the opposite of its usual meaning (e.g., an :equal_to restriction will
39
+ # be "not equal to".
40
+ #
41
+ # ==== Parameters
42
+ #
43
+ # field_name<Symbol>:: Name of the field to which the restriction applies
44
+ # restriction_type<Class>::
45
+ # Subclass of Sunspot::Query::Restriction::Base to instantiate
46
+ # value<Object>::
47
+ # Value against which the restriction applies (e.g. less_than(2) has a
48
+ # value of 2)
49
+ #
50
+ def add_negated_restriction(field_name, restriction_type, value)
51
+ add_restriction(field_name, restriction_type, value, true)
52
+ end
53
+
54
+ #
55
+ # Add a disjunction to the scope. The disjunction can then take a set of
56
+ # restrictions, which are combined with OR semantics.
57
+ #
58
+ # ==== Returns
59
+ #
60
+ # Connective::Disjunction:: New disjunction
61
+ #
62
+ def add_disjunction
63
+ add_component(disjunction = Connective::Disjunction.new(setup))
64
+ disjunction
65
+ end
66
+
67
+ #
68
+ # Add a conjunction to the scope. In most cases, this will simply return
69
+ # the Scope object itself, since scopes by default combine their
70
+ # restrictions with OR semantics. The Connective::Disjunction class
71
+ # overrides this method to return a Connective::Conjunction.
72
+ #
73
+ # ==== Returns
74
+ #
75
+ # Scope:: Self or another scope with conjunctive semantics.
76
+ #
77
+ def add_conjunction
78
+ self
79
+ end
80
+
81
+ #
82
+ # Exclude a particular instance from the search results
83
+ #
84
+ # ==== Parameters
85
+ #
86
+ # instance<Object>:: instance to exclude from results
87
+ #
88
+ def exclude_instance(instance)
89
+ add_component(Restriction::SameAs.new(instance, true))
90
+ end
91
+
92
+ #
93
+ # Generate a DynamicQuery instance for the given base name.
94
+ # This gives you access to a subset of the Query API but the operations
95
+ # apply to dynamic fields inside the dynamic field definition specified
96
+ # by +base_name+.
97
+ #
98
+ # ==== Parameters
99
+ #
100
+ # base_name<Symbol>::
101
+ # Base name of the dynamic field definition to use in the dynamic query
102
+ # operations
103
+ #
104
+ # ==== Returns
105
+ #
106
+ # DynamicQuery::
107
+ # Instance providing dynamic query functionality for the given field
108
+ # definitions.
109
+ #
110
+ def dynamic_query(base_name)
111
+ DynamicQuery.new(setup.dynamic_field_factory(base_name), self)
112
+ end
113
+
114
+ #
115
+ # Determine which restriction type to add based on the type of the value.
116
+ # Used to interpret query conditions passed as a hash, as well as the
117
+ # short-form DSL::Scope#with method.
118
+ #
119
+ # ==== Parameters
120
+ #
121
+ # field_name<Symbol>:: Name of the field on which to apply the restriction
122
+ # value<Object,Array,Range>:: Value to which to apply to the restriction
123
+ #--
124
+ # negated<Boolean>:: Whether to negate the restriction.
125
+ #
126
+ def add_shorthand_restriction(field_name, value, negated = false) #:nodoc:
127
+ restriction_type =
128
+ case value
129
+ when Range
130
+ Restriction::Between
131
+ when Array
132
+ Restriction::AnyOf
133
+ else
134
+ Restriction::EqualTo
135
+ end
136
+ add_restriction(field_name, restriction_type, value, negated)
137
+ end
138
+
139
+ #
140
+ # Add a negated shorthand restriction. See #add_shorthand_restriction
141
+ #
142
+ def add_negated_shorthand_restriction(field_name, value)
143
+ add_shorthand_restriction(field_name, value, true)
144
+ end
145
+
146
+ private
147
+
148
+ #
149
+ # Build a field with the given field name. Subclasses may override this
150
+ # method.
151
+ #
152
+ def build_field(field_name)
153
+ setup.field(field_name)
154
+ end
155
+
156
+ #
157
+ # Return a setup object which can return a field object given a name.
158
+ # Subclasses may override this method.
159
+ #
160
+ def setup
161
+ @setup
162
+ end
163
+ end
164
+ end
165
+ end
@@ -1,32 +1,35 @@
1
1
  module Sunspot
2
- class Query
2
+ module Query
3
3
  #
4
4
  # The Sort class is a query component representing a sort by a given field.
5
5
  #
6
6
  class Sort #:nodoc:
7
- ASCENDING = Set.new([:asc, :ascending])
8
- DESCENDING = Set.new([:desc, :descending])
7
+ DIRECTIONS = {
8
+ :asc => 'asc',
9
+ :ascending => 'asc',
10
+ :desc => 'desc',
11
+ :descending => 'desc'
12
+ }
9
13
 
10
14
  def initialize(field, direction = nil)
15
+ if field.multiple?
16
+ raise(ArgumentError, "#{field.name} cannot be used for ordering because it is a multiple-value field")
17
+ end
11
18
  @field, @direction = field, (direction || :asc).to_sym
12
19
  end
13
20
 
14
- def to_params
15
- { :sort => [{ @field.indexed_name.to_sym => direction_for_solr }] }
21
+ def to_param
22
+ "#{@field.indexed_name.to_sym} #{direction_for_solr}"
16
23
  end
17
24
 
18
25
  private
19
26
 
20
27
  def direction_for_solr
21
- case
22
- when ASCENDING.include?(@direction)
23
- :ascending
24
- when DESCENDING.include?(@direction)
25
- :descending
26
- else
27
- raise ArgumentError,
28
- "Unknown sort direction #{@direction}. Acceptable input is: #{(ASCENDING + DESCENDING).map { |input| input.inspect } * ', '}"
29
- end
28
+ DIRECTIONS[@direction] ||
29
+ raise(
30
+ ArgumentError,
31
+ "Unknown sort direction #{@direction}. Acceptable input is: #{DIRECTIONS.keys.map { |input| input.inspect } * ', '}"
32
+ )
30
33
  end
31
34
  end
32
35
  end
@@ -0,0 +1,33 @@
1
+ module Sunspot
2
+ module Query
3
+ #
4
+ # The SortComposite class encapsulates an ordered collection of Sort
5
+ # objects. It's necessary to keep this as a separate class as Solr takes
6
+ # the sort as a single parameter, so adding sorts as regular components
7
+ # would not merge correctly in the #to_params method.
8
+ #
9
+ class SortComposite #:nodoc:
10
+ def initialize
11
+ @sorts = []
12
+ end
13
+
14
+ #
15
+ # Add a sort to the composite
16
+ #
17
+ def <<(sort)
18
+ @sorts << sort
19
+ end
20
+
21
+ #
22
+ # Combine the sorts into a single param by joining them
23
+ #
24
+ def to_params
25
+ unless @sorts.empty?
26
+ { :sort => @sorts.map { |sort| sort.to_param } * ', ' }
27
+ else
28
+ {}
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end