UnderpantsGnome-sunspot 0.9.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. data/History.txt +39 -0
  2. data/LICENSE +18 -0
  3. data/README.rdoc +154 -0
  4. data/Rakefile +9 -0
  5. data/TODO +4 -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 +470 -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/date_facet.rb +36 -0
  16. data/lib/sunspot/date_facet_row.rb +17 -0
  17. data/lib/sunspot/dsl.rb +3 -0
  18. data/lib/sunspot/dsl/field_query.rb +72 -0
  19. data/lib/sunspot/dsl/fields.rb +86 -0
  20. data/lib/sunspot/dsl/query.rb +59 -0
  21. data/lib/sunspot/dsl/query_facet.rb +31 -0
  22. data/lib/sunspot/dsl/restriction.rb +25 -0
  23. data/lib/sunspot/dsl/scope.rb +193 -0
  24. data/lib/sunspot/dsl/search.rb +30 -0
  25. data/lib/sunspot/facet.rb +51 -0
  26. data/lib/sunspot/facet_row.rb +34 -0
  27. data/lib/sunspot/field.rb +157 -0
  28. data/lib/sunspot/field_factory.rb +126 -0
  29. data/lib/sunspot/indexer.rb +127 -0
  30. data/lib/sunspot/instantiated_facet.rb +38 -0
  31. data/lib/sunspot/instantiated_facet_row.rb +12 -0
  32. data/lib/sunspot/query.rb +190 -0
  33. data/lib/sunspot/query/base_query.rb +90 -0
  34. data/lib/sunspot/query/connective.rb +77 -0
  35. data/lib/sunspot/query/dynamic_query.rb +69 -0
  36. data/lib/sunspot/query/field_facet.rb +149 -0
  37. data/lib/sunspot/query/field_query.rb +57 -0
  38. data/lib/sunspot/query/pagination.rb +39 -0
  39. data/lib/sunspot/query/query_facet.rb +72 -0
  40. data/lib/sunspot/query/query_facet_row.rb +19 -0
  41. data/lib/sunspot/query/restriction.rb +225 -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/query_facet.rb +33 -0
  46. data/lib/sunspot/query_facet_row.rb +21 -0
  47. data/lib/sunspot/schema.rb +165 -0
  48. data/lib/sunspot/search.rb +222 -0
  49. data/lib/sunspot/search/hit.rb +62 -0
  50. data/lib/sunspot/session.rb +201 -0
  51. data/lib/sunspot/setup.rb +271 -0
  52. data/lib/sunspot/type.rb +200 -0
  53. data/lib/sunspot/util.rb +164 -0
  54. data/solr/etc/jetty.xml +212 -0
  55. data/solr/etc/webdefault.xml +379 -0
  56. data/solr/lib/jetty-6.1.3.jar +0 -0
  57. data/solr/lib/jetty-util-6.1.3.jar +0 -0
  58. data/solr/lib/jsp-2.1/ant-1.6.5.jar +0 -0
  59. data/solr/lib/jsp-2.1/core-3.1.1.jar +0 -0
  60. data/solr/lib/jsp-2.1/jsp-2.1.jar +0 -0
  61. data/solr/lib/jsp-2.1/jsp-api-2.1.jar +0 -0
  62. data/solr/lib/servlet-api-2.5-6.1.3.jar +0 -0
  63. data/solr/solr/conf/elevate.xml +36 -0
  64. data/solr/solr/conf/protwords.txt +21 -0
  65. data/solr/solr/conf/schema.xml +50 -0
  66. data/solr/solr/conf/solrconfig.xml +696 -0
  67. data/solr/solr/conf/stopwords.txt +57 -0
  68. data/solr/solr/conf/synonyms.txt +31 -0
  69. data/solr/start.jar +0 -0
  70. data/solr/webapps/solr.war +0 -0
  71. data/spec/api/adapters_spec.rb +33 -0
  72. data/spec/api/build_search_spec.rb +918 -0
  73. data/spec/api/indexer_spec.rb +311 -0
  74. data/spec/api/query_spec.rb +153 -0
  75. data/spec/api/search_retrieval_spec.rb +325 -0
  76. data/spec/api/session_spec.rb +157 -0
  77. data/spec/api/spec_helper.rb +1 -0
  78. data/spec/api/sunspot_spec.rb +18 -0
  79. data/spec/integration/dynamic_fields_spec.rb +55 -0
  80. data/spec/integration/faceting_spec.rb +169 -0
  81. data/spec/integration/keyword_search_spec.rb +83 -0
  82. data/spec/integration/scoped_search_spec.rb +188 -0
  83. data/spec/integration/spec_helper.rb +1 -0
  84. data/spec/integration/stored_fields_spec.rb +10 -0
  85. data/spec/integration/test_pagination.rb +32 -0
  86. data/spec/mocks/adapters.rb +32 -0
  87. data/spec/mocks/blog.rb +3 -0
  88. data/spec/mocks/comment.rb +19 -0
  89. data/spec/mocks/connection.rb +84 -0
  90. data/spec/mocks/mock_adapter.rb +30 -0
  91. data/spec/mocks/mock_record.rb +41 -0
  92. data/spec/mocks/photo.rb +8 -0
  93. data/spec/mocks/post.rb +70 -0
  94. data/spec/mocks/user.rb +8 -0
  95. data/spec/spec_helper.rb +47 -0
  96. data/tasks/gemspec.rake +25 -0
  97. data/tasks/rcov.rake +28 -0
  98. data/tasks/rdoc.rake +21 -0
  99. data/tasks/schema.rake +19 -0
  100. data/tasks/spec.rake +24 -0
  101. data/tasks/todo.rake +4 -0
  102. data/templates/schema.xml.haml +24 -0
  103. metadata +245 -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,918 @@
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 all text fields for searched class' do
31
+ session.search Post do
32
+ keywords 'keyword search'
33
+ end
34
+ connection.searches.last[:qf].split(' ').sort.should == %w(backwards_title_text body_text title_text)
35
+ end
36
+
37
+ it 'should search only specified text fields when specified' do
38
+ session.search Post do
39
+ keywords 'keyword search', :fields => [:title, :body]
40
+ end
41
+ connection.searches.last[:qf].split(' ').sort.should == %w(body_text title_text)
42
+ end
43
+
44
+ it 'should request score when keywords used' do
45
+ session.search Post, :keywords => 'keyword search'
46
+ connection.should have_last_search_with(:fl => '* score')
47
+ end
48
+
49
+ it 'should not request score when keywords not used' do
50
+ session.search Post
51
+ connection.should_not have_last_search_with(:fl)
52
+ end
53
+
54
+ it 'should scope by exact match with a string from DSL' do
55
+ session.search Post do
56
+ with :title, 'My Pet Post'
57
+ end
58
+ connection.should have_last_search_with(:fq => ['title_ss:My\ Pet\ Post'])
59
+ end
60
+
61
+ it 'should scope by exact match with a string from options' do
62
+ session.search Post, :conditions => { :title => 'My Pet Post' }
63
+ connection.should have_last_search_with(:fq => ['title_ss:My\ Pet\ Post'])
64
+ end
65
+
66
+ it 'should ignore nonexistant fields in hash scope' do
67
+ session.search Post, :conditions => { :bogus => 'Field' }
68
+ connection.should_not have_last_search_with(:fq)
69
+ end
70
+
71
+ it 'should scope by exact match with time' do
72
+ time = Time.parse('1983-07-08 05:00:00 -0400')
73
+ session.search Post do
74
+ with :published_at, time
75
+ end
76
+ connection.should have_last_search_with(
77
+ :fq => ['published_at_d:1983\-07\-08T09\:00\:00Z']
78
+ )
79
+ end
80
+
81
+ it 'should scope by exact match with date' do
82
+ date = Date.new(1983, 7, 8)
83
+ session.search Post do
84
+ with :expire_date, date
85
+ end
86
+ connection.should have_last_search_with(
87
+ :fq => ['expire_date_d:1983\-07\-08T00\:00\:00Z']
88
+ )
89
+ end
90
+
91
+ it 'should scope by exact match with boolean' do
92
+ session.search Post do
93
+ with :featured, false
94
+ end
95
+ connection.should have_last_search_with(:fq => ['featured_b:false'])
96
+ end
97
+
98
+ it 'should scope by less than match with float' do
99
+ session.search Post do
100
+ with(:average_rating).less_than 3.0
101
+ end
102
+ connection.should have_last_search_with(:fq => ['average_rating_f:[* TO 3\.0]'])
103
+ end
104
+
105
+ it 'should scope by greater than match with float' do
106
+ session.search Post do
107
+ with(:average_rating).greater_than 3.0
108
+ end
109
+ connection.should have_last_search_with(:fq => ['average_rating_f:[3\.0 TO *]'])
110
+ end
111
+
112
+ it 'should scope by short-form between match with integers' do
113
+ session.search Post do
114
+ with :blog_id, 2..4
115
+ end
116
+ connection.should have_last_search_with(:fq => ['blog_id_i:[2 TO 4]'])
117
+ end
118
+
119
+ it 'should scope by between match with float' do
120
+ session.search Post do
121
+ with(:average_rating).between 2.0..4.0
122
+ end
123
+ connection.should have_last_search_with(:fq => ['average_rating_f:[2\.0 TO 4\.0]'])
124
+ end
125
+
126
+ it 'should scope by any match with integer using DSL' do
127
+ session.search Post do
128
+ with(:category_ids).any_of [2, 7, 12]
129
+ end
130
+ connection.should have_last_search_with(:fq => ['category_ids_im:(2 OR 7 OR 12)'])
131
+ end
132
+
133
+ it 'should scope by any match with integer using options' do
134
+ session.search Post, :conditions => { :category_ids => [2, 7, 12] }
135
+ connection.should have_last_search_with(:fq => ['category_ids_im:(2 OR 7 OR 12)'])
136
+ end
137
+
138
+ it 'should scope by short-form any-of match with integers' do
139
+ session.search Post do
140
+ with :category_ids, [2, 7, 12]
141
+ end
142
+ connection.should have_last_search_with(:fq => ['category_ids_im:(2 OR 7 OR 12)'])
143
+ end
144
+
145
+ it 'should scope by all match with integer' do
146
+ session.search Post do
147
+ with(:category_ids).all_of [2, 7, 12]
148
+ end
149
+ connection.should have_last_search_with(:fq => ['category_ids_im:(2 AND 7 AND 12)'])
150
+ end
151
+
152
+ it 'should scope by not equal match with string' do
153
+ session.search Post do
154
+ without :title, 'Bad Post'
155
+ end
156
+ connection.should have_last_search_with(:fq => ['-title_ss:Bad\ Post'])
157
+ end
158
+
159
+ it 'should scope by not less than match with float' do
160
+ session.search Post do
161
+ without(:average_rating).less_than 3.0
162
+ end
163
+ connection.should have_last_search_with(:fq => ['-average_rating_f:[* TO 3\.0]'])
164
+ end
165
+
166
+ it 'should scope by not greater than match with float' do
167
+ session.search Post do
168
+ without(:average_rating).greater_than 3.0
169
+ end
170
+ connection.should have_last_search_with(:fq => ['-average_rating_f:[3\.0 TO *]'])
171
+ end
172
+
173
+ it 'should scope by not between match with shorthand' do
174
+ session.search Post do
175
+ without(:blog_id, 2..4)
176
+ end
177
+ connection.should have_last_search_with(:fq => ['-blog_id_i:[2 TO 4]'])
178
+ end
179
+
180
+ it 'should scope by not between match with float' do
181
+ session.search Post do
182
+ without(:average_rating).between 2.0..4.0
183
+ end
184
+ connection.should have_last_search_with(:fq => ['-average_rating_f:[2\.0 TO 4\.0]'])
185
+ end
186
+
187
+ it 'should scope by not any match with integer' do
188
+ session.search Post do
189
+ without(:category_ids).any_of [2, 7, 12]
190
+ end
191
+ connection.should have_last_search_with(:fq => ['-category_ids_im:(2 OR 7 OR 12)'])
192
+ end
193
+
194
+
195
+ it 'should scope by not all match with integer' do
196
+ session.search Post do
197
+ without(:category_ids).all_of [2, 7, 12]
198
+ end
199
+ connection.should have_last_search_with(:fq => ['-category_ids_im:(2 AND 7 AND 12)'])
200
+ end
201
+
202
+ it 'should scope by empty field' do
203
+ session.search Post do
204
+ with :average_rating, nil
205
+ end
206
+ connection.should have_last_search_with(:fq => ['-average_rating_f:[* TO *]'])
207
+ end
208
+
209
+ it 'should scope by non-empty field' do
210
+ session.search Post do
211
+ without :average_rating, nil
212
+ end
213
+ connection.should have_last_search_with(:fq => ['average_rating_f:[* TO *]'])
214
+ end
215
+
216
+ it 'should exclude by object identity' do
217
+ post = Post.new
218
+ session.search Post do
219
+ without post
220
+ end
221
+ connection.should have_last_search_with(:fq => ["-id:Post\\ #{post.id}"])
222
+ end
223
+
224
+ it 'should exclude multiple objects passed as varargs by object identity' do
225
+ post1, post2 = Post.new, Post.new
226
+ session.search Post do
227
+ without post1, post2
228
+ end
229
+ connection.should have_last_search_with(
230
+ :fq => ["-id:Post\\ #{post1.id}", "-id:Post\\ #{post2.id}"]
231
+ )
232
+ end
233
+
234
+ it 'should exclude multiple objects passed as array by object identity' do
235
+ posts = [Post.new, Post.new]
236
+ session.search Post do
237
+ without posts
238
+ end
239
+ connection.should have_last_search_with(
240
+ :fq => ["-id:Post\\ #{posts.first.id}", "-id:Post\\ #{posts.last.id}"]
241
+ )
242
+ end
243
+
244
+ it 'should create a disjunction between two restrictions' do
245
+ session.search Post do
246
+ any_of do
247
+ with :category_ids, 1
248
+ with :blog_id, 2
249
+ end
250
+ end
251
+ connection.should have_last_search_with(
252
+ :fq => '(category_ids_im:1 OR blog_id_i:2)'
253
+ )
254
+ end
255
+
256
+ it 'should create a conjunction inside of a disjunction' do
257
+ session.search Post do
258
+ any_of do
259
+ with :blog_id, 2
260
+ all_of do
261
+ with :category_ids, 1
262
+ with(:average_rating).greater_than(3.0)
263
+ end
264
+ end
265
+ end
266
+ connection.should have_last_search_with(
267
+ :fq => '(blog_id_i:2 OR (category_ids_im:1 AND average_rating_f:[3\.0 TO *]))'
268
+ )
269
+ end
270
+
271
+ it 'should do nothing special if #all_of called from the top level' do
272
+ session.search Post do
273
+ all_of do
274
+ with :blog_id, 2
275
+ with :category_ids, 1
276
+ end
277
+ end
278
+ connection.should have_last_search_with(
279
+ :fq => ['blog_id_i:2', 'category_ids_im:1']
280
+ )
281
+ end
282
+
283
+ it 'should create a disjunction with negated restrictions' do
284
+ session.search Post do
285
+ any_of do
286
+ with :category_ids, 1
287
+ without(:average_rating).greater_than(3.0)
288
+ end
289
+ end
290
+ connection.should have_last_search_with(
291
+ :fq => '(category_ids_im:1 OR -average_rating_f:[3\.0 TO *])'
292
+ )
293
+ end
294
+
295
+ it 'should create a disjunction with instance exclusion' do
296
+ post = Post.new
297
+ session.search Post do
298
+ any_of do
299
+ without(post)
300
+ with(:category_ids, 1)
301
+ end
302
+ end
303
+ connection.should have_last_search_with(
304
+ :fq => "(-id:Post\\ #{post.id} OR category_ids_im:1)"
305
+ )
306
+ end
307
+
308
+ it 'should restrict by dynamic string field with equality restriction' do
309
+ session.search Post do
310
+ dynamic :custom_string do
311
+ with :test, 'string'
312
+ end
313
+ end
314
+ connection.should have_last_search_with(:fq => ['custom_string\:test_s:string'])
315
+ end
316
+
317
+ it 'should restrict by dynamic integer field with less than restriction' do
318
+ session.search Post do
319
+ dynamic :custom_integer do
320
+ with(:test).less_than(1)
321
+ end
322
+ end
323
+ connection.should have_last_search_with(:fq => ['custom_integer\:test_i:[* TO 1]'])
324
+ end
325
+
326
+ it 'should restrict by dynamic float field with between restriction' do
327
+ session.search Post do
328
+ dynamic :custom_float do
329
+ with(:test).between(2.2..3.3)
330
+ end
331
+ end
332
+ connection.should have_last_search_with(:fq => ['custom_float\:test_fm:[2\.2 TO 3\.3]'])
333
+ end
334
+
335
+ it 'should restrict by dynamic time field with any of restriction' do
336
+ session.search Post do
337
+ dynamic :custom_time do
338
+ with(:test).any_of([Time.parse('2009-02-10 14:00:00 UTC'),
339
+ Time.parse('2009-02-13 18:00:00 UTC')])
340
+ end
341
+ end
342
+ connection.should have_last_search_with(:fq => ['custom_time\:test_d:(2009\-02\-10T14\:00\:00Z OR 2009\-02\-13T18\:00\:00Z)'])
343
+ end
344
+
345
+ it 'should restrict by dynamic boolean field with equality restriction' do
346
+ session.search Post do
347
+ dynamic :custom_boolean do
348
+ with :test, false
349
+ end
350
+ end
351
+ connection.should have_last_search_with(:fq => ['custom_boolean\:test_b:false'])
352
+ end
353
+
354
+ it 'should negate a dynamic field restriction' do
355
+ session.search Post do
356
+ dynamic :custom_string do
357
+ without :test, 'foo'
358
+ end
359
+ end
360
+ connection.should have_last_search_with(:fq => ['-custom_string\:test_s:foo'])
361
+ end
362
+
363
+ it 'should search by a dynamic field inside a disjunction' do
364
+ session.search Post do
365
+ any_of do
366
+ dynamic :custom_string do
367
+ with :test, 'foo'
368
+ end
369
+ with :title, 'bar'
370
+ end
371
+ end
372
+ connection.should have_last_search_with(
373
+ :fq => '(custom_string\:test_s:foo OR title_ss:bar)'
374
+ )
375
+ end
376
+
377
+ it 'should throw an UnrecognizedFieldError if an unknown dynamic field is searched by' do
378
+ lambda do
379
+ session.search Post do
380
+ dynamic(:bogus) { with :some, 'value' }
381
+ end
382
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
383
+ end
384
+
385
+ it 'should throw a NoMethodError if pagination is attempted in a dynamic query' do
386
+ lambda do
387
+ session.search Post do
388
+ dynamic :custom_string do
389
+ paginate 3, 10
390
+ end
391
+ end
392
+ end.should raise_error(NoMethodError)
393
+ end
394
+
395
+ it 'should paginate using default per_page when page not provided' do
396
+ session.search Post
397
+ connection.should have_last_search_with(:rows => 30)
398
+ end
399
+
400
+ it 'should paginate using default per_page when page provided in DSL' do
401
+ session.search Post do
402
+ paginate :page => 2
403
+ end
404
+ connection.should have_last_search_with(:rows => 30, :start => 30)
405
+ end
406
+
407
+ it 'should paginate using default per_page when page provided in options' do
408
+ session.search Post, :page => 2
409
+ connection.should have_last_search_with(:rows => 30, :start => 30)
410
+ end
411
+
412
+ it 'should paginate using provided per_page in DSL' do
413
+ session.search Post do
414
+ paginate :page => 4, :per_page => 15
415
+ end
416
+ connection.should have_last_search_with(:rows => 15, :start => 45)
417
+ end
418
+
419
+ it 'should paginate using provided per_page in options' do
420
+ session.search Post, :page => 4, :per_page => 15
421
+ connection.should have_last_search_with(:rows => 15, :start => 45)
422
+ end
423
+
424
+ it 'should order in DSL' do
425
+ session.search Post do
426
+ order_by :average_rating, :desc
427
+ end
428
+ connection.should have_last_search_with(:sort => 'average_rating_f desc')
429
+ end
430
+
431
+ it 'should order in keywords' do
432
+ session.search Post, :order => 'average_rating desc'
433
+ connection.should have_last_search_with(:sort => 'average_rating_f desc')
434
+ end
435
+
436
+ it 'should order by multiple fields in DSL' do
437
+ session.search Post do
438
+ order_by :average_rating, :desc
439
+ order_by :sort_title, :asc
440
+ end
441
+ connection.should have_last_search_with(:sort => 'average_rating_f desc, sort_title_s asc')
442
+ end
443
+
444
+ it 'should order by multiple fields in options' do
445
+ session.search Post, :order => ['average_rating desc', 'sort_title asc']
446
+ connection.should have_last_search_with(:sort => 'average_rating_f desc, sort_title_s asc')
447
+ end
448
+
449
+ it 'should order by a dynamic field' do
450
+ session.search Post do
451
+ dynamic :custom_integer do
452
+ order_by :test, :desc
453
+ end
454
+ end
455
+ connection.should have_last_search_with(:sort => 'custom_integer:test_i desc')
456
+ end
457
+
458
+ it 'should order by a dynamic field and static field, with given precedence' do
459
+ session.search Post do
460
+ dynamic :custom_integer do
461
+ order_by :test, :desc
462
+ end
463
+ order_by :sort_title, :asc
464
+ end
465
+ connection.should have_last_search_with(:sort => 'custom_integer:test_i desc, sort_title_s asc')
466
+ end
467
+
468
+ it 'should order by random' do
469
+ session.search Post do
470
+ order_by_random
471
+ end
472
+ connection.searches.last[:sort].should =~ /^random_\d+ asc$/
473
+ end
474
+
475
+ it 'should throw an ArgumentError if a bogus order direction is given' do
476
+ lambda do
477
+ session.search Post do
478
+ order_by :sort_title, :sideways
479
+ end
480
+ end.should raise_error(ArgumentError)
481
+ end
482
+
483
+ it 'should not allow ordering by multiple-value fields' do
484
+ lambda do
485
+ session.search Post do
486
+ order_by :category_ids
487
+ end
488
+ end.should raise_error(ArgumentError)
489
+ end
490
+
491
+ it 'should not turn faceting on if no facet requested' do
492
+ session.search(Post)
493
+ connection.should_not have_last_search_with('facet')
494
+ end
495
+
496
+ it 'should turn faceting on if facet is requested' do
497
+ session.search Post do
498
+ facet :category_ids
499
+ end
500
+ connection.should have_last_search_with(:facet => 'true')
501
+ end
502
+
503
+ it 'should request single field facet' do
504
+ session.search Post do
505
+ facet :category_ids
506
+ end
507
+ connection.should have_last_search_with(:"facet.field" => %w(category_ids_im))
508
+ end
509
+
510
+ it 'should request multiple field facets' do
511
+ session.search Post do
512
+ facet :category_ids, :blog_id
513
+ end
514
+ connection.should have_last_search_with(:"facet.field" => %w(category_ids_im blog_id_i))
515
+ end
516
+
517
+ it 'should set facet sort by count' do
518
+ session.search Post do
519
+ facet :category_ids, :sort => :count
520
+ end
521
+ connection.should have_last_search_with(:"f.category_ids_im.facet.sort" => 'true')
522
+ end
523
+
524
+ it 'should set facet sort by index' do
525
+ session.search Post do
526
+ facet :category_ids, :sort => :index
527
+ end
528
+ connection.should have_last_search_with(:"f.category_ids_im.facet.sort" => 'false')
529
+ end
530
+
531
+ it 'should throw ArgumentError if bogus facet sort provided' do
532
+ lambda do
533
+ session.search Post do
534
+ facet :category_ids, :sort => :sideways
535
+ end
536
+ end.should raise_error(ArgumentError)
537
+ end
538
+
539
+ it 'should set the facet limit' do
540
+ session.search Post do
541
+ facet :category_ids, :limit => 10
542
+ end
543
+ connection.should have_last_search_with(:"f.category_ids_im.facet.limit" => 10)
544
+ end
545
+
546
+ it 'should set the facet minimum count' do
547
+ session.search Post do
548
+ facet :category_ids, :minimum_count => 5
549
+ end
550
+ connection.should have_last_search_with(:"f.category_ids_im.facet.mincount" => 5)
551
+ end
552
+
553
+ it 'should set the facet minimum count to zero if zeros are allowed' do
554
+ session.search Post do
555
+ facet :category_ids, :zeros => true
556
+ end
557
+ connection.should have_last_search_with(:"f.category_ids_im.facet.mincount" => 0)
558
+ end
559
+
560
+ it 'should set the facet minimum count to one by default' do
561
+ session.search Post do
562
+ facet :category_ids
563
+ end
564
+ connection.should have_last_search_with(:"f.category_ids_im.facet.mincount" => 1)
565
+ end
566
+
567
+ describe 'with date faceting' do
568
+ before :each do
569
+ @time_range = (Time.parse('2009-06-01 00:00:00 -0400')..
570
+ Time.parse('2009-07-01 00:00:00 -0400'))
571
+ end
572
+
573
+ it 'should not send date facet parameters if time range is not specified' do
574
+ session.search Post do |query|
575
+ query.facet :published_at
576
+ end
577
+ connection.should_not have_last_search_with(:"facet.date")
578
+ end
579
+
580
+ it 'should set the facet to a date facet' do
581
+ session.search Post do |query|
582
+ query.facet :published_at, :time_range => @time_range
583
+ end
584
+ connection.should have_last_search_with(:"facet.date" => ['published_at_d'])
585
+ end
586
+
587
+ it 'should set the facet start and end' do
588
+ session.search Post do |query|
589
+ query.facet :published_at, :time_range => @time_range
590
+ end
591
+ connection.should have_last_search_with(
592
+ :"f.published_at_d.facet.date.start" => '2009-06-01T04:00:00Z',
593
+ :"f.published_at_d.facet.date.end" => '2009-07-01T04:00:00Z'
594
+ )
595
+ end
596
+
597
+ it 'should default the time interval to 1 day' do
598
+ session.search Post do |query|
599
+ query.facet :published_at, :time_range => @time_range
600
+ end
601
+ connection.should have_last_search_with(:"f.published_at_d.facet.date.gap" => "+86400SECONDS")
602
+ end
603
+
604
+ it 'should use custom time interval' do
605
+ session.search Post do |query|
606
+ query.facet :published_at, :time_range => @time_range, :time_interval => 3600
607
+ end
608
+ connection.should have_last_search_with(:"f.published_at_d.facet.date.gap" => "+3600SECONDS")
609
+ end
610
+
611
+ it 'should allow computation of one other time' do
612
+ session.search Post do |query|
613
+ query.facet :published_at, :time_range => @time_range, :time_other => :before
614
+ end
615
+ connection.should have_last_search_with(:"f.published_at_d.facet.date.other" => %w(before))
616
+ end
617
+
618
+ it 'should allow computation of two other times' do
619
+ session.search Post do |query|
620
+ query.facet :published_at, :time_range => @time_range, :time_other => [:before, :after]
621
+ end
622
+ connection.should have_last_search_with(:"f.published_at_d.facet.date.other" => %w(before after))
623
+ end
624
+
625
+ it 'should not allow computation of bogus other time' do
626
+ lambda do
627
+ session.search Post do |query|
628
+ query.facet :published_at, :time_range => @time_range, :time_other => :bogus
629
+ end
630
+ end.should raise_error(ArgumentError)
631
+ end
632
+
633
+ it 'should not allow date faceting on a non-date field' do
634
+ lambda do
635
+ session.search Post do |query|
636
+ query.facet :blog_id, :time_range => @time_range
637
+ end
638
+ end.should raise_error(ArgumentError)
639
+ end
640
+ end
641
+
642
+ describe 'with query faceting' do
643
+ it 'should turn faceting on' do
644
+ session.search Post do
645
+ facet :foo do
646
+ row :bar do
647
+ with(:average_rating).between(4.0..5.0)
648
+ end
649
+ end
650
+ end
651
+ connection.should have_last_search_with(:facet => 'true')
652
+ end
653
+
654
+ it 'should facet by query' do
655
+ session.search Post do
656
+ facet :foo do
657
+ row :bar do
658
+ with(:average_rating).between(4.0..5.0)
659
+ end
660
+ end
661
+ end
662
+ connection.should have_last_search_with(:"facet.query" => 'average_rating_f:[4\.0 TO 5\.0]')
663
+ end
664
+
665
+ it 'should request multiple query facets' do
666
+ session.search Post do
667
+ facet :foo do
668
+ row :bar do
669
+ with(:average_rating).between(3.0..4.0)
670
+ end
671
+ row :baz do
672
+ with(:average_rating).between(4.0..5.0)
673
+ end
674
+ end
675
+ end
676
+ connection.should have_last_search_with(
677
+ :"facet.query" => [
678
+ 'average_rating_f:[3\.0 TO 4\.0]',
679
+ 'average_rating_f:[4\.0 TO 5\.0]'
680
+ ]
681
+ )
682
+ end
683
+
684
+ it 'should request query facet with multiple conditions' do
685
+ session.search Post do
686
+ facet :foo do
687
+ row :bar do
688
+ with(:category_ids, 1)
689
+ with(:blog_id, 2)
690
+ end
691
+ end
692
+ end
693
+ connection.should have_last_search_with(
694
+ :"facet.query" => '(category_ids_im:1 AND blog_id_i:2)'
695
+ )
696
+ end
697
+
698
+ it 'should request query facet with disjunction' do
699
+ session.search Post do
700
+ facet :foo do
701
+ row :bar do
702
+ any_of do
703
+ with(:category_ids, 1)
704
+ with(:blog_id, 2)
705
+ end
706
+ end
707
+ end
708
+ end
709
+ connection.should have_last_search_with(
710
+ :"facet.query" => '(category_ids_im:1 OR blog_id_i:2)'
711
+ )
712
+ end
713
+
714
+ it 'should request query facet with internal dynamic field' do
715
+ session.search Post do
716
+ facet :test do
717
+ row 'foo' do
718
+ dynamic :custom_string do
719
+ with :test, 'foo'
720
+ end
721
+ end
722
+ end
723
+ end
724
+ connection.should have_last_search_with(
725
+ :"facet.query" => 'custom_string\:test_s:foo'
726
+ )
727
+ end
728
+
729
+ it 'should request query facet with external dynamic field' do
730
+ session.search Post do
731
+ dynamic :custom_string do
732
+ facet :test do
733
+ row 'foo' do
734
+ with :test, 'foo'
735
+ end
736
+ end
737
+ end
738
+ end
739
+ connection.should have_last_search_with(
740
+ :"facet.query" => 'custom_string\:test_s:foo'
741
+ )
742
+ end
743
+
744
+ it 'should not allow 0 arguments to facet method with block' do
745
+ lambda do
746
+ session.search Post do
747
+ facet do
748
+ end
749
+ end
750
+ end.should raise_error(ArgumentError)
751
+ end
752
+
753
+ it 'should not allow more than 1 argument to facet method with block' do
754
+ lambda do
755
+ session.search Post do
756
+ facet :foo, :bar do
757
+ end
758
+ end
759
+ end.should raise_error(ArgumentError)
760
+ end
761
+ end
762
+
763
+ it 'should allow faceting by dynamic string field' do
764
+ session.search Post do
765
+ dynamic :custom_string do
766
+ facet :test
767
+ end
768
+ end
769
+ connection.should have_last_search_with(:"facet.field" => %w(custom_string:test_s))
770
+ end
771
+
772
+ it 'should properly escape namespaced type names' do
773
+ session.search(Namespaced::Comment)
774
+ connection.should have_last_search_with(:q => 'type:Namespaced\:\:Comment')
775
+ end
776
+
777
+ it 'should build search for multiple types' do
778
+ session.search(Post, Namespaced::Comment)
779
+ connection.should have_last_search_with(:q => 'type:(Post OR Namespaced\:\:Comment)')
780
+ end
781
+
782
+ it 'should allow search on fields common to all types with DSL' do
783
+ time = Time.parse('1983-07-08 05:00:00 -0400')
784
+ session.search Post, Namespaced::Comment do
785
+ with :published_at, time
786
+ end
787
+ connection.should have_last_search_with(:fq => ['published_at_d:1983\-07\-08T09\:00\:00Z'])
788
+ end
789
+
790
+ it 'should allow search on fields common to all types with conditions' do
791
+ time = Time.parse('1983-07-08 05:00:00 -0400')
792
+ session.search Post, Namespaced::Comment, :conditions => { :published_at => time }
793
+ connection.should have_last_search_with(:fq => ['published_at_d:1983\-07\-08T09\:00\:00Z'])
794
+ end
795
+
796
+ it 'should allow search on dynamic fields common to all types' do
797
+ session.search Post, Namespaced::Comment do
798
+ dynamic :custom_string do
799
+ with(:test, 'test')
800
+ end
801
+ end
802
+ connection.should have_last_search_with(:fq => ['custom_string\\:test_s:test'])
803
+ end
804
+
805
+ it 'should combine all text fields' do
806
+ session.search Post, Namespaced::Comment do
807
+ keywords 'keywords'
808
+ end
809
+ connection.searches.last[:qf].split(' ').sort.should ==
810
+ %w(author_name_text backwards_title_text body_text title_text)
811
+ end
812
+
813
+ it 'should allow specification of a text field that only exists in one type' do
814
+ session.search Post, Namespaced::Comment do
815
+ keywords 'keywords', :fields => :author_name
816
+ end
817
+ connection.searches.last[:qf].should == 'author_name_text'
818
+ end
819
+
820
+ it 'should raise Sunspot::UnrecognizedFieldError if search scoped to field not common to all types' do
821
+ lambda do
822
+ session.search Post, Namespaced::Comment do
823
+ with :blog_id, 1
824
+ end
825
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
826
+ end
827
+
828
+ it 'should raise Sunspot::UnrecognizedFieldError if search scoped to field configured differently between types' do
829
+ lambda do
830
+ session.search Post, Namespaced::Comment do
831
+ with :average_rating, 2.2 # this is a float in Post but an integer in Comment
832
+ end
833
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
834
+ end
835
+
836
+ it 'should raise Sunspot::UnrecognizedFieldError if a text field that does not exist for any type is specified' do
837
+ lambda do
838
+ session.search Post, Namespaced::Comment do
839
+ keywords 'fulltext', :fields => :bogus
840
+ end
841
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
842
+ end
843
+
844
+ it 'should ignore condition if field is not common to all types' do
845
+ session.search Post, Namespaced::Comment, :conditions => { :blog_id => 1 }
846
+ connection.should_not have_last_search_with(:fq)
847
+ end
848
+
849
+ it 'should allow building search using block argument rather than instance_eval' do
850
+ @blog_id = 1
851
+ session.search Post do |query|
852
+ query.with(:blog_id, @blog_id)
853
+ end
854
+ connection.should have_last_search_with(:fq => ['blog_id_i:1'])
855
+ end
856
+
857
+ it 'should raise Sunspot::UnrecognizedFieldError for nonexistant fields in block scope' do
858
+ lambda do
859
+ session.search Post do
860
+ with :bogus, 'Field'
861
+ end
862
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
863
+ end
864
+
865
+ it 'should raise Sunspot::UnrecognizedFieldError for nonexistant fields in keywords' do
866
+ lambda do
867
+ session.search Post do
868
+ keywords 'text', :fields => :bogus
869
+ end
870
+ end.should raise_error(Sunspot::UnrecognizedFieldError)
871
+ end
872
+
873
+ it 'should raise NoMethodError if bogus operator referenced' do
874
+ lambda do
875
+ session.search Post do
876
+ with(:category_ids).resembling :bogus_condition
877
+ end
878
+ end.should raise_error(NoMethodError)
879
+ end
880
+
881
+ it 'should raise ArgumentError if no :page argument given to paginate' do
882
+ lambda do
883
+ session.search Post do
884
+ paginate
885
+ end
886
+ end.should raise_error(ArgumentError)
887
+ end
888
+
889
+ it 'should raise ArgumentError if bogus argument given to paginate' do
890
+ lambda do
891
+ session.search Post do
892
+ paginate :page => 4, :ugly => :puppy
893
+ end
894
+ end.should raise_error(ArgumentError)
895
+ end
896
+
897
+ it 'should raise ArgumentError if more than two arguments passed to scope method' do
898
+ lambda do
899
+ session.search Post do
900
+ with(:category_ids, 4, 5)
901
+ end
902
+ end.should raise_error(ArgumentError)
903
+ end
904
+
905
+ private
906
+
907
+ def config
908
+ @config ||= Sunspot::Configuration.build
909
+ end
910
+
911
+ def connection
912
+ @connection ||= Mock::Connection.new
913
+ end
914
+
915
+ def session
916
+ @session ||= Sunspot::Session.new(config, connection)
917
+ end
918
+ end