friendlyfashion-thinking-sphinx 2.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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