thinking-sphinx 1.2.12

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