ultrasphinx 1.5.3 → 1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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