hariton-thinking-sphinx 1.2.7.0

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 (93) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +156 -0
  3. data/VERSION.yml +4 -0
  4. data/lib/thinking_sphinx.rb +210 -0
  5. data/lib/thinking_sphinx/active_record.rb +298 -0
  6. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  7. data/lib/thinking_sphinx/active_record/delta.rb +87 -0
  8. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  9. data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
  10. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  11. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  12. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +136 -0
  13. data/lib/thinking_sphinx/association.rb +164 -0
  14. data/lib/thinking_sphinx/attribute.rb +329 -0
  15. data/lib/thinking_sphinx/class_facet.rb +15 -0
  16. data/lib/thinking_sphinx/configuration.rb +282 -0
  17. data/lib/thinking_sphinx/core/string.rb +15 -0
  18. data/lib/thinking_sphinx/deltas.rb +30 -0
  19. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  20. data/lib/thinking_sphinx/deltas/default_delta.rb +68 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta.rb +30 -0
  22. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  23. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  24. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  25. data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
  26. data/lib/thinking_sphinx/excerpter.rb +22 -0
  27. data/lib/thinking_sphinx/facet.rb +108 -0
  28. data/lib/thinking_sphinx/facet_search.rb +134 -0
  29. data/lib/thinking_sphinx/field.rb +82 -0
  30. data/lib/thinking_sphinx/index.rb +99 -0
  31. data/lib/thinking_sphinx/index/builder.rb +287 -0
  32. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  33. data/lib/thinking_sphinx/property.rb +160 -0
  34. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  35. data/lib/thinking_sphinx/search.rb +671 -0
  36. data/lib/thinking_sphinx/search_methods.rb +421 -0
  37. data/lib/thinking_sphinx/source.rb +150 -0
  38. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  39. data/lib/thinking_sphinx/source/sql.rb +128 -0
  40. data/lib/thinking_sphinx/tasks.rb +165 -0
  41. data/rails/init.rb +14 -0
  42. data/spec/lib/thinking_sphinx/active_record/delta_spec.rb +136 -0
  43. data/spec/lib/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  44. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
  45. data/spec/lib/thinking_sphinx/active_record_spec.rb +354 -0
  46. data/spec/lib/thinking_sphinx/association_spec.rb +246 -0
  47. data/spec/lib/thinking_sphinx/attribute_spec.rb +465 -0
  48. data/spec/lib/thinking_sphinx/configuration_spec.rb +268 -0
  49. data/spec/lib/thinking_sphinx/core/string_spec.rb +9 -0
  50. data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
  51. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  52. data/spec/lib/thinking_sphinx/facet_spec.rb +302 -0
  53. data/spec/lib/thinking_sphinx/field_spec.rb +154 -0
  54. data/spec/lib/thinking_sphinx/index/builder_spec.rb +355 -0
  55. data/spec/lib/thinking_sphinx/index/faux_column_spec.rb +30 -0
  56. data/spec/lib/thinking_sphinx/index_spec.rb +45 -0
  57. data/spec/lib/thinking_sphinx/rails_additions_spec.rb +203 -0
  58. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  59. data/spec/lib/thinking_sphinx/search_spec.rb +993 -0
  60. data/spec/lib/thinking_sphinx/source_spec.rb +217 -0
  61. data/spec/lib/thinking_sphinx_spec.rb +161 -0
  62. data/tasks/distribution.rb +49 -0
  63. data/tasks/rails.rake +1 -0
  64. data/tasks/testing.rb +78 -0
  65. data/vendor/after_commit/LICENSE +20 -0
  66. data/vendor/after_commit/README +16 -0
  67. data/vendor/after_commit/Rakefile +22 -0
  68. data/vendor/after_commit/init.rb +8 -0
  69. data/vendor/after_commit/lib/after_commit.rb +45 -0
  70. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  71. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  72. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  73. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  74. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  75. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  76. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  77. data/vendor/riddle/lib/riddle.rb +30 -0
  78. data/vendor/riddle/lib/riddle/client.rb +719 -0
  79. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  80. data/vendor/riddle/lib/riddle/client/message.rb +70 -0
  81. data/vendor/riddle/lib/riddle/client/response.rb +94 -0
  82. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  83. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +49 -0
  84. data/vendor/riddle/lib/riddle/configuration/index.rb +146 -0
  85. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  86. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  87. data/vendor/riddle/lib/riddle/configuration/searchd.rb +46 -0
  88. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  89. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  90. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +39 -0
  91. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  92. data/vendor/riddle/lib/riddle/controller.rb +54 -0
  93. metadata +169 -0
@@ -0,0 +1,152 @@
1
+ require 'spec/spec_helper'
2
+
3
+ describe ThinkingSphinx::SearchMethods do
4
+ it "should be included into models with indexes" do
5
+ Alpha.included_modules.should include(ThinkingSphinx::SearchMethods)
6
+ end
7
+
8
+ it "should not be included into models that don't have indexes" do
9
+ Gamma.included_modules.should_not include(ThinkingSphinx::SearchMethods)
10
+ end
11
+
12
+ describe '.search_context' do
13
+ it "should return nil if not within a model" do
14
+ ThinkingSphinx.search_context.should be_nil
15
+ end
16
+
17
+ it "should return the model if within one" do
18
+ Alpha.search_context.should == Alpha
19
+ end
20
+ end
21
+
22
+ describe '.search' do
23
+ it "should return an instance of ThinkingSphinx::Search" do
24
+ Alpha.search.class.should == ThinkingSphinx::Search
25
+ end
26
+
27
+ it "should set the classes option if not already set" do
28
+ search = Alpha.search
29
+ search.options[:classes].should == [Alpha]
30
+ end
31
+
32
+ it "shouldn't set the classes option if already defined" do
33
+ search = Alpha.search :classes => [Beta]
34
+ search.options[:classes].should == [Beta]
35
+ end
36
+
37
+ it "should default to nil for the classes options" do
38
+ ThinkingSphinx.search.options[:classes].should be_nil
39
+ end
40
+ end
41
+
42
+ describe '.search_for_ids' do
43
+ it "should return an instance of ThinkingSphinx::Search" do
44
+ Alpha.search.class.should == ThinkingSphinx::Search
45
+ end
46
+
47
+ it "should set the classes option if not already set" do
48
+ search = Alpha.search_for_ids
49
+ search.options[:classes].should == [Alpha]
50
+ end
51
+
52
+ it "shouldn't set the classes option if already defined" do
53
+ search = Alpha.search_for_ids :classes => [Beta]
54
+ search.options[:classes].should == [Beta]
55
+ end
56
+
57
+ it "should set ids_only to true" do
58
+ search = Alpha.search_for_ids
59
+ search.options[:ids_only].should be_true
60
+ end
61
+ end
62
+
63
+ describe '.search_for_id' do
64
+ before :each do
65
+ @config = ThinkingSphinx::Configuration.instance
66
+ @client = Riddle::Client.new
67
+
68
+ @config.stub!(:client => @client)
69
+ @client.stub!(:query => {:matches => [], :total_found => 0})
70
+ end
71
+
72
+ it "should set the id range to the given id value" do
73
+ ThinkingSphinx.search_for_id(101, 'alpha_core')
74
+
75
+ @client.id_range.should == (101..101)
76
+ end
77
+
78
+ it "should not make any calls to the database" do
79
+ Alpha.should_not_receive(:find)
80
+
81
+ ThinkingSphinx.search_for_id(101, 'alpha_core', :classes => [Alpha])
82
+ end
83
+
84
+ it "should return true if there is a record" do
85
+ @client.stub!(:query => {:matches => [
86
+ {:attributes => {'sphinx_internal_id' => 100}}
87
+ ], :total_found => 1})
88
+
89
+ ThinkingSphinx.search_for_id(101, 'alpha_core').should be_true
90
+ end
91
+
92
+ it "should return false if there isn't a record" do
93
+ ThinkingSphinx.search_for_id(101, 'alpha_core').should be_false
94
+ end
95
+ end
96
+
97
+ describe '.count' do
98
+ before :each do
99
+ @config = ThinkingSphinx::Configuration.instance
100
+ @client = Riddle::Client.new
101
+
102
+ @config.stub!(:client => @client)
103
+ @client.stub!(:query => {:matches => [], :total_found => 42})
104
+ end
105
+
106
+ it "should fall through to ActiveRecord if called on a class" do
107
+ @client.should_not_receive(:query)
108
+
109
+ Alpha.count
110
+ end
111
+
112
+ it "should return the total number of results if called globally" do
113
+ ThinkingSphinx.count.should == 42
114
+ end
115
+ end
116
+
117
+ describe '.search_count' do
118
+ before :each do
119
+ @config = ThinkingSphinx::Configuration.instance
120
+ @client = Riddle::Client.new
121
+
122
+ @config.stub!(:client => @client)
123
+ @client.stub!(:query => {:matches => [], :total_found => 42})
124
+ end
125
+
126
+ it "should return the total number of results" do
127
+ Alpha.search_count.should == 42
128
+ end
129
+
130
+ it "should not make any calls to the database" do
131
+ Alpha.should_not_receive(:find)
132
+
133
+ Alpha.search_count
134
+ end
135
+ end
136
+
137
+ describe '.facets' do
138
+ it "should return a FacetSearch instance" do
139
+ Alpha.facets.should be_a(ThinkingSphinx::FacetSearch)
140
+ end
141
+
142
+ it "should set the classes option if not already set" do
143
+ facets = Alpha.facets
144
+ facets.options[:classes].should == [Alpha]
145
+ end
146
+
147
+ it "shouldn't set the classes option if already defined" do
148
+ facets = Alpha.facets :classes => [Beta]
149
+ facets.options[:classes].should == [Beta]
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,993 @@
1
+ require 'spec/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 '#populated?' do
36
+ before :each do
37
+ @search = ThinkingSphinx::Search.new
38
+ end
39
+
40
+ it "should be false if the client request has not been made" do
41
+ @search.populated?.should be_false
42
+ end
43
+
44
+ it "should be true once the client request has been made" do
45
+ @search.first
46
+ @search.populated?.should be_true
47
+ end
48
+ end
49
+
50
+ describe '#results' do
51
+ it "should populate search results before returning" do
52
+ @search = ThinkingSphinx::Search.new
53
+ @search.populated?.should be_false
54
+
55
+ @search.results
56
+ @search.populated?.should be_true
57
+ end
58
+ end
59
+
60
+ describe '#method_missing' do
61
+ before :each do
62
+ Alpha.sphinx_scope(:by_name) { |name|
63
+ {:conditions => {:name => name}}
64
+ }
65
+ Alpha.sphinx_scope(:ids_only) { {:ids_only => true} }
66
+ end
67
+
68
+ after :each do
69
+ Alpha.remove_sphinx_scopes
70
+ end
71
+
72
+ it "should handle Array methods" do
73
+ ThinkingSphinx::Search.new.private_methods.should be_an(Array)
74
+ end
75
+
76
+ it "should raise a NoMethodError exception if unknown method" do
77
+ lambda {
78
+ ThinkingSphinx::Search.new.foo
79
+ }.should raise_error(NoMethodError)
80
+ end
81
+
82
+ it "should not request results from client if method does not exist" do
83
+ @client.should_not_receive(:query)
84
+
85
+ lambda {
86
+ ThinkingSphinx::Search.new.foo
87
+ }.should raise_error(NoMethodError)
88
+ end
89
+
90
+ it "should accept sphinx scopes" do
91
+ search = ThinkingSphinx::Search.new(:classes => [Alpha])
92
+
93
+ lambda {
94
+ search.by_name('Pat')
95
+ }.should_not raise_error(NoMethodError)
96
+ end
97
+
98
+ it "should return itself when using a sphinx scope" do
99
+ search = ThinkingSphinx::Search.new(:classes => [Alpha])
100
+ search.by_name('Pat').object_id.should == search.object_id
101
+ end
102
+
103
+ it "should keep the same search object when chaining multiple scopes" do
104
+ search = ThinkingSphinx::Search.new(:classes => [Alpha])
105
+ search.by_name('Pat').ids_only.object_id.should == search.object_id
106
+ end
107
+ end
108
+
109
+ describe '.search' do
110
+ it "return the output of ThinkingSphinx.search" do
111
+ @results = [] # to confirm same object
112
+ ThinkingSphinx.stub!(:search => @results)
113
+
114
+ ThinkingSphinx::Search.search.object_id.should == @results.object_id
115
+ end
116
+ end
117
+
118
+ describe '.search_for_ids' do
119
+ it "return the output of ThinkingSphinx.search_for_ids" do
120
+ @results = [] # to confirm same object
121
+ ThinkingSphinx.stub!(:search_for_ids => @results)
122
+
123
+ ThinkingSphinx::Search.search_for_ids.object_id.
124
+ should == @results.object_id
125
+ end
126
+ end
127
+
128
+ describe '.search_for_id' do
129
+ it "return the output of ThinkingSphinx.search_for_ids" do
130
+ @results = [] # to confirm same object
131
+ ThinkingSphinx.stub!(:search_for_id => @results)
132
+
133
+ ThinkingSphinx::Search.search_for_id.object_id.
134
+ should == @results.object_id
135
+ end
136
+ end
137
+
138
+ describe '.count' do
139
+ it "return the output of ThinkingSphinx.search" do
140
+ @results = [] # to confirm same object
141
+ ThinkingSphinx.stub!(:count => @results)
142
+
143
+ ThinkingSphinx::Search.count.object_id.should == @results.object_id
144
+ end
145
+ end
146
+
147
+ describe '.facets' do
148
+ it "return the output of ThinkingSphinx.facets" do
149
+ @results = [] # to confirm same object
150
+ ThinkingSphinx.stub!(:facets => @results)
151
+
152
+ ThinkingSphinx::Search.facets.object_id.should == @results.object_id
153
+ end
154
+ end
155
+
156
+ describe '#populate' do
157
+ before :each do
158
+ @alpha_a, @alpha_b = Alpha.new, Alpha.new
159
+ @beta_a, @beta_b = Beta.new, Beta.new
160
+
161
+ @alpha_a.stub! :id => 1, :read_attribute => 1
162
+ @alpha_b.stub! :id => 2, :read_attribute => 2
163
+ @beta_a.stub! :id => 1, :read_attribute => 1
164
+ @beta_b.stub! :id => 2, :read_attribute => 2
165
+
166
+ @client.stub! :query => {
167
+ :matches => minimal_result_hashes(@alpha_a, @beta_b, @alpha_b, @beta_a)
168
+ }
169
+ Alpha.stub! :find => [@alpha_a, @alpha_b]
170
+ Beta.stub! :find => [@beta_a, @beta_b]
171
+ end
172
+
173
+ it "should issue only one select per model" do
174
+ Alpha.should_receive(:find).once.and_return([@alpha_a, @alpha_b])
175
+ Beta.should_receive(:find).once.and_return([@beta_a, @beta_b])
176
+
177
+ ThinkingSphinx::Search.new.first
178
+ end
179
+
180
+ it "should mix the results from different models" do
181
+ search = ThinkingSphinx::Search.new
182
+ search[0].should be_a(Alpha)
183
+ search[1].should be_a(Beta)
184
+ search[2].should be_a(Alpha)
185
+ search[3].should be_a(Beta)
186
+ end
187
+
188
+ it "should maintain the Xoopit ordering for results" do
189
+ search = ThinkingSphinx::Search.new
190
+ search[0].id.should == 1
191
+ search[1].id.should == 2
192
+ search[2].id.should == 2
193
+ search[3].id.should == 1
194
+ end
195
+
196
+ describe 'query' do
197
+ it "should concatenate arguments with spaces" do
198
+ @client.should_receive(:query) do |query, index, comment|
199
+ query.should == 'two words'
200
+ end
201
+
202
+ ThinkingSphinx::Search.new('two', 'words').first
203
+ end
204
+
205
+ it "should append conditions to the query" do
206
+ @client.should_receive(:query) do |query, index, comment|
207
+ query.should == 'general @focused specific'
208
+ end
209
+
210
+ ThinkingSphinx::Search.new('general', :conditions => {
211
+ :focused => 'specific'
212
+ }).first
213
+ end
214
+
215
+ it "append multiple conditions together" do
216
+ @client.should_receive(:query) do |query, index, comment|
217
+ query.should match(/general.+@foo word/)
218
+ query.should match(/general.+@bar word/)
219
+ end
220
+
221
+ ThinkingSphinx::Search.new('general', :conditions => {
222
+ :foo => 'word', :bar => 'word'
223
+ }).first
224
+ end
225
+
226
+ it "should apply stars if requested, and handle full extended syntax" do
227
+ 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)}
228
+ 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*)}
229
+
230
+ @client.should_receive(:query) do |query, index, comment|
231
+ query.should == expected
232
+ end
233
+
234
+ ThinkingSphinx::Search.new(input, :star => true).first
235
+ end
236
+
237
+ it "should default to /\w+/ as token for auto-starring" do
238
+ @client.should_receive(:query) do |query, index, comment|
239
+ query.should == '*foo*@*bar*.*com*'
240
+ end
241
+
242
+ ThinkingSphinx::Search.new('foo@bar.com', :star => true).first
243
+ end
244
+
245
+ it "should honour custom star tokens" do
246
+ @client.should_receive(:query) do |query, index, comment|
247
+ query.should == '*foo@bar.com* -*foo-bar*'
248
+ end
249
+
250
+ ThinkingSphinx::Search.new(
251
+ 'foo@bar.com -foo-bar', :star => /[\w@.-]+/u
252
+ ).first
253
+ end
254
+ end
255
+
256
+ describe 'comment' do
257
+ it "should add comment if explicitly provided" do
258
+ @client.should_receive(:query) do |query, index, comment|
259
+ comment.should == 'custom log'
260
+ end
261
+
262
+ ThinkingSphinx::Search.new(:comment => 'custom log').first
263
+ end
264
+
265
+ it "should default to a blank comment" do
266
+ @client.should_receive(:query) do |query, index, comment|
267
+ comment.should == ''
268
+ end
269
+
270
+ ThinkingSphinx::Search.new.first
271
+ end
272
+ end
273
+
274
+ describe 'match mode' do
275
+ it "should default to :all" do
276
+ ThinkingSphinx::Search.new.first
277
+
278
+ @client.match_mode.should == :all
279
+ end
280
+
281
+ it "should default to :extended if conditions are supplied" do
282
+ ThinkingSphinx::Search.new('general', :conditions => {
283
+ :foo => 'word', :bar => 'word'
284
+ }).first
285
+
286
+ @client.match_mode.should == :extended
287
+ end
288
+
289
+ it "should use explicit match modes" do
290
+ ThinkingSphinx::Search.new('general', :conditions => {
291
+ :foo => 'word', :bar => 'word'
292
+ }, :match_mode => :extended2).first
293
+
294
+ @client.match_mode.should == :extended2
295
+ end
296
+ end
297
+
298
+ describe 'pagination' do
299
+ it "should set the limit using per_page" do
300
+ ThinkingSphinx::Search.new(:per_page => 30).first
301
+ @client.limit.should == 30
302
+ end
303
+
304
+ it "should set the offset if pagination is requested" do
305
+ ThinkingSphinx::Search.new(:page => 3).first
306
+ @client.offset.should == 40
307
+ end
308
+
309
+ it "should set the offset by the per_page value" do
310
+ ThinkingSphinx::Search.new(:page => 3, :per_page => 30).first
311
+ @client.offset.should == 60
312
+ end
313
+ end
314
+
315
+ describe 'filters' do
316
+ it "should filter out deleted values by default" do
317
+ ThinkingSphinx::Search.new.first
318
+
319
+ filter = @client.filters.last
320
+ filter.values.should == [0]
321
+ filter.attribute.should == 'sphinx_deleted'
322
+ filter.exclude?.should be_false
323
+ end
324
+
325
+ it "should add class filters for explicit classes" do
326
+ ThinkingSphinx::Search.new(:classes => [Alpha, Beta]).first
327
+
328
+ filter = @client.filters.last
329
+ filter.values.should == [Alpha.to_crc32, Beta.to_crc32]
330
+ filter.attribute.should == 'class_crc'
331
+ filter.exclude?.should be_false
332
+ end
333
+
334
+ it "should add class filters for subclasses of requested classes" do
335
+ ThinkingSphinx::Search.new(:classes => [Person]).first
336
+
337
+ filter = @client.filters.last
338
+ filter.values.should == [
339
+ Parent.to_crc32, Admin::Person.to_crc32,
340
+ Child.to_crc32, Person.to_crc32
341
+ ]
342
+ filter.attribute.should == 'class_crc'
343
+ filter.exclude?.should be_false
344
+ end
345
+
346
+ it "should append inclusive filters of integers" do
347
+ ThinkingSphinx::Search.new(:with => {:int => 1}).first
348
+
349
+ filter = @client.filters.last
350
+ filter.values.should == [1]
351
+ filter.attribute.should == 'int'
352
+ filter.exclude?.should be_false
353
+ end
354
+
355
+ it "should append inclusive filters of floats" do
356
+ ThinkingSphinx::Search.new(:with => {:float => 1.5}).first
357
+
358
+ filter = @client.filters.last
359
+ filter.values.should == [1.5]
360
+ filter.attribute.should == 'float'
361
+ filter.exclude?.should be_false
362
+ end
363
+
364
+ it "should append inclusive filters of booleans" do
365
+ ThinkingSphinx::Search.new(:with => {:boolean => true}).first
366
+
367
+ filter = @client.filters.last
368
+ filter.values.should == [true]
369
+ filter.attribute.should == 'boolean'
370
+ filter.exclude?.should be_false
371
+ end
372
+
373
+ it "should append inclusive filters of arrays" do
374
+ ThinkingSphinx::Search.new(:with => {:ints => [1, 2, 3]}).first
375
+
376
+ filter = @client.filters.last
377
+ filter.values.should == [1, 2, 3]
378
+ filter.attribute.should == 'ints'
379
+ filter.exclude?.should be_false
380
+ end
381
+
382
+ it "should append inclusive filters of time ranges" do
383
+ first, last = 1.week.ago, Time.now
384
+ ThinkingSphinx::Search.new(:with => {
385
+ :time => first..last
386
+ }).first
387
+
388
+ filter = @client.filters.last
389
+ filter.values.should == (first.to_i..last.to_i)
390
+ filter.attribute.should == 'time'
391
+ filter.exclude?.should be_false
392
+ end
393
+
394
+ it "should append exclusive filters of integers" do
395
+ ThinkingSphinx::Search.new(:without => {:int => 1}).first
396
+
397
+ filter = @client.filters.last
398
+ filter.values.should == [1]
399
+ filter.attribute.should == 'int'
400
+ filter.exclude?.should be_true
401
+ end
402
+
403
+ it "should append exclusive filters of floats" do
404
+ ThinkingSphinx::Search.new(:without => {:float => 1.5}).first
405
+
406
+ filter = @client.filters.last
407
+ filter.values.should == [1.5]
408
+ filter.attribute.should == 'float'
409
+ filter.exclude?.should be_true
410
+ end
411
+
412
+ it "should append exclusive filters of booleans" do
413
+ ThinkingSphinx::Search.new(:without => {:boolean => true}).first
414
+
415
+ filter = @client.filters.last
416
+ filter.values.should == [true]
417
+ filter.attribute.should == 'boolean'
418
+ filter.exclude?.should be_true
419
+ end
420
+
421
+ it "should append exclusive filters of arrays" do
422
+ ThinkingSphinx::Search.new(:without => {:ints => [1, 2, 3]}).first
423
+
424
+ filter = @client.filters.last
425
+ filter.values.should == [1, 2, 3]
426
+ filter.attribute.should == 'ints'
427
+ filter.exclude?.should be_true
428
+ end
429
+
430
+ it "should append exclusive filters of time ranges" do
431
+ first, last = 1.week.ago, Time.now
432
+ ThinkingSphinx::Search.new(:without => {
433
+ :time => first..last
434
+ }).first
435
+
436
+ filter = @client.filters.last
437
+ filter.values.should == (first.to_i..last.to_i)
438
+ filter.attribute.should == 'time'
439
+ filter.exclude?.should be_true
440
+ end
441
+
442
+ it "should add separate filters for each item in a with_all value" do
443
+ ThinkingSphinx::Search.new(:with_all => {:ints => [1, 2, 3]}).first
444
+
445
+ filters = @client.filters[-3, 3]
446
+ filters.each do |filter|
447
+ filter.attribute.should == 'ints'
448
+ filter.exclude?.should be_false
449
+ end
450
+
451
+ filters[0].values.should == [1]
452
+ filters[1].values.should == [2]
453
+ filters[2].values.should == [3]
454
+ end
455
+
456
+ it "should filter out specific ids using :without_ids" do
457
+ ThinkingSphinx::Search.new(:without_ids => [4, 5, 6]).first
458
+
459
+ filter = @client.filters.last
460
+ filter.values.should == [4, 5, 6]
461
+ filter.attribute.should == 'sphinx_internal_id'
462
+ filter.exclude?.should be_true
463
+ end
464
+
465
+ describe 'in :conditions' do
466
+ it "should add as filters for known attributes in :conditions option" do
467
+ ThinkingSphinx::Search.new('general',
468
+ :conditions => {:word => 'specific', :lat => 1.5},
469
+ :classes => [Alpha]
470
+ ).first
471
+
472
+ filter = @client.filters.last
473
+ filter.values.should == [1.5]
474
+ filter.attribute.should == 'lat'
475
+ filter.exclude?.should be_false
476
+ end
477
+
478
+ it "should not add the filter to the query string" do
479
+ @client.should_receive(:query) do |query, index, comment|
480
+ query.should == 'general @word specific'
481
+ end
482
+
483
+ ThinkingSphinx::Search.new('general',
484
+ :conditions => {:word => 'specific', :lat => 1.5},
485
+ :classes => [Alpha]
486
+ ).first
487
+ end
488
+ end
489
+ end
490
+
491
+ describe 'sort mode' do
492
+ it "should use :relevance as a default" do
493
+ ThinkingSphinx::Search.new.first
494
+ @client.sort_mode.should == :relevance
495
+ end
496
+
497
+ it "should use :attr_asc if a symbol is supplied to :order" do
498
+ ThinkingSphinx::Search.new(:order => :created_at).first
499
+ @client.sort_mode.should == :attr_asc
500
+ end
501
+
502
+ it "should use :attr_desc if :desc is the mode" do
503
+ ThinkingSphinx::Search.new(
504
+ :order => :created_at, :sort_mode => :desc
505
+ ).first
506
+ @client.sort_mode.should == :attr_desc
507
+ end
508
+
509
+ it "should use :extended if a string is supplied to :order" do
510
+ ThinkingSphinx::Search.new(:order => "created_at ASC").first
511
+ @client.sort_mode.should == :extended
512
+ end
513
+
514
+ it "should use :expr if explicitly requested" do
515
+ ThinkingSphinx::Search.new(
516
+ :order => "created_at ASC", :sort_mode => :expr
517
+ ).first
518
+ @client.sort_mode.should == :expr
519
+ end
520
+
521
+ it "should use :attr_desc if explicitly requested" do
522
+ ThinkingSphinx::Search.new(
523
+ :order => "created_at", :sort_mode => :desc
524
+ ).first
525
+ @client.sort_mode.should == :attr_desc
526
+ end
527
+ end
528
+
529
+ describe 'sort by' do
530
+ it "should presume order symbols are attributes" do
531
+ ThinkingSphinx::Search.new(:order => :created_at).first
532
+ @client.sort_by.should == 'created_at'
533
+ end
534
+
535
+ it "replace field names with their sortable attributes" do
536
+ ThinkingSphinx::Search.new(:order => :name, :classes => [Alpha]).first
537
+ @client.sort_by.should == 'name_sort'
538
+ end
539
+
540
+ it "should replace field names in strings" do
541
+ ThinkingSphinx::Search.new(
542
+ :order => "created_at ASC, name DESC", :classes => [Alpha]
543
+ ).first
544
+ @client.sort_by.should == 'created_at ASC, name_sort DESC'
545
+ end
546
+ end
547
+
548
+ describe 'max matches' do
549
+ it "should use the global setting by default" do
550
+ ThinkingSphinx::Search.new.first
551
+ @client.max_matches.should == 1000
552
+ end
553
+
554
+ it "should use explicit setting" do
555
+ ThinkingSphinx::Search.new(:max_matches => 2000).first
556
+ @client.max_matches.should == 2000
557
+ end
558
+ end
559
+
560
+ describe 'field weights' do
561
+ it "should set field weights as provided" do
562
+ ThinkingSphinx::Search.new(
563
+ :field_weights => {'foo' => 10, 'bar' => 5}
564
+ ).first
565
+
566
+ @client.field_weights.should == {
567
+ 'foo' => 10, 'bar' => 5
568
+ }
569
+ end
570
+
571
+ it "should use field weights set in the index" do
572
+ ThinkingSphinx::Search.new(:classes => [Alpha]).first
573
+
574
+ @client.field_weights.should == {'name' => 10}
575
+ end
576
+ end
577
+
578
+ describe 'index weights' do
579
+ it "should send index weights through to the client" do
580
+ ThinkingSphinx::Search.new(:index_weights => {'foo' => 100}).first
581
+ @client.index_weights.should == {'foo' => 100}
582
+ end
583
+
584
+ it "should convert classes to their core and delta index names" do
585
+ ThinkingSphinx::Search.new(:index_weights => {Alpha => 100}).first
586
+ @client.index_weights.should == {
587
+ 'alpha_core' => 100,
588
+ 'alpha_delta' => 100
589
+ }
590
+ end
591
+ end
592
+
593
+ describe 'grouping' do
594
+ it "should convert group into group_by and group_function" do
595
+ ThinkingSphinx::Search.new(:group => :edition).first
596
+
597
+ @client.group_function.should == :attr
598
+ @client.group_by.should == "edition"
599
+ end
600
+
601
+ it "should pass on explicit grouping arguments" do
602
+ ThinkingSphinx::Search.new(
603
+ :group_by => 'created_at',
604
+ :group_function => :attr,
605
+ :group_clause => 'clause',
606
+ :group_distinct => 'distinct'
607
+ ).first
608
+
609
+ @client.group_by.should == 'created_at'
610
+ @client.group_function.should == :attr
611
+ @client.group_clause.should == 'clause'
612
+ @client.group_distinct.should == 'distinct'
613
+ end
614
+ end
615
+
616
+ describe 'anchor' do
617
+ it "should detect lat and lng attributes on the given model" do
618
+ ThinkingSphinx::Search.new(
619
+ :geo => [1.0, -1.0],
620
+ :classes => [Alpha]
621
+ ).first
622
+
623
+ @client.anchor[:latitude_attribute].should == 'lat'
624
+ @client.anchor[:longitude_attribute].should == 'lng'
625
+ end
626
+
627
+ it "should detect lat and lon attributes on the given model" do
628
+ ThinkingSphinx::Search.new(
629
+ :geo => [1.0, -1.0],
630
+ :classes => [Beta]
631
+ ).first
632
+
633
+ @client.anchor[:latitude_attribute].should == 'lat'
634
+ @client.anchor[:longitude_attribute].should == 'lon'
635
+ end
636
+
637
+ it "should detect latitude and longitude attributes on the given model" do
638
+ ThinkingSphinx::Search.new(
639
+ :geo => [1.0, -1.0],
640
+ :classes => [Person]
641
+ ).first
642
+
643
+ @client.anchor[:latitude_attribute].should == 'latitude'
644
+ @client.anchor[:longitude_attribute].should == 'longitude'
645
+ end
646
+
647
+ it "should accept manually defined latitude and longitude attributes" do
648
+ ThinkingSphinx::Search.new(
649
+ :geo => [1.0, -1.0],
650
+ :classes => [Alpha],
651
+ :latitude_attr => :updown,
652
+ :longitude_attr => :leftright
653
+ ).first
654
+
655
+ @client.anchor[:latitude_attribute].should == 'updown'
656
+ @client.anchor[:longitude_attribute].should == 'leftright'
657
+ end
658
+
659
+ it "should accept manually defined latitude and longitude attributes in the given model" do
660
+ ThinkingSphinx::Search.new(
661
+ :geo => [1.0, -1.0],
662
+ :classes => [Friendship]
663
+ ).first
664
+
665
+ @client.anchor[:latitude_attribute].should == 'person_id'
666
+ @client.anchor[:longitude_attribute].should == 'person_id'
667
+ end
668
+
669
+ it "should accept geo array for geo-position values" do
670
+ ThinkingSphinx::Search.new(
671
+ :geo => [1.0, -1.0],
672
+ :classes => [Alpha]
673
+ ).first
674
+
675
+ @client.anchor[:latitude].should == 1.0
676
+ @client.anchor[:longitude].should == -1.0
677
+ end
678
+
679
+ it "should accept lat and lng options for geo-position values" do
680
+ ThinkingSphinx::Search.new(
681
+ :lat => 1.0,
682
+ :lng => -1.0,
683
+ :classes => [Alpha]
684
+ ).first
685
+
686
+ @client.anchor[:latitude].should == 1.0
687
+ @client.anchor[:longitude].should == -1.0
688
+ end
689
+ end
690
+
691
+ describe 'excerpts' do
692
+ before :each do
693
+ @search = ThinkingSphinx::Search.new
694
+ end
695
+
696
+ it "should add excerpts method if objects don't already have one" do
697
+ @search.first.should respond_to(:excerpts)
698
+ end
699
+
700
+ it "should return an instance of ThinkingSphinx::Excerpter" do
701
+ @search.first.excerpts.should be_a(ThinkingSphinx::Excerpter)
702
+ end
703
+
704
+ it "should not add excerpts method if objects already have one" do
705
+ @search.last.excerpts.should_not be_a(ThinkingSphinx::Excerpter)
706
+ end
707
+
708
+ it "should set up the excerpter with the instances and search" do
709
+ ThinkingSphinx::Excerpter.should_receive(:new).with(@search, @alpha_a)
710
+ ThinkingSphinx::Excerpter.should_receive(:new).with(@search, @alpha_b)
711
+
712
+ @search.first
713
+ end
714
+ end
715
+ end
716
+
717
+ describe '#current_page' do
718
+ it "should return 1 by default" do
719
+ ThinkingSphinx::Search.new.current_page.should == 1
720
+ end
721
+
722
+ it "should handle string page values" do
723
+ ThinkingSphinx::Search.new(:page => '2').current_page.should == 2
724
+ end
725
+
726
+ it "should handle empty string page values" do
727
+ ThinkingSphinx::Search.new(:page => '').current_page.should == 1
728
+ end
729
+
730
+ it "should return the requested page" do
731
+ ThinkingSphinx::Search.new(:page => 10).current_page.should == 10
732
+ end
733
+ end
734
+
735
+ describe '#per_page' do
736
+ it "should return 20 by default" do
737
+ ThinkingSphinx::Search.new.per_page.should == 20
738
+ end
739
+
740
+ it "should allow for custom values" do
741
+ ThinkingSphinx::Search.new(:per_page => 30).per_page.should == 30
742
+ end
743
+
744
+ it "should prioritise :limit over :per_page if given" do
745
+ ThinkingSphinx::Search.new(
746
+ :per_page => 30, :limit => 40
747
+ ).per_page.should == 40
748
+ end
749
+ end
750
+
751
+ describe '#total_pages' do
752
+ it "should calculate the total pages depending on per_page and total_entries" do
753
+ ThinkingSphinx::Search.new.total_pages.should == 3
754
+ end
755
+
756
+ it "should allow for custom per_page values" do
757
+ ThinkingSphinx::Search.new(:per_page => 30).total_pages.should == 2
758
+ end
759
+
760
+ it "should not overstep the max_matches implied limit" do
761
+ @client.stub!(:query => {
762
+ :matches => [], :total_found => 41, :total => 40
763
+ })
764
+
765
+ ThinkingSphinx::Search.new.total_pages.should == 2
766
+ end
767
+ end
768
+
769
+ describe '#next_page' do
770
+ it "should return one more than the current page" do
771
+ ThinkingSphinx::Search.new.next_page.should == 2
772
+ end
773
+
774
+ it "should return nil if on the last page" do
775
+ ThinkingSphinx::Search.new(:page => 3).next_page.should be_nil
776
+ end
777
+ end
778
+
779
+ describe '#previous_page' do
780
+ it "should return one less than the current page" do
781
+ ThinkingSphinx::Search.new(:page => 2).previous_page.should == 1
782
+ end
783
+
784
+ it "should return nil if on the first page" do
785
+ ThinkingSphinx::Search.new.previous_page.should be_nil
786
+ end
787
+ end
788
+
789
+ describe '#total_entries' do
790
+ it "should return the total number of results, not just the amount on the page" do
791
+ ThinkingSphinx::Search.new.total_entries.should == 41
792
+ end
793
+ end
794
+
795
+ describe '#offset' do
796
+ it "should default to 0" do
797
+ ThinkingSphinx::Search.new.offset.should == 0
798
+ end
799
+
800
+ it "should increase by the per_page value for each page in" do
801
+ ThinkingSphinx::Search.new(:per_page => 25, :page => 2).offset.should == 25
802
+ end
803
+ end
804
+
805
+ describe '.each_with_groupby_and_count' do
806
+ before :each do
807
+ @alpha = Alpha.new
808
+ @alpha.stub!(:id => 1, :read_attribute => 1)
809
+
810
+ @client.stub! :query => {
811
+ :matches => [{
812
+ :attributes => {
813
+ 'sphinx_internal_id' => @alpha.id,
814
+ 'class_crc' => Alpha.to_crc32,
815
+ '@groupby' => 101,
816
+ '@count' => 5
817
+ }
818
+ }]
819
+ }
820
+ Alpha.stub!(:find => [@alpha])
821
+ end
822
+
823
+ it "should yield the match, group and count" do
824
+ search = ThinkingSphinx::Search.new
825
+ search.each_with_groupby_and_count do |obj, group, count|
826
+ obj.should == @alpha
827
+ group.should == 101
828
+ count.should == 5
829
+ end
830
+ end
831
+ end
832
+
833
+ describe '.each_with_weighting' do
834
+ before :each do
835
+ @alpha = Alpha.new
836
+ @alpha.stub!(:id => 1, :read_attribute => 1)
837
+
838
+ @client.stub! :query => {
839
+ :matches => [{
840
+ :attributes => {
841
+ 'sphinx_internal_id' => @alpha.id,
842
+ 'class_crc' => Alpha.to_crc32
843
+ }, :weight => 12
844
+ }]
845
+ }
846
+ Alpha.stub!(:find => [@alpha])
847
+ end
848
+
849
+ it "should yield the match and weight" do
850
+ search = ThinkingSphinx::Search.new
851
+ search.each_with_weighting do |obj, weight|
852
+ obj.should == @alpha
853
+ weight.should == 12
854
+ end
855
+ end
856
+ end
857
+
858
+ describe '.each_with_*' do
859
+ before :each do
860
+ @alpha = Alpha.new
861
+ @alpha.stub!(:id => 1, :read_attribute => 1)
862
+
863
+ @client.stub! :query => {
864
+ :matches => [{
865
+ :attributes => {
866
+ 'sphinx_internal_id' => @alpha.id,
867
+ 'class_crc' => Alpha.to_crc32,
868
+ '@geodist' => 101,
869
+ '@groupby' => 102,
870
+ '@count' => 103
871
+ }, :weight => 12
872
+ }]
873
+ }
874
+ Alpha.stub!(:find => [@alpha])
875
+
876
+ @search = ThinkingSphinx::Search.new
877
+ end
878
+
879
+ it "should yield geodist if requested" do
880
+ @search.each_with_geodist do |obj, distance|
881
+ obj.should == @alpha
882
+ distance.should == 101
883
+ end
884
+ end
885
+
886
+ it "should yield count if requested" do
887
+ @search.each_with_count do |obj, count|
888
+ obj.should == @alpha
889
+ count.should == 103
890
+ end
891
+ end
892
+
893
+ it "should yield groupby if requested" do
894
+ @search.each_with_groupby do |obj, group|
895
+ obj.should == @alpha
896
+ group.should == 102
897
+ end
898
+ end
899
+
900
+ it "should still use the array's each_with_index" do
901
+ @search.each_with_index do |obj, index|
902
+ obj.should == @alpha
903
+ index.should == 0
904
+ end
905
+ end
906
+ end
907
+
908
+ describe '#excerpt_for' do
909
+ before :each do
910
+ @client.stub!(:excerpts => ['excerpted string'])
911
+ @client.stub!(:query => {
912
+ :matches => [],
913
+ :words => {'one' => {}, 'two' => {}}
914
+ })
915
+ @search = ThinkingSphinx::Search.new(:classes => [Alpha])
916
+ end
917
+
918
+ it "should return the Sphinx excerpt value" do
919
+ @search.excerpt_for('string').should == 'excerpted string'
920
+ end
921
+
922
+ it "should use the given model's core index" do
923
+ @client.should_receive(:excerpts) do |options|
924
+ options[:index].should == 'alpha_core'
925
+ end
926
+
927
+ @search.excerpt_for('string')
928
+ end
929
+
930
+ it "should optionally take a second argument to allow for multi-model searches" do
931
+ @client.should_receive(:excerpts) do |options|
932
+ options[:index].should == 'beta_core'
933
+ end
934
+
935
+ @search.excerpt_for('string', Beta)
936
+ end
937
+
938
+ it "should join the words together" do
939
+ @client.should_receive(:excerpts) do |options|
940
+ options[:words].should == @search.results[:words].keys.join(' ')
941
+ end
942
+
943
+ @search.excerpt_for('string', Beta)
944
+ end
945
+ end
946
+
947
+ describe '#search' do
948
+ before :each do
949
+ @search = ThinkingSphinx::Search.new('word',
950
+ :conditions => {:field => 'field'},
951
+ :with => {:int => 5}
952
+ )
953
+ end
954
+
955
+ it "should return itself" do
956
+ @search.search.object_id.should == @search.object_id
957
+ end
958
+
959
+ it "should merge in arguments" do
960
+ @client.should_receive(:query) do |query, index, comments|
961
+ query.should == 'word more @field field'
962
+ end
963
+
964
+ @search.search('more').first
965
+ end
966
+
967
+ it "should merge conditions" do
968
+ @client.should_receive(:query) do |query, index, comments|
969
+ query.should match(/@name plato/)
970
+ query.should match(/@field field/)
971
+ end
972
+
973
+ @search.search(:conditions => {:name => 'plato'}).first
974
+ end
975
+
976
+ it "should merge filters" do
977
+ @search.search(:with => {:float => 1.5}).first
978
+
979
+ @client.filters.detect { |filter|
980
+ filter.attribute == 'float'
981
+ }.should_not be_nil
982
+ @client.filters.detect { |filter|
983
+ filter.attribute == 'int'
984
+ }.should_not be_nil
985
+ end
986
+ end
987
+ end
988
+
989
+ describe ThinkingSphinx::Search, "playing nice with Search model" do
990
+ it "should not conflict with models called Search" do
991
+ lambda { Search.find(:all) }.should_not raise_error
992
+ end
993
+ end