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
@@ -0,0 +1,31 @@
1
+ module Sunspot
2
+ module DSL
3
+ #
4
+ # This tiny DSL class implements the DSL for the FieldQuery.facet
5
+ # method.
6
+ #
7
+ class QueryFacet
8
+ def initialize(query_facet) #:nodoc:
9
+ @query_facet = query_facet
10
+ end
11
+
12
+ #
13
+ # Add a row to this query facet. The label argument can be anything; it's
14
+ # simply the value that's passed into the Sunspot::QueryFacetRow object
15
+ # corresponding to the row that's created. Use whatever seems most
16
+ # intuitive.
17
+ #
18
+ # The block is evaluated in the context of a Sunspot::DSL::Scope, meaning
19
+ # any restrictions can be placed on the documents matching this facet row.
20
+ #
21
+ # ==== Parameters
22
+ #
23
+ # label<Object>::
24
+ # An object used to identify this facet row in the results.
25
+ #
26
+ def row(label, &block)
27
+ Scope.new(@query_facet.add_row(label)).instance_eval(&block)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -17,14 +17,17 @@ module Sunspot
17
17
  #
18
18
  # Build a positive restriction. With one argument, this method returns
19
19
  # another DSL object which presents methods for attaching various
20
- # restriction types. With two arguments, acts as a shorthand for creating
21
- # an equality restriction.
20
+ # restriction types. With two arguments, this creates a shorthand
21
+ # restriction: if the second argument is a scalar, an equality restriction
22
+ # is created; if it is a Range, a between restriction will be created; and
23
+ # if it is an Array, an any_of restriction will be created.
22
24
  #
23
25
  # ==== Parameters
24
26
  #
25
27
  # field_name<Symbol>:: Name of the field on which to place the restriction
26
- # value<Symbol>::
27
- # If passed, creates an equality restriction with this value
28
+ # value<Object,Range,Array>::
29
+ # If passed, creates an equality, range, or any-of restriction based on
30
+ # the type of value passed.
28
31
  #
29
32
  # ==== Returns
30
33
  #
@@ -38,6 +41,18 @@ module Sunspot
38
41
  # Sunspot.search do
39
42
  # with(:blog_id, 1)
40
43
  # end
44
+ #
45
+ # Restrict by range:
46
+ #
47
+ # Sunspot.search do
48
+ # with(:average_rating, 3.0..5.0)
49
+ # end
50
+ #
51
+ # Restrict by a set of allowed values:
52
+ #
53
+ # Sunspot.search do
54
+ # with(:category_ids, [1, 5, 9])
55
+ # end
41
56
  #
42
57
  # Other restriction types:
43
58
  #
@@ -49,7 +64,7 @@ module Sunspot
49
64
  if value == NONE
50
65
  DSL::Restriction.new(field_name.to_sym, @query, false)
51
66
  else
52
- @query.add_restriction(field_name, Sunspot::Query::Restriction::EqualTo, value, false)
67
+ @query.add_shorthand_restriction(field_name, value)
53
68
  end
54
69
  end
55
70
 
@@ -98,7 +113,7 @@ module Sunspot
98
113
  if value == NONE
99
114
  DSL::Restriction.new(field_name.to_sym, @query, true)
100
115
  else
101
- @query.add_negated_restriction(field_name, Sunspot::Query::Restriction::EqualTo, value)
116
+ @query.add_negated_shorthand_restriction(field_name, value)
102
117
  end
103
118
  else
104
119
  instances = args
@@ -108,29 +123,70 @@ module Sunspot
108
123
  end
109
124
  end
110
125
 
111
- # Specify the order that results should be returned in. This method can
112
- # be called multiple times; precedence will be in the order given.
126
+ #
127
+ # Create a disjunction, scoping the results to documents that match any
128
+ # of the enclosed restrictions.
113
129
  #
114
- # ==== Parameters
130
+ # ==== Example
131
+ #
132
+ # Sunspot.search(Post) do
133
+ # any_of do
134
+ # with(:expired_at).greater_than Time.now
135
+ # with :expired_at, nil
136
+ # end
137
+ # end
115
138
  #
116
- # field_name<Symbol>:: the field to use for ordering
117
- # direction<Symbol>:: :asc or :desc (default :asc)
139
+ # This will return all documents who either have an expiration time in the
140
+ # future, or who do not have any expiration time at all.
118
141
  #
119
- def order_by(field_name, direction = nil)
120
- @query.order_by(field_name, direction)
142
+ def any_of(&block)
143
+ Util.instance_eval_or_call(Scope.new(@query.add_disjunction), &block)
121
144
  end
122
145
 
123
- # Request facets on the given field names. See Sunspot::Search#facet and
124
- # Sunspot::Facet for information on what is returned.
146
+ #
147
+ # Create a conjunction, scoping the results to documents that match all of
148
+ # the enclosed restrictions. When called from the top level of a search
149
+ # block, this has no effect, but can be useful for grouping a conjunction
150
+ # inside a disjunction.
151
+ #
152
+ # ==== Example
125
153
  #
154
+ # Sunspot.search(Post) do
155
+ # any_of do
156
+ # with(:blog_id, 1)
157
+ # all_of do
158
+ # with(:blog_id, 2)
159
+ # with(:category_ids, 3)
160
+ # end
161
+ # end
162
+ # end
163
+ #
164
+ def all_of(&block)
165
+ Util.instance_eval_or_call(Scope.new(@query.add_conjunction), &block)
166
+ end
167
+
168
+ #
169
+ # Apply restrictions, facets, and ordering to dynamic field instances.
170
+ # The block API is implemented by Sunspot::DSL::FieldQuery, which is a
171
+ # superclass of the Query DSL (thus providing a subset of the API, in
172
+ # particular only methods that refer to particular fields).
173
+ #
126
174
  # ==== Parameters
175
+ #
176
+ # base_name<Symbol>:: The base name for the dynamic field definition
127
177
  #
128
- # field_names...<Symbol>:: fields for which to return field facets
178
+ # ==== Example
129
179
  #
130
- def facet(*field_names)
131
- for field_name in field_names
132
- @query.add_field_facet(field_name)
133
- end
180
+ # Sunspot.search Post do
181
+ # dynamic :custom do
182
+ # with :cuisine, 'Pizza'
183
+ # facet :atmosphere
184
+ # order_by :chef_name
185
+ # end
186
+ # end
187
+ #
188
+ def dynamic(base_name, &block)
189
+ FieldQuery.new(@query.dynamic_query(base_name)).instance_eval(&block)
134
190
  end
135
191
  end
136
192
  end
@@ -0,0 +1,30 @@
1
+ module Sunspot
2
+ module DSL
3
+ #
4
+ # This top-level DSL class is the context in which the block passed to
5
+ # Sunspot.query. See Sunspot::DSL::Query, Sunspot::DSL::FieldQuery, and
6
+ # Sunspot::DSL::Scope for the full API presented.
7
+ #
8
+ class Search < Query
9
+ def initialize(search) #:nodoc:
10
+ @search = search
11
+ @query = search.query
12
+ end
13
+
14
+ #
15
+ # Retrieve the data accessor used to load instances of the given class
16
+ # out of persistent storage. Data accessors are free to implement any
17
+ # extra methods that may be useful in this context.
18
+ #
19
+ # ==== Example
20
+ #
21
+ # Sunspot.search Post do
22
+ # data_acccessor_for(Post).includes = [:blog, :comments]
23
+ # end
24
+ #
25
+ def data_accessor_for(clazz)
26
+ @search.data_accessor_for(clazz)
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/sunspot/dsl.rb CHANGED
@@ -1,3 +1,3 @@
1
- %w(fields scope query restriction).each do |file|
1
+ %w(fields scope field_query query query_facet restriction search).each do |file|
2
2
  require File.join(File.dirname(__FILE__), 'dsl', file)
3
3
  end
data/lib/sunspot/facet.rb CHANGED
@@ -1,12 +1,16 @@
1
+ require 'enumerator'
2
+
1
3
  module Sunspot
2
4
  #
3
5
  # The facet class encapsulates the information returned by Solr for a
4
- # particular facet request.
6
+ # field facet request.
5
7
  #
6
8
  # See http://wiki.apache.org/solr/SolrFacetingOverview for more information
7
9
  # on Solr's faceting capabilities.
8
10
  #
9
11
  class Facet
12
+ attr_reader :field
13
+
10
14
  def initialize(facet_values, field) #:nodoc:
11
15
  @facet_values, @field = facet_values, field
12
16
  end
@@ -29,9 +33,19 @@ module Sunspot
29
33
  #
30
34
  def rows
31
35
  @rows ||=
32
- @facet_values.map do |facet_value|
33
- FacetRow.new(facet_value, @field)
36
+ begin
37
+ rows = []
38
+ @facet_values.each_slice(2) do |pair|
39
+ rows << new_row(pair)
40
+ end
41
+ rows
34
42
  end
35
43
  end
44
+
45
+ private
46
+
47
+ def new_row(pair)
48
+ FacetRow.new(pair, self)
49
+ end
36
50
  end
37
51
  end
@@ -1,8 +1,8 @@
1
1
  module Sunspot
2
2
  # This class encapsulates a facet row (value) for a facet.
3
3
  class FacetRow
4
- def initialize(facet_value, field) #:nodoc:
5
- @facet_value, @field = facet_value, field
4
+ def initialize(pair, facet) #:nodoc:
5
+ @pair, @facet = pair, facet
6
6
  end
7
7
 
8
8
  # The value associated with the facet. This will be cast according to the
@@ -17,7 +17,7 @@ module Sunspot
17
17
  # Object:: The value associated with the row, cast to the appropriate type
18
18
  #
19
19
  def value
20
- @value ||= @field.cast(@facet_value.name)
20
+ @value ||= @facet.field.cast(@pair[0])
21
21
  end
22
22
 
23
23
  # The number of documents matching the search parameters that have this
@@ -28,7 +28,7 @@ module Sunspot
28
28
  # Integer:: Document count for this value
29
29
  #
30
30
  def count
31
- @count ||= @facet_value.value
31
+ @count ||= @pair[1]
32
32
  end
33
33
  end
34
34
  end
data/lib/sunspot/field.rb CHANGED
@@ -1,234 +1,157 @@
1
1
  module Sunspot
2
- #
3
- # The Field functionality in Sunspot is comprised of two roles:
4
- #
5
- # Field definitions::
6
- # Field definitions encompass the information that the user enters when
7
- # setting up the field, such as field name, type of access, data type,
8
- # whether multiple values are allowed, etc.
9
- # They are also capable of extracting data from a model in a format
10
- # that can be passed directly to the indexer.
11
- # Field instances::
12
- # Field instances represent an actual field in Solr; thus, they are able to
13
- # return the indexed field name, convert the value to its appropriate type,
14
- # etc.
15
- #
16
- # StaticField objects play both the definition and the instance role.
17
- # DynamicField objects act only as definitions, and spawn DynamicFieldInstance
18
- # objects to play the instance role.
19
- #
20
- module Field #:nodoc: all
21
- #
22
- # The FieldInstance module encapsulates functionality associated with
23
- # acting as a concrete instance of a field for the purposes of search.
24
- # In particular, FieldInstances need to be able to return indexed names,
25
- # convert values to their indexed representation, and cast returned values
26
- # to the appropriate native Ruby type.
27
- #
28
- module FieldInstance
29
- # The name of the field as it is indexed in Solr. The indexed name
30
- # contains a suffix that contains information about the type as well as
31
- # whether the field allows multiple values for a document.
32
- #
33
- # ==== Returns
34
- #
35
- # String:: The field's indexed name
36
- #
37
- def indexed_name
38
- "#{@type.indexed_name(name)}#{'m' if @multiple}"
39
- end
40
-
41
- # Convert a value to its representation for Solr indexing. This delegates
42
- # to the #to_indexed method on the field's type.
43
- #
44
- # ==== Parameters
45
- #
46
- # value<Object>:: Value to convert to Solr representation
47
- #
48
- # ==== Returns
49
- #
50
- # String:: Solr representation of the object
51
- #
52
- # ==== Raises
53
- #
54
- # ArgumentError::
55
- # the value is an array, but this field does not allow multiple values
56
- #
57
- def to_indexed(value)
58
- if value.is_a? Array
59
- if @multiple
60
- value.map { |val| to_indexed(val) }
61
- else
62
- raise ArgumentError, "#{name} is not a multiple-value field, so it cannot index values #{value.inspect}"
63
- end
64
- else
65
- @type.to_indexed(value)
66
- end
67
- end
2
+ class Field #:nodoc:
3
+ attr_accessor :name # The public-facing name of the field
4
+ attr_accessor :type # The Type of the field
5
+ attr_accessor :reference # Model class that the value of this field refers to
6
+ attr_accessor :attributes
68
7
 
69
- # Cast the value into the appropriate Ruby class for the field's type
70
- #
71
- # ==== Parameters
72
- #
73
- # value<String>:: Solr's representation of the value
74
- #
75
- # ==== Returns
76
- #
77
- # Object:: The cast value
78
- #
79
- def cast(value)
80
- @type.cast(value)
81
- end
82
- end
83
-
84
- #
85
- # This module adds a (class) method for building a field definition given
86
- # a standard set of arguments
87
8
  #
88
- module Buildable
89
- #
90
- # Build a field definition based on a standard argument API. If a block
91
- # is passed, use virtual extraction; otherwise, use attribute extraction.
92
- #
93
- def build(name, type, options = {}, &block)
94
- data_extractor =
95
- if block
96
- DataExtractor::BlockExtractor.new(&block)
97
- else
98
- DataExtractor::AttributeExtractor.new(options.delete(:using) || name)
99
- end
100
- new(name, type, data_extractor, options)
101
- end
9
+ #
10
+ def initialize(name, type) #:nodoc
11
+ @name, @type = name.to_sym, type
12
+ @attributes = {}
102
13
  end
103
14
 
15
+ # Convert a value to its representation for Solr indexing. This delegates
16
+ # to the #to_indexed method on the field's type.
104
17
  #
105
- # Field classes encapsulate information about a field that has been configured
106
- # for search and indexing. They expose methods that are useful for both
107
- # operations.
18
+ # ==== Parameters
108
19
  #
109
- # Subclasses of Field::Base must implement the method #value_for
20
+ # value<Object>:: Value to convert to Solr representation
110
21
  #
111
- class StaticField
112
- include FieldInstance
113
- extend Buildable
114
-
115
- attr_accessor :name # The public-facing name of the field
116
- attr_accessor :type # The Type of the field
117
-
118
- def initialize(name, type, data_extractor, options = {}) #:nodoc
119
- unless name.to_s =~ /^\w+$/
120
- raise ArgumentError, "Invalid field name #{name}: only letters, numbers, and underscores are allowed."
121
- end
122
- @name, @type, @data_extractor = name.to_sym, type, data_extractor
123
- @multiple = !!options.delete(:multiple)
124
- raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
125
- end
126
-
127
- # A key-value pair where the key is the field's indexed name and the
128
- # value is the value that should be indexed for the given model. This can
129
- # be merged directly into the document hash for adding to solr-ruby.
130
- #
131
- # ==== Parameters
132
- #
133
- # model<Object>:: the model from which to extract the value
134
- #
135
- # ==== Returns
136
- #
137
- # Hash:: a single key-value pair with the field name and value
138
- #
139
- def pairs_for(model)
140
- unless (value = @data_extractor.value_for(model)).nil?
141
- { indexed_name.to_sym => to_indexed(value) }
22
+ # ==== Returns
23
+ #
24
+ # String:: Solr representation of the object
25
+ #
26
+ # ==== Raises
27
+ #
28
+ # ArgumentError::
29
+ # the value is an array, but this field does not allow multiple values
30
+ #
31
+ def to_indexed(value)
32
+ if value.is_a? Array
33
+ if @multiple
34
+ value.map { |val| to_indexed(val) }
142
35
  else
143
- {}
36
+ raise ArgumentError, "#{name} is not a multiple-value field, so it cannot index values #{value.inspect}"
144
37
  end
38
+ else
39
+ @type.to_indexed(value)
145
40
  end
146
41
  end
147
42
 
43
+ # Cast the value into the appropriate Ruby class for the field's type
44
+ #
45
+ # ==== Parameters
46
+ #
47
+ # value<String>:: Solr's representation of the value
48
+ #
49
+ # ==== Returns
50
+ #
51
+ # Object:: The cast value
52
+ #
53
+ def cast(value)
54
+ @type.cast(value)
55
+ end
56
+
148
57
  #
149
- # A DynamicField is a field definition that allows actual fields to be
150
- # dynamically specified at search/index time. Indexed objects specify
151
- # the actual fields to be indexed using a hash, whose keys are the dynamic
152
- # field names and whose values are the values to be indexed.
153
- #
154
- # When indexed, dynamic fields are stored using the dynamic field's base
155
- # name, and the runtime-specified dynamic name, separated by a colon. Since
156
- # colons are not permitted in static Sunspot field names, namespace
157
- # collisions are prevented.
158
- #
159
- # The use cases for dynamic fields are fairly limited, but certain
160
- # applications with highly dynamic data models might find them userful.
161
- #
162
- class DynamicField
163
- extend Buildable
58
+ # Name with which this field is indexed internally. Based on public name and
59
+ # type.
60
+ #
61
+ # ==== Returns
62
+ #
63
+ # String:: Internal name of the field
64
+ #
65
+ def indexed_name
66
+ @type.indexed_name(@name)
67
+ end
164
68
 
165
- attr_accessor :name # Base name of the dynamic field.
69
+ #
70
+ # Whether this field accepts multiple values.
71
+ #
72
+ # ==== Returns
73
+ #
74
+ # Boolean:: True if this field accepts multiple values.
75
+ #
76
+ def multiple?
77
+ !!@multiple
78
+ end
79
+ end
166
80
 
167
- def initialize(name, type, data_extractor, options)
168
- @name, @type, @data_extractor = name, type, data_extractor
169
- @multiple = !!options.delete(:multiple)
81
+ #
82
+ # FulltextField instances represent fields that are indexed as fulltext.
83
+ # These fields are tokenized in the index, and can have boost applied to
84
+ # them. They also always allow multiple values (since the only downside of
85
+ # allowing multiple values is that it prevents the field from being sortable,
86
+ # and sorting on tokenized fields is nonsensical anyway, there is no reason
87
+ # to do otherwise). FulltextField instances always have the type TextType.
88
+ #
89
+ class FulltextField < Field #:nodoc:
90
+ def initialize(name, options = {})
91
+ super(name, Type::TextType)
92
+ if options.has_key?(:boost)
93
+ @attributes[:boost] = options.delete(:boost)
170
94
  end
95
+ @multiple = true
96
+ raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
97
+ end
98
+ end
171
99
 
172
- #
173
- # Return a hash whose keys are fully-qualified field names and whose
174
- # values are values to be indexed, representing the data to be indexed
175
- # by this field definition for this model.
176
- #
177
- # ==== Parameters
178
- #
179
- # model<Object>:: the model from which to extract the value
180
- #
181
- # ==== Returns
182
- #
183
- # Hash::
184
- # Key-value pairs representing field names and values to be indexed.
185
- #
186
- #
187
- def pairs_for(model)
188
- pairs = {}
189
- if values = @data_extractor.value_for(model)
190
- values.each_pair do |dynamic_name, value|
191
- field_instance = build(dynamic_name)
192
- pairs[field_instance.indexed_name.to_sym] = field_instance.to_indexed(value)
193
- end
100
+ #
101
+ # AttributeField instances encapsulate non-tokenized attribute data.
102
+ # AttributeFields can have any type except TextType, and can also have
103
+ # a reference (for instantiated facets), optionally allow multiple values
104
+ # (false by default), and can store their values (false by default). All
105
+ # scoping, sorting, and faceting is done with attribute fields.
106
+ #
107
+ class AttributeField < Field #:nodoc:
108
+ def initialize(name, type, options = {})
109
+ super(name, type)
110
+ @multiple = !!options.delete(:multiple)
111
+ @reference =
112
+ if (reference = options.delete(:references)).respond_to?(:name)
113
+ reference.name
114
+ elsif reference.respond_to?(:to_sym)
115
+ reference.to_sym
194
116
  end
195
- pairs
196
- end
117
+ @stored = !!options.delete(:stored)
118
+ raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
119
+ end
197
120
 
198
- #
199
- # Build a DynamicFieldInstance representing an actual field to be indexed
200
- # or searched in Solr.
201
- #
202
- # ==== Parameters
203
- #
204
- # dynamic_name<Symbol>:: dynamic name for the field instance
205
- #
206
- # ==== Returns
207
- #
208
- # DynamicFieldInstance:: Dynamic field instance
209
- #
210
- def build(dynamic_name)
211
- DynamicFieldInstance.new(@name, dynamic_name, @type, @data_extractor, @multiple)
212
- end
121
+ # The name of the field as it is indexed in Solr. The indexed name
122
+ # contains a suffix that contains information about the type as well as
123
+ # whether the field allows multiple values for a document.
124
+ #
125
+ # ==== Returns
126
+ #
127
+ # String:: The field's indexed name
128
+ #
129
+ def indexed_name
130
+ "#{super}#{'m' if @multiple}#{'s' if @stored}"
213
131
  end
132
+ end
214
133
 
134
+ #
135
+ # RandomField instances are used for random sorting.
136
+ #
137
+ class RandomField #:nodoc:
215
138
  #
216
- # This class represents actual dynamic fields as they are indexed in Solr.
217
- # Thus, they have knowledge of the base name and dynamic name, type, etc.
218
- #
219
- class DynamicFieldInstance
220
- include FieldInstance
221
-
222
- def initialize(base_name, dynamic_name, type, data_extractor, multiple)
223
- @base_name, @dynamic_name, @type, @data_extractor, @multiple =
224
- base_name, dynamic_name, type, data_extractor, multiple
225
- end
226
-
227
- protected
139
+ # Never multiple, but this has to return false so Sunspot doesn't barf
140
+ # when you try to order by it.
141
+ #
142
+ def multiple?
143
+ false
144
+ end
228
145
 
229
- def name
230
- "#{@base_name}:#{@dynamic_name}"
231
- end
146
+ #
147
+ # Solr uses the dynamic field name as a seed for random, so we randomize the
148
+ # field name accordingly.
149
+ #
150
+ # #XXX I think it's bad to use a random number as a seed. Would it be
151
+ # better to pass in the current timestamp or some such thing?
152
+ #
153
+ def indexed_name
154
+ "random_#{rand(1<<16)}"
232
155
  end
233
156
  end
234
157
  end