outoftime-sunspot 0.0.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 builder restriction configuration field indexer query search session type util dsl).each do |filename|
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
- VERSION='0.0.1'
13
- end
25
+ UnrecognizedFieldError = Class.new(Exception)
26
+ UnrecognizedRestrictionError = Class.new(Exception)
14
27
 
15
- class <<Sunspot
16
- def setup(clazz, &block)
17
- ::Sunspot::DSL::Fields.new(clazz).instance_eval(&block) if block
18
- end
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
- def index(*objects)
21
- session.index(*objects)
22
- end
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
- def search(*types, &block)
25
- session.search(*types, &block)
26
- end
27
-
28
- def remove(*objects)
29
- session.remove(*objects)
30
- end
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
- def remove_all(*classes)
33
- session.remove_all(*classes)
34
- end
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
- def config
37
- session.config
38
- end
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
- def reset!
41
- @session = nil
42
- end
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
- private
265
+ private
45
266
 
46
- def session
47
- @session ||= ::Sunspot::Session.new
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.title 'My Pet Post'
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)', hash_including(:filter_queries => []))
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.published_at time
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.average_rating.less_than 3.0
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.average_rating.greater_than 3.0
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.average_rating.between 2.0..4.0
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.category_ids.any_of [2, 7, 12]
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.category_ids.all_of [2, 7, 12]
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)', :filter_queries => [], :rows => 15, :start => 45).twice
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.published_at time
223
+ with :published_at, time
118
224
  end
119
225
  end
120
226
 
121
- it 'should raise exception if search scoped to field not common to all types' do
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.blog_id 1
230
+ with :blog_id, 1
125
231
  end
126
- end.should raise_error(ArgumentError)
232
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
127
233
  end
128
234
 
129
- it 'should raise exception if search scoped to field configured differently between types' do
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.average_rating 2.2 # this is a float in Post but an integer in Comment
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(ArgumentError)
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))', hash_including(:filter_queries => []))
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 ArgumentError if bogus field scoped' do
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.bogus.equal_to :field
251
+ with :bogus, 'Field'
146
252
  end
147
- end.should raise_error(ArgumentError)
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.category_ids.resembling :bogus_condition
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 NoMethodError if more than one argument passed to scope method' do # or should it?
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.category_ids 4, 5
283
+ with(:category_ids, 4, 5)
178
284
  end
179
- end.should raise_error(NoMethodError)
285
+ end.should raise_error(ArgumentError)
180
286
  end
181
287
 
182
288
  private
@@ -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 :average_rating => 2.23
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)