ultrasphinx 1.5.3 → 1.6

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.
@@ -5,7 +5,7 @@
5
5
  crypted_password: "2fdefe5c83d80a03a828dd65e90cfff65f0fb42d043a254ca2cad6af968d0e15" #password
6
6
  email: <%= "user#{num}@test.com" %>
7
7
  salt: "1000"
8
- created_at: <%= (Time.now - 30).to_s :db %>
9
- updated_at: <%= Time.now.to_s :db %>
8
+ updated_at: <%= (time = Time.parse("Tue Oct 23 04:28:11")).to_s(:db) %>
9
+ created_at: <%= (time - 180).to_s(:db) %>
10
10
  deleted: <%= num == 41 ? true : false %>
11
11
  <% end %>
@@ -14,22 +14,42 @@ class SearchTest < Test::Unit::TestCase
14
14
  assert_equal 20, @s.results.size
15
15
  end
16
16
 
17
- def test_query_must_be_run
18
- @s = S.new
19
- assert_raises(E) { @s.total_entries }
20
- assert_raises(E) { @s.response }
21
- assert_raises(E) { @s.facets }
22
- assert_raises(E) { @s.results }
23
- end
24
-
25
- def test_subtotals
17
+ def test_with_subtotals_option
18
+ S.client_options['with_subtotals'] = true
26
19
  @s = S.new.run
27
20
  assert_equal @s.total_entries, @s.subtotals.values._sum
21
+ S.client_options['with_subtotals'] = false
22
+ end
23
+
24
+ def test_ignore_missing_records_option
25
+ @s = S.new(:per_page => 1).run
26
+ @record = @s.first
27
+ assert_equal 1, @s.size
28
+
29
+ @record.destroy
30
+
31
+ assert_raises(ActiveRecord::RecordNotFound) do
32
+ @s = S.new(:per_page => 1).run
33
+ end
34
+
35
+ S.client_options['ignore_missing_records'] = true
36
+ assert_nothing_raised do
37
+ @s = S.new(:per_page => 1).run
38
+ end
39
+ assert_equal 0, @s.size
40
+ assert_equal 1, @s.per_page
41
+
42
+ S.client_options['ignore_missing_records'] = false
43
+
44
+ # Re-insert the record... ugh
45
+ @new_record = @record.class.new(@record.attributes)
46
+ @new_record.id = @record.id
47
+ @new_record.save!
28
48
  end
29
49
 
30
50
  def test_query_retries_and_fails
31
51
  system("cd #{RAILS_ROOT}; rake ultrasphinx:daemon:stop &> /dev/null")
32
- assert_raises(Sphinx::SphinxConnectError) do
52
+ assert_raises(Ultrasphinx::DaemonError) do
33
53
  S.new.run
34
54
  end
35
55
  system("cd #{RAILS_ROOT}; rake ultrasphinx:daemon:start &> /dev/null")
@@ -77,11 +97,10 @@ class SearchTest < Test::Unit::TestCase
77
97
  end
78
98
 
79
99
  def test_sort_by_string
80
- # XXX waiting for feedback from Andrew; this seems like a Sphinx bug
81
- # assert_equal(
82
- # Seller.find(:all, :limit => 5, :order => 'mission_statement DESC').map(&:mission_statement),
83
- # S.new(:class_names => 'Seller', :sort_by => 'mission_statement', :sort_mode => 'descending', :per_page => 5).run.map(&:mission_statement)
84
- # )
100
+ assert_equal(
101
+ Seller.find(:all, :limit => 5, :order => 'mission_statement ASC').map(&:mission_statement),
102
+ S.new(:class_names => 'Seller', :sort_by => 'mission_statement', :sort_mode => 'ascending', :per_page => 5).run.map(&:mission_statement)
103
+ )
85
104
  end
86
105
 
87
106
  def test_filter
@@ -104,20 +123,19 @@ class SearchTest < Test::Unit::TestCase
104
123
  end
105
124
 
106
125
  def test_date_range_filter
107
- # XXX some strange boundary error in this one
108
126
  @first, @last = Seller.find(5).created_at, Seller.find(10).created_at
109
- @items = Seller.find(:all, :conditions => ['created_at => ? AND created_at <= ?', @last, @first]).sort_by(&:id)
127
+ @items = Seller.find(:all, :conditions => ['created_at >= ? AND created_at <= ?', @last, @first]).sort_by(&:id)
110
128
  @count = @items.size
111
129
 
112
130
  @search = S.new(:class_names => 'Seller', :filters => {'created_at' => @first..@last}).run.sort_by(&:id)
113
131
  assert_equal(@count, @search.size)
114
- assert_equal(@items.first, @search.first)
115
- assert_equal(@items.last, @search.last)
132
+ assert_equal(@items.first.created_at, @search.first.created_at)
133
+ assert_equal(@items.last.created_at, @search.last.created_at)
116
134
 
117
135
  assert_equal(@count,
118
136
  S.new(:class_names => 'Seller', :filters => {'created_at' => @last..@first}).run.size)
119
137
  assert_equal(@count,
120
- S.new(:class_names => 'Seller', :filters => {'created_at' => @last.strftime(STRFTIME)...@first.strftime(STRFTIME)}).run.size)
138
+ S.new(:class_names => 'Seller', :filters => {'created_at' => @last.strftime(STRFTIME)..@first.strftime(STRFTIME)}).run.size)
121
139
 
122
140
  assert_raises(Ultrasphinx::UsageError) do
123
141
  S.new(:class_names => 'Seller', :filters => {'created_at' => "bogus".."sugob"}).run.size
@@ -132,7 +150,7 @@ class SearchTest < Test::Unit::TestCase
132
150
  end
133
151
 
134
152
  def test_invalid_filter
135
- assert_raises(Sphinx::SphinxArgumentError) do
153
+ assert_raises(Ultrasphinx::UsageError) do
136
154
  S.new(:class_names => 'Seller', :filters => {'bogus' => 17}).run
137
155
  end
138
156
  end
@@ -140,14 +158,16 @@ class SearchTest < Test::Unit::TestCase
140
158
  def test_conditions
141
159
  @deleted_count = User.count(:conditions => {:deleted => true })
142
160
  assert_equal 1, @deleted_count
143
- assert_equal User.count - @deleted_count, S.new(:class_name => 'User').run.total_entries
161
+ assert_equal User.count - @deleted_count, S.new(:class_names => 'User').run.total_entries
144
162
  end
145
163
 
146
- # def test_mismatched_facet_configuration
147
- # assert_raises(Ultrasphinx::ConfigurationError) do
148
- # Ultrasphinx::Search.new(:facets => 'company_name').run
149
- # end
150
- # end
164
+ # def test_mismatched_facet_configuration
165
+ # # XXX Should be caught at configuration time. For now it's your own fault
166
+ # # if you do it and get confused.
167
+ # assert_raises(Ultrasphinx::ConfigurationError) do
168
+ # Ultrasphinx::Search.new(:facets => 'company_name').run
169
+ # end
170
+ # end
151
171
 
152
172
  def test_bogus_facet_name
153
173
  assert_raises(Ultrasphinx::UsageError) do
@@ -156,14 +176,21 @@ class SearchTest < Test::Unit::TestCase
156
176
  end
157
177
 
158
178
  def test_unconfigured_sortable_name
159
- assert_raises(Sphinx::SphinxInternalError) do
160
- S.new(:class_names => 'Seller', :sort_by => 'company_name',:per_page => 5).run
179
+ assert_raises(Ultrasphinx::UsageError) do
180
+ S.new(:class_names => 'Seller', :sort_by => 'company_name', :sort_mode => 'ascending', :per_page => 5).run
161
181
  end
162
182
  end
163
183
 
184
+ def test_sorting_by_field_with_relevance_order
185
+ assert_raises(Ultrasphinx::UsageError) do
186
+ # Defaults to :sort_mode => 'relevance'
187
+ S.new(:class_names => 'Seller', :sort_by => 'created_at', :per_page => 5).run
188
+ end
189
+ end
190
+
164
191
  def test_nonexistent_sortable_name
165
- assert_raises(Sphinx::SphinxInternalError) do
166
- S.new(:class_names => 'Seller', :sort_by => 'bogus',:per_page => 5).run
192
+ assert_raises(Ultrasphinx::UsageError) do
193
+ S.new(:class_names => 'Seller', :sort_by => 'bogus', :per_page => 5).run
167
194
  end
168
195
  end
169
196
 
@@ -175,7 +202,7 @@ class SearchTest < Test::Unit::TestCase
175
202
  def test_numeric_facet
176
203
  @user_id_count = Seller.find(:all).map(&:user_id).uniq.size
177
204
 
178
- @s = Ultrasphinx::Search.new(:class_name => 'Seller', :facets => 'user_id').run
205
+ @s = Ultrasphinx::Search.new(:class_names => 'Seller', :facets => 'user_id').run
179
206
  assert_equal @user_id_count, @s.facets['user_id'].size
180
207
 
181
208
  @s = Ultrasphinx::Search.new(:facets => 'user_id').run
@@ -193,25 +220,25 @@ class SearchTest < Test::Unit::TestCase
193
220
  end
194
221
 
195
222
  def test_float_facet
196
- @s = Ultrasphinx::Search.new(:class_name => 'Seller', :facets => 'capitalization').run
223
+ @s = Ultrasphinx::Search.new(:class_names => 'Seller', :facets => 'capitalization').run
197
224
  @s.facets['capitalization'].keys.each do |key|
198
- # XXX Requires full Sphinx 0.9.8 compatibility
225
+ # XXX http://www.sphinxsearch.com/forum/view.html?id=963
199
226
  # assert key.is_a?(Float)
200
227
  end
201
228
  end
202
229
 
203
- def test_association_sql
204
- # XXX
230
+ def test_table_aliasing_and_association_sql
231
+ assert_equal 2, Ultrasphinx::Search.new(:class_names => 'User', :query => 'company_two:replacement').run.size
205
232
  end
206
233
 
207
234
  def test_weights
208
- @unweighted = Ultrasphinx::Search.new(:query => 'seller1', :per_page => 1).run.first
209
- @weighted = Ultrasphinx::Search.new(:query => 'seller1', :weights => {'company' => 2}, :per_page => 1).run.first
210
- assert_not_equal @unweighted.class, @weighted.class
235
+ @unweighted = Ultrasphinx::Search.new(:query => 'seller16', :per_page => 1).run.first
236
+ @weighted = Ultrasphinx::Search.new(:query => 'seller16', :weights => {'company' => 2}, :per_page => 1).run.first
237
+ assert_not_equal @unweighted, @weighted
211
238
  end
212
239
 
213
240
  def test_excerpts
214
- @s = Ultrasphinx::Search.new(:query => 'seller10')
241
+ @s = Ultrasphinx::Search.new(:class_names => 'Seller', :query => 'seller10')
215
242
  @excerpted_item = @s.excerpt.first
216
243
  @item = @s.run.first
217
244
  assert_not_equal @item.name, @excerpted_item.name
@@ -5,6 +5,9 @@ puts ["Please make sure that test/integration/app/vendor/rails is symlinked",
5
5
  "to a Rails trunk checkout in order for the rake tasks to run properly."]
6
6
 
7
7
  Dir.chdir "#{File.dirname(__FILE__)}/integration/app/" do
8
+ Dir.chdir "vendor/plugins" do
9
+ system("rm ultrasphinx; ln -s ../../../../../ ultrasphinx")
10
+ end
8
11
  system("rake db:create")
9
12
  system("rake db:migrate db:fixtures:load")
10
13
  system("rake us:boot")
@@ -1,21 +1,28 @@
1
1
 
2
- # Gem::Specification for Ultrasphinx-1.5.3
2
+ # Gem::Specification for Ultrasphinx-1.6
3
3
  # Originally generated by Echoe
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = %q{ultrasphinx}
7
- s.version = "1.5.3"
8
- s.date = %q{2007-10-11}
9
- s.summary = %q{Ruby on Rails configurator and client to the Sphinx fulltext search engine.}
7
+ s.version = "1.6"
8
+
9
+ s.specification_version = 2 if s.respond_to? :specification_version=
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.authors = [""]
13
+ s.date = %q{2007-11-14}
14
+ s.description = %q{Ruby on Rails configurator and client to the Sphinx fulltext search engine.}
10
15
  s.email = %q{}
16
+ s.files = ["CHANGELOG", "examples/ap.multi", "examples/default.base", "init.rb", "lib/ultrasphinx/autoload.rb", "lib/ultrasphinx/configure.rb", "lib/ultrasphinx/core_extensions.rb", "lib/ultrasphinx/fields.rb", "lib/ultrasphinx/hex_to_int.sql", "lib/ultrasphinx/is_indexed.rb", "lib/ultrasphinx/search/internals.rb", "lib/ultrasphinx/search/parser.rb", "lib/ultrasphinx/search.rb", "lib/ultrasphinx/spell.rb", "lib/ultrasphinx/ultrasphinx.rb", "lib/ultrasphinx.rb", "LICENSE", "Manifest", "README", "tasks/ultrasphinx.rake", "test/config/ultrasphinx/test.base", "test/integration/app/app/controllers/addresses_controller.rb", "test/integration/app/app/controllers/application.rb", "test/integration/app/app/controllers/sellers_controller.rb", "test/integration/app/app/controllers/states_controller.rb", "test/integration/app/app/controllers/users_controller.rb", "test/integration/app/app/helpers/addresses_helper.rb", "test/integration/app/app/helpers/application_helper.rb", "test/integration/app/app/helpers/sellers_helper.rb", "test/integration/app/app/helpers/states_helper.rb", "test/integration/app/app/helpers/users_helper.rb", "test/integration/app/app/models/geo/address.rb", "test/integration/app/app/models/geo/state.rb", "test/integration/app/app/models/person/user.rb", "test/integration/app/app/models/seller.rb", "test/integration/app/app/views/addresses/edit.html.erb", "test/integration/app/app/views/addresses/index.html.erb", "test/integration/app/app/views/addresses/new.html.erb", "test/integration/app/app/views/addresses/show.html.erb", "test/integration/app/app/views/layouts/addresses.html.erb", "test/integration/app/app/views/layouts/sellers.html.erb", "test/integration/app/app/views/layouts/states.html.erb", "test/integration/app/app/views/layouts/users.html.erb", "test/integration/app/app/views/sellers/edit.html.erb", "test/integration/app/app/views/sellers/index.html.erb", "test/integration/app/app/views/sellers/new.html.erb", "test/integration/app/app/views/sellers/show.html.erb", "test/integration/app/app/views/states/edit.html.erb", "test/integration/app/app/views/states/index.html.erb", "test/integration/app/app/views/states/new.html.erb", "test/integration/app/app/views/states/show.html.erb", "test/integration/app/app/views/users/edit.html.erb", "test/integration/app/app/views/users/index.html.erb", "test/integration/app/app/views/users/new.html.erb", "test/integration/app/app/views/users/show.html.erb", "test/integration/app/config/boot.rb", "test/integration/app/config/database.yml", "test/integration/app/config/environment.rb", "test/integration/app/config/environments/development.rb", "test/integration/app/config/environments/production.rb", "test/integration/app/config/environments/test.rb", "test/integration/app/config/locomotive.yml", "test/integration/app/config/routes.rb", "test/integration/app/config/ultrasphinx/default.base", "test/integration/app/config/ultrasphinx/development.conf", "test/integration/app/config/ultrasphinx/development.conf.canonical", "test/integration/app/db/migrate/001_create_users.rb", "test/integration/app/db/migrate/002_create_sellers.rb", "test/integration/app/db/migrate/003_create_addresses.rb", "test/integration/app/db/migrate/004_create_states.rb", "test/integration/app/db/migrate/005_add_capitalization_to_seller.rb", "test/integration/app/db/migrate/006_add_deleted_to_user.rb", "test/integration/app/db/migrate/007_add_lat_and_long_to_address.rb", "test/integration/app/db/migrate/008_add_mission_statement_to_seller.rb", "test/integration/app/db/schema.rb", "test/integration/app/doc/README_FOR_APP", "test/integration/app/public/404.html", "test/integration/app/public/500.html", "test/integration/app/public/dispatch.cgi", "test/integration/app/public/dispatch.fcgi", "test/integration/app/public/dispatch.rb", "test/integration/app/public/favicon.ico", "test/integration/app/public/images/rails.png", "test/integration/app/public/index.html", "test/integration/app/public/javascripts/application.js", "test/integration/app/public/javascripts/controls.js", "test/integration/app/public/javascripts/dragdrop.js", "test/integration/app/public/javascripts/effects.js", "test/integration/app/public/javascripts/prototype.js", "test/integration/app/public/robots.txt", "test/integration/app/public/stylesheets/scaffold.css", "test/integration/app/Rakefile", "test/integration/app/README", "test/integration/app/script/about", "test/integration/app/script/breakpointer", "test/integration/app/script/console", "test/integration/app/script/destroy", "test/integration/app/script/generate", "test/integration/app/script/performance/benchmarker", "test/integration/app/script/performance/profiler", "test/integration/app/script/plugin", "test/integration/app/script/process/inspector", "test/integration/app/script/process/reaper", "test/integration/app/script/process/spawner", "test/integration/app/script/runner", "test/integration/app/script/server", "test/integration/app/test/fixtures/addresses.yml", "test/integration/app/test/fixtures/sellers.yml", "test/integration/app/test/fixtures/states.yml", "test/integration/app/test/fixtures/users.yml", "test/integration/app/test/functional/addresses_controller_test.rb", "test/integration/app/test/functional/sellers_controller_test.rb", "test/integration/app/test/functional/states_controller_test.rb", "test/integration/app/test/functional/users_controller_test.rb", "test/integration/app/test/test_helper.rb", "test/integration/app/test/unit/address_test.rb", "test/integration/app/test/unit/seller_test.rb", "test/integration/app/test/unit/state_test.rb", "test/integration/app/test/unit/user_test.rb", "test/integration/configure_test.rb", "test/integration/search_test.rb", "test/integration/server_test.rb", "test/integration/spell_test.rb", "test/setup.rb", "test/test_all.rb", "test/test_helper.rb", "test/unit/parser_test.rb", "TODO", "vendor/riddle/MIT-LICENSE", "vendor/riddle/riddle/client/filter.rb", "vendor/riddle/riddle/client/message.rb", "vendor/riddle/riddle/client/response.rb", "vendor/riddle/riddle/client.rb", "vendor/riddle/riddle.rb", "vendor/will_paginate/LICENSE", "ultrasphinx.gemspec"]
17
+ s.has_rdoc = true
11
18
  s.homepage = %q{http://blog.evanweaver.com/files/doc/fauna/ultrasphinx/}
19
+ s.require_paths = ["lib"]
12
20
  s.rubyforge_project = %q{fauna}
13
- s.description = %q{Ruby on Rails configurator and client to the Sphinx fulltext search engine.}
14
- s.has_rdoc = true
15
- s.authors = [""]
16
- s.files = ["CHANGELOG", "examples/ap.multi", "examples/default.base", "init.rb", "lib/ultrasphinx/autoload.rb", "lib/ultrasphinx/configure.rb", "lib/ultrasphinx/core_extensions.rb", "lib/ultrasphinx/fields.rb", "lib/ultrasphinx/hex_to_int.sql", "lib/ultrasphinx/is_indexed.rb", "lib/ultrasphinx/search/internals.rb", "lib/ultrasphinx/search/parser.rb", "lib/ultrasphinx/search.rb", "lib/ultrasphinx/spell.rb", "lib/ultrasphinx/ultrasphinx.rb", "lib/ultrasphinx.rb", "LICENSE", "Manifest", "README", "tasks/ultrasphinx.rake", "test/config/ultrasphinx/test.base", "test/integration/app/app/controllers/addresses_controller.rb", "test/integration/app/app/controllers/application.rb", "test/integration/app/app/controllers/sellers_controller.rb", "test/integration/app/app/controllers/states_controller.rb", "test/integration/app/app/controllers/users_controller.rb", "test/integration/app/app/helpers/addresses_helper.rb", "test/integration/app/app/helpers/application_helper.rb", "test/integration/app/app/helpers/sellers_helper.rb", "test/integration/app/app/helpers/states_helper.rb", "test/integration/app/app/helpers/users_helper.rb", "test/integration/app/app/models/geo/address.rb", "test/integration/app/app/models/geo/state.rb", "test/integration/app/app/models/person/user.rb", "test/integration/app/app/models/seller.rb", "test/integration/app/app/views/addresses/edit.html.erb", "test/integration/app/app/views/addresses/index.html.erb", "test/integration/app/app/views/addresses/new.html.erb", "test/integration/app/app/views/addresses/show.html.erb", "test/integration/app/app/views/layouts/addresses.html.erb", "test/integration/app/app/views/layouts/sellers.html.erb", "test/integration/app/app/views/layouts/states.html.erb", "test/integration/app/app/views/layouts/users.html.erb", "test/integration/app/app/views/sellers/edit.html.erb", "test/integration/app/app/views/sellers/index.html.erb", "test/integration/app/app/views/sellers/new.html.erb", "test/integration/app/app/views/sellers/show.html.erb", "test/integration/app/app/views/states/edit.html.erb", "test/integration/app/app/views/states/index.html.erb", "test/integration/app/app/views/states/new.html.erb", "test/integration/app/app/views/states/show.html.erb", "test/integration/app/app/views/users/edit.html.erb", "test/integration/app/app/views/users/index.html.erb", "test/integration/app/app/views/users/new.html.erb", "test/integration/app/app/views/users/show.html.erb", "test/integration/app/config/boot.rb", "test/integration/app/config/database.yml", "test/integration/app/config/environment.rb", "test/integration/app/config/environments/development.rb", "test/integration/app/config/environments/production.rb", "test/integration/app/config/environments/test.rb", "test/integration/app/config/locomotive.yml", "test/integration/app/config/routes.rb", "test/integration/app/config/ultrasphinx/default.base", "test/integration/app/config/ultrasphinx/development.conf", "test/integration/app/config/ultrasphinx/development.conf.canonical", "test/integration/app/db/migrate/001_create_users.rb", "test/integration/app/db/migrate/002_create_sellers.rb", "test/integration/app/db/migrate/003_create_addresses.rb", "test/integration/app/db/migrate/004_create_states.rb", "test/integration/app/db/migrate/005_add_capitalization_to_seller.rb", "test/integration/app/db/migrate/006_add_deleted_to_user.rb", "test/integration/app/db/migrate/007_add_lat_and_long_to_address.rb", "test/integration/app/db/migrate/008_add_mission_statement_to_seller.rb", "test/integration/app/db/schema.rb", "test/integration/app/doc/README_FOR_APP", "test/integration/app/public/404.html", "test/integration/app/public/500.html", "test/integration/app/public/dispatch.cgi", "test/integration/app/public/dispatch.fcgi", "test/integration/app/public/dispatch.rb", "test/integration/app/public/favicon.ico", "test/integration/app/public/images/rails.png", "test/integration/app/public/index.html", "test/integration/app/public/javascripts/application.js", "test/integration/app/public/javascripts/controls.js", "test/integration/app/public/javascripts/dragdrop.js", "test/integration/app/public/javascripts/effects.js", "test/integration/app/public/javascripts/prototype.js", "test/integration/app/public/robots.txt", "test/integration/app/public/stylesheets/scaffold.css", "test/integration/app/Rakefile", "test/integration/app/README", "test/integration/app/script/about", "test/integration/app/script/breakpointer", "test/integration/app/script/console", "test/integration/app/script/destroy", "test/integration/app/script/generate", "test/integration/app/script/performance/benchmarker", "test/integration/app/script/performance/profiler", "test/integration/app/script/plugin", "test/integration/app/script/process/inspector", "test/integration/app/script/process/reaper", "test/integration/app/script/process/spawner", "test/integration/app/script/runner", "test/integration/app/script/server", "test/integration/app/test/fixtures/addresses.yml", "test/integration/app/test/fixtures/sellers.yml", "test/integration/app/test/fixtures/states.yml", "test/integration/app/test/fixtures/users.yml", "test/integration/app/test/functional/addresses_controller_test.rb", "test/integration/app/test/functional/sellers_controller_test.rb", "test/integration/app/test/functional/states_controller_test.rb", "test/integration/app/test/functional/users_controller_test.rb", "test/integration/app/test/test_helper.rb", "test/integration/app/test/unit/address_test.rb", "test/integration/app/test/unit/seller_test.rb", "test/integration/app/test/unit/state_test.rb", "test/integration/app/test/unit/user_test.rb", "test/integration/configure_test.rb", "test/integration/search_test.rb", "test/integration/server_test.rb", "test/integration/spell_test.rb", "test/setup.rb", "test/test_all.rb", "test/test_helper.rb", "test/ts.multi", "test/unit/parser_test.rb", "TODO", "vendor/sphinx/init.rb", "vendor/sphinx/lib/client.rb", "vendor/sphinx/LICENSE", "vendor/sphinx/Rakefile", "vendor/sphinx/README", "vendor/will_paginate/LICENSE", "ultrasphinx.gemspec"]
21
+ s.rubygems_version = %q{0.9.4.6}
22
+ s.summary = %q{Ruby on Rails configurator and client to the Sphinx fulltext search engine.}
17
23
  s.test_files = ["test/test_all.rb"]
18
- s.add_dependency(%q<chronic>, ["> 0.0.0"])
24
+
25
+ s.add_dependency(%q<chronic>, [">= 0"])
19
26
  end
20
27
 
21
28
 
@@ -31,12 +38,6 @@ end
31
38
  # p.docs_host = "blog.evanweaver.com:~/www/bax/public/files/doc/"
32
39
  # p.rdoc_pattern = /is_indexed.rb|search.rb|spell.rb|ultrasphinx.rb|^README|TODO|CHANGELOG|^LICENSE/
33
40
  # p.dependencies = "chronic"
34
- # end
35
- #
36
- # require 'rcov/rcovtask'
37
- #
38
- # Rcov::RcovTask.new(:coverage) do |t|
39
- # t.test_files = FileList['test/integration/*.rb', 'test/unit/*.rb']
40
- # t.rcov_opts << '--include-file test\/integration\/app\/vendor\/plugins\/ultrasphinx\/lib\/.*\.rb'
41
- # t.verbose = true
41
+ # p.test_pattern = ["test/integration/*.rb", "test/unit/*.rb"]
42
+ # p.rcov_options << '--include-file test\/integration\/app\/vendor\/plugins\/ultrasphinx\/lib\/.*\.rb'
42
43
  # end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Pat Allan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,15 @@
1
+ require 'riddle/client'
2
+ require 'riddle/client/filter'
3
+ require 'riddle/client/message'
4
+ require 'riddle/client/response'
5
+
6
+ module Riddle #:nodoc:
7
+ module Version #:nodoc:
8
+ Major = 0
9
+ Minor = 9
10
+ Tiny = 8
11
+ Rev = 871
12
+
13
+ String = [Major, Minor, Tiny].join('.') + "r#{Rev}"
14
+ end
15
+ end
@@ -0,0 +1,409 @@
1
+ module Riddle
2
+ class VersionError < StandardError; end
3
+ class ResponseError < StandardError; end
4
+
5
+ # This class was heavily based on the existing Client API by Dmytro Shteflyuk
6
+ # and Alexy Kovyrin. Their code worked fine, I just wanted something a bit
7
+ # more Ruby-ish (ie. lowercase and underscored method names). I also have
8
+ # used a few helper classes, just to neaten things up.
9
+ #
10
+ # I do plan to release this part of the plugin as a standalone library at
11
+ # some point - if you're interested in using it, and are feeling impatient,
12
+ # feel free to hassle me.
13
+ #
14
+ class Client
15
+ Commands = {
16
+ :search => 0, # SEARCHD_COMMAND_SEARCH
17
+ :excerpt => 1, # SEARCHD_COMMAND_EXCERPT
18
+ :update => 2 # SEARCHD_COMMAND_UPDATE
19
+ }
20
+
21
+ Versions = {
22
+ :search => 0x10F, # VER_COMMAND_SEARCH
23
+ :excerpt => 0x100, # VER_COMMAND_EXCERPT
24
+ :update => 0x100 # VER_COMMAND_UPDATE
25
+ }
26
+
27
+ Statuses = {
28
+ :ok => 0, # SEARCHD_OK
29
+ :error => 1, # SEARCHD_ERROR
30
+ :retry => 2, # SEARCHD_RETRY
31
+ :warning => 3 # SEARCHD_WARNING
32
+ }
33
+
34
+ MatchModes = {
35
+ :all => 0, # SPH_MATCH_ALL
36
+ :any => 1, # SPH_MATCH_ANY
37
+ :phrase => 2, # SPH_MATCH_PHRASE
38
+ :boolean => 3, # SPH_MATCH_BOOLEAN
39
+ :extended => 4 # SPH_MATCH_EXTENDED
40
+ }
41
+
42
+ SortModes = {
43
+ :relevance => 0, # SPH_SORT_RELEVANCE
44
+ :attr_desc => 1, # SPH_SORT_ATTR_DESC
45
+ :attr_asc => 2, # SPH_SORT_ATTR_ASC
46
+ :time_segments => 3, # SPH_SORT_TIME_SEGMENTS
47
+ :extended => 4 # SPH_SORT_EXTENDED
48
+ }
49
+
50
+ AttributeTypes = {
51
+ :integer => 1, # SPH_ATTR_INTEGER
52
+ :timestamp => 2, # SPH_ATTR_TIMESTAMP
53
+ :ordinal => 3, # SPH_ATTR_ORDINAL
54
+ :bool => 4, # SPH_ATTR_BOOL
55
+ :float => 5, # SPH_ATTR_FLOAT
56
+ :multi => 0x40000000 # SPH_ATTR_MULTI
57
+ }
58
+
59
+ GroupFunctions = {
60
+ :day => 0, # SPH_GROUPBY_DAY
61
+ :week => 1, # SPH_GROUPBY_WEEK
62
+ :month => 2, # SPH_GROUPBY_MONTH
63
+ :year => 3, # SPH_GROUPBY_YEAR
64
+ :attr => 4, # SPH_GROUPBY_ATTR
65
+ :attrpair => 5 # SPH_GROUPBY_ATTRPAIR
66
+ }
67
+
68
+ FilterTypes = {
69
+ :values => 0, # SPH_FILTER_VALUES
70
+ :range => 1, # SPH_FILTER_RANGE
71
+ :float_range => 2 # SPH_FILTER_FLOATRANGE
72
+ }
73
+
74
+ attr_accessor :server, :port, :offset, :limit, :max_matches,
75
+ :match_mode, :sort_mode, :sort_by, :weights, :id_range, :filters,
76
+ :group_by, :group_function, :group_clause, :group_distinct, :cut_off,
77
+ :retry_count, :retry_delay, :anchor
78
+ attr_reader :queue
79
+
80
+ # Can instantiate with a specific server and port - otherwise it assumes
81
+ # defaults of localhost and 3312 respectively. All other settings can be
82
+ # accessed and changed via the attribute accessors.
83
+ def initialize(server=nil, port=nil)
84
+ @server = server || "localhost"
85
+ @port = port || 3312
86
+
87
+ # defaults
88
+ @offset = 0
89
+ @limit = 20
90
+ @max_matches = 1000
91
+ @match_mode = :all
92
+ @sort_mode = :relevance
93
+ @sort_by = ''
94
+ @weights = []
95
+ @id_range = 0..0
96
+ @filters = []
97
+ @group_by = ''
98
+ @group_function = :day
99
+ @group_clause = '@group desc'
100
+ @group_distinct = ''
101
+ @cut_off = 0
102
+ @retry_count = 0
103
+ @retry_delay = 0
104
+ @anchor = {}
105
+ # string keys are index names, integer values are weightings
106
+ @index_weights = {}
107
+
108
+ @queue = []
109
+ end
110
+
111
+ def set_anchor(lat_attr, lat, long_attr, long)
112
+ @anchor = {
113
+ :latitude_attribute => lat_attr,
114
+ :latitude => lat,
115
+ :longtitude_attribute => long_attr,
116
+ :longtitude => long
117
+ }
118
+ end
119
+
120
+ def append_query(search, index = '*')
121
+ @queue << query_message(search, index)
122
+ end
123
+
124
+ def run
125
+ response = Response.new request(:search, @queue)
126
+
127
+ results = @queue.collect do
128
+ result = {
129
+ :matches => [],
130
+ :fields => [],
131
+ :attributes => {},
132
+ :attribute_names => [],
133
+ :words => {}
134
+ }
135
+
136
+ result[:status] = response.next_int
137
+ case result[:status]
138
+ when Statuses[:warning]
139
+ result[:warning] = response.next
140
+ when Statuses[:error]
141
+ result[:error] = response.next
142
+ next result
143
+ end
144
+
145
+ result[:fields] = response.next_array
146
+
147
+ attributes = response.next_int
148
+ for i in 0...attributes
149
+ attribute_name = response.next
150
+ type = response.next_int
151
+
152
+ result[:attributes][attribute_name] = type
153
+ result[:attribute_names] << attribute_name
154
+ end
155
+
156
+ matches = response.next_int
157
+ is_64_bit = response.next_int
158
+ for i in 0...matches
159
+ doc = is_64_bit > 0 ? (response.next_int() << 32) + response.next_int : response.next_int
160
+ weight = response.next_int
161
+
162
+ result[:matches] << {:doc => doc, :weight => weight, :index => i, :attributes => {}}
163
+ result[:attribute_names].each do |attr|
164
+ case result[:attributes][attr]
165
+ when AttributeTypes[:float]
166
+ result[:matches].last[:attributes][attr] = response.next_float
167
+ when AttributeTypes[:multi]
168
+ result[:matches].last[:attributes][attr] = response.next_int_array
169
+ else
170
+ result[:matches].last[:attributes][attr] = response.next_int
171
+ end
172
+ end
173
+ end
174
+
175
+ result[:total] = response.next_int.to_i || 0
176
+ result[:total_found] = response.next_int.to_i || 0
177
+ result[:time] = ('%.3f' % (response.next_int / 1000.0)).to_f || 0.0
178
+
179
+ words = response.next_int
180
+ for i in 0...words
181
+ word = response.next
182
+ docs = response.next_int
183
+ hits = response.next_int
184
+ result[:words][word] = {:docs => docs, :hits => hits}
185
+ end
186
+
187
+ result
188
+ end
189
+
190
+ @queue.clear
191
+ results
192
+ end
193
+
194
+ # Query the Sphinx daemon - defaulting to all indexes, but you can specify
195
+ # a specific one if you wish. The search parameter should be a string
196
+ # following Sphinx's expectations.
197
+ def query(search, index = '*')
198
+ @queue << query_message(search, index)
199
+ self.run.first
200
+ end
201
+
202
+ # Grab excerpts from the indexes. As part of the options, you will need to
203
+ # define:
204
+ # * :docs
205
+ # * :words
206
+ # * :index
207
+ #
208
+ # Optional settings include:
209
+ # * :before_match (defaults to <span class="match">)
210
+ # * :after_match (defaults to </span>)
211
+ # * :chunk_separator (defaults to ' &#8230; ' - which is an HTML ellipsis)
212
+ # * :limit (defaults to 256)
213
+ # * :around (defaults to 5)
214
+ #
215
+ # The defaults differ from the official PHP client, as I've opted for
216
+ # semantic HTML markup.
217
+ def excerpts(options = {})
218
+ options[:index] ||= '*'
219
+ options[:before_match] ||= '<span class="match">'
220
+ options[:after_match] ||= '</span>'
221
+ options[:chunk_separator] ||= ' &#8230; ' # ellipsis
222
+ options[:limit] ||= 256
223
+ options[:around] ||= 5
224
+
225
+ response = Response.new request(:excerpt, excerpts_message(options))
226
+
227
+ options[:docs].collect { response.next }
228
+ end
229
+
230
+ # Update attributes
231
+ def update(index, attributes, values_by_doc)
232
+ response = Response.new request(
233
+ :update,
234
+ update_message(index, attributes, values_by_doc)
235
+ )
236
+
237
+ response.next_int
238
+ end
239
+
240
+ private
241
+
242
+ # Connects to the Sphinx daemon, and yields a socket to use. The socket is
243
+ # closed at the end of the block.
244
+ def connect(&block)
245
+ socket = TCPSocket.new @server, @port
246
+
247
+ # Checking version
248
+ version = socket.recv(4).unpack('N*').first
249
+ if version < 1
250
+ socket.close
251
+ raise VersionError, "Can only connect to searchd version 1.0 or better, not version #{version}"
252
+ end
253
+
254
+ # Send version
255
+ socket.send [1].pack('N'), 0
256
+
257
+ begin
258
+ yield socket
259
+ ensure
260
+ socket.close
261
+ end
262
+ end
263
+
264
+ # Send a collection of messages, for a command type (eg, search, excerpts,
265
+ # update), to the Sphinx daemon.
266
+ def request(command, messages)
267
+ response = ""
268
+ status = -1
269
+ version = 0
270
+ length = 0
271
+ message = Array(messages).join("")
272
+
273
+ connect do |socket|
274
+ case command
275
+ when :search
276
+ # Message length is +4 to account for the following count value for
277
+ # the number of messages (well, that's what I'm assuming).
278
+ socket.send [
279
+ Commands[command], Versions[command],
280
+ 4+message.length, messages.length
281
+ ].pack("nnNN") + message, 0
282
+ else
283
+ socket.send [
284
+ Commands[command], Versions[command], message.length
285
+ ].pack("nnN") + message, 0
286
+ end
287
+
288
+ header = socket.recv(8)
289
+ status, version, length = header.unpack('n2N')
290
+
291
+ while response.length < length
292
+ part = socket.recv(length - response.length)
293
+ response << part if part
294
+ end
295
+ end
296
+
297
+ if response.empty? || response.length != length
298
+ raise ResponseError, "No response from searchd (status: #{status}, version: #{version})"
299
+ end
300
+
301
+ case status
302
+ when Statuses[:ok]
303
+ if version < Versions[command]
304
+ puts format("searchd command v.%d.%d older than client (v.%d.%d)",
305
+ version >> 8, version & 0xff,
306
+ Versions[command] >> 8, Versions[command] & 0xff)
307
+ end
308
+ response
309
+ when Statuses[:warning]
310
+ length = response[0, 4].unpack('N*').first
311
+ puts response[4, length]
312
+ response[4 + length, response.length - 4 - length]
313
+ when Statuses[:error], Statuses[:retry]
314
+ raise ResponseError, "searchd error (status: #{status}): #{response[4, response.length - 4]}"
315
+ else
316
+ raise ResponseError, "Unknown searchd error (status: #{status})"
317
+ end
318
+ end
319
+
320
+ # Generation of the message to send to Sphinx for a search.
321
+ def query_message(search, index)
322
+ message = Message.new
323
+
324
+ # Mode, Limits, Sort Mode
325
+ message.append_ints @offset, @limit, MatchModes[@match_mode], SortModes[@sort_mode]
326
+ message.append_string @sort_by
327
+
328
+ # Query
329
+ message.append_string search
330
+
331
+ # Weights
332
+ message.append_int @weights.length
333
+ message.append_ints *@weights
334
+
335
+ # Index
336
+ message.append_string index
337
+
338
+ # ID Range
339
+ message.append_ints 0, @id_range.first, @id_range.last
340
+
341
+ # Filters
342
+ message.append_int @filters.length
343
+ @filters.each { |filter| message.append filter.query_message }
344
+
345
+ # Grouping
346
+ message.append_int GroupFunctions[@group_function]
347
+ message.append_string @group_by
348
+ message.append_int @max_matches
349
+ message.append_string @group_clause
350
+ message.append_ints @cut_off, @retry_count, @retry_delay
351
+ message.append_string @group_distinct
352
+
353
+ # Anchor Point
354
+ if @anchor.empty?
355
+ message.append_int 0
356
+ else
357
+ message.append_int 1
358
+ message.append_string @anchor[:latitude_attribute]
359
+ message.append_string @anchor[:longtitude_attribute]
360
+ message.append_floats @anchor[:latitude], @anchor[:longtitude]
361
+ end
362
+
363
+ # Per Index Weights
364
+ message.append_int @index_weights.length
365
+ @index_weights.each do |key,val|
366
+ message.append_string key
367
+ message.append_int val
368
+ end
369
+
370
+ message.to_s
371
+ end
372
+
373
+ # Generation of the message to send to Sphinx for an excerpts request.
374
+ def excerpts_message(options)
375
+ message = Message.new
376
+
377
+ message.append [0, 1].pack('N2') # mode = 0, flags = 1
378
+ message.append_string options[:index]
379
+ message.append_string options[:words]
380
+
381
+ # options
382
+ message.append_string options[:before_match]
383
+ message.append_string options[:after_match]
384
+ message.append_string options[:chunk_separator]
385
+ message.append_ints options[:limit], options[:around]
386
+
387
+ message.append_array options[:docs]
388
+
389
+ message.to_s
390
+ end
391
+
392
+ # Generation of the message to send to Sphinx to update attributes of a
393
+ # document.
394
+ def update_message(index, attributes, values_by_doc)
395
+ message = Message.new
396
+
397
+ message.append_string index
398
+ message.append_array attributes
399
+
400
+ message.append_int values_by_doc.length
401
+ values_by_doc.each do |key,values|
402
+ message.append_int key # document ID
403
+ message.append_ints *values # array of new values (integers)
404
+ end
405
+
406
+ message.to_s
407
+ end
408
+ end
409
+ end