friendlyfashion-thinking-sphinx 2.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. data/HISTORY +244 -0
  2. data/LICENCE +20 -0
  3. data/README.textile +235 -0
  4. data/features/abstract_inheritance.feature +10 -0
  5. data/features/alternate_primary_key.feature +27 -0
  6. data/features/attribute_transformation.feature +22 -0
  7. data/features/attribute_updates.feature +77 -0
  8. data/features/deleting_instances.feature +67 -0
  9. data/features/direct_attributes.feature +11 -0
  10. data/features/excerpts.feature +21 -0
  11. data/features/extensible_delta_indexing.feature +9 -0
  12. data/features/facets.feature +88 -0
  13. data/features/facets_across_model.feature +29 -0
  14. data/features/field_sorting.feature +18 -0
  15. data/features/handling_edits.feature +94 -0
  16. data/features/retry_stale_indexes.feature +24 -0
  17. data/features/searching_across_models.feature +20 -0
  18. data/features/searching_by_index.feature +40 -0
  19. data/features/searching_by_model.feature +175 -0
  20. data/features/searching_with_find_arguments.feature +56 -0
  21. data/features/sphinx_detection.feature +25 -0
  22. data/features/sphinx_scopes.feature +68 -0
  23. data/features/step_definitions/alpha_steps.rb +16 -0
  24. data/features/step_definitions/beta_steps.rb +7 -0
  25. data/features/step_definitions/common_steps.rb +201 -0
  26. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  27. data/features/step_definitions/facet_steps.rb +96 -0
  28. data/features/step_definitions/find_arguments_steps.rb +36 -0
  29. data/features/step_definitions/gamma_steps.rb +15 -0
  30. data/features/step_definitions/scope_steps.rb +19 -0
  31. data/features/step_definitions/search_steps.rb +94 -0
  32. data/features/step_definitions/sphinx_steps.rb +35 -0
  33. data/features/sti_searching.feature +19 -0
  34. data/features/support/env.rb +27 -0
  35. data/features/support/lib/generic_delta_handler.rb +8 -0
  36. data/features/thinking_sphinx/database.example.yml +3 -0
  37. data/features/thinking_sphinx/db/.gitignore +1 -0
  38. data/features/thinking_sphinx/db/fixtures/alphas.rb +8 -0
  39. data/features/thinking_sphinx/db/fixtures/authors.rb +1 -0
  40. data/features/thinking_sphinx/db/fixtures/betas.rb +11 -0
  41. data/features/thinking_sphinx/db/fixtures/boxes.rb +9 -0
  42. data/features/thinking_sphinx/db/fixtures/categories.rb +1 -0
  43. data/features/thinking_sphinx/db/fixtures/cats.rb +3 -0
  44. data/features/thinking_sphinx/db/fixtures/comments.rb +24 -0
  45. data/features/thinking_sphinx/db/fixtures/developers.rb +31 -0
  46. data/features/thinking_sphinx/db/fixtures/dogs.rb +3 -0
  47. data/features/thinking_sphinx/db/fixtures/extensible_betas.rb +10 -0
  48. data/features/thinking_sphinx/db/fixtures/foxes.rb +3 -0
  49. data/features/thinking_sphinx/db/fixtures/gammas.rb +10 -0
  50. data/features/thinking_sphinx/db/fixtures/music.rb +4 -0
  51. data/features/thinking_sphinx/db/fixtures/people.rb +1001 -0
  52. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +1 -0
  53. data/features/thinking_sphinx/db/fixtures/posts.rb +10 -0
  54. data/features/thinking_sphinx/db/fixtures/robots.rb +8 -0
  55. data/features/thinking_sphinx/db/fixtures/tags.rb +27 -0
  56. data/features/thinking_sphinx/db/migrations/create_alphas.rb +8 -0
  57. data/features/thinking_sphinx/db/migrations/create_animals.rb +5 -0
  58. data/features/thinking_sphinx/db/migrations/create_authors.rb +3 -0
  59. data/features/thinking_sphinx/db/migrations/create_authors_posts.rb +6 -0
  60. data/features/thinking_sphinx/db/migrations/create_betas.rb +5 -0
  61. data/features/thinking_sphinx/db/migrations/create_boxes.rb +5 -0
  62. data/features/thinking_sphinx/db/migrations/create_categories.rb +3 -0
  63. data/features/thinking_sphinx/db/migrations/create_comments.rb +10 -0
  64. data/features/thinking_sphinx/db/migrations/create_developers.rb +7 -0
  65. data/features/thinking_sphinx/db/migrations/create_extensible_betas.rb +5 -0
  66. data/features/thinking_sphinx/db/migrations/create_gammas.rb +3 -0
  67. data/features/thinking_sphinx/db/migrations/create_genres.rb +3 -0
  68. data/features/thinking_sphinx/db/migrations/create_music.rb +6 -0
  69. data/features/thinking_sphinx/db/migrations/create_people.rb +13 -0
  70. data/features/thinking_sphinx/db/migrations/create_posts.rb +6 -0
  71. data/features/thinking_sphinx/db/migrations/create_robots.rb +4 -0
  72. data/features/thinking_sphinx/db/migrations/create_taggings.rb +5 -0
  73. data/features/thinking_sphinx/db/migrations/create_tags.rb +4 -0
  74. data/features/thinking_sphinx/models/alpha.rb +23 -0
  75. data/features/thinking_sphinx/models/andrew.rb +17 -0
  76. data/features/thinking_sphinx/models/animal.rb +5 -0
  77. data/features/thinking_sphinx/models/author.rb +3 -0
  78. data/features/thinking_sphinx/models/beta.rb +13 -0
  79. data/features/thinking_sphinx/models/box.rb +8 -0
  80. data/features/thinking_sphinx/models/cat.rb +3 -0
  81. data/features/thinking_sphinx/models/category.rb +4 -0
  82. data/features/thinking_sphinx/models/comment.rb +10 -0
  83. data/features/thinking_sphinx/models/developer.rb +21 -0
  84. data/features/thinking_sphinx/models/dog.rb +3 -0
  85. data/features/thinking_sphinx/models/extensible_beta.rb +9 -0
  86. data/features/thinking_sphinx/models/fox.rb +5 -0
  87. data/features/thinking_sphinx/models/gamma.rb +5 -0
  88. data/features/thinking_sphinx/models/genre.rb +3 -0
  89. data/features/thinking_sphinx/models/medium.rb +5 -0
  90. data/features/thinking_sphinx/models/music.rb +10 -0
  91. data/features/thinking_sphinx/models/person.rb +24 -0
  92. data/features/thinking_sphinx/models/post.rb +22 -0
  93. data/features/thinking_sphinx/models/robot.rb +12 -0
  94. data/features/thinking_sphinx/models/tag.rb +3 -0
  95. data/features/thinking_sphinx/models/tagging.rb +4 -0
  96. data/lib/cucumber/thinking_sphinx/external_world.rb +12 -0
  97. data/lib/cucumber/thinking_sphinx/internal_world.rb +137 -0
  98. data/lib/cucumber/thinking_sphinx/sql_logger.rb +28 -0
  99. data/lib/thinking-sphinx.rb +1 -0
  100. data/lib/thinking_sphinx/action_controller.rb +31 -0
  101. data/lib/thinking_sphinx/active_record/attribute_updates.rb +53 -0
  102. data/lib/thinking_sphinx/active_record/collection_proxy.rb +47 -0
  103. data/lib/thinking_sphinx/active_record/collection_proxy_with_scopes.rb +27 -0
  104. data/lib/thinking_sphinx/active_record/delta.rb +67 -0
  105. data/lib/thinking_sphinx/active_record/has_many_association.rb +44 -0
  106. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +21 -0
  107. data/lib/thinking_sphinx/active_record/log_subscriber.rb +61 -0
  108. data/lib/thinking_sphinx/active_record/scopes.rb +110 -0
  109. data/lib/thinking_sphinx/active_record.rb +386 -0
  110. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +87 -0
  111. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +62 -0
  112. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +188 -0
  113. data/lib/thinking_sphinx/association.rb +230 -0
  114. data/lib/thinking_sphinx/attribute.rb +405 -0
  115. data/lib/thinking_sphinx/auto_version.rb +40 -0
  116. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  117. data/lib/thinking_sphinx/class_facet.rb +20 -0
  118. data/lib/thinking_sphinx/configuration.rb +375 -0
  119. data/lib/thinking_sphinx/context.rb +76 -0
  120. data/lib/thinking_sphinx/core/string.rb +15 -0
  121. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  122. data/lib/thinking_sphinx/deltas.rb +28 -0
  123. data/lib/thinking_sphinx/deploy/capistrano.rb +99 -0
  124. data/lib/thinking_sphinx/excerpter.rb +23 -0
  125. data/lib/thinking_sphinx/facet.rb +135 -0
  126. data/lib/thinking_sphinx/facet_search.rb +170 -0
  127. data/lib/thinking_sphinx/field.rb +98 -0
  128. data/lib/thinking_sphinx/index/builder.rb +315 -0
  129. data/lib/thinking_sphinx/index/faux_column.rb +118 -0
  130. data/lib/thinking_sphinx/index.rb +159 -0
  131. data/lib/thinking_sphinx/join.rb +37 -0
  132. data/lib/thinking_sphinx/property.rb +187 -0
  133. data/lib/thinking_sphinx/railtie.rb +43 -0
  134. data/lib/thinking_sphinx/search.rb +1061 -0
  135. data/lib/thinking_sphinx/search_methods.rb +439 -0
  136. data/lib/thinking_sphinx/sinatra.rb +7 -0
  137. data/lib/thinking_sphinx/source/internal_properties.rb +51 -0
  138. data/lib/thinking_sphinx/source/sql.rb +174 -0
  139. data/lib/thinking_sphinx/source.rb +194 -0
  140. data/lib/thinking_sphinx/tasks.rb +142 -0
  141. data/lib/thinking_sphinx/test.rb +55 -0
  142. data/lib/thinking_sphinx/version.rb +3 -0
  143. data/lib/thinking_sphinx.rb +297 -0
  144. data/spec/fixtures/data.sql +32 -0
  145. data/spec/fixtures/database.yml.default +3 -0
  146. data/spec/fixtures/models.rb +164 -0
  147. data/spec/fixtures/structure.sql +146 -0
  148. data/spec/spec_helper.rb +61 -0
  149. data/spec/sphinx_helper.rb +60 -0
  150. data/spec/support/rails.rb +25 -0
  151. data/spec/thinking_sphinx/active_record/delta_spec.rb +122 -0
  152. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +173 -0
  153. data/spec/thinking_sphinx/active_record/scopes_spec.rb +176 -0
  154. data/spec/thinking_sphinx/active_record_spec.rb +573 -0
  155. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +145 -0
  156. data/spec/thinking_sphinx/association_spec.rb +250 -0
  157. data/spec/thinking_sphinx/attribute_spec.rb +552 -0
  158. data/spec/thinking_sphinx/auto_version_spec.rb +103 -0
  159. data/spec/thinking_sphinx/configuration_spec.rb +326 -0
  160. data/spec/thinking_sphinx/context_spec.rb +126 -0
  161. data/spec/thinking_sphinx/core/array_spec.rb +9 -0
  162. data/spec/thinking_sphinx/core/string_spec.rb +9 -0
  163. data/spec/thinking_sphinx/excerpter_spec.rb +49 -0
  164. data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
  165. data/spec/thinking_sphinx/facet_spec.rb +359 -0
  166. data/spec/thinking_sphinx/field_spec.rb +127 -0
  167. data/spec/thinking_sphinx/index/builder_spec.rb +532 -0
  168. data/spec/thinking_sphinx/index/faux_column_spec.rb +36 -0
  169. data/spec/thinking_sphinx/index_spec.rb +189 -0
  170. data/spec/thinking_sphinx/search_methods_spec.rb +156 -0
  171. data/spec/thinking_sphinx/search_spec.rb +1455 -0
  172. data/spec/thinking_sphinx/source_spec.rb +267 -0
  173. data/spec/thinking_sphinx/test_spec.rb +20 -0
  174. data/spec/thinking_sphinx_spec.rb +204 -0
  175. metadata +524 -0
@@ -0,0 +1,1455 @@
1
+ require 'spec_helper'
2
+ require 'will_paginate/collection'
3
+
4
+ describe ThinkingSphinx::Search do
5
+ before :each do
6
+ @config = ThinkingSphinx::Configuration.instance
7
+ @client = Riddle::Client.new
8
+
9
+ @config.stub!(:client => @client)
10
+ @client.stub!(:query => {:matches => [], :total_found => 41, :total => 41})
11
+ end
12
+
13
+ it "not request results from the client if not accessing items" do
14
+ @config.should_not_receive(:client)
15
+
16
+ ThinkingSphinx::Search.new.class
17
+ end
18
+
19
+ it "should request results if access is required" do
20
+ @config.should_receive(:client)
21
+
22
+ ThinkingSphinx::Search.new.first
23
+ end
24
+
25
+ describe '#respond_to?' do
26
+ it "should respond to Array methods" do
27
+ ThinkingSphinx::Search.new.respond_to?(:each).should be_true
28
+ end
29
+
30
+ it "should respond to Search methods" do
31
+ ThinkingSphinx::Search.new.respond_to?(:per_page).should be_true
32
+ end
33
+ end
34
+
35
+ describe '#==' do
36
+ it "populates the search results when checking equality" do
37
+ search = ThinkingSphinx::Search.new
38
+ search == []
39
+
40
+ search.should be_populated
41
+ end
42
+ end
43
+
44
+ describe '#populated?' do
45
+ before :each do
46
+ @search = ThinkingSphinx::Search.new
47
+ end
48
+
49
+ it "should be false if the client request has not been made" do
50
+ @search.populated?.should be_false
51
+ end
52
+
53
+ it "should be true once the client request has been made" do
54
+ @search.first
55
+ @search.should be_populated
56
+ end
57
+
58
+ it "should be populated if :populate is set to true" do
59
+ search = ThinkingSphinx::Search.new(:populate => true)
60
+ search.should be_populated
61
+ end
62
+ end
63
+
64
+ describe '#error?' do
65
+ before :each do
66
+ @search = ThinkingSphinx::Search.new
67
+ end
68
+
69
+ it "should be false if client requests have not resulted in an error" do
70
+ @search.should_receive(:error).and_return(nil)
71
+ @search.error?.should_not be_true
72
+ end
73
+
74
+ it "should be true when client requests result in an error" do
75
+ @search.should_receive(:error).and_return("error message")
76
+ @search.error?.should be_true
77
+ end
78
+ end
79
+
80
+ describe '#warning?' do
81
+ before :each do
82
+ @search = ThinkingSphinx::Search.new
83
+ end
84
+
85
+ it "should be false if client requests have not resulted in a warning" do
86
+ @search.should_receive(:warning).and_return(nil)
87
+ @search.warning?.should_not be_true
88
+ end
89
+
90
+ it "should be true when client requests result in an error" do
91
+ @search.should_receive(:warning).and_return("warning message")
92
+ @search.warning?.should be_true
93
+ end
94
+ end
95
+
96
+ describe '#results' do
97
+ it "should populate search results before returning" do
98
+ @search = ThinkingSphinx::Search.new
99
+ @search.populated?.should be_false
100
+
101
+ @search.results
102
+ @search.populated?.should be_true
103
+ end
104
+ end
105
+
106
+ describe '#method_missing' do
107
+ before :each do
108
+ Alpha.sphinx_scope(:by_name) { |name|
109
+ {:conditions => {:name => name}}
110
+ }
111
+ Alpha.sphinx_scope(:ids_only) { {:ids_only => true} }
112
+ end
113
+
114
+ after :each do
115
+ Alpha.remove_sphinx_scopes
116
+ end
117
+
118
+ it "should handle Array methods" do
119
+ ThinkingSphinx::Search.new.private_methods.should be_an(Array)
120
+ end
121
+
122
+ it "should raise a NoMethodError exception if unknown method" do
123
+ lambda {
124
+ ThinkingSphinx::Search.new.foo
125
+ }.should raise_error(NoMethodError)
126
+ end
127
+
128
+ it "should not request results from client if method does not exist" do
129
+ @client.should_not_receive(:query)
130
+
131
+ lambda {
132
+ ThinkingSphinx::Search.new.foo
133
+ }.should raise_error(NoMethodError)
134
+ end
135
+
136
+ it "should accept sphinx scopes" do
137
+ search = ThinkingSphinx::Search.new(:classes => [Alpha])
138
+
139
+ lambda {
140
+ search.by_name('Pat')
141
+ }.should_not raise_error(NoMethodError)
142
+ end
143
+
144
+ it "should return itself when using a sphinx scope" do
145
+ search = ThinkingSphinx::Search.new(:classes => [Alpha])
146
+ search.by_name('Pat').object_id.should == search.object_id
147
+ end
148
+
149
+ it "should keep the same search object when chaining multiple scopes" do
150
+ search = ThinkingSphinx::Search.new(:classes => [Alpha])
151
+ search.by_name('Pat').ids_only.object_id.should == search.object_id
152
+ end
153
+ end
154
+
155
+ describe '.search' do
156
+ it "return the output of ThinkingSphinx.search" do
157
+ @results = [] # to confirm same object
158
+ ThinkingSphinx.stub!(:search => @results)
159
+
160
+ ThinkingSphinx.search.object_id.should == @results.object_id
161
+ end
162
+ end
163
+
164
+ describe '.search_for_ids' do
165
+ it "return the output of ThinkingSphinx.search_for_ids" do
166
+ @results = [] # to confirm same object
167
+ ThinkingSphinx.stub!(:search_for_ids => @results)
168
+
169
+ ThinkingSphinx.search_for_ids.object_id.
170
+ should == @results.object_id
171
+ end
172
+ end
173
+
174
+ describe '.search_for_id' do
175
+ it "return the output of ThinkingSphinx.search_for_ids" do
176
+ @results = [] # to confirm same object
177
+ ThinkingSphinx.stub!(:search_for_id => @results)
178
+
179
+ ThinkingSphinx.search_for_id.object_id.
180
+ should == @results.object_id
181
+ end
182
+ end
183
+
184
+ describe '.count' do
185
+ it "return the output of ThinkingSphinx.search" do
186
+ @results = [] # to confirm same object
187
+ ThinkingSphinx.stub!(:count => @results)
188
+
189
+ ThinkingSphinx.count.object_id.should == @results.object_id
190
+ end
191
+ end
192
+
193
+ describe '.facets' do
194
+ it "return the output of ThinkingSphinx.facets" do
195
+ @results = [] # to confirm same object
196
+ ThinkingSphinx.stub!(:facets => @results)
197
+
198
+ ThinkingSphinx.facets.object_id.should == @results.object_id
199
+ end
200
+ end
201
+
202
+ describe '.matching_fields' do
203
+ it "should return objects with indexes matching 1's in the bitmask" do
204
+ fields = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta']
205
+ ThinkingSphinx::Search.matching_fields(fields, 85).
206
+ should == ['alpha', 'gamma', 'epsilon', 'eta']
207
+
208
+ ThinkingSphinx::Search.matching_fields(fields, 42).
209
+ should == ['beta', 'delta', 'zeta']
210
+ end
211
+ end
212
+
213
+ describe '#populate' do
214
+ before :each do
215
+ @alpha_a, @alpha_b = Alpha.new, Alpha.new
216
+ @beta_a, @beta_b = Beta.new, Beta.new
217
+
218
+ @alpha_a.stub! :id => 1, :read_attribute => 1
219
+ @alpha_b.stub! :id => 2, :read_attribute => 2
220
+ @beta_a.stub! :id => 1, :read_attribute => 1
221
+ @beta_b.stub! :id => 2, :read_attribute => 2
222
+
223
+ @client.stub! :query => {
224
+ :matches => minimal_result_hashes(@alpha_a, @beta_b, @alpha_b, @beta_a),
225
+ :fields => ["one", "two", "three", "four", "five"]
226
+ }
227
+ Alpha.stub! :find => [@alpha_a, @alpha_b], :unscoped => Alpha
228
+ Beta.stub! :find => [@beta_a, @beta_b], :unscoped => Beta
229
+ end
230
+
231
+ it "should issue only one select per model" do
232
+ Alpha.should_receive(:find).once.and_return([@alpha_a, @alpha_b])
233
+ Beta.should_receive(:find).once.and_return([@beta_a, @beta_b])
234
+
235
+ ThinkingSphinx::Search.new.first
236
+ end
237
+
238
+ it "should mix the results from different models" do
239
+ search = ThinkingSphinx::Search.new
240
+ search[0].should be_a(Alpha)
241
+ search[1].should be_a(Beta)
242
+ search[2].should be_a(Alpha)
243
+ search[3].should be_a(Beta)
244
+ end
245
+
246
+ it "should maintain the Xoopit ordering for results" do
247
+ search = ThinkingSphinx::Search.new
248
+ search[0].id.should == 1
249
+ search[1].id.should == 2
250
+ search[2].id.should == 2
251
+ search[3].id.should == 1
252
+ end
253
+
254
+ it "should use the requested classes to generate the index argument" do
255
+ @client.should_receive(:query) do |query, index, comment|
256
+ index.should == 'alpha_core,beta_core,beta_delta'
257
+ end
258
+
259
+ ThinkingSphinx::Search.new(:classes => [Alpha, Beta]).first
260
+ end
261
+
262
+ it "should restrict includes to the relevant classes" do
263
+ Alpha.should_receive(:find) do |type, options|
264
+ options[:include].should == [:betas]
265
+ [@alpha_a, @alpha_b]
266
+ end
267
+
268
+ Beta.should_receive(:find) do |type, options|
269
+ options[:include].should == [:gammas]
270
+ [@beta_a, @beta_b]
271
+ end
272
+
273
+ ThinkingSphinx::Search.new(:include => [:betas, :gammas]).first
274
+ end
275
+
276
+ it "should restrict single includes to the relevant classes" do
277
+ Alpha.should_receive(:find) do |type, options|
278
+ options[:include].should == :betas
279
+ [@alpha_a, @alpha_b]
280
+ end
281
+
282
+ Beta.should_receive(:find) do |type, options|
283
+ options[:include].should be_nil
284
+ [@beta_a, @beta_b]
285
+ end
286
+
287
+ ThinkingSphinx::Search.new(:include => :betas).first
288
+ end
289
+
290
+ it "should respect complex includes" do
291
+ Alpha.should_receive(:find) do |type, options|
292
+ options[:include].should == [:thetas, {:betas => :gammas}]
293
+ [@alpha_a, @alpha_b]
294
+ end
295
+
296
+ Beta.should_receive(:find) do |type, options|
297
+ options[:include].should be_nil
298
+ [@beta_a, @beta_b]
299
+ end
300
+
301
+ ThinkingSphinx::Search.new(:include => [:thetas, {:betas => :gammas}]).first
302
+ end
303
+
304
+ it "should respect hash includes" do
305
+ Alpha.should_receive(:find) do |type, options|
306
+ options[:include].should == {:betas => :gammas}
307
+ [@alpha_a, @alpha_b]
308
+ end
309
+
310
+ Beta.should_receive(:find) do |type, options|
311
+ options[:include].should be_nil
312
+ [@beta_a, @beta_b]
313
+ end
314
+
315
+ ThinkingSphinx::Search.new(:include => {:betas => :gammas}).first
316
+ end
317
+
318
+ it "should respect includes for single class searches" do
319
+ Alpha.should_receive(:find) do |type, options|
320
+ options[:include].should == {:betas => :gammas}
321
+ [@alpha_a, @alpha_b]
322
+ end
323
+
324
+ ThinkingSphinx::Search.new(
325
+ :include => {:betas => :gammas},
326
+ :classes => [Alpha]
327
+ ).first
328
+ end
329
+
330
+ describe 'query' do
331
+ it "should concatenate arguments with spaces" do
332
+ @client.should_receive(:query) do |query, index, comment|
333
+ query.should == 'two words'
334
+ end
335
+
336
+ ThinkingSphinx::Search.new('two', 'words').first
337
+ end
338
+
339
+ it "should append conditions to the query" do
340
+ @client.should_receive(:query) do |query, index, comment|
341
+ query.should == 'general @focused specific'
342
+ end
343
+
344
+ ThinkingSphinx::Search.new('general', :conditions => {
345
+ :focused => 'specific'
346
+ }).first
347
+ end
348
+
349
+ it "append multiple conditions together" do
350
+ @client.should_receive(:query) do |query, index, comment|
351
+ query.should match(/general.+@foo word/)
352
+ query.should match(/general.+@bar word/)
353
+ end
354
+
355
+ ThinkingSphinx::Search.new('general', :conditions => {
356
+ :foo => 'word', :bar => 'word'
357
+ }).first
358
+ end
359
+
360
+ it "should apply stars if requested, and handle full extended syntax" do
361
+ input = %{a b* c (d | e) 123 5&6 (f_f g) !h "i j" "k l"~10 "m n"/3 @o p -(q|r)}
362
+ expected = %{*a* b* *c* (*d* | *e*) *123* *5*&*6* (*f_f* *g*) !*h* "i j" "k l"~10 "m n"/3 @o *p* -(*q*|*r*)}
363
+
364
+ @client.should_receive(:query) do |query, index, comment|
365
+ query.should == expected
366
+ end
367
+
368
+ ThinkingSphinx::Search.new(input, :star => true).first
369
+ end
370
+
371
+ it "should default to /\w+/ as token for auto-starring" do
372
+ @client.should_receive(:query) do |query, index, comment|
373
+ query.should == '*foo*@*bar*.*com*'
374
+ end
375
+
376
+ ThinkingSphinx::Search.new('foo@bar.com', :star => true).first
377
+ end
378
+
379
+ it "should honour custom star tokens" do
380
+ @client.should_receive(:query) do |query, index, comment|
381
+ query.should == '*foo@bar.com* -*foo-bar*'
382
+ end
383
+
384
+ ThinkingSphinx::Search.new(
385
+ 'foo@bar.com -foo-bar', :star => /[\w@.-]+/u
386
+ ).first
387
+ end
388
+
389
+ it "should ignore multi-field limitations" do
390
+ @client.should_receive(:query) do |query, index, comment|
391
+ query.should == '@(foo,bar) *baz*'
392
+ end
393
+
394
+ ThinkingSphinx::Search.new('@(foo,bar) baz', :star => true).first
395
+ end
396
+
397
+ it "should ignore multi-field limitations with spaces" do
398
+ @client.should_receive(:query) do |query, index, comment|
399
+ query.should == '@(foo bar) *baz*'
400
+ end
401
+
402
+ ThinkingSphinx::Search.new('@(foo bar) baz', :star => true).first
403
+ end
404
+
405
+ it "should ignore multi-field limitations in the middle of queries" do
406
+ @client.should_receive(:query) do |query, index, comment|
407
+ query.should == '*baz* @foo *bar* @(foo,bar) *baz*'
408
+ end
409
+
410
+ ThinkingSphinx::Search.new(
411
+ 'baz @foo bar @(foo,bar) baz', :star => true
412
+ ).first
413
+ end
414
+
415
+ it "should try retry query up to the hard_retry_count option times if it catches an exception" do
416
+ @client.should_receive(:query).exactly(4).and_raise("Test Exception")
417
+
418
+ expect { ThinkingSphinx::Search.new(:hard_retry_count => 3).first }.
419
+ to raise_error("Test Exception")
420
+ end
421
+
422
+ it "should not retry query if hard_retry_count option is not set" do
423
+ @client.should_receive(:query).exactly(1).and_raise("Test Exception")
424
+
425
+ expect { ThinkingSphinx::Search.new.first }.
426
+ to raise_error("Test Exception")
427
+ end
428
+
429
+ it "should allow the hard_retry_count to be globally set as a configuration option" do
430
+ @config.hard_retry_count = 2
431
+
432
+ @client.should_receive(:query).exactly(3).and_raise("Test Exception")
433
+
434
+ expect { ThinkingSphinx::Search.new.first }.
435
+ to raise_error("Test Exception")
436
+ end
437
+
438
+ it "should give priority to the hard_retry_count search option over the globally configured option" do
439
+ @config.hard_retry_count = 4
440
+
441
+ @client.should_receive(:query).exactly(2).and_raise("Test Exception")
442
+
443
+ expect { ThinkingSphinx::Search.new(:hard_retry_count => 1).first }.
444
+ to raise_error("Test Exception")
445
+ end
446
+ end
447
+
448
+ describe 'comment' do
449
+ it "should add comment if explicitly provided" do
450
+ @client.should_receive(:query) do |query, index, comment|
451
+ comment.should == 'custom log'
452
+ end
453
+
454
+ ThinkingSphinx::Search.new(:comment => 'custom log').first
455
+ end
456
+
457
+ it "should default to a blank comment" do
458
+ @client.should_receive(:query) do |query, index, comment|
459
+ comment.should == ''
460
+ end
461
+
462
+ ThinkingSphinx::Search.new.first
463
+ end
464
+ end
465
+
466
+ describe 'match mode' do
467
+ it "should default to :all" do
468
+ ThinkingSphinx::Search.new.first
469
+
470
+ @client.match_mode.should == :all
471
+ end
472
+
473
+ it "should default to :extended if conditions are supplied" do
474
+ ThinkingSphinx::Search.new('general', :conditions => {
475
+ :foo => 'word', :bar => 'word'
476
+ }).first
477
+
478
+ @client.match_mode.should == :extended
479
+ end
480
+
481
+ it "should use explicit match modes" do
482
+ ThinkingSphinx::Search.new('general', :conditions => {
483
+ :foo => 'word', :bar => 'word'
484
+ }, :match_mode => :extended2).first
485
+
486
+ @client.match_mode.should == :extended2
487
+ end
488
+ end
489
+
490
+ describe 'sphinx_select' do
491
+ it "should default to *" do
492
+ ThinkingSphinx::Search.new.first
493
+
494
+ @client.select.should == "*"
495
+ end
496
+
497
+ it "should get set on the client if specified" do
498
+ ThinkingSphinx::Search.new('general',
499
+ :sphinx_select => "*, foo as bar"
500
+ ).first
501
+
502
+ @client.select.should == "*, foo as bar"
503
+ end
504
+
505
+ end
506
+
507
+ describe 'pagination' do
508
+ it "should set the limit using per_page" do
509
+ ThinkingSphinx::Search.new(:per_page => 30).first
510
+ @client.limit.should == 30
511
+ end
512
+
513
+ it "should set the offset if pagination is requested" do
514
+ ThinkingSphinx::Search.new(:page => 3).first
515
+ @client.offset.should == 40
516
+ end
517
+
518
+ it "should set the offset by the per_page value" do
519
+ ThinkingSphinx::Search.new(:page => 3, :per_page => 30).first
520
+ @client.offset.should == 60
521
+ end
522
+ end
523
+
524
+ describe 'filters' do
525
+ it "should filter out deleted values by default" do
526
+ ThinkingSphinx::Search.new.first
527
+
528
+ filter = @client.filters.last
529
+ filter.values.should == [0]
530
+ filter.attribute.should == 'sphinx_deleted'
531
+ filter.exclude?.should be_false
532
+ end
533
+
534
+ it "should add class filters for explicit classes" do
535
+ ThinkingSphinx::Search.new(:classes => [Alpha, Beta]).first
536
+
537
+ filter = @client.filters.last
538
+ filter.values.should == [Alpha.to_crc32, Beta.to_crc32]
539
+ filter.attribute.should == 'class_crc'
540
+ filter.exclude?.should be_false
541
+ end
542
+
543
+ it "should add class filters for subclasses of requested classes" do
544
+ ThinkingSphinx::Search.new(:classes => [Person]).first
545
+
546
+ filter = @client.filters.last
547
+ filter.values.should == [
548
+ Parent.to_crc32, Admin::Person.to_crc32,
549
+ Child.to_crc32, Teenager.to_crc32, Person.to_crc32
550
+ ]
551
+ filter.attribute.should == 'class_crc'
552
+ filter.exclude?.should be_false
553
+ end
554
+
555
+ it "should append inclusive filters of integers" do
556
+ ThinkingSphinx::Search.new(:with => {:int => 1}).first
557
+
558
+ filter = @client.filters.last
559
+ filter.values.should == [1]
560
+ filter.attribute.should == 'int'
561
+ filter.exclude?.should be_false
562
+ end
563
+
564
+ it "should append inclusive filters of floats" do
565
+ ThinkingSphinx::Search.new(:with => {:float => 1.5}).first
566
+
567
+ filter = @client.filters.last
568
+ filter.values.should == [1.5]
569
+ filter.attribute.should == 'float'
570
+ filter.exclude?.should be_false
571
+ end
572
+
573
+ it "should append inclusive filters of booleans" do
574
+ ThinkingSphinx::Search.new(:with => {:boolean => true}).first
575
+
576
+ filter = @client.filters.last
577
+ filter.values.should == [true]
578
+ filter.attribute.should == 'boolean'
579
+ filter.exclude?.should be_false
580
+ end
581
+
582
+ it "should append inclusive filters of arrays" do
583
+ ThinkingSphinx::Search.new(:with => {:ints => [1, 2, 3]}).first
584
+
585
+ filter = @client.filters.last
586
+ filter.values.should == [1, 2, 3]
587
+ filter.attribute.should == 'ints'
588
+ filter.exclude?.should be_false
589
+ end
590
+
591
+ it "should treat nils in arrays as 0" do
592
+ ThinkingSphinx::Search.new(:with => {:ints => [nil, 1, 2, 3]}).first
593
+
594
+ filter = @client.filters.last
595
+ filter.values.should == [0, 1, 2, 3]
596
+ end
597
+
598
+ it "should append inclusive filters of time ranges" do
599
+ first, last = 1.week.ago, Time.now
600
+ ThinkingSphinx::Search.new(:with => {
601
+ :time => first..last
602
+ }).first
603
+
604
+ filter = @client.filters.last
605
+ filter.values.should == (first.to_i..last.to_i)
606
+ filter.attribute.should == 'time'
607
+ filter.exclude?.should be_false
608
+ end
609
+
610
+ it "should append exclusive filters of integers" do
611
+ ThinkingSphinx::Search.new(:without => {:int => 1}).first
612
+
613
+ filter = @client.filters.last
614
+ filter.values.should == [1]
615
+ filter.attribute.should == 'int'
616
+ filter.exclude?.should be_true
617
+ end
618
+
619
+ it "should append exclusive filters of floats" do
620
+ ThinkingSphinx::Search.new(:without => {:float => 1.5}).first
621
+
622
+ filter = @client.filters.last
623
+ filter.values.should == [1.5]
624
+ filter.attribute.should == 'float'
625
+ filter.exclude?.should be_true
626
+ end
627
+
628
+ it "should append exclusive filters of booleans" do
629
+ ThinkingSphinx::Search.new(:without => {:boolean => true}).first
630
+
631
+ filter = @client.filters.last
632
+ filter.values.should == [true]
633
+ filter.attribute.should == 'boolean'
634
+ filter.exclude?.should be_true
635
+ end
636
+
637
+ it "should append exclusive filters of arrays" do
638
+ ThinkingSphinx::Search.new(:without => {:ints => [1, 2, 3]}).first
639
+
640
+ filter = @client.filters.last
641
+ filter.values.should == [1, 2, 3]
642
+ filter.attribute.should == 'ints'
643
+ filter.exclude?.should be_true
644
+ end
645
+
646
+ it "should append exclusive filters of time ranges" do
647
+ first, last = 1.week.ago, Time.now
648
+ ThinkingSphinx::Search.new(:without => {
649
+ :time => first..last
650
+ }).first
651
+
652
+ filter = @client.filters.last
653
+ filter.values.should == (first.to_i..last.to_i)
654
+ filter.attribute.should == 'time'
655
+ filter.exclude?.should be_true
656
+ end
657
+
658
+ it "should add separate filters for each item in a with_all value" do
659
+ ThinkingSphinx::Search.new(:with_all => {:ints => [1, 2, 3]}).first
660
+
661
+ filters = @client.filters[-3, 3]
662
+ filters.each do |filter|
663
+ filter.attribute.should == 'ints'
664
+ filter.exclude?.should be_false
665
+ end
666
+
667
+ filters[0].values.should == [1]
668
+ filters[1].values.should == [2]
669
+ filters[2].values.should == [3]
670
+ end
671
+
672
+ it "should filter out specific ids using :without_ids" do
673
+ ThinkingSphinx::Search.new(:without_ids => [4, 5, 6]).first
674
+
675
+ filter = @client.filters.last
676
+ filter.values.should == [4, 5, 6]
677
+ filter.attribute.should == 'sphinx_internal_id'
678
+ filter.exclude?.should be_true
679
+ end
680
+
681
+ it "should not filter out any ids if :without_ids is an empty array" do
682
+ ThinkingSphinx::Search.new(:without_ids => []).first
683
+
684
+ filter = @client.filters.last
685
+ filter.attribute.should_not == 'sphinx_internal_id'
686
+ end
687
+ end
688
+
689
+ describe 'sort mode' do
690
+ it "should use :relevance as a default" do
691
+ ThinkingSphinx::Search.new.first
692
+ @client.sort_mode.should == :relevance
693
+ end
694
+
695
+ it "should use :attr_asc if a symbol is supplied to :order" do
696
+ ThinkingSphinx::Search.new(:order => :created_at).first
697
+ @client.sort_mode.should == :attr_asc
698
+ end
699
+
700
+ it "should use :attr_desc if :desc is the mode" do
701
+ ThinkingSphinx::Search.new(
702
+ :order => :created_at, :sort_mode => :desc
703
+ ).first
704
+ @client.sort_mode.should == :attr_desc
705
+ end
706
+
707
+ it "should use :extended if a string is supplied to :order" do
708
+ ThinkingSphinx::Search.new(:order => "created_at ASC").first
709
+ @client.sort_mode.should == :extended
710
+ end
711
+
712
+ it "should use :expr if explicitly requested" do
713
+ ThinkingSphinx::Search.new(
714
+ :order => "created_at ASC", :sort_mode => :expr
715
+ ).first
716
+ @client.sort_mode.should == :expr
717
+ end
718
+
719
+ it "should use :attr_desc if explicitly requested" do
720
+ ThinkingSphinx::Search.new(
721
+ :order => "created_at", :sort_mode => :desc
722
+ ).first
723
+ @client.sort_mode.should == :attr_desc
724
+ end
725
+ end
726
+
727
+ describe 'sort by' do
728
+ it "should presume order symbols are attributes" do
729
+ ThinkingSphinx::Search.new(:order => :created_at).first
730
+ @client.sort_by.should == 'created_at'
731
+ end
732
+
733
+ it "replace field names with their sortable attributes" do
734
+ ThinkingSphinx::Search.new(:order => :name, :classes => [Alpha]).first
735
+ @client.sort_by.should == 'name_sort'
736
+ end
737
+
738
+ it "should replace field names in strings" do
739
+ ThinkingSphinx::Search.new(
740
+ :order => "created_at ASC, name DESC", :classes => [Alpha]
741
+ ).first
742
+ @client.sort_by.should == 'created_at ASC, name_sort DESC'
743
+ end
744
+ end
745
+
746
+ describe 'max matches' do
747
+ it "should use the global setting by default" do
748
+ ThinkingSphinx::Search.new.first
749
+ @client.max_matches.should == 1000
750
+ end
751
+
752
+ it "should use explicit setting" do
753
+ ThinkingSphinx::Search.new(:max_matches => 2000).first
754
+ @client.max_matches.should == 2000
755
+ end
756
+ end
757
+
758
+ describe 'field weights' do
759
+ it "should set field weights as provided" do
760
+ ThinkingSphinx::Search.new(
761
+ :field_weights => {'foo' => 10, 'bar' => 5}
762
+ ).first
763
+
764
+ @client.field_weights.should == {
765
+ 'foo' => 10, 'bar' => 5
766
+ }
767
+ end
768
+
769
+ it "should use field weights set in the index" do
770
+ ThinkingSphinx::Search.new(:classes => [Alpha]).first
771
+
772
+ @client.field_weights.should == {'name' => 10}
773
+ end
774
+ end
775
+
776
+ describe 'index weights' do
777
+ it "should send index weights through to the client" do
778
+ ThinkingSphinx::Search.new(:index_weights => {'foo' => 100}).first
779
+ @client.index_weights.should == {'foo' => 100}
780
+ end
781
+
782
+ it "should convert classes to their core and delta index names" do
783
+ ThinkingSphinx::Search.new(:index_weights => {Alpha => 100}).first
784
+ @client.index_weights.should == {
785
+ 'alpha_core' => 100,
786
+ 'alpha_delta' => 100
787
+ }
788
+ end
789
+ end
790
+
791
+ describe 'grouping' do
792
+ it "should convert group into group_by and group_function" do
793
+ ThinkingSphinx::Search.new(:group => :edition).first
794
+
795
+ @client.group_function.should == :attr
796
+ @client.group_by.should == "edition"
797
+ end
798
+
799
+ it "should pass on explicit grouping arguments" do
800
+ ThinkingSphinx::Search.new(
801
+ :group_by => 'created_at',
802
+ :group_function => :attr,
803
+ :group_clause => 'clause',
804
+ :group_distinct => 'distinct'
805
+ ).first
806
+
807
+ @client.group_by.should == 'created_at'
808
+ @client.group_function.should == :attr
809
+ @client.group_clause.should == 'clause'
810
+ @client.group_distinct.should == 'distinct'
811
+ end
812
+ end
813
+
814
+ describe 'anchor' do
815
+ it "should detect lat and lng attributes on the given model" do
816
+ ThinkingSphinx::Search.new(
817
+ :geo => [1.0, -1.0],
818
+ :classes => [Alpha]
819
+ ).first
820
+
821
+ @client.anchor[:latitude_attribute].should == 'lat'
822
+ @client.anchor[:longitude_attribute].should == 'lng'
823
+ end
824
+
825
+ it "should detect lat and lon attributes on the given model" do
826
+ ThinkingSphinx::Search.new(
827
+ :geo => [1.0, -1.0],
828
+ :classes => [Beta]
829
+ ).first
830
+
831
+ @client.anchor[:latitude_attribute].should == 'lat'
832
+ @client.anchor[:longitude_attribute].should == 'lon'
833
+ end
834
+
835
+ it "should detect latitude and longitude attributes on the given model" do
836
+ ThinkingSphinx::Search.new(
837
+ :geo => [1.0, -1.0],
838
+ :classes => [Person]
839
+ ).first
840
+
841
+ @client.anchor[:latitude_attribute].should == 'latitude'
842
+ @client.anchor[:longitude_attribute].should == 'longitude'
843
+ end
844
+
845
+ it "should accept manually defined latitude and longitude attributes" do
846
+ ThinkingSphinx::Search.new(
847
+ :geo => [1.0, -1.0],
848
+ :classes => [Alpha],
849
+ :latitude_attr => :updown,
850
+ :longitude_attr => :leftright
851
+ ).first
852
+
853
+ @client.anchor[:latitude_attribute].should == 'updown'
854
+ @client.anchor[:longitude_attribute].should == 'leftright'
855
+ end
856
+
857
+ it "should accept manually defined latitude and longitude attributes in the given model" do
858
+ ThinkingSphinx::Search.new(
859
+ :geo => [1.0, -1.0],
860
+ :classes => [Friendship]
861
+ ).first
862
+
863
+ @client.anchor[:latitude_attribute].should == 'person_id'
864
+ @client.anchor[:longitude_attribute].should == 'person_id'
865
+ end
866
+
867
+ it "should accept geo array for geo-position values" do
868
+ ThinkingSphinx::Search.new(
869
+ :geo => [1.0, -1.0],
870
+ :classes => [Alpha]
871
+ ).first
872
+
873
+ @client.anchor[:latitude].should == 1.0
874
+ @client.anchor[:longitude].should == -1.0
875
+ end
876
+
877
+ it "should accept lat and lng options for geo-position values" do
878
+ ThinkingSphinx::Search.new(
879
+ :lat => 1.0,
880
+ :lng => -1.0,
881
+ :classes => [Alpha]
882
+ ).first
883
+
884
+ @client.anchor[:latitude].should == 1.0
885
+ @client.anchor[:longitude].should == -1.0
886
+ end
887
+ end
888
+
889
+ describe 'sql ordering' do
890
+ before :each do
891
+ @client.stub! :query => {
892
+ :matches => minimal_result_hashes(@alpha_b, @alpha_a)
893
+ }
894
+ Alpha.stub! :find => [@alpha_a, @alpha_b]
895
+ end
896
+
897
+ it "shouldn't re-sort SQL results based on Sphinx information" do
898
+ search = ThinkingSphinx::Search.new(
899
+ :classes => [Alpha],
900
+ :sql_order => 'id'
901
+ )
902
+ search.first.should == @alpha_a
903
+ search.last.should == @alpha_b
904
+ end
905
+
906
+ it "should use the option for the ActiveRecord::Base#find calls" do
907
+ Alpha.should_receive(:find) do |mode, options|
908
+ options[:order].should == 'id'
909
+ end
910
+
911
+ ThinkingSphinx::Search.new(
912
+ :classes => [Alpha],
913
+ :sql_order => 'id'
914
+ ).first
915
+ end
916
+ end
917
+
918
+ describe ':only option' do
919
+ it "returns the requested attribute as an array" do
920
+ ThinkingSphinx::Search.new(:only => :class_crc).first.
921
+ should == Alpha.to_crc32
922
+ end
923
+
924
+ it "returns multiple attributes as hashes with values" do
925
+ ThinkingSphinx::Search.new(
926
+ :only => [:class_crc, :sphinx_internal_id]
927
+ ).first.should == {
928
+ :class_crc => Alpha.to_crc32,
929
+ :sphinx_internal_id => @alpha_a.id
930
+ }
931
+ end
932
+
933
+ it "handles strings for a single attribute name" do
934
+ ThinkingSphinx::Search.new(:only => 'class_crc').first.
935
+ should == Alpha.to_crc32
936
+ end
937
+
938
+ it "handles strings for multiple attribute names" do
939
+ ThinkingSphinx::Search.new(
940
+ :only => ['class_crc', 'sphinx_internal_id']
941
+ ).first.should == {
942
+ :class_crc => Alpha.to_crc32,
943
+ :sphinx_internal_id => @alpha_a.id
944
+ }
945
+ end
946
+ end
947
+
948
+ describe ':attributes_only option' do
949
+ it "returns the attributes as hashes with values" do
950
+ ThinkingSphinx::Search.new(
951
+ :attributes_only => true
952
+ ).first.should == {
953
+ :sphinx_internal_class => "Alpha",
954
+ :class_crc => Alpha.to_crc32,
955
+ :sphinx_internal_id => 1
956
+ }
957
+ end
958
+ end
959
+
960
+ context 'result objects' do
961
+ describe '#excerpts' do
962
+ before :each do
963
+ @search = ThinkingSphinx::Search.new
964
+ end
965
+
966
+ it "should add excerpts method if objects don't already have one" do
967
+ @search.first.should respond_to(:excerpts)
968
+ end
969
+
970
+ it "should return an instance of ThinkingSphinx::Excerpter" do
971
+ @search.first.excerpts.should be_a(ThinkingSphinx::Excerpter)
972
+ end
973
+
974
+ it "should not add excerpts method if objects already have one" do
975
+ @search.last.excerpts.should_not be_a(ThinkingSphinx::Excerpter)
976
+ end
977
+
978
+ # Fails in Ruby 1.9 (or maybe it's an RSpec update). Not sure why.
979
+ it "should set up the excerpter with the instances and search" do
980
+ [@alpha_a, @beta_b, @alpha_b, @beta_a].each do |object|
981
+ ThinkingSphinx::Excerpter.should_receive(:new).with(@search, object)
982
+ end
983
+
984
+ @search.first
985
+ end
986
+ end
987
+
988
+ describe '#sphinx_attributes' do
989
+ before :each do
990
+ @search = ThinkingSphinx::Search.new
991
+ end
992
+
993
+ it "should add sphinx_attributes method if objects don't already have one" do
994
+ @search.last.should respond_to(:sphinx_attributes)
995
+ end
996
+
997
+ it "should return a hash" do
998
+ @search.last.sphinx_attributes.should be_a(Hash)
999
+ end
1000
+
1001
+ it "should not add sphinx_attributes if objects have a method of that name already" do
1002
+ @search.first.sphinx_attributes.should_not be_a(Hash)
1003
+ end
1004
+
1005
+ it "should pair sphinx_attributes with the correct hash" do
1006
+ hash = @search.last.sphinx_attributes
1007
+ hash['sphinx_internal_id'].should == @search.last.id
1008
+ hash['class_crc'].should == @search.last.class.to_crc32
1009
+ end
1010
+ end
1011
+
1012
+ describe '#matching_fields' do
1013
+ it "should add matching_fields method if using fieldmask ranking mode" do
1014
+ search = ThinkingSphinx::Search.new :rank_mode => :fieldmask
1015
+ search.first.should respond_to(:matching_fields)
1016
+ end
1017
+
1018
+ it "should not add matching_fields method if object already have one" do
1019
+ search = ThinkingSphinx::Search.new :rank_mode => :fieldmask
1020
+ search.last.matching_fields.should_not be_an(Array)
1021
+ end
1022
+
1023
+ it "should return an array" do
1024
+ search = ThinkingSphinx::Search.new :rank_mode => :fieldmask
1025
+ search.first.matching_fields.should be_an(Array)
1026
+ end
1027
+
1028
+ it "should return the fields that the bitmask match" do
1029
+ search = ThinkingSphinx::Search.new :rank_mode => :fieldmask
1030
+ search.first.matching_fields.should == ['one', 'three', 'five']
1031
+ end
1032
+ end
1033
+ end
1034
+
1035
+ context 'Sphinx errors' do
1036
+ describe '#error?' do
1037
+ before :each do
1038
+ @client.stub! :query => {
1039
+ :error => @warning = "Not good"
1040
+ }
1041
+ # @search.should_receive(:error).and_return(nil)
1042
+ end
1043
+ it "should raise an error" do
1044
+ lambda{
1045
+ ThinkingSphinx::Search.new.first
1046
+ }.should raise_error(ThinkingSphinx::SphinxError)
1047
+ end
1048
+ it "should not raise an error when ignore_errors is true" do
1049
+ lambda{
1050
+ ThinkingSphinx::Search.new(:ignore_errors => true).first
1051
+ }.should_not raise_error(ThinkingSphinx::SphinxError)
1052
+ end
1053
+ end
1054
+ end
1055
+ end
1056
+
1057
+ describe '#current_page' do
1058
+ it "should return 1 by default" do
1059
+ ThinkingSphinx::Search.new.current_page.should == 1
1060
+ end
1061
+
1062
+ it "should handle string page values" do
1063
+ ThinkingSphinx::Search.new(:page => '2').current_page.should == 2
1064
+ end
1065
+
1066
+ it "should handle empty string page values" do
1067
+ ThinkingSphinx::Search.new(:page => '').current_page.should == 1
1068
+ end
1069
+
1070
+ it "should return the requested page" do
1071
+ ThinkingSphinx::Search.new(:page => 10).current_page.should == 10
1072
+ end
1073
+ end
1074
+
1075
+ describe '#per_page' do
1076
+ it "should return 20 by default" do
1077
+ ThinkingSphinx::Search.new.per_page.should == 20
1078
+ end
1079
+
1080
+ it "should allow for custom values" do
1081
+ ThinkingSphinx::Search.new(:per_page => 30).per_page.should == 30
1082
+ end
1083
+
1084
+ it "should prioritise :limit over :per_page if given" do
1085
+ ThinkingSphinx::Search.new(
1086
+ :per_page => 30, :limit => 40
1087
+ ).per_page.should == 40
1088
+ end
1089
+
1090
+ it "should allow for string arguments" do
1091
+ ThinkingSphinx::Search.new(:per_page => '10').per_page.should == 10
1092
+ end
1093
+ end
1094
+
1095
+ describe '#total_pages' do
1096
+ it "should calculate the total pages depending on per_page and total_entries" do
1097
+ ThinkingSphinx::Search.new.total_pages.should == 3
1098
+ end
1099
+
1100
+ it "should allow for custom per_page values" do
1101
+ ThinkingSphinx::Search.new(:per_page => 30).total_pages.should == 2
1102
+ end
1103
+
1104
+ it "should not overstep the max_matches implied limit" do
1105
+ @client.stub!(:query => {
1106
+ :matches => [], :total_found => 41, :total => 40
1107
+ })
1108
+
1109
+ ThinkingSphinx::Search.new.total_pages.should == 2
1110
+ end
1111
+
1112
+ it "should return 0 if there is no index and therefore no results" do
1113
+ @client.stub!(:query => {
1114
+ :matches => [], :total_found => nil, :total => nil
1115
+ })
1116
+
1117
+ ThinkingSphinx::Search.new.total_pages.should == 0
1118
+ end
1119
+ end
1120
+
1121
+ describe '#next_page' do
1122
+ it "should return one more than the current page" do
1123
+ ThinkingSphinx::Search.new.next_page.should == 2
1124
+ end
1125
+
1126
+ it "should return nil if on the last page" do
1127
+ ThinkingSphinx::Search.new(:page => 3).next_page.should be_nil
1128
+ end
1129
+ end
1130
+
1131
+ describe '#previous_page' do
1132
+ it "should return one less than the current page" do
1133
+ ThinkingSphinx::Search.new(:page => 2).previous_page.should == 1
1134
+ end
1135
+
1136
+ it "should return nil if on the first page" do
1137
+ ThinkingSphinx::Search.new.previous_page.should be_nil
1138
+ end
1139
+ end
1140
+
1141
+ describe '#total_entries' do
1142
+ it "should return the total number of results, not just the amount on the page" do
1143
+ ThinkingSphinx::Search.new.total_entries.should == 41
1144
+ end
1145
+
1146
+ it "should return 0 if there is no index and therefore no results" do
1147
+ @client.stub!(:query => {
1148
+ :matches => [], :total_found => nil
1149
+ })
1150
+
1151
+ ThinkingSphinx::Search.new.total_entries.should == 0
1152
+ end
1153
+ end
1154
+
1155
+ describe '#offset' do
1156
+ it "should default to 0" do
1157
+ ThinkingSphinx::Search.new.offset.should == 0
1158
+ end
1159
+
1160
+ it "should increase by the per_page value for each page in" do
1161
+ ThinkingSphinx::Search.new(:per_page => 25, :page => 2).offset.should == 25
1162
+ end
1163
+
1164
+ it "should prioritise explicit :offset over calculated if given" do
1165
+ ThinkingSphinx::Search.new(:offset => 5).offset.should == 5
1166
+ end
1167
+ end
1168
+
1169
+ describe '#indexes' do
1170
+ it "should default to '*'" do
1171
+ ThinkingSphinx::Search.new.indexes.should == '*'
1172
+ end
1173
+
1174
+ it "should use given class to determine index name" do
1175
+ ThinkingSphinx::Search.new(:classes => [Alpha]).indexes.
1176
+ should == 'alpha_core'
1177
+ end
1178
+
1179
+ it "should add both core and delta indexes for given classes" do
1180
+ ThinkingSphinx::Search.new(:classes => [Alpha, Beta]).indexes.
1181
+ should == 'alpha_core,beta_core,beta_delta'
1182
+ end
1183
+
1184
+ it "should respect the :index option" do
1185
+ ThinkingSphinx::Search.new(:classes => [Alpha], :index => '*').indexes.
1186
+ should == '*'
1187
+ end
1188
+ end
1189
+
1190
+ describe '.each_with_groupby_and_count' do
1191
+ before :each do
1192
+ @alpha = Alpha.new
1193
+ @alpha.stub!(:id => 1, :read_attribute => 1)
1194
+
1195
+ @client.stub! :query => {
1196
+ :matches => [{
1197
+ :attributes => {
1198
+ 'sphinx_internal_id' => @alpha.id,
1199
+ 'sphinx_internal_class' => 'Alpha',
1200
+ 'class_crc' => Alpha.to_crc32,
1201
+ '@groupby' => 101,
1202
+ '@count' => 5
1203
+ }
1204
+ }]
1205
+ }
1206
+ Alpha.stub :find => [@alpha], :unscoped => Alpha
1207
+ end
1208
+
1209
+ it "should yield the match, group and count" do
1210
+ search = ThinkingSphinx::Search.new
1211
+ search.each_with_groupby_and_count do |obj, group, count|
1212
+ obj.should == @alpha
1213
+ group.should == 101
1214
+ count.should == 5
1215
+ end
1216
+ end
1217
+
1218
+ it "should be aliased to each_with_group_and_count" do
1219
+ search = ThinkingSphinx::Search.new
1220
+ search.each_with_group_and_count do |obj, group, count|
1221
+ obj.should == @alpha
1222
+ group.should == 101
1223
+ count.should == 5
1224
+ end
1225
+ end
1226
+ end
1227
+
1228
+ describe '.each_with_weighting' do
1229
+ before :each do
1230
+ @alpha = Alpha.new
1231
+ @alpha.stub!(:id => 1, :read_attribute => 1)
1232
+
1233
+ @client.stub! :query => {
1234
+ :matches => [{
1235
+ :attributes => {
1236
+ 'sphinx_internal_id' => @alpha.id,
1237
+ 'sphinx_internal_class' => 'Alpha',
1238
+ 'class_crc' => Alpha.to_crc32
1239
+ }, :weight => 12
1240
+ }]
1241
+ }
1242
+ Alpha.stub :find => [@alpha], :unscoped => Alpha
1243
+ end
1244
+
1245
+ it "should yield the match and weight" do
1246
+ search = ThinkingSphinx::Search.new
1247
+ search.each_with_weighting do |obj, weight|
1248
+ obj.should == @alpha
1249
+ weight.should == 12
1250
+ end
1251
+ end
1252
+ end
1253
+
1254
+ describe '.each_with_*' do
1255
+ before :each do
1256
+ @alpha = Alpha.new
1257
+ @alpha.stub!(:id => 1, :read_attribute => 1)
1258
+
1259
+ @client.stub! :query => {
1260
+ :matches => [{
1261
+ :attributes => {
1262
+ 'sphinx_internal_id' => @alpha.id,
1263
+ 'sphinx_internal_class' => 'Alpha',
1264
+ 'class_crc' => Alpha.to_crc32,
1265
+ '@geodist' => 101,
1266
+ '@groupby' => 102,
1267
+ '@count' => 103
1268
+ }, :weight => 12
1269
+ }]
1270
+ }
1271
+ Alpha.stub :find => [@alpha], :unscoped => Alpha
1272
+
1273
+ @search = ThinkingSphinx::Search.new
1274
+ end
1275
+
1276
+ it "should yield geodist if requested" do
1277
+ @search.each_with_geodist do |obj, distance|
1278
+ obj.should == @alpha
1279
+ distance.should == 101
1280
+ end
1281
+ end
1282
+
1283
+ it "should yield count if requested" do
1284
+ @search.each_with_count do |obj, count|
1285
+ obj.should == @alpha
1286
+ count.should == 103
1287
+ end
1288
+ end
1289
+
1290
+ it "should yield groupby if requested" do
1291
+ @search.each_with_groupby do |obj, group|
1292
+ obj.should == @alpha
1293
+ group.should == 102
1294
+ end
1295
+ end
1296
+
1297
+ it "should still use the array's each_with_index" do
1298
+ @search.each_with_index do |obj, index|
1299
+ obj.should == @alpha
1300
+ index.should == 0
1301
+ end
1302
+ end
1303
+ end
1304
+
1305
+ describe '#excerpt_for' do
1306
+ before :each do
1307
+ @client.stub!(:excerpts => ['excerpted string'])
1308
+ @client.stub!(:query => {
1309
+ :matches => [],
1310
+ :words => {'one' => {}, 'two' => {}}
1311
+ })
1312
+ @search = ThinkingSphinx::Search.new('reading comprehension', :classes => [Alpha])
1313
+ end
1314
+
1315
+ it "should return the Sphinx excerpt value" do
1316
+ @search.excerpt_for('string').should == 'excerpted string'
1317
+ end
1318
+
1319
+ it "should use the given model's core index" do
1320
+ @client.should_receive(:excerpts) do |options|
1321
+ options[:index].should == 'alpha_core'
1322
+ end
1323
+
1324
+ @search.excerpt_for('string')
1325
+ end
1326
+
1327
+ it "should respect the provided index option" do
1328
+ @search = ThinkingSphinx::Search.new(:classes => [Alpha], :index => 'foo')
1329
+ @client.should_receive(:excerpts) do |options|
1330
+ options[:index].should == 'foo'
1331
+ end
1332
+
1333
+ @search.excerpt_for('string')
1334
+ end
1335
+
1336
+ it "should optionally take a second argument to allow for multi-model searches" do
1337
+ @client.should_receive(:excerpts) do |options|
1338
+ options[:index].should == 'beta_core'
1339
+ end
1340
+
1341
+ @search.excerpt_for('string', Beta)
1342
+ end
1343
+
1344
+ it "should use the query string" do
1345
+ @client.should_receive(:excerpts) do |options|
1346
+ options[:words].should == 'reading comprehension'
1347
+ end
1348
+
1349
+ @search.excerpt_for('string', Beta)
1350
+ end
1351
+
1352
+ it "should use the correct index in STI situations" do
1353
+ @client.should_receive(:excerpts) do |options|
1354
+ options[:index].should == 'person_core'
1355
+ end
1356
+
1357
+ @search.excerpt_for('string', Parent)
1358
+ end
1359
+ end
1360
+
1361
+ describe '#search' do
1362
+ before :each do
1363
+ @search = ThinkingSphinx::Search.new('word',
1364
+ :conditions => {:field => 'field'},
1365
+ :with => {:int => 5}
1366
+ )
1367
+ end
1368
+
1369
+ it "should return itself" do
1370
+ @search.search.object_id.should == @search.object_id
1371
+ end
1372
+
1373
+ it "should merge in arguments" do
1374
+ @client.should_receive(:query) do |query, index, comments|
1375
+ query.should == 'word more @field field'
1376
+ end
1377
+
1378
+ @search.search('more').first
1379
+ end
1380
+
1381
+ it "should merge conditions" do
1382
+ @client.should_receive(:query) do |query, index, comments|
1383
+ query.should match(/@name plato/)
1384
+ query.should match(/@field field/)
1385
+ end
1386
+
1387
+ @search.search(:conditions => {:name => 'plato'}).first
1388
+ end
1389
+
1390
+ it "should merge filters" do
1391
+ @search.search(:with => {:float => 1.5}).first
1392
+
1393
+ @client.filters.detect { |filter|
1394
+ filter.attribute == 'float'
1395
+ }.should_not be_nil
1396
+ @client.filters.detect { |filter|
1397
+ filter.attribute == 'int'
1398
+ }.should_not be_nil
1399
+ end
1400
+ end
1401
+
1402
+ describe '#all' do
1403
+ before :each do
1404
+ @search = ThinkingSphinx::Search.new
1405
+ end
1406
+
1407
+ it "should populate the result set" do
1408
+ @search.all
1409
+ @search.should be_populated
1410
+ end
1411
+
1412
+ it "should return the Search object" do
1413
+ @search.all.should be_a(ThinkingSphinx::Search)
1414
+ end
1415
+ end
1416
+
1417
+ describe '#freeze' do
1418
+ before :each do
1419
+ @search = ThinkingSphinx::Search.new
1420
+ end
1421
+
1422
+ it "should populate the result set" do
1423
+ @search.freeze
1424
+ @search.should be_populated
1425
+ end
1426
+
1427
+ it "should freeze the underlying array" do
1428
+ @search.freeze
1429
+ @search.to_a.should be_frozen
1430
+ end
1431
+
1432
+ it "should return the Search object" do
1433
+ @search.freeze.should be_a(ThinkingSphinx::Search)
1434
+ end
1435
+ end
1436
+
1437
+ describe '#client' do
1438
+ let(:client) { Riddle::Client.new }
1439
+ it "should respect the client in options" do
1440
+ search = ThinkingSphinx::Search.new :client => client
1441
+ search.client.should == client
1442
+ end
1443
+
1444
+ it "should get a new client from the configuration singleton by default" do
1445
+ ThinkingSphinx::Configuration.instance.stub!(:client => client)
1446
+ ThinkingSphinx::Search.new.client.should == client
1447
+ end
1448
+ end
1449
+ end
1450
+
1451
+ describe ThinkingSphinx::Search, "playing nice with Search model" do
1452
+ it "should not conflict with models called Search" do
1453
+ lambda { Search.find(:all) }.should_not raise_error
1454
+ end
1455
+ end