thinking-sphinx 1.2.12

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 (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