warp-thinking-sphinx 1.2.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (190) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +144 -0
  3. data/VERSION.yml +4 -0
  4. data/features/a.rb +17 -0
  5. data/features/alternate_primary_key.feature +27 -0
  6. data/features/attribute_transformation.feature +22 -0
  7. data/features/attribute_updates.feature +33 -0
  8. data/features/datetime_deltas.feature +66 -0
  9. data/features/delayed_delta_indexing.feature +37 -0
  10. data/features/deleting_instances.feature +64 -0
  11. data/features/direct_attributes.feature +11 -0
  12. data/features/excerpts.feature +13 -0
  13. data/features/extensible_delta_indexing.feature +9 -0
  14. data/features/facets.feature +76 -0
  15. data/features/facets_across_model.feature +29 -0
  16. data/features/handling_edits.feature +92 -0
  17. data/features/retry_stale_indexes.feature +24 -0
  18. data/features/searching_across_models.feature +20 -0
  19. data/features/searching_by_model.feature +175 -0
  20. data/features/searching_with_find_arguments.feature +56 -0
  21. data/features/sphinx_detection.feature +25 -0
  22. data/features/sphinx_scopes.feature +35 -0
  23. data/features/step_definitions/alpha_steps.rb +3 -0
  24. data/features/step_definitions/beta_steps.rb +7 -0
  25. data/features/step_definitions/common_steps.rb +178 -0
  26. data/features/step_definitions/datetime_delta_steps.rb +15 -0
  27. data/features/step_definitions/delayed_delta_indexing_steps.rb +7 -0
  28. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  29. data/features/step_definitions/facet_steps.rb +92 -0
  30. data/features/step_definitions/find_arguments_steps.rb +36 -0
  31. data/features/step_definitions/gamma_steps.rb +15 -0
  32. data/features/step_definitions/scope_steps.rb +11 -0
  33. data/features/step_definitions/search_steps.rb +89 -0
  34. data/features/step_definitions/sphinx_steps.rb +31 -0
  35. data/features/sti_searching.feature +14 -0
  36. data/features/support/db/active_record.rb +40 -0
  37. data/features/support/db/database.example.yml +3 -0
  38. data/features/support/db/fixtures/alphas.rb +10 -0
  39. data/features/support/db/fixtures/authors.rb +1 -0
  40. data/features/support/db/fixtures/betas.rb +10 -0
  41. data/features/support/db/fixtures/boxes.rb +9 -0
  42. data/features/support/db/fixtures/categories.rb +1 -0
  43. data/features/support/db/fixtures/cats.rb +3 -0
  44. data/features/support/db/fixtures/comments.rb +24 -0
  45. data/features/support/db/fixtures/delayed_betas.rb +10 -0
  46. data/features/support/db/fixtures/developers.rb +29 -0
  47. data/features/support/db/fixtures/dogs.rb +3 -0
  48. data/features/support/db/fixtures/extensible_betas.rb +10 -0
  49. data/features/support/db/fixtures/gammas.rb +10 -0
  50. data/features/support/db/fixtures/people.rb +1001 -0
  51. data/features/support/db/fixtures/posts.rb +6 -0
  52. data/features/support/db/fixtures/robots.rb +14 -0
  53. data/features/support/db/fixtures/tags.rb +27 -0
  54. data/features/support/db/fixtures/thetas.rb +10 -0
  55. data/features/support/db/migrations/create_alphas.rb +7 -0
  56. data/features/support/db/migrations/create_animals.rb +5 -0
  57. data/features/support/db/migrations/create_authors.rb +3 -0
  58. data/features/support/db/migrations/create_authors_posts.rb +6 -0
  59. data/features/support/db/migrations/create_betas.rb +5 -0
  60. data/features/support/db/migrations/create_boxes.rb +5 -0
  61. data/features/support/db/migrations/create_categories.rb +3 -0
  62. data/features/support/db/migrations/create_comments.rb +10 -0
  63. data/features/support/db/migrations/create_delayed_betas.rb +17 -0
  64. data/features/support/db/migrations/create_developers.rb +9 -0
  65. data/features/support/db/migrations/create_extensible_betas.rb +5 -0
  66. data/features/support/db/migrations/create_gammas.rb +3 -0
  67. data/features/support/db/migrations/create_people.rb +13 -0
  68. data/features/support/db/migrations/create_posts.rb +5 -0
  69. data/features/support/db/migrations/create_robots.rb +5 -0
  70. data/features/support/db/migrations/create_taggings.rb +5 -0
  71. data/features/support/db/migrations/create_tags.rb +4 -0
  72. data/features/support/db/migrations/create_thetas.rb +5 -0
  73. data/features/support/db/mysql.rb +3 -0
  74. data/features/support/db/postgresql.rb +3 -0
  75. data/features/support/env.rb +6 -0
  76. data/features/support/lib/generic_delta_handler.rb +8 -0
  77. data/features/support/models/alpha.rb +10 -0
  78. data/features/support/models/animal.rb +5 -0
  79. data/features/support/models/author.rb +3 -0
  80. data/features/support/models/beta.rb +8 -0
  81. data/features/support/models/box.rb +8 -0
  82. data/features/support/models/cat.rb +3 -0
  83. data/features/support/models/category.rb +4 -0
  84. data/features/support/models/comment.rb +10 -0
  85. data/features/support/models/delayed_beta.rb +7 -0
  86. data/features/support/models/developer.rb +16 -0
  87. data/features/support/models/dog.rb +3 -0
  88. data/features/support/models/extensible_beta.rb +9 -0
  89. data/features/support/models/gamma.rb +5 -0
  90. data/features/support/models/person.rb +23 -0
  91. data/features/support/models/post.rb +20 -0
  92. data/features/support/models/robot.rb +8 -0
  93. data/features/support/models/tag.rb +3 -0
  94. data/features/support/models/tagging.rb +4 -0
  95. data/features/support/models/theta.rb +7 -0
  96. data/features/support/post_database.rb +43 -0
  97. data/features/support/z.rb +19 -0
  98. data/lib/thinking_sphinx.rb +212 -0
  99. data/lib/thinking_sphinx/active_record.rb +306 -0
  100. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  101. data/lib/thinking_sphinx/active_record/delta.rb +87 -0
  102. data/lib/thinking_sphinx/active_record/has_many_association.rb +28 -0
  103. data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
  104. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  105. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  106. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +136 -0
  107. data/lib/thinking_sphinx/association.rb +243 -0
  108. data/lib/thinking_sphinx/attribute.rb +340 -0
  109. data/lib/thinking_sphinx/class_facet.rb +15 -0
  110. data/lib/thinking_sphinx/configuration.rb +282 -0
  111. data/lib/thinking_sphinx/core/array.rb +7 -0
  112. data/lib/thinking_sphinx/core/string.rb +15 -0
  113. data/lib/thinking_sphinx/deltas.rb +30 -0
  114. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  115. data/lib/thinking_sphinx/deltas/default_delta.rb +68 -0
  116. data/lib/thinking_sphinx/deltas/delayed_delta.rb +30 -0
  117. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  118. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  119. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  120. data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
  121. data/lib/thinking_sphinx/excerpter.rb +22 -0
  122. data/lib/thinking_sphinx/facet.rb +125 -0
  123. data/lib/thinking_sphinx/facet_search.rb +134 -0
  124. data/lib/thinking_sphinx/field.rb +81 -0
  125. data/lib/thinking_sphinx/index.rb +99 -0
  126. data/lib/thinking_sphinx/index/builder.rb +286 -0
  127. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  128. data/lib/thinking_sphinx/property.rb +163 -0
  129. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  130. data/lib/thinking_sphinx/search.rb +708 -0
  131. data/lib/thinking_sphinx/search_methods.rb +421 -0
  132. data/lib/thinking_sphinx/source.rb +152 -0
  133. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  134. data/lib/thinking_sphinx/source/sql.rb +127 -0
  135. data/lib/thinking_sphinx/tasks.rb +165 -0
  136. data/rails/init.rb +14 -0
  137. data/spec/lib/thinking_sphinx/active_record/delta_spec.rb +130 -0
  138. data/spec/lib/thinking_sphinx/active_record/has_many_association_spec.rb +49 -0
  139. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
  140. data/spec/lib/thinking_sphinx/active_record_spec.rb +353 -0
  141. data/spec/lib/thinking_sphinx/association_spec.rb +239 -0
  142. data/spec/lib/thinking_sphinx/attribute_spec.rb +507 -0
  143. data/spec/lib/thinking_sphinx/configuration_spec.rb +268 -0
  144. data/spec/lib/thinking_sphinx/core/array_spec.rb +9 -0
  145. data/spec/lib/thinking_sphinx/core/string_spec.rb +9 -0
  146. data/spec/lib/thinking_sphinx/deltas/job_spec.rb +32 -0
  147. data/spec/lib/thinking_sphinx/excerpter_spec.rb +57 -0
  148. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  149. data/spec/lib/thinking_sphinx/facet_spec.rb +333 -0
  150. data/spec/lib/thinking_sphinx/field_spec.rb +154 -0
  151. data/spec/lib/thinking_sphinx/index/builder_spec.rb +455 -0
  152. data/spec/lib/thinking_sphinx/index/faux_column_spec.rb +30 -0
  153. data/spec/lib/thinking_sphinx/index_spec.rb +45 -0
  154. data/spec/lib/thinking_sphinx/rails_additions_spec.rb +203 -0
  155. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  156. data/spec/lib/thinking_sphinx/search_spec.rb +1101 -0
  157. data/spec/lib/thinking_sphinx/source_spec.rb +227 -0
  158. data/spec/lib/thinking_sphinx_spec.rb +162 -0
  159. data/tasks/distribution.rb +55 -0
  160. data/tasks/rails.rake +1 -0
  161. data/tasks/testing.rb +83 -0
  162. data/vendor/after_commit/LICENSE +20 -0
  163. data/vendor/after_commit/README +16 -0
  164. data/vendor/after_commit/Rakefile +22 -0
  165. data/vendor/after_commit/init.rb +8 -0
  166. data/vendor/after_commit/lib/after_commit.rb +45 -0
  167. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  168. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  169. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  170. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  171. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  172. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  173. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  174. data/vendor/riddle/lib/riddle.rb +30 -0
  175. data/vendor/riddle/lib/riddle/client.rb +635 -0
  176. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  177. data/vendor/riddle/lib/riddle/client/message.rb +66 -0
  178. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  179. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  180. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  181. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  182. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  183. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  184. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  185. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  186. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  187. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  188. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  189. data/vendor/riddle/lib/riddle/controller.rb +53 -0
  190. metadata +267 -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,1101 @@
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
+
923
+ it "should be aliased to each_with_group_and_count" do
924
+ search = ThinkingSphinx::Search.new
925
+ search.each_with_group_and_count do |obj, group, count|
926
+ obj.should == @alpha
927
+ group.should == 101
928
+ count.should == 5
929
+ end
930
+ end
931
+ end
932
+
933
+ describe '.each_with_weighting' do
934
+ before :each do
935
+ @alpha = Alpha.new
936
+ @alpha.stub!(:id => 1, :read_attribute => 1)
937
+
938
+ @client.stub! :query => {
939
+ :matches => [{
940
+ :attributes => {
941
+ 'sphinx_internal_id' => @alpha.id,
942
+ 'class_crc' => Alpha.to_crc32
943
+ }, :weight => 12
944
+ }]
945
+ }
946
+ Alpha.stub!(:find => [@alpha])
947
+ end
948
+
949
+ it "should yield the match and weight" do
950
+ search = ThinkingSphinx::Search.new
951
+ search.each_with_weighting do |obj, weight|
952
+ obj.should == @alpha
953
+ weight.should == 12
954
+ end
955
+ end
956
+ end
957
+
958
+ describe '.each_with_*' do
959
+ before :each do
960
+ @alpha = Alpha.new
961
+ @alpha.stub!(:id => 1, :read_attribute => 1)
962
+
963
+ @client.stub! :query => {
964
+ :matches => [{
965
+ :attributes => {
966
+ 'sphinx_internal_id' => @alpha.id,
967
+ 'class_crc' => Alpha.to_crc32,
968
+ '@geodist' => 101,
969
+ '@groupby' => 102,
970
+ '@count' => 103
971
+ }, :weight => 12
972
+ }]
973
+ }
974
+ Alpha.stub!(:find => [@alpha])
975
+
976
+ @search = ThinkingSphinx::Search.new
977
+ end
978
+
979
+ it "should yield geodist if requested" do
980
+ @search.each_with_geodist do |obj, distance|
981
+ obj.should == @alpha
982
+ distance.should == 101
983
+ end
984
+ end
985
+
986
+ it "should yield count if requested" do
987
+ @search.each_with_count do |obj, count|
988
+ obj.should == @alpha
989
+ count.should == 103
990
+ end
991
+ end
992
+
993
+ it "should yield groupby if requested" do
994
+ @search.each_with_groupby do |obj, group|
995
+ obj.should == @alpha
996
+ group.should == 102
997
+ end
998
+ end
999
+
1000
+ it "should still use the array's each_with_index" do
1001
+ @search.each_with_index do |obj, index|
1002
+ obj.should == @alpha
1003
+ index.should == 0
1004
+ end
1005
+ end
1006
+ end
1007
+
1008
+ describe '#excerpt_for' do
1009
+ before :each do
1010
+ @client.stub!(:excerpts => ['excerpted string'])
1011
+ @client.stub!(:query => {
1012
+ :matches => [],
1013
+ :words => {'one' => {}, 'two' => {}}
1014
+ })
1015
+ @search = ThinkingSphinx::Search.new(:classes => [Alpha])
1016
+ end
1017
+
1018
+ it "should return the Sphinx excerpt value" do
1019
+ @search.excerpt_for('string').should == 'excerpted string'
1020
+ end
1021
+
1022
+ it "should use the given model's core index" do
1023
+ @client.should_receive(:excerpts) do |options|
1024
+ options[:index].should == 'alpha_core'
1025
+ end
1026
+
1027
+ @search.excerpt_for('string')
1028
+ end
1029
+
1030
+ it "should optionally take a second argument to allow for multi-model searches" do
1031
+ @client.should_receive(:excerpts) do |options|
1032
+ options[:index].should == 'beta_core'
1033
+ end
1034
+
1035
+ @search.excerpt_for('string', Beta)
1036
+ end
1037
+
1038
+ it "should join the words together" do
1039
+ @client.should_receive(:excerpts) do |options|
1040
+ options[:words].should == @search.results[:words].keys.join(' ')
1041
+ end
1042
+
1043
+ @search.excerpt_for('string', Beta)
1044
+ end
1045
+
1046
+ it "should use the correct index in STI situations" do
1047
+ @client.should_receive(:excerpts) do |options|
1048
+ options[:index].should == 'person_core'
1049
+ end
1050
+
1051
+ @search.excerpt_for('string', Parent)
1052
+ end
1053
+ end
1054
+
1055
+ describe '#search' do
1056
+ before :each do
1057
+ @search = ThinkingSphinx::Search.new('word',
1058
+ :conditions => {:field => 'field'},
1059
+ :with => {:int => 5}
1060
+ )
1061
+ end
1062
+
1063
+ it "should return itself" do
1064
+ @search.search.object_id.should == @search.object_id
1065
+ end
1066
+
1067
+ it "should merge in arguments" do
1068
+ @client.should_receive(:query) do |query, index, comments|
1069
+ query.should == 'word more @field field'
1070
+ end
1071
+
1072
+ @search.search('more').first
1073
+ end
1074
+
1075
+ it "should merge conditions" do
1076
+ @client.should_receive(:query) do |query, index, comments|
1077
+ query.should match(/@name plato/)
1078
+ query.should match(/@field field/)
1079
+ end
1080
+
1081
+ @search.search(:conditions => {:name => 'plato'}).first
1082
+ end
1083
+
1084
+ it "should merge filters" do
1085
+ @search.search(:with => {:float => 1.5}).first
1086
+
1087
+ @client.filters.detect { |filter|
1088
+ filter.attribute == 'float'
1089
+ }.should_not be_nil
1090
+ @client.filters.detect { |filter|
1091
+ filter.attribute == 'int'
1092
+ }.should_not be_nil
1093
+ end
1094
+ end
1095
+ end
1096
+
1097
+ describe ThinkingSphinx::Search, "playing nice with Search model" do
1098
+ it "should not conflict with models called Search" do
1099
+ lambda { Search.find(:all) }.should_not raise_error
1100
+ end
1101
+ end