sunspot 0.9.7

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.
Files changed (101) hide show
  1. data/History.txt +83 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +154 -0
  4. data/Rakefile +9 -0
  5. data/TODO +9 -0
  6. data/VERSION.yml +4 -0
  7. data/bin/sunspot-configure-solr +46 -0
  8. data/bin/sunspot-solr +62 -0
  9. data/lib/light_config.rb +40 -0
  10. data/lib/sunspot.rb +469 -0
  11. data/lib/sunspot/adapters.rb +265 -0
  12. data/lib/sunspot/composite_setup.rb +186 -0
  13. data/lib/sunspot/configuration.rb +38 -0
  14. data/lib/sunspot/data_extractor.rb +47 -0
  15. data/lib/sunspot/dsl.rb +3 -0
  16. data/lib/sunspot/dsl/field_query.rb +72 -0
  17. data/lib/sunspot/dsl/fields.rb +86 -0
  18. data/lib/sunspot/dsl/query.rb +59 -0
  19. data/lib/sunspot/dsl/query_facet.rb +31 -0
  20. data/lib/sunspot/dsl/restriction.rb +25 -0
  21. data/lib/sunspot/dsl/scope.rb +193 -0
  22. data/lib/sunspot/dsl/search.rb +30 -0
  23. data/lib/sunspot/facet.rb +16 -0
  24. data/lib/sunspot/facet_data.rb +120 -0
  25. data/lib/sunspot/facet_row.rb +10 -0
  26. data/lib/sunspot/field.rb +157 -0
  27. data/lib/sunspot/field_factory.rb +126 -0
  28. data/lib/sunspot/indexer.rb +123 -0
  29. data/lib/sunspot/instantiated_facet.rb +42 -0
  30. data/lib/sunspot/instantiated_facet_row.rb +22 -0
  31. data/lib/sunspot/query.rb +191 -0
  32. data/lib/sunspot/query/base_query.rb +90 -0
  33. data/lib/sunspot/query/connective.rb +126 -0
  34. data/lib/sunspot/query/dynamic_query.rb +69 -0
  35. data/lib/sunspot/query/field_facet.rb +151 -0
  36. data/lib/sunspot/query/field_query.rb +63 -0
  37. data/lib/sunspot/query/pagination.rb +39 -0
  38. data/lib/sunspot/query/query_facet.rb +73 -0
  39. data/lib/sunspot/query/query_facet_row.rb +19 -0
  40. data/lib/sunspot/query/query_field_facet.rb +13 -0
  41. data/lib/sunspot/query/restriction.rb +233 -0
  42. data/lib/sunspot/query/scope.rb +165 -0
  43. data/lib/sunspot/query/sort.rb +36 -0
  44. data/lib/sunspot/query/sort_composite.rb +33 -0
  45. data/lib/sunspot/schema.rb +165 -0
  46. data/lib/sunspot/search.rb +219 -0
  47. data/lib/sunspot/search/hit.rb +66 -0
  48. data/lib/sunspot/session.rb +201 -0
  49. data/lib/sunspot/setup.rb +271 -0
  50. data/lib/sunspot/type.rb +200 -0
  51. data/lib/sunspot/util.rb +164 -0
  52. data/solr/etc/jetty.xml +212 -0
  53. data/solr/etc/webdefault.xml +379 -0
  54. data/solr/lib/jetty-6.1.3.jar +0 -0
  55. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  56. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  57. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  58. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  59. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  60. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  61. data/solr/solr/conf/elevate.xml +36 -0
  62. data/solr/solr/conf/protwords.txt +21 -0
  63. data/solr/solr/conf/schema.xml +50 -0
  64. data/solr/solr/conf/solrconfig.xml +696 -0
  65. data/solr/solr/conf/stopwords.txt +57 -0
  66. data/solr/solr/conf/synonyms.txt +31 -0
  67. data/solr/start.jar +0 -0
  68. data/solr/webapps/solr.war +0 -0
  69. data/spec/api/adapters_spec.rb +33 -0
  70. data/spec/api/build_search_spec.rb +1039 -0
  71. data/spec/api/indexer_spec.rb +311 -0
  72. data/spec/api/query_spec.rb +153 -0
  73. data/spec/api/search_retrieval_spec.rb +362 -0
  74. data/spec/api/session_spec.rb +157 -0
  75. data/spec/api/spec_helper.rb +1 -0
  76. data/spec/api/sunspot_spec.rb +18 -0
  77. data/spec/integration/dynamic_fields_spec.rb +55 -0
  78. data/spec/integration/faceting_spec.rb +169 -0
  79. data/spec/integration/keyword_search_spec.rb +83 -0
  80. data/spec/integration/scoped_search_spec.rb +289 -0
  81. data/spec/integration/spec_helper.rb +1 -0
  82. data/spec/integration/stored_fields_spec.rb +10 -0
  83. data/spec/integration/test_pagination.rb +32 -0
  84. data/spec/mocks/adapters.rb +32 -0
  85. data/spec/mocks/blog.rb +3 -0
  86. data/spec/mocks/comment.rb +19 -0
  87. data/spec/mocks/connection.rb +84 -0
  88. data/spec/mocks/mock_adapter.rb +30 -0
  89. data/spec/mocks/mock_record.rb +48 -0
  90. data/spec/mocks/photo.rb +8 -0
  91. data/spec/mocks/post.rb +73 -0
  92. data/spec/mocks/user.rb +8 -0
  93. data/spec/spec_helper.rb +47 -0
  94. data/tasks/gemspec.rake +25 -0
  95. data/tasks/rcov.rake +28 -0
  96. data/tasks/rdoc.rake +22 -0
  97. data/tasks/schema.rake +19 -0
  98. data/tasks/spec.rake +24 -0
  99. data/tasks/todo.rake +4 -0
  100. data/templates/schema.xml.haml +24 -0
  101. metadata +246 -0
@@ -0,0 +1,57 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one or more
2
+ # contributor license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright ownership.
4
+ # The ASF licenses this file to You under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with
6
+ # the License. You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ #-----------------------------------------------------------------------
17
+ # a couple of test stopwords to test that the words are really being
18
+ # configured from this file:
19
+ stopworda
20
+ stopwordb
21
+
22
+ #Standard english stop words taken from Lucene's StopAnalyzer
23
+ an
24
+ and
25
+ are
26
+ as
27
+ at
28
+ be
29
+ but
30
+ by
31
+ for
32
+ if
33
+ in
34
+ into
35
+ is
36
+ it
37
+ no
38
+ not
39
+ of
40
+ on
41
+ or
42
+ s
43
+ such
44
+ t
45
+ that
46
+ the
47
+ their
48
+ then
49
+ there
50
+ these
51
+ they
52
+ this
53
+ to
54
+ was
55
+ will
56
+ with
57
+
@@ -0,0 +1,31 @@
1
+ # The ASF licenses this file to You under the Apache License, Version 2.0
2
+ # (the "License"); you may not use this file except in compliance with
3
+ # the License. You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ #-----------------------------------------------------------------------
14
+ #some test synonym mappings unlikely to appear in real input text
15
+ aaa => aaaa
16
+ bbb => bbbb1 bbbb2
17
+ ccc => cccc1,cccc2
18
+ a\=>a => b\=>b
19
+ a\,a => b\,b
20
+ fooaaa,baraaa,bazaaa
21
+
22
+ # Some synonym groups specific to this example
23
+ GB,gib,gigabyte,gigabytes
24
+ MB,mib,megabyte,megabytes
25
+ Television, Televisions, TV, TVs
26
+ #notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming
27
+ #after us won't split it into two words.
28
+
29
+ # Synonym mappings can be used for spelling correction too
30
+ pixima => pixma
31
+
data/solr/start.jar ADDED
Binary file
Binary file
@@ -0,0 +1,33 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Sunspot::Adapters::InstanceAdapter do
4
+ it "finds adapter by superclass" do
5
+ Sunspot::Adapters::InstanceAdapter::for(Model).should be(AbstractModelInstanceAdapter)
6
+ end
7
+
8
+ it "finds adapter by mixin" do
9
+ Sunspot::Adapters::InstanceAdapter::for(MixModel).should be(MixInModelInstanceAdapter)
10
+ end
11
+
12
+ it 'throws NoAdapterError if anonymous module passed in' do
13
+ lambda do
14
+ Sunspot::Adapters::InstanceAdapter::for(Module.new)
15
+ end.should raise_error(Sunspot::NoAdapterError)
16
+ end
17
+ end
18
+
19
+ describe Sunspot::Adapters::DataAccessor do
20
+ it "finds adapter by superclass" do
21
+ Sunspot::Adapters::DataAccessor::for(Model).should be(AbstractModelDataAccessor)
22
+ end
23
+
24
+ it "finds adapter by mixin" do
25
+ Sunspot::Adapters::DataAccessor::for(MixModel).should be(MixInModelDataAccessor)
26
+ end
27
+
28
+ it 'throws NoAdapterError if anonymous module passed in' do
29
+ lambda do
30
+ Sunspot::Adapters::DataAccessor::for(Module.new)
31
+ end.should raise_error(Sunspot::NoAdapterError)
32
+ end
33
+ end
@@ -0,0 +1,1039 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'Search' do
4
+ it 'should search by keywords from DSL' do
5
+ session.search Post do
6
+ keywords 'keyword search'
7
+ end
8
+ connection.should have_last_search_with(:q => 'keyword search')
9
+ end
10
+
11
+ it 'should search by keywords from options' do
12
+ session.search Post, :keywords => 'keyword search'
13
+ connection.should have_last_search_with(:q => 'keyword search')
14
+ end
15
+
16
+ it 'should set default query parser to dismax when keywords used' do
17
+ session.search Post do
18
+ keywords 'keyword search'
19
+ end
20
+ connection.should have_last_search_with(:defType => 'dismax')
21
+ end
22
+
23
+ it 'should search types in filter query if keywords used' do
24
+ session.search Post do
25
+ keywords 'keyword search'
26
+ end
27
+ connection.should have_last_search_with(:fq => 'type:Post')
28
+ end
29
+
30
+ it 'should search types in main query if keywords not used' do
31
+ session.search Post
32
+ connection.should have_last_search_with(:q => 'type:Post')
33
+ end
34
+
35
+ it 'should search type of subclass when superclass is configured' do
36
+ session.search PhotoPost
37
+ connection.should have_last_search_with(:q => 'type:PhotoPost')
38
+ end
39
+
40
+ it 'should search all text fields for searched class' do
41
+ session.search Post do
42
+ keywords 'keyword search'
43
+ end
44
+ connection.searches.last[:qf].split(' ').sort.should == %w(backwards_title_text body_text title_text)
45
+ end
46
+
47
+ it 'should search only specified text fields when specified' do
48
+ session.search Post do
49
+ keywords 'keyword search', :fields => [:title, :body]
50
+ end
51
+ connection.searches.last[:qf].split(' ').sort.should == %w(body_text title_text)
52
+ end
53
+
54
+ it 'should request score when keywords used' do
55
+ session.search Post, :keywords => 'keyword search'
56
+ connection.should have_last_search_with(:fl => '* score')
57
+ end
58
+
59
+ it 'should not request score when keywords not used' do
60
+ session.search Post
61
+ connection.should_not have_last_search_with(:fl)
62
+ end
63
+
64
+ it 'should scope by exact match with a string from DSL' do
65
+ session.search Post do
66
+ with :title, 'My Pet Post'
67
+ end
68
+ connection.should have_last_search_with(:fq => ['title_ss:My\ Pet\ Post'])
69
+ end
70
+
71
+ it 'should scope by exact match with a string from options' do
72
+ session.search Post, :conditions => { :title => 'My Pet Post' }
73
+ connection.should have_last_search_with(:fq => ['title_ss:My\ Pet\ Post'])
74
+ end
75
+
76
+ it 'should ignore nonexistant fields in hash scope' do
77
+ session.search Post, :conditions => { :bogus => 'Field' }
78
+ connection.should_not have_last_search_with(:fq)
79
+ end
80
+
81
+ it 'should scope by exact match with time' do
82
+ time = Time.parse('1983-07-08 05:00:00 -0400')
83
+ session.search Post do
84
+ with :published_at, time
85
+ end
86
+ connection.should have_last_search_with(
87
+ :fq => ['published_at_d:1983\-07\-08T09\:00\:00Z']
88
+ )
89
+ end
90
+
91
+ it 'should scope by exact match with date' do
92
+ date = Date.new(1983, 7, 8)
93
+ session.search Post do
94
+ with :expire_date, date
95
+ end
96
+ connection.should have_last_search_with(
97
+ :fq => ['expire_date_d:1983\-07\-08T00\:00\:00Z']
98
+ )
99
+ end
100
+
101
+ it 'should scope by exact match with boolean' do
102
+ session.search Post do
103
+ with :featured, false
104
+ end
105
+ connection.should have_last_search_with(:fq => ['featured_b:false'])
106
+ end
107
+
108
+ it 'should scope by less than match with float' do
109
+ session.search Post do
110
+ with(:average_rating).less_than 3.0
111
+ end
112
+ connection.should have_last_search_with(:fq => ['average_rating_f:[* TO 3\.0]'])
113
+ end
114
+
115
+ it 'should scope by greater than match with float' do
116
+ session.search Post do
117
+ with(:average_rating).greater_than 3.0
118
+ end
119
+ connection.should have_last_search_with(:fq => ['average_rating_f:[3\.0 TO *]'])
120
+ end
121
+
122
+ it 'should scope by short-form between match with integers' do
123
+ session.search Post do
124
+ with :blog_id, 2..4
125
+ end
126
+ connection.should have_last_search_with(:fq => ['blog_id_i:[2 TO 4]'])
127
+ end
128
+
129
+ it 'should scope by between match with float' do
130
+ session.search Post do
131
+ with(:average_rating).between 2.0..4.0
132
+ end
133
+ connection.should have_last_search_with(:fq => ['average_rating_f:[2\.0 TO 4\.0]'])
134
+ end
135
+
136
+ it 'should scope by any match with integer using DSL' do
137
+ session.search Post do
138
+ with(:category_ids).any_of [2, 7, 12]
139
+ end
140
+ connection.should have_last_search_with(:fq => ['category_ids_im:(2 OR 7 OR 12)'])
141
+ end
142
+
143
+ it 'should scope by any match with integer using options' do
144
+ session.search Post, :conditions => { :category_ids => [2, 7, 12] }
145
+ connection.should have_last_search_with(:fq => ['category_ids_im:(2 OR 7 OR 12)'])
146
+ end
147
+
148
+ it 'should scope by short-form any-of match with integers' do
149
+ session.search Post do
150
+ with :category_ids, [2, 7, 12]
151
+ end
152
+ connection.should have_last_search_with(:fq => ['category_ids_im:(2 OR 7 OR 12)'])
153
+ end
154
+
155
+ it 'should scope by all match with integer' do
156
+ session.search Post do
157
+ with(:category_ids).all_of [2, 7, 12]
158
+ end
159
+ connection.should have_last_search_with(:fq => ['category_ids_im:(2 AND 7 AND 12)'])
160
+ end
161
+
162
+ it 'should scope by not equal match with string' do
163
+ session.search Post do
164
+ without :title, 'Bad Post'
165
+ end
166
+ connection.should have_last_search_with(:fq => ['-title_ss:Bad\ Post'])
167
+ end
168
+
169
+ it 'should scope by not less than match with float' do
170
+ session.search Post do
171
+ without(:average_rating).less_than 3.0
172
+ end
173
+ connection.should have_last_search_with(:fq => ['-average_rating_f:[* TO 3\.0]'])
174
+ end
175
+
176
+ it 'should scope by not greater than match with float' do
177
+ session.search Post do
178
+ without(:average_rating).greater_than 3.0
179
+ end
180
+ connection.should have_last_search_with(:fq => ['-average_rating_f:[3\.0 TO *]'])
181
+ end
182
+
183
+ it 'should scope by not between match with shorthand' do
184
+ session.search Post do
185
+ without(:blog_id, 2..4)
186
+ end
187
+ connection.should have_last_search_with(:fq => ['-blog_id_i:[2 TO 4]'])
188
+ end
189
+
190
+ it 'should scope by not between match with float' do
191
+ session.search Post do
192
+ without(:average_rating).between 2.0..4.0
193
+ end
194
+ connection.should have_last_search_with(:fq => ['-average_rating_f:[2\.0 TO 4\.0]'])
195
+ end
196
+
197
+ it 'should scope by not any match with integer' do
198
+ session.search Post do
199
+ without(:category_ids).any_of [2, 7, 12]
200
+ end
201
+ connection.should have_last_search_with(:fq => ['-category_ids_im:(2 OR 7 OR 12)'])
202
+ end
203
+
204
+
205
+ it 'should scope by not all match with integer' do
206
+ session.search Post do
207
+ without(:category_ids).all_of [2, 7, 12]
208
+ end
209
+ connection.should have_last_search_with(:fq => ['-category_ids_im:(2 AND 7 AND 12)'])
210
+ end
211
+
212
+ it 'should scope by empty field' do
213
+ session.search Post do
214
+ with :average_rating, nil
215
+ end
216
+ connection.should have_last_search_with(:fq => ['-average_rating_f:[* TO *]'])
217
+ end
218
+
219
+ it 'should scope by non-empty field' do
220
+ session.search Post do
221
+ without :average_rating, nil
222
+ end
223
+ connection.should have_last_search_with(:fq => ['average_rating_f:[* TO *]'])
224
+ end
225
+
226
+ it 'should exclude by object identity' do
227
+ post = Post.new
228
+ session.search Post do
229
+ without post
230
+ end
231
+ connection.should have_last_search_with(:fq => ["-id:Post\\ #{post.id}"])
232
+ end
233
+
234
+ it 'should exclude multiple objects passed as varargs by object identity' do
235
+ post1, post2 = Post.new, Post.new
236
+ session.search Post do
237
+ without post1, post2
238
+ end
239
+ connection.should have_last_search_with(
240
+ :fq => ["-id:Post\\ #{post1.id}", "-id:Post\\ #{post2.id}"]
241
+ )
242
+ end
243
+
244
+ it 'should exclude multiple objects passed as array by object identity' do
245
+ posts = [Post.new, Post.new]
246
+ session.search Post do
247
+ without posts
248
+ end
249
+ connection.should have_last_search_with(
250
+ :fq => ["-id:Post\\ #{posts.first.id}", "-id:Post\\ #{posts.last.id}"]
251
+ )
252
+ end
253
+
254
+ it 'should create a disjunction between two restrictions' do
255
+ session.search Post do
256
+ any_of do
257
+ with :category_ids, 1
258
+ with :blog_id, 2
259
+ end
260
+ end
261
+ connection.should have_last_search_with(
262
+ :fq => '(category_ids_im:1 OR blog_id_i:2)'
263
+ )
264
+ end
265
+
266
+ it 'should create a conjunction inside of a disjunction' do
267
+ session.search Post do
268
+ any_of do
269
+ with :blog_id, 2
270
+ all_of do
271
+ with :category_ids, 1
272
+ with(:average_rating).greater_than(3.0)
273
+ end
274
+ end
275
+ end
276
+ connection.should have_last_search_with(
277
+ :fq => '(blog_id_i:2 OR (category_ids_im:1 AND average_rating_f:[3\.0 TO *]))'
278
+ )
279
+ end
280
+
281
+ it 'should do nothing special if #all_of called from the top level' do
282
+ session.search Post do
283
+ all_of do
284
+ with :blog_id, 2
285
+ with :category_ids, 1
286
+ end
287
+ end
288
+ connection.should have_last_search_with(
289
+ :fq => ['blog_id_i:2', 'category_ids_im:1']
290
+ )
291
+ end
292
+
293
+ it 'should create a disjunction with negated restrictions' do
294
+ session.search Post do
295
+ any_of do
296
+ with :category_ids, 1
297
+ without(:average_rating).greater_than(3.0)
298
+ end
299
+ end
300
+ connection.should have_last_search_with(
301
+ :fq => '-(-category_ids_im:1 AND average_rating_f:[3\.0 TO *])'
302
+ )
303
+ end
304
+
305
+ it 'should create a disjunction with nested conjunction with negated restrictions' do
306
+ session.search Post do
307
+ any_of do
308
+ with :category_ids, 1
309
+ all_of do
310
+ without(:average_rating).greater_than(3.0)
311
+ with(:blog_id, 1)
312
+ end
313
+ end
314
+ end
315
+ connection.should have_last_search_with(
316
+ :fq => '(category_ids_im:1 OR (-average_rating_f:[3\.0 TO *] AND blog_id_i:1))'
317
+ )
318
+ end
319
+
320
+ it 'should create a disjunction with nested conjunction with nested disjunction with negated restriction' do
321
+ session.search(Post) do
322
+ any_of do
323
+ with(:title, 'Yes')
324
+ all_of do
325
+ with(:blog_id, 1)
326
+ any_of do
327
+ with(:category_ids, 4)
328
+ without(:average_rating, 2.0)
329
+ end
330
+ end
331
+ end
332
+ end
333
+ connection.should have_last_search_with(
334
+ :fq => '(title_ss:Yes OR (blog_id_i:1 AND -(-category_ids_im:4 AND average_rating_f:2\.0)))'
335
+ )
336
+ end
337
+
338
+ it 'should create a disjunction with a negated restriction and a nested disjunction in a conjunction with a negated restriction' do
339
+ session.search(Post) do
340
+ any_of do
341
+ without(:title, 'Yes')
342
+ all_of do
343
+ with(:blog_id, 1)
344
+ any_of do
345
+ with(:category_ids, 4)
346
+ without(:average_rating, 2.0)
347
+ end
348
+ end
349
+ end
350
+ end
351
+ connection.should have_last_search_with(
352
+ :fq => '-(title_ss:Yes AND -(blog_id_i:1 AND -(-category_ids_im:4 AND average_rating_f:2\.0)))'
353
+ )
354
+ end
355
+
356
+ #
357
+ # This is important because if a disjunction could be nested in another
358
+ # disjunction, then the inner disjunction could denormalize (and thus
359
+ # become negated) after the outer disjunction denormalized (checking to
360
+ # see if the inner one is negated). Since conjunctions never need to
361
+ # denormalize, if a disjunction can only contain conjunctions or restrictions,
362
+ # we can guarantee that the negation state of a disjunction's components will
363
+ # not change when #to_params is called on them.
364
+ #
365
+ # Since disjunction is associative, this behavior has no effect on the actual
366
+ # logical semantics of the disjunction.
367
+ #
368
+ it 'should create a single disjunction when disjunctions nested' do
369
+ session.search(Post) do
370
+ any_of do
371
+ with(:title, 'Yes')
372
+ any_of do
373
+ with(:blog_id, 1)
374
+ with(:category_ids, 4)
375
+ end
376
+ end
377
+ end
378
+ connection.should have_last_search_with(
379
+ :fq => '(title_ss:Yes OR blog_id_i:1 OR category_ids_im:4)'
380
+ )
381
+ end
382
+
383
+ it 'should create a disjunction with instance exclusion' do
384
+ post = Post.new
385
+ session.search Post do
386
+ any_of do
387
+ without(post)
388
+ with(:category_ids, 1)
389
+ end
390
+ end
391
+ connection.should have_last_search_with(
392
+ :fq => "-(id:Post\\ #{post.id} AND -category_ids_im:1)"
393
+ )
394
+ end
395
+
396
+ it 'should create a disjunction with empty restriction' do
397
+ session.search Post do
398
+ any_of do
399
+ with(:average_rating, nil)
400
+ with(:average_rating).greater_than(3.0)
401
+ end
402
+ end
403
+ connection.should have_last_search_with(
404
+ :fq => '-(average_rating_f:[* TO *] AND -average_rating_f:[3\.0 TO *])'
405
+ )
406
+ end
407
+
408
+ it 'should restrict by dynamic string field with equality restriction' do
409
+ session.search Post do
410
+ dynamic :custom_string do
411
+ with :test, 'string'
412
+ end
413
+ end
414
+ connection.should have_last_search_with(:fq => ['custom_string\:test_s:string'])
415
+ end
416
+
417
+ it 'should restrict by dynamic integer field with less than restriction' do
418
+ session.search Post do
419
+ dynamic :custom_integer do
420
+ with(:test).less_than(1)
421
+ end
422
+ end
423
+ connection.should have_last_search_with(:fq => ['custom_integer\:test_i:[* TO 1]'])
424
+ end
425
+
426
+ it 'should restrict by dynamic float field with between restriction' do
427
+ session.search Post do
428
+ dynamic :custom_float do
429
+ with(:test).between(2.2..3.3)
430
+ end
431
+ end
432
+ connection.should have_last_search_with(:fq => ['custom_float\:test_fm:[2\.2 TO 3\.3]'])
433
+ end
434
+
435
+ it 'should restrict by dynamic time field with any of restriction' do
436
+ session.search Post do
437
+ dynamic :custom_time do
438
+ with(:test).any_of([Time.parse('2009-02-10 14:00:00 UTC'),
439
+ Time.parse('2009-02-13 18:00:00 UTC')])
440
+ end
441
+ end
442
+ connection.should have_last_search_with(:fq => ['custom_time\:test_d:(2009\-02\-10T14\:00\:00Z OR 2009\-02\-13T18\:00\:00Z)'])
443
+ end
444
+
445
+ it 'should restrict by dynamic boolean field with equality restriction' do
446
+ session.search Post do
447
+ dynamic :custom_boolean do
448
+ with :test, false
449
+ end
450
+ end
451
+ connection.should have_last_search_with(:fq => ['custom_boolean\:test_b:false'])
452
+ end
453
+
454
+ it 'should negate a dynamic field restriction' do
455
+ session.search Post do
456
+ dynamic :custom_string do
457
+ without :test, 'foo'
458
+ end
459
+ end
460
+ connection.should have_last_search_with(:fq => ['-custom_string\:test_s:foo'])
461
+ end
462
+
463
+ it 'should search by a dynamic field inside a disjunction' do
464
+ session.search Post do
465
+ any_of do
466
+ dynamic :custom_string do
467
+ with :test, 'foo'
468
+ end
469
+ with :title, 'bar'
470
+ end
471
+ end
472
+ connection.should have_last_search_with(
473
+ :fq => '(custom_string\:test_s:foo OR title_ss:bar)'
474
+ )
475
+ end
476
+
477
+ it 'should throw an UnrecognizedFieldError if an unknown dynamic field is searched by' do
478
+ lambda do
479
+ session.search Post do
480
+ dynamic(:bogus) { with :some, 'value' }
481
+ end
482
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
483
+ end
484
+
485
+ it 'should throw a NoMethodError if pagination is attempted in a dynamic query' do
486
+ lambda do
487
+ session.search Post do
488
+ dynamic :custom_string do
489
+ paginate 3, 10
490
+ end
491
+ end
492
+ end.should raise_error(NoMethodError)
493
+ end
494
+
495
+ it 'should paginate using default per_page when page not provided' do
496
+ session.search Post
497
+ connection.should have_last_search_with(:rows => 30)
498
+ end
499
+
500
+ it 'should paginate using default per_page when page provided in DSL' do
501
+ session.search Post do
502
+ paginate :page => 2
503
+ end
504
+ connection.should have_last_search_with(:rows => 30, :start => 30)
505
+ end
506
+
507
+ it 'should paginate using default per_page when page provided in options' do
508
+ session.search Post, :page => 2
509
+ connection.should have_last_search_with(:rows => 30, :start => 30)
510
+ end
511
+
512
+ it 'should paginate using provided per_page in DSL' do
513
+ session.search Post do
514
+ paginate :page => 4, :per_page => 15
515
+ end
516
+ connection.should have_last_search_with(:rows => 15, :start => 45)
517
+ end
518
+
519
+ it 'should paginate using provided per_page in options' do
520
+ session.search Post, :page => 4, :per_page => 15
521
+ connection.should have_last_search_with(:rows => 15, :start => 45)
522
+ end
523
+
524
+ it 'should order in DSL' do
525
+ session.search Post do
526
+ order_by :average_rating, :desc
527
+ end
528
+ connection.should have_last_search_with(:sort => 'average_rating_f desc')
529
+ end
530
+
531
+ it 'should order in keywords' do
532
+ session.search Post, :order => 'average_rating desc'
533
+ connection.should have_last_search_with(:sort => 'average_rating_f desc')
534
+ end
535
+
536
+ it 'should order by multiple fields in DSL' do
537
+ session.search Post do
538
+ order_by :average_rating, :desc
539
+ order_by :sort_title, :asc
540
+ end
541
+ connection.should have_last_search_with(:sort => 'average_rating_f desc, sort_title_s asc')
542
+ end
543
+
544
+ it 'should order by multiple fields in options' do
545
+ session.search Post, :order => ['average_rating desc', 'sort_title asc']
546
+ connection.should have_last_search_with(:sort => 'average_rating_f desc, sort_title_s asc')
547
+ end
548
+
549
+ it 'should order by a dynamic field' do
550
+ session.search Post do
551
+ dynamic :custom_integer do
552
+ order_by :test, :desc
553
+ end
554
+ end
555
+ connection.should have_last_search_with(:sort => 'custom_integer:test_i desc')
556
+ end
557
+
558
+ it 'should order by a dynamic field and static field, with given precedence' do
559
+ session.search Post do
560
+ dynamic :custom_integer do
561
+ order_by :test, :desc
562
+ end
563
+ order_by :sort_title, :asc
564
+ end
565
+ connection.should have_last_search_with(:sort => 'custom_integer:test_i desc, sort_title_s asc')
566
+ end
567
+
568
+ it 'should order by random' do
569
+ session.search Post do
570
+ order_by_random
571
+ end
572
+ connection.searches.last[:sort].should =~ /^random_\d+ asc$/
573
+ end
574
+
575
+ it 'should throw an ArgumentError if a bogus order direction is given' do
576
+ lambda do
577
+ session.search Post do
578
+ order_by :sort_title, :sideways
579
+ end
580
+ end.should raise_error(ArgumentError)
581
+ end
582
+
583
+ it 'should not allow ordering by multiple-value fields' do
584
+ lambda do
585
+ session.search Post do
586
+ order_by :category_ids
587
+ end
588
+ end.should raise_error(ArgumentError)
589
+ end
590
+
591
+ it 'should not turn faceting on if no facet requested' do
592
+ session.search(Post)
593
+ connection.should_not have_last_search_with('facet')
594
+ end
595
+
596
+ it 'should turn faceting on if facet is requested' do
597
+ session.search Post do
598
+ facet :category_ids
599
+ end
600
+ connection.should have_last_search_with(:facet => 'true')
601
+ end
602
+
603
+ it 'should request single field facet' do
604
+ session.search Post do
605
+ facet :category_ids
606
+ end
607
+ connection.should have_last_search_with(:"facet.field" => %w(category_ids_im))
608
+ end
609
+
610
+ it 'should request multiple field facets' do
611
+ session.search Post do
612
+ facet :category_ids, :blog_id
613
+ end
614
+ connection.should have_last_search_with(:"facet.field" => %w(category_ids_im blog_id_i))
615
+ end
616
+
617
+ it 'should set facet sort by count' do
618
+ session.search Post do
619
+ facet :category_ids, :sort => :count
620
+ end
621
+ connection.should have_last_search_with(:"f.category_ids_im.facet.sort" => 'true')
622
+ end
623
+
624
+ it 'should set facet sort by index' do
625
+ session.search Post do
626
+ facet :category_ids, :sort => :index
627
+ end
628
+ connection.should have_last_search_with(:"f.category_ids_im.facet.sort" => 'false')
629
+ end
630
+
631
+ it 'should throw ArgumentError if bogus facet sort provided' do
632
+ lambda do
633
+ session.search Post do
634
+ facet :category_ids, :sort => :sideways
635
+ end
636
+ end.should raise_error(ArgumentError)
637
+ end
638
+
639
+ it 'should set the facet limit' do
640
+ session.search Post do
641
+ facet :category_ids, :limit => 10
642
+ end
643
+ connection.should have_last_search_with(:"f.category_ids_im.facet.limit" => 10)
644
+ end
645
+
646
+ it 'should set the facet minimum count' do
647
+ session.search Post do
648
+ facet :category_ids, :minimum_count => 5
649
+ end
650
+ connection.should have_last_search_with(:"f.category_ids_im.facet.mincount" => 5)
651
+ end
652
+
653
+ it 'should set the facet minimum count to zero if zeros are allowed' do
654
+ session.search Post do
655
+ facet :category_ids, :zeros => true
656
+ end
657
+ connection.should have_last_search_with(:"f.category_ids_im.facet.mincount" => 0)
658
+ end
659
+
660
+ it 'should set the facet minimum count to one by default' do
661
+ session.search Post do
662
+ facet :category_ids
663
+ end
664
+ connection.should have_last_search_with(:"f.category_ids_im.facet.mincount" => 1)
665
+ end
666
+
667
+ describe 'with date faceting' do
668
+ before :each do
669
+ @time_range = (Time.parse('2009-06-01 00:00:00 -0400')..
670
+ Time.parse('2009-07-01 00:00:00 -0400'))
671
+ end
672
+
673
+ it 'should not send date facet parameters if time range is not specified' do
674
+ session.search Post do |query|
675
+ query.facet :published_at
676
+ end
677
+ connection.should_not have_last_search_with(:"facet.date")
678
+ end
679
+
680
+ it 'should set the facet to a date facet' do
681
+ session.search Post do |query|
682
+ query.facet :published_at, :time_range => @time_range
683
+ end
684
+ connection.should have_last_search_with(:"facet.date" => ['published_at_d'])
685
+ end
686
+
687
+ it 'should set the facet start and end' do
688
+ session.search Post do |query|
689
+ query.facet :published_at, :time_range => @time_range
690
+ end
691
+ connection.should have_last_search_with(
692
+ :"f.published_at_d.facet.date.start" => '2009-06-01T04:00:00Z',
693
+ :"f.published_at_d.facet.date.end" => '2009-07-01T04:00:00Z'
694
+ )
695
+ end
696
+
697
+ it 'should default the time interval to 1 day' do
698
+ session.search Post do |query|
699
+ query.facet :published_at, :time_range => @time_range
700
+ end
701
+ connection.should have_last_search_with(:"f.published_at_d.facet.date.gap" => "+86400SECONDS")
702
+ end
703
+
704
+ it 'should use custom time interval' do
705
+ session.search Post do |query|
706
+ query.facet :published_at, :time_range => @time_range, :time_interval => 3600
707
+ end
708
+ connection.should have_last_search_with(:"f.published_at_d.facet.date.gap" => "+3600SECONDS")
709
+ end
710
+
711
+ it 'should allow computation of one other time' do
712
+ session.search Post do |query|
713
+ query.facet :published_at, :time_range => @time_range, :time_other => :before
714
+ end
715
+ connection.should have_last_search_with(:"f.published_at_d.facet.date.other" => %w(before))
716
+ end
717
+
718
+ it 'should allow computation of two other times' do
719
+ session.search Post do |query|
720
+ query.facet :published_at, :time_range => @time_range, :time_other => [:before, :after]
721
+ end
722
+ connection.should have_last_search_with(:"f.published_at_d.facet.date.other" => %w(before after))
723
+ end
724
+
725
+ it 'should not allow computation of bogus other time' do
726
+ lambda do
727
+ session.search Post do |query|
728
+ query.facet :published_at, :time_range => @time_range, :time_other => :bogus
729
+ end
730
+ end.should raise_error(ArgumentError)
731
+ end
732
+
733
+ it 'should not allow date faceting on a non-date field' do
734
+ lambda do
735
+ session.search Post do |query|
736
+ query.facet :blog_id, :time_range => @time_range
737
+ end
738
+ end.should raise_error(ArgumentError)
739
+ end
740
+ end
741
+
742
+ describe 'with query faceting' do
743
+ it 'should turn faceting on' do
744
+ session.search Post do
745
+ facet :foo do
746
+ row :bar do
747
+ with(:average_rating).between(4.0..5.0)
748
+ end
749
+ end
750
+ end
751
+ connection.should have_last_search_with(:facet => 'true')
752
+ end
753
+
754
+ it 'should facet by query' do
755
+ session.search Post do
756
+ facet :foo do
757
+ row :bar do
758
+ with(:average_rating).between(4.0..5.0)
759
+ end
760
+ end
761
+ end
762
+ connection.should have_last_search_with(:"facet.query" => 'average_rating_f:[4\.0 TO 5\.0]')
763
+ end
764
+
765
+ it 'should request multiple query facets' do
766
+ session.search Post do
767
+ facet :foo do
768
+ row :bar do
769
+ with(:average_rating).between(3.0..4.0)
770
+ end
771
+ row :baz do
772
+ with(:average_rating).between(4.0..5.0)
773
+ end
774
+ end
775
+ end
776
+ connection.should have_last_search_with(
777
+ :"facet.query" => [
778
+ 'average_rating_f:[3\.0 TO 4\.0]',
779
+ 'average_rating_f:[4\.0 TO 5\.0]'
780
+ ]
781
+ )
782
+ end
783
+
784
+ it 'should request query facet with multiple conditions' do
785
+ session.search Post do
786
+ facet :foo do
787
+ row :bar do
788
+ with(:category_ids, 1)
789
+ with(:blog_id, 2)
790
+ end
791
+ end
792
+ end
793
+ connection.should have_last_search_with(
794
+ :"facet.query" => '(category_ids_im:1 AND blog_id_i:2)'
795
+ )
796
+ end
797
+
798
+ it 'should request query facet with disjunction' do
799
+ session.search Post do
800
+ facet :foo do
801
+ row :bar do
802
+ any_of do
803
+ with(:category_ids, 1)
804
+ with(:blog_id, 2)
805
+ end
806
+ end
807
+ end
808
+ end
809
+ connection.should have_last_search_with(
810
+ :"facet.query" => '(category_ids_im:1 OR blog_id_i:2)'
811
+ )
812
+ end
813
+
814
+ it 'should request query facet with internal dynamic field' do
815
+ session.search Post do
816
+ facet :test do
817
+ row 'foo' do
818
+ dynamic :custom_string do
819
+ with :test, 'foo'
820
+ end
821
+ end
822
+ end
823
+ end
824
+ connection.should have_last_search_with(
825
+ :"facet.query" => 'custom_string\:test_s:foo'
826
+ )
827
+ end
828
+
829
+ it 'should request query facet with external dynamic field' do
830
+ session.search Post do
831
+ dynamic :custom_string do
832
+ facet :test do
833
+ row 'foo' do
834
+ with :test, 'foo'
835
+ end
836
+ end
837
+ end
838
+ end
839
+ connection.should have_last_search_with(
840
+ :"facet.query" => 'custom_string\:test_s:foo'
841
+ )
842
+ end
843
+
844
+ it 'should not allow 0 arguments to facet method with block' do
845
+ lambda do
846
+ session.search Post do
847
+ facet do
848
+ end
849
+ end
850
+ end.should raise_error(ArgumentError)
851
+ end
852
+
853
+ it 'should not allow more than 1 argument to facet method with block' do
854
+ lambda do
855
+ session.search Post do
856
+ facet :foo, :bar do
857
+ end
858
+ end
859
+ end.should raise_error(ArgumentError)
860
+ end
861
+ end
862
+
863
+ it 'builds query facets when passed :only argument to field facet declaration' do
864
+ session.search Post do
865
+ facet :category_ids, :only => [1, 3]
866
+ end
867
+ connection.should have_last_search_with(
868
+ :"facet.query" => ['category_ids_im:1', 'category_ids_im:3']
869
+ )
870
+ end
871
+
872
+ it 'converts limited query facet values to the correct type' do
873
+ session.search Post do
874
+ facet :published_at, :only => [Time.utc(2009, 8, 28, 15, 33), Time.utc(2008,8, 28, 15, 33)]
875
+ end
876
+ connection.should have_last_search_with(
877
+ :"facet.query" => [
878
+ 'published_at_d:2009\-08\-28T15\:33\:00Z',
879
+ 'published_at_d:2008\-08\-28T15\:33\:00Z'
880
+ ]
881
+ )
882
+ end
883
+
884
+ it 'should allow faceting by dynamic string field' do
885
+ session.search Post do
886
+ dynamic :custom_string do
887
+ facet :test
888
+ end
889
+ end
890
+ connection.should have_last_search_with(:"facet.field" => %w(custom_string:test_s))
891
+ end
892
+
893
+ it 'should properly escape namespaced type names' do
894
+ session.search(Namespaced::Comment)
895
+ connection.should have_last_search_with(:q => 'type:Namespaced\:\:Comment')
896
+ end
897
+
898
+ it 'should build search for multiple types' do
899
+ session.search(Post, Namespaced::Comment)
900
+ connection.should have_last_search_with(:q => 'type:(Post OR Namespaced\:\:Comment)')
901
+ end
902
+
903
+ it 'should allow search on fields common to all types with DSL' do
904
+ time = Time.parse('1983-07-08 05:00:00 -0400')
905
+ session.search Post, Namespaced::Comment do
906
+ with :published_at, time
907
+ end
908
+ connection.should have_last_search_with(:fq => ['published_at_d:1983\-07\-08T09\:00\:00Z'])
909
+ end
910
+
911
+ it 'should allow search on fields common to all types with conditions' do
912
+ time = Time.parse('1983-07-08 05:00:00 -0400')
913
+ session.search Post, Namespaced::Comment, :conditions => { :published_at => time }
914
+ connection.should have_last_search_with(:fq => ['published_at_d:1983\-07\-08T09\:00\:00Z'])
915
+ end
916
+
917
+ it 'should allow search on dynamic fields common to all types' do
918
+ session.search Post, Namespaced::Comment do
919
+ dynamic :custom_string do
920
+ with(:test, 'test')
921
+ end
922
+ end
923
+ connection.should have_last_search_with(:fq => ['custom_string\\:test_s:test'])
924
+ end
925
+
926
+ it 'should combine all text fields' do
927
+ session.search Post, Namespaced::Comment do
928
+ keywords 'keywords'
929
+ end
930
+ connection.searches.last[:qf].split(' ').sort.should ==
931
+ %w(author_name_text backwards_title_text body_text title_text)
932
+ end
933
+
934
+ it 'should allow specification of a text field that only exists in one type' do
935
+ session.search Post, Namespaced::Comment do
936
+ keywords 'keywords', :fields => :author_name
937
+ end
938
+ connection.searches.last[:qf].should == 'author_name_text'
939
+ end
940
+
941
+ it 'should raise Sunspot::UnrecognizedFieldError if search scoped to field not common to all types' do
942
+ lambda do
943
+ session.search Post, Namespaced::Comment do
944
+ with :blog_id, 1
945
+ end
946
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
947
+ end
948
+
949
+ it 'should raise Sunspot::UnrecognizedFieldError if search scoped to field configured differently between types' do
950
+ lambda do
951
+ session.search Post, Namespaced::Comment do
952
+ with :average_rating, 2.2 # this is a float in Post but an integer in Comment
953
+ end
954
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
955
+ end
956
+
957
+ it 'should raise Sunspot::UnrecognizedFieldError if a text field that does not exist for any type is specified' do
958
+ lambda do
959
+ session.search Post, Namespaced::Comment do
960
+ keywords 'fulltext', :fields => :bogus
961
+ end
962
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
963
+ end
964
+
965
+ it 'should ignore condition if field is not common to all types' do
966
+ session.search Post, Namespaced::Comment, :conditions => { :blog_id => 1 }
967
+ connection.should_not have_last_search_with(:fq)
968
+ end
969
+
970
+ it 'should allow building search using block argument rather than instance_eval' do
971
+ @blog_id = 1
972
+ session.search Post do |query|
973
+ query.with(:blog_id, @blog_id)
974
+ end
975
+ connection.should have_last_search_with(:fq => ['blog_id_i:1'])
976
+ end
977
+
978
+ it 'should raise Sunspot::UnrecognizedFieldError for nonexistant fields in block scope' do
979
+ lambda do
980
+ session.search Post do
981
+ with :bogus, 'Field'
982
+ end
983
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
984
+ end
985
+
986
+ it 'should raise Sunspot::UnrecognizedFieldError for nonexistant fields in keywords' do
987
+ lambda do
988
+ session.search Post do
989
+ keywords 'text', :fields => :bogus
990
+ end
991
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
992
+ end
993
+
994
+ it 'should raise NoMethodError if bogus operator referenced' do
995
+ lambda do
996
+ session.search Post do
997
+ with(:category_ids).resembling :bogus_condition
998
+ end
999
+ end.should raise_error(NoMethodError)
1000
+ end
1001
+
1002
+ it 'should raise ArgumentError if no :page argument given to paginate' do
1003
+ lambda do
1004
+ session.search Post do
1005
+ paginate
1006
+ end
1007
+ end.should raise_error(ArgumentError)
1008
+ end
1009
+
1010
+ it 'should raise ArgumentError if bogus argument given to paginate' do
1011
+ lambda do
1012
+ session.search Post do
1013
+ paginate :page => 4, :ugly => :puppy
1014
+ end
1015
+ end.should raise_error(ArgumentError)
1016
+ end
1017
+
1018
+ it 'should raise ArgumentError if more than two arguments passed to scope method' do
1019
+ lambda do
1020
+ session.search Post do
1021
+ with(:category_ids, 4, 5)
1022
+ end
1023
+ end.should raise_error(ArgumentError)
1024
+ end
1025
+
1026
+ private
1027
+
1028
+ def config
1029
+ @config ||= Sunspot::Configuration.build
1030
+ end
1031
+
1032
+ def connection
1033
+ @connection ||= Mock::Connection.new
1034
+ end
1035
+
1036
+ def session
1037
+ @session ||= Sunspot::Session.new(config, connection)
1038
+ end
1039
+ end