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/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)