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/History.txt +26 -1
- data/LICENSE +18 -0
- data/README.rdoc +31 -25
- data/Rakefile +1 -1
- data/TODO +1 -5
- data/VERSION.yml +2 -2
- data/lib/sunspot/adapters.rb +28 -11
- data/lib/sunspot/data_extractor.rb +37 -0
- data/lib/sunspot/dsl/fields.rb +6 -17
- data/lib/sunspot/dsl/query.rb +22 -120
- data/lib/sunspot/dsl/restriction.rb +25 -0
- data/lib/sunspot/dsl/scope.rb +127 -15
- data/lib/sunspot/dsl.rb +1 -1
- data/lib/sunspot/field.rb +151 -81
- data/lib/sunspot/indexer.rb +1 -1
- data/lib/sunspot/query/dynamic_query.rb +86 -0
- data/lib/sunspot/{facets.rb → query/field_facet.rb} +1 -1
- data/lib/sunspot/query/pagination.rb +39 -0
- data/lib/sunspot/query/restriction.rb +223 -0
- data/lib/sunspot/query/sort.rb +33 -0
- data/lib/sunspot/query.rb +217 -117
- data/lib/sunspot/search.rb +43 -5
- data/lib/sunspot/session.rb +20 -5
- data/lib/sunspot/setup.rb +39 -10
- data/lib/sunspot/type.rb +15 -7
- data/lib/sunspot/util.rb +17 -0
- data/lib/sunspot.rb +83 -2
- data/spec/api/build_search_spec.rb +120 -2
- data/spec/api/indexer_spec.rb +44 -0
- data/spec/api/query_spec.rb +129 -0
- data/spec/api/search_retrieval_spec.rb +6 -0
- data/spec/integration/dynamic_fields_spec.rb +55 -0
- data/spec/mocks/post.rb +27 -1
- data/spec/spec_helper.rb +10 -3
- data/tasks/gemspec.rake +6 -1
- data/tasks/rdoc.rake +14 -1
- metadata +79 -56
- data/lib/sunspot/restriction.rb +0 -216
data/lib/sunspot/field.rb
CHANGED
@@ -1,42 +1,31 @@
|
|
1
1
|
module Sunspot
|
2
|
-
|
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
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
#
|
104
|
-
#
|
105
|
-
#
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
109
|
+
# Subclasses of Field::Base must implement the method #value_for
|
110
|
+
#
|
111
|
+
class StaticField
|
112
|
+
include FieldInstance
|
113
|
+
extend Buildable
|
113
114
|
|
114
|
-
|
115
|
+
attr_accessor :name # The public-facing name of the field
|
116
|
+
attr_accessor :type # The Type of the field
|
115
117
|
|
116
|
-
|
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.
|
118
130
|
#
|
119
131
|
# ==== Parameters
|
120
132
|
#
|
121
|
-
# model<Object>::
|
133
|
+
# model<Object>:: the model from which to extract the value
|
122
134
|
#
|
123
135
|
# ==== Returns
|
124
136
|
#
|
125
|
-
#
|
137
|
+
# Hash:: a single key-value pair with the field name and value
|
126
138
|
#
|
127
|
-
def
|
128
|
-
|
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
|
-
#
|
134
|
-
#
|
135
|
-
#
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
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
|
-
#
|
146
|
-
#
|
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>::
|
179
|
+
# model<Object>:: the model from which to extract the value
|
151
180
|
#
|
152
181
|
# ==== Returns
|
153
182
|
#
|
154
|
-
#
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
data/lib/sunspot/indexer.rb
CHANGED
@@ -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
|
@@ -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
|