outoftime-sunspot 0.7.3 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sunspot/field.rb CHANGED
@@ -1,42 +1,31 @@
1
1
  module Sunspot
2
- module Field #:nodoc[all]
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
3
21
  #
4
- # Field classes encapsulate information about a field that has been configured
5
- # for search and indexing. They expose methods that are useful for both
6
- # operations.
7
- #
8
- # Subclasses of Field::Base must implement the method #value_for
9
- #
10
- class Base
11
- attr_accessor :name # The public-facing name of the field
12
- attr_accessor :type # The Type of the field
13
-
14
- def initialize(name, type, options = {}) #:nodoc
15
- @name, @type = name.to_sym, type
16
- @multiple = options.delete(:multiple)
17
- raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
18
- end
19
-
20
- # A key-value pair where the key is the field's indexed name and the
21
- # value is the value that should be indexed for the given model. This can
22
- # be merged directly into the document hash for adding to solr-ruby.
23
- #
24
- # ==== Parameters
25
- #
26
- # model<Object>:: the model from which to extract the value
27
- #
28
- # ==== Returns
29
- #
30
- # Hash:: a single key-value pair with the field name and value
31
- #
32
- def pair_for(model)
33
- unless (value = value_for(model)).nil?
34
- { indexed_name.to_sym => to_indexed(value) }
35
- else
36
- {}
37
- end
38
- end
39
-
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
40
29
  # The name of the field as it is indexed in Solr. The indexed name
41
30
  # contains a suffix that contains information about the type as well as
42
31
  # whether the field allows multiple values for a document.
@@ -46,9 +35,9 @@ module Sunspot
46
35
  # String:: The field's indexed name
47
36
  #
48
37
  def indexed_name
49
- "#{type.indexed_name(name)}#{'m' if multiple?}"
38
+ "#{@type.indexed_name(name)}#{'m' if @multiple}"
50
39
  end
51
-
40
+
52
41
  # Convert a value to its representation for Solr indexing. This delegates
53
42
  # to the #to_indexed method on the field's type.
54
43
  #
@@ -67,13 +56,13 @@ module Sunspot
67
56
  #
68
57
  def to_indexed(value)
69
58
  if value.is_a? Array
70
- if multiple?
59
+ if @multiple
71
60
  value.map { |val| to_indexed(val) }
72
61
  else
73
62
  raise ArgumentError, "#{name} is not a multiple-value field, so it cannot index values #{value.inspect}"
74
63
  end
75
64
  else
76
- type.to_indexed(value)
65
+ @type.to_indexed(value)
77
66
  end
78
67
  end
79
68
 
@@ -88,76 +77,157 @@ module Sunspot
88
77
  # Object:: The cast value
89
78
  #
90
79
  def cast(value)
91
- type.cast(value)
80
+ @type.cast(value)
92
81
  end
82
+ end
93
83
 
94
- # ==== Returns
95
- #
96
- # Boolean:: true if the field allows multiple values; false if not
97
- def multiple?
98
- !!@multiple
84
+ #
85
+ # This module adds a (class) method for building a field definition given
86
+ # a standard set of arguments
87
+ #
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)
99
101
  end
100
102
  end
101
103
 
102
104
  #
103
- # AttributeFields call methods directly on indexed objects and index the
104
- # return value of the method. By default, the field name is also the
105
- # attribute that provides the value for indexing, but this can be overridden
106
- # with the :using option.
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.
107
108
  #
108
- class AttributeField < Base
109
- def initialize(name, type, options = {})
110
- @attribute_name = options.delete(:using) || name
111
- super
112
- end
109
+ # Subclasses of Field::Base must implement the method #value_for
110
+ #
111
+ class StaticField
112
+ include FieldInstance
113
+ extend Buildable
113
114
 
114
- protected
115
+ attr_accessor :name # The public-facing name of the field
116
+ attr_accessor :type # The Type of the field
115
117
 
116
- #
117
- # Call the field's attribute name on the given model and return the value.
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.
118
130
  #
119
131
  # ==== Parameters
120
132
  #
121
- # model<Object>:: The object from which to extract the value
133
+ # model<Object>:: the model from which to extract the value
122
134
  #
123
135
  # ==== Returns
124
136
  #
125
- # Object:: The value to index
137
+ # Hash:: a single key-value pair with the field name and value
126
138
  #
127
- def value_for(model)
128
- model.send(@attribute_name)
139
+ def pairs_for(model)
140
+ unless (value = @data_extractor.value_for(model)).nil?
141
+ { indexed_name.to_sym => to_indexed(value) }
142
+ else
143
+ {}
144
+ end
129
145
  end
130
146
  end
131
147
 
148
+ #
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.
132
153
  #
133
- # VirtualFields extract data by evaluating the provided block in the context
134
- # of the model instance.
135
- #
136
- class VirtualField < Base
137
- def initialize(name, type, options = {}, &block)
138
- super(name, type, options)
139
- @block = block
140
- end
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
141
164
 
142
- protected
165
+ attr_accessor :name # Base name of the dynamic field.
166
+
167
+ def initialize(name, type, data_extractor, options)
168
+ @name, @type, @data_extractor = name, type, data_extractor
169
+ @multiple = !!options.delete(:multiple)
170
+ end
143
171
 
144
172
  #
145
- # Evaluate the block in the model's context and return the block's return
146
- # value.
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.
147
176
  #
148
177
  # ==== Parameters
149
178
  #
150
- # model<Object>:: The object from which to extract the value
179
+ # model<Object>:: the model from which to extract the value
151
180
  #
152
181
  # ==== Returns
153
182
  #
154
- # Object:: The value to index
155
- def value_for(model)
156
- if @block.arity <= 0
157
- model.instance_eval(&@block)
158
- else
159
- @block.call(model)
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
160
194
  end
195
+ pairs
196
+ end
197
+
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
213
+ end
214
+
215
+ #
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
228
+
229
+ def name
230
+ "#{@base_name}:#{@dynamic_name}"
161
231
  end
162
232
  end
163
233
  end
@@ -21,7 +21,7 @@ module Sunspot
21
21
  def add(model)
22
22
  hash = static_hash_for(model)
23
23
  for field in @setup.all_fields
24
- hash.merge!(field.pair_for(model))
24
+ hash.merge!(field.pairs_for(model))
25
25
  end
26
26
  @connection.add(hash)
27
27
  end
@@ -0,0 +1,86 @@
1
+ module Sunspot
2
+ class Query
3
+ #
4
+ # A dynamic query is a proxy object that implements a subset of the API of
5
+ # the Query class, but wraps a dynamic field definition and thus applies the
6
+ # query components using dynamic field instances.
7
+ #--
8
+ # Dynamic queries do not hold their own state, but rather proxy to the query
9
+ # that generated them, adding components directly to the owning query's
10
+ # internal state.
11
+ #++
12
+ # DynamicQuery instances are publicly generated by the Query#dynamic_query
13
+ # factory method.
14
+ #
15
+ class DynamicQuery
16
+ def initialize(dynamic_field, query) #:nodoc:
17
+ @dynamic_field, @query = dynamic_field, query
18
+ end
19
+
20
+ #
21
+ # Add a restriction based on the dynamic field definition and dynamic name
22
+ # given.
23
+ #
24
+ # ==== Parameters
25
+ #
26
+ # dynamic_name<Symbol>::
27
+ # Dynamic name to apply to the field in the restriction.
28
+ # restriction_type<Symbol,Class>::
29
+ # Type of restriction to apply (e.g. Sunspot::Query::Restriction::EqualTo), or
30
+ # symbol shorthand (e.g. :equal_to)
31
+ # value::
32
+ # Value to apply to the restriction.
33
+ # negated::
34
+ # Whether to negate the restriction (prefer #add_negated_restriction)
35
+ #
36
+ def add_restriction(dynamic_name, restriction_type, value, negated = false)
37
+ if restriction_type.is_a?(Symbol)
38
+ restriction_type = Restriction[restriction_type]
39
+ end
40
+ @query.add_component(restriction_type.new(@dynamic_field.build(dynamic_name), value, negated))
41
+ end
42
+
43
+ #
44
+ # Add a negated restriction based on the dynamic field definition and
45
+ # dynamic name given.
46
+ #
47
+ # ==== Parameters
48
+ #
49
+ # dynamic_name<Symbol>::
50
+ # Dynamic name to apply to the field in the restriction.
51
+ # restriction_type<Symbol,Class>::
52
+ # Type of restriction to apply (e.g. Sunspot::Query::Restriction::EqualTo), or
53
+ # symbol shorthand (e.g. :equal_to)
54
+ # value::
55
+ # Value to apply to the restriction.
56
+ #
57
+ def add_negated_restriction(dynamic_name, restriction_type, value)
58
+ add_restriction(dynamic_name, restriction_type, value, true)
59
+ end
60
+
61
+ #
62
+ # Add a field facet based on the dynamic field definition and dynamic name
63
+ # given.
64
+ #
65
+ # ==== Parameters
66
+ #
67
+ # dynamic_name<Symbol>:: Dynamic name to facet on
68
+ #
69
+ def add_field_facet(dynamic_name)
70
+ @query.add_component(FieldFacet.new(@dynamic_field.build(dynamic_name)))
71
+ end
72
+
73
+ #
74
+ # Order by the given dynamic field.
75
+ #
76
+ # ==== Parameters
77
+ #
78
+ # dynamic_name<Symbol>:: Dynamic name of ordering field
79
+ # direction<Symbol>:: Direction in which to order (:asc, :desc)
80
+ #
81
+ def order_by(dynamic_name, direction)
82
+ @query.add_component(Sort.new(@dynamic_field.build(dynamic_name), direction))
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,5 +1,5 @@
1
1
  module Sunspot
2
- module Facets
2
+ class Query
3
3
  #
4
4
  # Encapsulates a query component representing a field facet. Users create
5
5
  # instances using DSL::Query#facet
@@ -0,0 +1,39 @@
1
+ module Sunspot
2
+ class Query
3
+ #
4
+ # A query component that holds information about pagination. Unlike other
5
+ # query components, this one is mutable, because the query itself holds a
6
+ # reference to it and updates it if pagination is changed.
7
+ #
8
+ class Pagination #:nodoc:
9
+ attr_reader :page, :per_page
10
+
11
+ def initialize(configuration, page = nil, per_page = nil)
12
+ @configuration = configuration
13
+ self.page, self.per_page = page, per_page
14
+ end
15
+
16
+ def to_params
17
+ { :start => start, :rows => rows }
18
+ end
19
+
20
+ def page=(page)
21
+ @page = page || 1
22
+ end
23
+
24
+ def per_page=(per_page)
25
+ @per_page = per_page || @configuration.pagination.default_per_page
26
+ end
27
+
28
+ private
29
+
30
+ def start
31
+ (@page - 1) * @per_page
32
+ end
33
+
34
+ def rows
35
+ @per_page
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,223 @@
1
+ module Sunspot
2
+ class Query
3
+ module Restriction #:nodoc:
4
+ class <<self
5
+ #
6
+ # Return the names of all of the restriction classes that should be made
7
+ # available to the DSL.
8
+ #
9
+ # ==== Returns
10
+ #
11
+ # Array:: Collection of restriction class names
12
+ #
13
+ def names
14
+ constants - %w(Base SameAs) #XXX this seems ugly
15
+ end
16
+
17
+ def [](restriction_name)
18
+ @types ||= {}
19
+ @types[restriction_name.to_sym] ||= const_get(Sunspot::Util.camel_case(restriction_name.to_s))
20
+ end
21
+ end
22
+
23
+ #
24
+ # Subclasses of this class represent restrictions that can be applied to
25
+ # a Sunspot query. The Sunspot::DSL::Restriction class presents a builder
26
+ # API for instances of this class.
27
+ #
28
+ # Implementations of this class must respond to #to_params and
29
+ # #to_negative_params. Instead of implementing those methods, they may
30
+ # choose to implement any of:
31
+ #
32
+ # * #to_positive_boolean_phrase, and optionally #to_negative_boolean_phrase
33
+ # * #to_solr_conditional
34
+ #
35
+ class Base #:nodoc:
36
+ def initialize(field, value, negative = false)
37
+ @field, @value, @negative = field, value, negative
38
+ end
39
+
40
+ #
41
+ # A hash representing this restriction in solr-ruby's parameter format.
42
+ # All restriction implementations must respond to this method; however,
43
+ # the base implementation delegates to the #to_positive_boolean_phrase method, so
44
+ # subclasses may (and probably should) choose to implement that method
45
+ # instead.
46
+ #
47
+ # ==== Returns
48
+ #
49
+ # Hash:: Representation of this restriction as solr-ruby parameters
50
+ #
51
+ def to_params
52
+ { :filter_queries => [to_boolean_phrase] }
53
+ end
54
+
55
+ #
56
+ # Return the boolean phrase associated with this restriction object.
57
+ # Differentiates between positive and negative boolean phrases depending
58
+ # on whether this restriction is negated.
59
+ #
60
+ def to_boolean_phrase
61
+ unless negative?
62
+ to_positive_boolean_phrase
63
+ else
64
+ to_negative_boolean_phrase
65
+ end
66
+ end
67
+
68
+ #
69
+ # Boolean phrase representing this restriction in the positive. Subclasses
70
+ # may choose to implement this method rather than #to_params; however,
71
+ # this method delegates to the abstract #to_solr_conditional method, which
72
+ # in most cases will be what subclasses will want to implement.
73
+ # #to_solr_conditional contains the boolean phrase representing the
74
+ # condition but leaves out the field name (see built-in implementations
75
+ # for examples)
76
+ #
77
+ # ==== Returns
78
+ #
79
+ # String:: Boolean phrase for restriction in the positive
80
+ #
81
+ def to_positive_boolean_phrase
82
+ "#{Solr::Util.query_parser_escape(@field.indexed_name)}:#{to_solr_conditional}"
83
+ end
84
+
85
+ #
86
+ # Boolean phrase representing this restriction in the negative. Subclasses
87
+ # may choose to implement this method, but it is not necessary, as the
88
+ # base implementation delegates to #to_positive_boolean_phrase.
89
+ #
90
+ # ==== Returns
91
+ #
92
+ # String:: Boolean phrase for restriction in the negative
93
+ #
94
+ def to_negative_boolean_phrase
95
+ "-#{to_positive_boolean_phrase}"
96
+ end
97
+
98
+ protected
99
+
100
+ #
101
+ # Whether this restriction should be negated from its original meaning
102
+ #
103
+ def negative?
104
+ !!@negative
105
+ end
106
+
107
+ #
108
+ # Return escaped Solr API representation of given value
109
+ #
110
+ # ==== Parameters
111
+ #
112
+ # value<Object>::
113
+ # value to convert to Solr representation (default: @value)
114
+ #
115
+ # ==== Returns
116
+ #
117
+ # String:: Solr API representation of given value
118
+ #
119
+ def solr_value(value = @value)
120
+ Solr::Util.query_parser_escape(@field.to_indexed(value))
121
+ end
122
+ end
123
+
124
+ #
125
+ # Results must have field with value equal to given value. If the value
126
+ # is nil, results must have no value for the given field.
127
+ #
128
+ class EqualTo < Base
129
+ def to_positive_boolean_phrase
130
+ unless @value.nil?
131
+ super
132
+ else
133
+ "-#{Solr::Util.query_parser_escape(@field.indexed_name)}:[* TO *]"
134
+ end
135
+ end
136
+
137
+ def to_negative_boolean_phrase
138
+ unless @value.nil?
139
+ super
140
+ else
141
+ "#{Solr::Util.query_parser_escape(@field.indexed_name)}:[* TO *]"
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ def to_solr_conditional
148
+ "#{solr_value}"
149
+ end
150
+ end
151
+
152
+ #
153
+ # Results must have field with value less than given value
154
+ #
155
+ class LessThan < Base
156
+ private
157
+
158
+ def to_solr_conditional
159
+ "[* TO #{solr_value}]"
160
+ end
161
+ end
162
+
163
+ #
164
+ # Results must have field with value greater than given value
165
+ #
166
+ class GreaterThan < Base
167
+ private
168
+
169
+ def to_solr_conditional
170
+ "[#{solr_value} TO *]"
171
+ end
172
+ end
173
+
174
+ #
175
+ # Results must have field with value in given range
176
+ #
177
+ class Between < Base
178
+ private
179
+
180
+ def to_solr_conditional
181
+ "[#{solr_value(@value.first)} TO #{solr_value(@value.last)}]"
182
+ end
183
+ end
184
+
185
+ #
186
+ # Results must have field with value included in given collection
187
+ #
188
+ class AnyOf < Base
189
+ private
190
+
191
+ def to_solr_conditional
192
+ "(#{@value.map { |v| solr_value v } * ' OR '})"
193
+ end
194
+ end
195
+
196
+ #
197
+ # Results must have field with values matching all values in given
198
+ # collection (only makes sense for fields with multiple values)
199
+ #
200
+ class AllOf < Base
201
+ private
202
+
203
+ def to_solr_conditional
204
+ "(#{@value.map { |v| solr_value v } * ' AND '})"
205
+ end
206
+ end
207
+
208
+ #
209
+ # Result must be the exact instance given (only useful when negated).
210
+ #
211
+ class SameAs < Base
212
+ def initialize(object, negative = false)
213
+ @object, @negative = object, negative
214
+ end
215
+
216
+ def to_positive_boolean_phrase
217
+ adapter = Adapters::InstanceAdapter.adapt(@object)
218
+ "id:#{Solr::Util.query_parser_escape(adapter.index_id)}"
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,33 @@
1
+ module Sunspot
2
+ class Query
3
+ #
4
+ # The Sort class is a query component representing a sort by a given field.
5
+ #
6
+ class Sort #:nodoc:
7
+ ASCENDING = Set.new([:asc, :ascending])
8
+ DESCENDING = Set.new([:desc, :descending])
9
+
10
+ def initialize(field, direction = nil)
11
+ @field, @direction = field, (direction || :asc).to_sym
12
+ end
13
+
14
+ def to_params
15
+ { :sort => [{ @field.indexed_name.to_sym => direction_for_solr }] }
16
+ end
17
+
18
+ private
19
+
20
+ 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
30
+ end
31
+ end
32
+ end
33
+ end