outoftime-sunspot 0.0.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/README.rdoc +26 -33
- data/TODO +7 -0
- data/VERSION.yml +2 -2
- data/lib/light_config.rb +5 -5
- data/lib/sunspot/adapters.rb +209 -33
- data/lib/sunspot/configuration.rb +25 -10
- data/lib/sunspot/dsl/fields.rb +42 -11
- data/lib/sunspot/dsl/query.rb +150 -6
- data/lib/sunspot/dsl/scope.rb +16 -26
- data/lib/sunspot/facet.rb +37 -0
- data/lib/sunspot/facet_row.rb +34 -0
- data/lib/sunspot/facets.rb +21 -0
- data/lib/sunspot/field.rb +112 -56
- data/lib/sunspot/indexer.rb +49 -46
- data/lib/sunspot/query.rb +217 -49
- data/lib/sunspot/restriction.rb +158 -14
- data/lib/sunspot/search.rb +79 -28
- data/lib/sunspot/session.rb +75 -8
- data/lib/sunspot/setup.rb +171 -0
- data/lib/sunspot/type.rb +116 -32
- data/lib/sunspot/util.rb +141 -7
- data/lib/sunspot.rb +260 -31
- data/spec/api/build_search_spec.rb +139 -33
- data/spec/api/indexer_spec.rb +33 -2
- data/spec/api/search_retrieval_spec.rb +85 -2
- data/spec/api/session_spec.rb +14 -6
- data/spec/integration/faceting_spec.rb +39 -0
- data/spec/integration/keyword_search_spec.rb +1 -1
- data/spec/integration/scoped_search_spec.rb +175 -0
- data/spec/mocks/mock_adapter.rb +7 -10
- data/spec/mocks/post.rb +7 -2
- data/tasks/rdoc.rake +7 -0
- data/tasks/spec.rake +3 -0
- data/tasks/todo.rake +4 -0
- metadata +12 -7
- data/lib/sunspot/builder.rb +0 -78
- data/spec/api/standard_search_builder_spec.rb +0 -76
- data/spec/custom_expectation.rb +0 -53
- data/spec/integration/field_types_spec.rb +0 -62
data/lib/sunspot.rb
CHANGED
@@ -1,49 +1,278 @@
|
|
1
1
|
gem 'solr-ruby'
|
2
|
-
gem 'extlib'
|
3
2
|
require 'solr'
|
4
|
-
require 'extlib'
|
5
3
|
require File.join(File.dirname(__FILE__), 'light_config')
|
6
4
|
|
7
|
-
%w(adapters
|
5
|
+
%w(adapters restriction configuration setup field facets indexer query search facet facet_row session type util dsl).each do |filename|
|
8
6
|
require File.join(File.dirname(__FILE__), 'sunspot', filename)
|
9
7
|
end
|
10
8
|
|
9
|
+
#
|
10
|
+
# The Sunspot module provides class-method entry points to most of the
|
11
|
+
# functionality provided by the Sunspot library. Internally, the Sunspot
|
12
|
+
# singleton class contains a (non-thread-safe!) instance of Sunspot::Session,
|
13
|
+
# to which it delegates most of the class methods it exposes. In the method
|
14
|
+
# documentation below, this instance is referred to as the "singleton session".
|
15
|
+
#
|
16
|
+
# Though the singleton session provides a convenient entry point to Sunspot,
|
17
|
+
# it is by no means required to use the Sunspot class methods. Multiple sessions
|
18
|
+
# may be instantiated and used (if you need to connect to multiple Solr
|
19
|
+
# instances, for example.)
|
20
|
+
#
|
21
|
+
# Note that the configuration of classes for index/search (the +setup+
|
22
|
+
# method) is _not_ session-specific, but rather global.
|
23
|
+
#
|
11
24
|
module Sunspot
|
12
|
-
|
13
|
-
|
25
|
+
UnrecognizedFieldError = Class.new(Exception)
|
26
|
+
UnrecognizedRestrictionError = Class.new(Exception)
|
14
27
|
|
15
|
-
class <<
|
16
|
-
|
17
|
-
|
18
|
-
|
28
|
+
class <<self
|
29
|
+
# Configures indexing and search for a given class.
|
30
|
+
#
|
31
|
+
# ==== Parameters
|
32
|
+
#
|
33
|
+
# clazz<Class>:: class to configure
|
34
|
+
#
|
35
|
+
# ==== Example
|
36
|
+
#
|
37
|
+
# Sunspot.setup(Post) do
|
38
|
+
# text :title, :body
|
39
|
+
# string :author_name
|
40
|
+
# integer :blog_id
|
41
|
+
# integer :category_ids
|
42
|
+
# float :average_rating, :using => :ratings_average
|
43
|
+
# time :published_at
|
44
|
+
# string :sort_title do
|
45
|
+
# title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# ====== Attribute Fields vs. Virtual Fields
|
50
|
+
#
|
51
|
+
# Attribute fields call a method on the indexed object and index the
|
52
|
+
# return value. All of the fields defined above except for the last one are
|
53
|
+
# attribute fields. By default, the field name will also be the attribute
|
54
|
+
# used; this can be overriden with the +:using+ option, as in
|
55
|
+
# +:average_rating+ above. In that case, the attribute +:ratings_average+
|
56
|
+
# will be indexed with the field name +:average_rating+.
|
57
|
+
#
|
58
|
+
# +:sort_title+ is a virtual field, which evaluates the block inside the
|
59
|
+
# context of the instance being indexed, and indexes the value returned
|
60
|
+
# by the block. If the block you pass takes an argument, it will be passed
|
61
|
+
# the instance rather than being evaluated inside of it; so, the following
|
62
|
+
# example is equivalent to the one above (assuming #title is public):
|
63
|
+
#
|
64
|
+
# Sunspot.setup(Post) do
|
65
|
+
# string :sort_title do |post|
|
66
|
+
# post.title.downcase.sub(/^(an?|the)\W+/, ''/) if title = self.title
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# ===== Field Types
|
71
|
+
#
|
72
|
+
# The available types are:
|
73
|
+
#
|
74
|
+
# * +text+
|
75
|
+
# * +string+
|
76
|
+
# * +integer+
|
77
|
+
# * +float+
|
78
|
+
# * +time+
|
79
|
+
# * +boolean+
|
80
|
+
#
|
81
|
+
# Note that the +text+ type behaves quite differently from the others -
|
82
|
+
# this is the type that is indexed as fulltext, and is searched using the
|
83
|
+
# +keywords+ method inside the search DSL. Text fields cannot have
|
84
|
+
# restrictions set on them, nor can they be used in order statements or
|
85
|
+
# for facets. All other types are indexed literally, and thus can be used
|
86
|
+
# for all of those operations. They will not, however, be searched in
|
87
|
+
# fulltext. In this way, Sunspot provides a complete barrier between
|
88
|
+
# fulltext fields and value fields.
|
89
|
+
#
|
90
|
+
# It is fine to specify a field both as a text field and a string field;
|
91
|
+
# internally, the fields will have different names so there is no danger
|
92
|
+
# of conflict.
|
93
|
+
#
|
94
|
+
def setup(clazz, &block)
|
95
|
+
Setup.setup(clazz, &block)
|
96
|
+
end
|
19
97
|
|
20
|
-
|
21
|
-
|
22
|
-
|
98
|
+
# Indexes objects on the singleton session.
|
99
|
+
#
|
100
|
+
# ==== Parameters
|
101
|
+
#
|
102
|
+
# objects...<Object>:: objects to index (may pass an array or varargs)
|
103
|
+
#
|
104
|
+
# ==== Example
|
105
|
+
#
|
106
|
+
# post1, post2 = Array(2) { Post.create }
|
107
|
+
# Sunspot.index(post1, post2)
|
108
|
+
#
|
109
|
+
# Note that indexed objects won't be reflected in search until a commit is
|
110
|
+
# sent - see Sunspot.index! and Sunspot.commit
|
111
|
+
#
|
112
|
+
def index(*objects)
|
113
|
+
session.index(*objects)
|
114
|
+
end
|
23
115
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
116
|
+
# Indexes objects on the singleton session and commits immediately.
|
117
|
+
#
|
118
|
+
# See: Sunspot.index and Sunspot.commit
|
119
|
+
#
|
120
|
+
# ==== Parameters
|
121
|
+
#
|
122
|
+
# objects...<Object>:: objects to index (may pass an array or varargs)
|
123
|
+
#
|
124
|
+
def index!(*objects)
|
125
|
+
session.index!(*objects)
|
126
|
+
end
|
31
127
|
|
32
|
-
|
33
|
-
|
34
|
-
|
128
|
+
# Commits the singleton session
|
129
|
+
#
|
130
|
+
# When documents are added to or removed from Solr, the changes are
|
131
|
+
# initially stored in memory, and are not reflected in Solr's existing
|
132
|
+
# searcher instance. When a commit message is sent, the changes are written
|
133
|
+
# to disk, and a new searcher is spawned. Commits are thus fairly
|
134
|
+
# expensive, so if your application needs to index several documents as part
|
135
|
+
# of a single operation, it is advisable to index them all and then call
|
136
|
+
# commit at the end of the operation.
|
137
|
+
#
|
138
|
+
# Note that Solr can also be configured to automatically perform a commit
|
139
|
+
# after either a specified interval after the last change, or after a
|
140
|
+
# specified number of documents are added. See
|
141
|
+
# http://wiki.apache.org/solr/SolrConfigXml
|
142
|
+
#
|
143
|
+
def commit
|
144
|
+
session.commit
|
145
|
+
end
|
35
146
|
|
36
|
-
|
37
|
-
|
38
|
-
|
147
|
+
# Search for objects in the index.
|
148
|
+
#
|
149
|
+
# ==== Parameters
|
150
|
+
#
|
151
|
+
# types<Class>...::
|
152
|
+
# Zero, one, or more types to search for. If no types are passed, all
|
153
|
+
# configured types will be searched.
|
154
|
+
#
|
155
|
+
# ==== Returns
|
156
|
+
#
|
157
|
+
# Sunspot::Search:: Object containing results, facets, count, etc.
|
158
|
+
#
|
159
|
+
# The fields available for restriction, ordering, etc. are those that meet
|
160
|
+
# the following criteria:
|
161
|
+
#
|
162
|
+
# * They are not of type +text+.
|
163
|
+
# * They are defined for all of the classes being searched
|
164
|
+
# * They have the same data type for all of the classes being searched
|
165
|
+
# * They have the same multiple flag for all of the classes being searched.
|
166
|
+
#
|
167
|
+
# The restrictions available are the constants defined in the
|
168
|
+
# Sunspot::Restriction class. The standard restrictions are:
|
169
|
+
#
|
170
|
+
# with(:field_name).equal_to(value)
|
171
|
+
# with(:field_name, value) # shorthand for above
|
172
|
+
# with(:field_name).less_than(value)
|
173
|
+
# with(:field_name).greater_than(value)
|
174
|
+
# with(:field_name).between(value1..value2)
|
175
|
+
# with(:field_name).any_of([value1, value2, value3])
|
176
|
+
# with(:field_name).all_of([value1, value2, value3])
|
177
|
+
# without(some_instance) # exclude that particular instance
|
178
|
+
#
|
179
|
+
# +without+ can be substituted for +with+, causing the restriction to be
|
180
|
+
# negated. In the last example above, only +without+ works, as it does not
|
181
|
+
# make sense to search only for an instance you already have.
|
182
|
+
#
|
183
|
+
# Equality restrictions can take +nil+ as a value, which restricts the
|
184
|
+
# results to documents that have no value for the given field. Passing +nil+
|
185
|
+
# as a value to other restriction types is illegal. Thus:
|
186
|
+
#
|
187
|
+
# with(:field_name, nil) # ok
|
188
|
+
# with(:field_name).equal_to(nil) # ok
|
189
|
+
# with(:field_name).less_than(nil) # bad
|
190
|
+
#
|
191
|
+
# ==== Example
|
192
|
+
#
|
193
|
+
# Sunspot.search(Post) do
|
194
|
+
# keywords 'great pizza'
|
195
|
+
# with(:published_at).less_than Time.now
|
196
|
+
# with :blog_id, 1
|
197
|
+
# without current_post
|
198
|
+
# facet :category_ids
|
199
|
+
# order_by :published_at, :desc
|
200
|
+
# paginate 2, 15
|
201
|
+
# end
|
202
|
+
#
|
203
|
+
# See Sunspot::DSL::Query for the full API presented inside the block.
|
204
|
+
#
|
205
|
+
def search(*types, &block)
|
206
|
+
session.search(*types, &block)
|
207
|
+
end
|
39
208
|
|
40
|
-
|
41
|
-
|
42
|
-
|
209
|
+
# Remove objects from the index. Any time an object is destroyed, it must
|
210
|
+
# be removed from the index; otherwise, the index will contain broken
|
211
|
+
# references to objects that do not exist, which will cause errors when
|
212
|
+
# those objects are matched in search results.
|
213
|
+
#
|
214
|
+
# ==== Parameters
|
215
|
+
#
|
216
|
+
# objects...<Object>::
|
217
|
+
# Objects to remove from the index (may pass an array or varargs)
|
218
|
+
#
|
219
|
+
# ==== Example
|
220
|
+
#
|
221
|
+
# post.destroy
|
222
|
+
# Sunspot.remove(post)
|
223
|
+
#
|
224
|
+
def remove(*objects)
|
225
|
+
session.remove(*objects)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Remove all objects of the given classes from the index. There isn't much
|
229
|
+
# use for this in general operations but it can be useful for maintenance,
|
230
|
+
# testing, etc.
|
231
|
+
#
|
232
|
+
# ==== Parameters
|
233
|
+
#
|
234
|
+
# classes...<Class>::
|
235
|
+
# classes for which to remove all instances from the index (may pass an
|
236
|
+
# array or varargs)
|
237
|
+
#
|
238
|
+
# ==== Example
|
239
|
+
#
|
240
|
+
# Sunspot.remove_all(Post, Blog)
|
241
|
+
#
|
242
|
+
def remove_all(*classes)
|
243
|
+
session.remove_all(*classes)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Returns the configuration associated with the singleton session. See
|
247
|
+
# Sunspot::Configuration for details.
|
248
|
+
#
|
249
|
+
# ==== Returns
|
250
|
+
#
|
251
|
+
# LightConfig::Configuration:: configuration for singleton session
|
252
|
+
#
|
253
|
+
def config
|
254
|
+
session.config
|
255
|
+
end
|
256
|
+
|
257
|
+
#
|
258
|
+
# Resets the singleton session. This is useful for clearing out all
|
259
|
+
# static data between tests, but probably nowhere else.
|
260
|
+
#
|
261
|
+
def reset!
|
262
|
+
@session = nil
|
263
|
+
end
|
43
264
|
|
44
|
-
|
265
|
+
private
|
45
266
|
|
46
|
-
|
47
|
-
|
267
|
+
#
|
268
|
+
# Get the singleton session, creating it if none yet exists.
|
269
|
+
#
|
270
|
+
# ==== Returns
|
271
|
+
#
|
272
|
+
# Sunspot::Session:: the singleton session
|
273
|
+
#
|
274
|
+
def session #:nodoc:
|
275
|
+
@session ||= Session.new
|
276
|
+
end
|
48
277
|
end
|
49
278
|
end
|
@@ -13,50 +13,51 @@ describe 'Search' do
|
|
13
13
|
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['title_s:My\ Pet\ Post'])).twice
|
14
14
|
session.search Post, :conditions => { :title => 'My Pet Post' }
|
15
15
|
session.search Post do
|
16
|
-
with
|
16
|
+
with :title, 'My Pet Post'
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'should ignore nonexistant fields in hash scope' do
|
21
|
-
connection.should_receive(:query).with('(type:Post)',
|
21
|
+
connection.should_receive(:query).with('(type:Post)', hash_not_including(:filter_queries))
|
22
22
|
session.search Post, :conditions => { :bogus => 'Field' }
|
23
23
|
end
|
24
24
|
|
25
|
-
it 'should raise an ArgumentError for nonexistant fields in block scope' do
|
26
|
-
lambda do
|
27
|
-
session.search Post do
|
28
|
-
with.bogus 'Field'
|
29
|
-
end
|
30
|
-
end.should raise_error(ArgumentError)
|
31
|
-
end
|
32
|
-
|
33
25
|
it 'should scope by exact match with time' do
|
34
26
|
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['published_at_d:1983\-07\-08T09\:00\:00Z'])).twice
|
35
27
|
time = Time.parse('1983-07-08 05:00:00 -0400')
|
36
28
|
session.search Post, :conditions => { :published_at => time }
|
37
29
|
session.search Post do
|
38
|
-
with
|
30
|
+
with :published_at, time
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should scope by exact match with boolean' do
|
35
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['featured_b:false'])).twice
|
36
|
+
session.search Post, :conditions => { :featured => false }
|
37
|
+
session.search Post do
|
38
|
+
with :featured, false
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
42
|
it 'should scope by less than match with float' do
|
43
43
|
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['average_rating_f:[* TO 3\.0]']))
|
44
44
|
session.search Post do
|
45
|
-
with
|
45
|
+
with(:average_rating).less_than 3.0
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
49
|
it 'should scope by greater than match with float' do
|
50
50
|
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['average_rating_f:[3\.0 TO *]']))
|
51
51
|
session.search Post do
|
52
|
-
with
|
52
|
+
with(:average_rating).greater_than 3.0
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
56
|
it 'should scope by between match with float' do
|
57
|
-
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['average_rating_f:[2\.0 TO 4\.0]']))
|
57
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['average_rating_f:[2\.0 TO 4\.0]'])).twice
|
58
|
+
session.search Post, :conditions => { :average_rating => 2.0..4.0 }
|
58
59
|
session.search Post do
|
59
|
-
with
|
60
|
+
with(:average_rating).between 2.0..4.0
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
@@ -64,14 +65,95 @@ describe 'Search' do
|
|
64
65
|
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['category_ids_im:(2 OR 7 OR 12)'])).twice
|
65
66
|
session.search Post, :conditions => { :category_ids => [2, 7, 12] }
|
66
67
|
session.search Post do
|
67
|
-
with
|
68
|
+
with(:category_ids).any_of [2, 7, 12]
|
68
69
|
end
|
69
70
|
end
|
70
71
|
|
71
72
|
it 'should scope by all match with integer' do
|
72
73
|
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['category_ids_im:(2 AND 7 AND 12)']))
|
73
74
|
session.search Post do
|
74
|
-
with
|
75
|
+
with(:category_ids).all_of [2, 7, 12]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should scope by not equal match with string' do
|
80
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['-title_s:Bad\ Post']))
|
81
|
+
session.search Post do
|
82
|
+
without :title, 'Bad Post'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should scope by not less than match with float' do
|
87
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['-average_rating_f:[* TO 3\.0]']))
|
88
|
+
session.search Post do
|
89
|
+
without(:average_rating).less_than 3.0
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should scope by not greater than match with float' do
|
94
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['-average_rating_f:[3\.0 TO *]']))
|
95
|
+
session.search Post do
|
96
|
+
without(:average_rating).greater_than 3.0
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should scope by not between match with float' do
|
101
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['-average_rating_f:[2\.0 TO 4\.0]']))
|
102
|
+
session.search Post do
|
103
|
+
without(:average_rating).between 2.0..4.0
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should scope by not any match with integer' do
|
108
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['-category_ids_im:(2 OR 7 OR 12)']))
|
109
|
+
session.search Post do
|
110
|
+
without(:category_ids).any_of [2, 7, 12]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
it 'should scope by not all match with integer' do
|
116
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['-category_ids_im:(2 AND 7 AND 12)']))
|
117
|
+
session.search Post do
|
118
|
+
without(:category_ids).all_of [2, 7, 12]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should scope by empty field' do
|
123
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['-average_rating_f:[* TO *]']))
|
124
|
+
session.search Post do
|
125
|
+
with :average_rating, nil
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should scope by non-empty field' do
|
130
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ['average_rating_f:[* TO *]']))
|
131
|
+
session.search Post do
|
132
|
+
without :average_rating, nil
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should exclude by object identity' do
|
137
|
+
post = Post.new
|
138
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ["-id:Post\\ #{post.id}"]))
|
139
|
+
session.search Post do
|
140
|
+
without post
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should exclude multiple objects passed as varargs by object identity' do
|
145
|
+
post1, post2 = Post.new, Post.new
|
146
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ["-id:Post\\ #{post1.id}", "-id:Post\\ #{post2.id}"]))
|
147
|
+
session.search Post do
|
148
|
+
without post1, post2
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'should exclude multiple objects passed as array by object identity' do
|
153
|
+
posts = [Post.new, Post.new]
|
154
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:filter_queries => ["-id:Post\\ #{posts.first.id}", "-id:Post\\ #{posts.last.id}"]))
|
155
|
+
session.search Post do
|
156
|
+
without posts
|
75
157
|
end
|
76
158
|
end
|
77
159
|
|
@@ -89,7 +171,7 @@ describe 'Search' do
|
|
89
171
|
end
|
90
172
|
|
91
173
|
it 'should paginate using provided per_page' do
|
92
|
-
connection.should_receive(:query).with('(type:Post)', :
|
174
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:rows => 15, :start => 45)).twice
|
93
175
|
session.search Post, :page => 4, :per_page => 15
|
94
176
|
session.search Post do
|
95
177
|
paginate :page => 4, :per_page => 15
|
@@ -104,6 +186,30 @@ describe 'Search' do
|
|
104
186
|
end
|
105
187
|
end
|
106
188
|
|
189
|
+
it 'should order by multiple columns' do
|
190
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:sort => [{ :average_rating_f => :descending },
|
191
|
+
{ :sort_title_s => :ascending }])).twice
|
192
|
+
session.search Post, :order => ['average_rating desc', 'sort_title asc']
|
193
|
+
session.search Post do
|
194
|
+
order_by :average_rating, :desc
|
195
|
+
order_by :sort_title, :asc
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'should request single field facet' do
|
200
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:facets => { :fields => %w(category_ids_im) }))
|
201
|
+
session.search Post do
|
202
|
+
facet :category_ids
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'should request multiple field facet' do
|
207
|
+
connection.should_receive(:query).with('(type:Post)', hash_including(:facets => { :fields => %w(category_ids_im blog_id_i) }))
|
208
|
+
session.search Post do
|
209
|
+
facet :category_ids, :blog_id
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
107
213
|
it 'should build search for multiple types' do
|
108
214
|
connection.should_receive(:query).with('(type:(Post OR Comment))', hash_including)
|
109
215
|
session.search(Post, Comment)
|
@@ -114,43 +220,43 @@ describe 'Search' do
|
|
114
220
|
time = Time.parse('1983-07-08 05:00:00 -0400')
|
115
221
|
session.search Post, Comment, :conditions => { :published_at => time }
|
116
222
|
session.search Post, Comment do
|
117
|
-
with
|
223
|
+
with :published_at, time
|
118
224
|
end
|
119
225
|
end
|
120
226
|
|
121
|
-
it 'should raise
|
227
|
+
it 'should raise Sunspot::UnrecognizedFieldError if search scoped to field not common to all types' do
|
122
228
|
lambda do
|
123
229
|
session.search Post, Comment do
|
124
|
-
with
|
230
|
+
with :blog_id, 1
|
125
231
|
end
|
126
|
-
end.should raise_error(
|
232
|
+
end.should raise_error(Sunspot::UnrecognizedFieldError)
|
127
233
|
end
|
128
234
|
|
129
|
-
it 'should raise
|
235
|
+
it 'should raise Sunspot::UnrecognizedFieldError if search scoped to field configured differently between types' do
|
130
236
|
lambda do
|
131
237
|
session.search Post, Comment do
|
132
|
-
with
|
238
|
+
with :average_rating, 2.2 # this is a float in Post but an integer in Comment
|
133
239
|
end
|
134
|
-
end.should raise_error(
|
240
|
+
end.should raise_error(Sunspot::UnrecognizedFieldError)
|
135
241
|
end
|
136
242
|
|
137
243
|
it 'should ignore condition if field is not common to all types' do
|
138
|
-
connection.should_receive(:query).with('(type:(Post OR Comment))',
|
244
|
+
connection.should_receive(:query).with('(type:(Post OR Comment))', hash_not_including(:filter_queries))
|
139
245
|
session.search Post, Comment, :conditions => { :blog_id => 1 }
|
140
246
|
end
|
141
247
|
|
142
|
-
it 'should raise
|
248
|
+
it 'should raise Sunspot::UnrecognizedFieldError for nonexistant fields in block scope' do
|
143
249
|
lambda do
|
144
250
|
session.search Post do
|
145
|
-
with
|
251
|
+
with :bogus, 'Field'
|
146
252
|
end
|
147
|
-
end.should raise_error(
|
253
|
+
end.should raise_error(Sunspot::UnrecognizedFieldError)
|
148
254
|
end
|
149
255
|
|
150
256
|
it 'should raise NoMethodError if bogus operator referenced' do
|
151
257
|
lambda do
|
152
258
|
session.search Post do
|
153
|
-
with
|
259
|
+
with(:category_ids).resembling :bogus_condition
|
154
260
|
end
|
155
261
|
end.should raise_error(NoMethodError)
|
156
262
|
end
|
@@ -171,12 +277,12 @@ describe 'Search' do
|
|
171
277
|
end.should raise_error(ArgumentError)
|
172
278
|
end
|
173
279
|
|
174
|
-
it 'should raise
|
280
|
+
it 'should raise ArgumentError if more than two arguments passed to scope method' do
|
175
281
|
lambda do
|
176
282
|
session.search Post do
|
177
|
-
with
|
283
|
+
with(:category_ids, 4, 5)
|
178
284
|
end
|
179
|
-
end.should raise_error(
|
285
|
+
end.should raise_error(ArgumentError)
|
180
286
|
end
|
181
287
|
|
182
288
|
private
|
data/spec/api/indexer_spec.rb
CHANGED
@@ -13,7 +13,7 @@ describe 'indexer' do
|
|
13
13
|
session.index post
|
14
14
|
end
|
15
15
|
|
16
|
-
it 'should correctly index a string attribute field' do
|
16
|
+
it 'should correctly index a string attribute field' do
|
17
17
|
post :title => 'A Title'
|
18
18
|
connection.should_receive(:add).with(hash_including(:title_s => 'A Title'))
|
19
19
|
session.index post
|
@@ -26,7 +26,7 @@ describe 'indexer' do
|
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'should correctly index a float attribute field' do
|
29
|
-
post :
|
29
|
+
post :ratings_average => 2.23
|
30
30
|
connection.should_receive(:add).with(hash_including(:average_rating_f => '2.23'))
|
31
31
|
session.index post
|
32
32
|
end
|
@@ -43,12 +43,36 @@ describe 'indexer' do
|
|
43
43
|
session.index post
|
44
44
|
end
|
45
45
|
|
46
|
+
it 'should correctly index a boolean field' do
|
47
|
+
post :featured => true
|
48
|
+
connection.should_receive(:add).with(hash_including(:featured_b => 'true'))
|
49
|
+
session.index post
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should correctly index a false boolean field' do
|
53
|
+
post :featured => false
|
54
|
+
connection.should_receive(:add).with(hash_including(:featured_b => 'false'))
|
55
|
+
session.index post
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should not index a nil boolean field' do
|
59
|
+
post
|
60
|
+
connection.should_receive(:add).with(hash_not_including(:featured_b))
|
61
|
+
session.index post
|
62
|
+
end
|
63
|
+
|
46
64
|
it 'should correctly index a virtual field' do
|
47
65
|
post :title => 'The Blog Post'
|
48
66
|
connection.should_receive(:add).with(hash_including(:sort_title_s => 'blog post'))
|
49
67
|
session.index post
|
50
68
|
end
|
51
69
|
|
70
|
+
it 'should correctly index an external virtual field' do
|
71
|
+
post :category_ids => [1, 2, 3]
|
72
|
+
connection.should_receive(:add).with(hash_including(:primary_category_id_i => '1'))
|
73
|
+
session.index post
|
74
|
+
end
|
75
|
+
|
52
76
|
it 'should correctly index a field that is defined on a superclass' do
|
53
77
|
Sunspot.setup(BaseClass) { string :author_name }
|
54
78
|
post :author_name => 'Mat Brown'
|
@@ -56,6 +80,13 @@ describe 'indexer' do
|
|
56
80
|
session.index post
|
57
81
|
end
|
58
82
|
|
83
|
+
it 'should commit immediately after index! called' do
|
84
|
+
post :title => 'The Blog Post'
|
85
|
+
connection.should_receive(:add).ordered
|
86
|
+
connection.should_receive(:commit).ordered
|
87
|
+
session.index!(post)
|
88
|
+
end
|
89
|
+
|
59
90
|
it 'should remove an object from the index' do
|
60
91
|
connection.should_receive(:delete).with("Post #{post.id}")
|
61
92
|
session.remove(post)
|