ultrasphinx 1.5.3 → 1.6

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,5 +1,11 @@
1
1
 
2
- v1.5.3. 90% test coverage; support multiple spelling dictionaries per machine (configurable in the .base file); association_name key is right out; some bugfixes.
2
+ vTODO. Use Pat Allan's association configurator.
3
+
4
+ v1.6. API changes! Drop Sphinx 0.9.7 compatibility; switch to Pat Allan's 0.9.8 client plugin; remove legacy keynames; fix string sorting bug; improve error handling.
5
+
6
+ v1.5.4. Various things.
7
+
8
+ v1.5.3. 90% test coverage; support multiple spelling dictionaries per machine (configurable in the .base file); association_name key is right out.
3
9
 
4
10
  v1.5.2. Fix association reloading issue; support float attributes on Sphinx 0.9.8; fix date range filters; import and update sample app (Mark Lane); start a comprehensive integration suite.
5
11
 
data/Manifest CHANGED
@@ -124,12 +124,12 @@ test/integration/spell_test.rb
124
124
  test/setup.rb
125
125
  test/test_all.rb
126
126
  test/test_helper.rb
127
- test/ts.multi
128
127
  test/unit/parser_test.rb
129
128
  TODO
130
- vendor/sphinx/init.rb
131
- vendor/sphinx/lib/client.rb
132
- vendor/sphinx/LICENSE
133
- vendor/sphinx/Rakefile
134
- vendor/sphinx/README
129
+ vendor/riddle/MIT-LICENSE
130
+ vendor/riddle/riddle/client/filter.rb
131
+ vendor/riddle/riddle/client/message.rb
132
+ vendor/riddle/riddle/client/response.rb
133
+ vendor/riddle/riddle/client.rb
134
+ vendor/riddle/riddle.rb
135
135
  vendor/will_paginate/LICENSE
data/README CHANGED
@@ -5,14 +5,14 @@ Ruby on Rails configurator and client to the Sphinx full text search engine.
5
5
 
6
6
  == License
7
7
 
8
- Copyright 2007 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file. Some portions copyright Dmytro Shteflyuk and Alexey Kovyrin, distributed under the Ruby License, and used with permission. Some portions copyright PJ Hyett and Mislav Marohnić, distributed under the MIT license, and used with permission.
8
+ Copyright 2007 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file. Some portions copyright Pat Allan, distributed under the MIT license, and used with permission. Some portions copyright PJ Hyett and Mislav Marohnić, distributed under the MIT license, and used with permission.
9
9
 
10
10
  The public certificate for the gem is at http://rubyforge.org/frs/download.php/25331/evan_weaver-original-public_cert.pem.
11
11
 
12
12
  == Requirements
13
13
 
14
14
  * MySQL (or Postgres, experimental)
15
- * Sphinx 0.97 or greater
15
+ * Sphinx 0.9.8-dev r877 or greater
16
16
  * Rails 1.2.3 or greater
17
17
 
18
18
  == Features
@@ -43,7 +43,7 @@ And some other things.
43
43
 
44
44
  == Installation
45
45
 
46
- First, compile and install Sphinx itself (http://www.sphinxsearch.com).
46
+ First, compile and install Sphinx itself using the 0.9.8 development snapshot (http://www.sphinxsearch.com).
47
47
 
48
48
  You also need the <tt>chronic</tt> gem:
49
49
  sudo gem install chronic
data/TODO CHANGED
@@ -2,4 +2,5 @@
2
2
  * Finish unifying filters and textfields
3
3
  * Support exclude filters
4
4
  * Make sure filters can be set to nil
5
-
5
+ * Geolocation search
6
+ * Use Treetop for the query parser instead of regexes
@@ -53,6 +53,7 @@ index
53
53
  morphology = stem_en
54
54
  stopwords = # /path/to/stopwords.txt
55
55
  min_word_len = 1
56
+ enable_star = 1
56
57
  charset_type = utf-8 # or sbcs (Single Byte Character Set)
57
58
  charset_table = 0..9, A..Z->a..z, -, _, ., &, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F,U+C5->U+E5, U+E5, U+C4->U+E4, U+E4, U+D6->U+F6, U+F6, U+16B, U+0c1->a, U+0c4->a, U+0c9->e, U+0cd->i, U+0d3->o, U+0d4->o, U+0da->u, U+0dd->y, U+0e1->a, U+0e4->a, U+0e9->e, U+0ed->i, U+0f3->o, U+0f4->o, U+0fa->u, U+0fd->y, U+104->U+105, U+105, U+106->U+107, U+10c->c, U+10d->c, U+10e->d, U+10f->d, U+116->U+117, U+117, U+118->U+119, U+11a->e, U+11b->e, U+12E->U+12F, U+12F, U+139->l, U+13a->l, U+13d->l, U+13e->l, U+141->U+142, U+142, U+143->U+144, U+144,U+147->n, U+148->n, U+154->r, U+155->r, U+158->r, U+159->r, U+15A->U+15B, U+15B, U+160->s, U+160->U+161, U+161->s, U+164->t, U+165->t, U+16A->U+16B, U+16B, U+16e->u, U+16f->u, U+172->U+173, U+173, U+179->U+17A, U+17A, U+17B->U+17C, U+17C, U+17d->z, U+17e->z,
58
59
  }
@@ -10,7 +10,8 @@ if defined? RAILS_ENV and RAILS_ENV == "development"
10
10
  end
11
11
  end
12
12
 
13
- require "#{File.dirname(__FILE__)}/../vendor/sphinx/lib/client"
13
+ $LOAD_PATH << "#{File.dirname(__FILE__)}/../vendor/riddle/"
14
+ require 'riddle'
14
15
 
15
16
  require 'ultrasphinx/ultrasphinx'
16
17
  require 'ultrasphinx/core_extensions'
@@ -93,9 +93,8 @@ module Ultrasphinx
93
93
 
94
94
  column_strings = [
95
95
  "(#{klass.table_name}.#{klass.primary_key} * #{MODEL_CONFIGURATION.size} + #{class_id}) AS id",
96
- "#{class_id} AS class_id", "'#{klass.name}' AS class",
97
- "'#{EMPTY_SEARCHABLE}' AS empty_searchable"]
98
- remaining_columns = fields.types.keys - ["class", "class_id", "empty_searchable"]
96
+ "#{class_id} AS class_id", "'#{klass.name}' AS class"]
97
+ remaining_columns = fields.types.keys - ["class", "class_id"]
99
98
  [column_strings, [], condition_strings, remaining_columns]
100
99
  end
101
100
 
@@ -162,7 +161,7 @@ module Ultrasphinx
162
161
 
163
162
  def build_regular_fields(klass, fields, entries, column_strings, join_strings, remaining_columns)
164
163
  entries.to_a.each do |entry|
165
- source_string = "#{klass.table_name}.#{entry['field']}"
164
+ source_string = "#{entry['table']}.#{entry['field']}"
166
165
  column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
167
166
  end
168
167
 
@@ -179,17 +178,17 @@ module Ultrasphinx
179
178
  raise ConfigurationError, "Unknown association from #{klass} to #{entry['class_name']}" if not association and not entry['association_sql']
180
179
 
181
180
  join_strings = install_join_unless_association_sql(entry['association_sql'], nil, join_strings) do
182
- "LEFT OUTER JOIN #{join_klass.table_name} ON " +
181
+ "LEFT OUTER JOIN #{join_klass.table_name} AS #{entry['table']} ON " +
183
182
  if (macro = association.macro) == :belongs_to
184
- "#{join_klass.table_name}.#{join_klass.primary_key} = #{klass.table_name}.#{association.primary_key_name}"
183
+ "#{entry['table']}.#{join_klass.primary_key} = #{klass.table_name}.#{association.primary_key_name}"
185
184
  elsif macro == :has_one
186
- "#{klass.table_name}.#{klass.primary_key} = #{join_klass.table_name}.#{association.primary_key_name}"
185
+ "#{klass.table_name}.#{klass.primary_key} = #{entry['table']}.#{association.primary_key_name}"
187
186
  else
188
187
  raise ConfigurationError, "Unidentified association macro #{macro.inspect}. Please use the :association_sql key to manually specify the JOIN syntax."
189
188
  end
190
189
  end
191
190
 
192
- source_string = "#{join_klass.table_name}.#{entry['field']}"
191
+ source_string = "#{entry['table']}.#{entry['field']}"
193
192
  column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
194
193
  end
195
194
 
@@ -207,17 +206,17 @@ module Ultrasphinx
207
206
  join_strings = install_join_unless_association_sql(entry['association_sql'], nil, join_strings) do
208
207
  # XXX make sure foreign key is right for polymorphic relationships
209
208
  association = association_by_class_name(klass, entry['class_name'])
210
- "LEFT OUTER JOIN #{join_klass.table_name} ON #{klass.table_name}.#{klass.primary_key} = #{join_klass.table_name}.#{association.primary_key_name}" +
209
+ "LEFT OUTER JOIN #{join_klass.table_name} AS #{entry['table']} ON #{klass.table_name}.#{klass.primary_key} = #{entry['table']}.#{association.primary_key_name}" +
211
210
  (entry['conditions'] ? " AND (#{entry['conditions']})" : "")
212
211
  end
213
212
 
214
- source_string = "GROUP_CONCAT(#{join_klass.table_name}.#{entry['field']} SEPARATOR ' ')"
213
+ source_string = "GROUP_CONCAT(DISTINCT #{entry['table']}.#{entry['field']} SEPARATOR ' ')"
215
214
  column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
216
215
 
217
216
  elsif entry['fields']
218
217
  # regular concats
219
218
  source_string = "CONCAT_WS(' ', " + entry['fields'].map do |subfield|
220
- "#{klass.table_name}.#{subfield}"
219
+ "#{entry['table']}.#{subfield}"
221
220
  end.join(', ') + ")"
222
221
 
223
222
  column_strings, remaining_columns = install_field(fields, source_string, entry['as'], entry['function_sql'], entry['facet'], column_strings, remaining_columns)
@@ -62,6 +62,7 @@ end
62
62
  ### Filter type coercion methods
63
63
 
64
64
  class String
65
+ # XXX Not used enough to justify such a strange abstraction
65
66
  def _to_numeric
66
67
  zeroless = self.squeeze(" ").strip.sub(/^0+(\d)/, '\1')
67
68
  zeroless.sub!(/(\...*?)0+$/, '\1')
@@ -69,7 +70,7 @@ class String
69
70
  zeroless.to_i
70
71
  elsif zeroless.to_f.to_s == zeroless
71
72
  zeroless.to_f
72
- elsif date = Chronic.parse(self)
73
+ elsif date = Chronic.parse(self.gsub(/(\d)([^\d\:\s])/, '\1 \2')) # Improve Chronic's flexibility a little
73
74
  date.to_i
74
75
  else
75
76
  raise Ultrasphinx::UsageError, "#{self.inspect} could not be coerced into a numeric value"
@@ -18,9 +18,7 @@ This is a special singleton configuration class that stores the index field conf
18
18
  'float' => 'float',
19
19
  'boolean' => 'integer'
20
20
  }
21
-
22
- VERSIONS_REQUIRED = {'float' => '0.9.8'}
23
-
21
+
24
22
  attr_accessor :classes, :types
25
23
 
26
24
  def initialize
@@ -37,7 +35,6 @@ This is a special singleton configuration class that stores the index field conf
37
35
 
38
36
  def save_and_verify_type(field, new_type, string_sortable, klass)
39
37
  # Smoosh fields together based on their name in the Sphinx query schema
40
- check_version(new_type.to_s)
41
38
  field, new_type = field.to_s, TYPE_MAP[new_type.to_s]
42
39
 
43
40
  if types[field]
@@ -88,16 +85,6 @@ This is a special singleton configuration class that stores the index field conf
88
85
  end + " AS #{field}"
89
86
  end
90
87
 
91
- def check_version(field)
92
- # XXX Awkward location for the compatibility check
93
- if req = VERSIONS_REQUIRED[field]
94
- unless SPHINX_VERSION.include? req
95
- # Will we eventually need to check version ranges?
96
- Ultrasphinx.say "warning: '#{field}' type requires Sphinx #{req}, but you have #{SPHINX_VERSION}"
97
- end
98
- end
99
- end
100
-
101
88
  def configure(configuration)
102
89
 
103
90
  configuration.each do |model, options|
@@ -112,7 +99,9 @@ This is a special singleton configuration class that stores the index field conf
112
99
  # We destructively canonicize them back onto the configuration hash
113
100
  options['fields'] = options['fields'].to_a.map do |entry|
114
101
  entry = {'field' => entry} unless entry.is_a? Hash
115
- entry['as'] = entry['field'] unless entry['as']
102
+
103
+ extract_table_alias!(entry, klass)
104
+ extract_field_alias!(entry, klass)
116
105
 
117
106
  unless klass.columns_hash[entry['field']]
118
107
  Ultrasphinx.say "warning: field #{entry['field']} is not present in #{model}"
@@ -130,15 +119,18 @@ This is a special singleton configuration class that stores the index field conf
130
119
 
131
120
  # Joins are whatever they are in the target
132
121
  options['include'].to_a.each do |entry|
133
- entry['as'] = entry['field'] unless entry['as']
134
-
122
+ extract_table_alias!(entry, klass)
123
+ extract_field_alias!(entry, klass)
124
+
135
125
  save_and_verify_type(entry['as'] || entry['field'], entry['class_name'].constantize.columns_hash[entry['field']].type, entry['sortable'], klass)
136
126
  end
137
127
 
138
128
  # Regular concats are CHAR, group_concats are BLOB and need to be cast to CHAR
139
129
  options['concatenate'].to_a.each do |entry|
140
- save_and_verify_type(entry['as'], 'text', entry['sortable'], klass)
130
+ extract_table_alias!(entry, klass) # XXX Doesn't actually do anything useful
131
+ save_and_verify_type(entry['as'], 'text', entry['sortable'], klass)
141
132
  end
133
+
142
134
  rescue ActiveRecord::StatementInvalid
143
135
  Ultrasphinx.say "warning: model #{model} does not exist in the database yet"
144
136
  end
@@ -147,6 +139,25 @@ This is a special singleton configuration class that stores the index field conf
147
139
  self
148
140
  end
149
141
 
142
+ def extract_field_alias!(entry, klass)
143
+ unless entry['as']
144
+ entry['as'] = entry['field']
145
+ end
146
+ end
147
+
148
+ def extract_table_alias!(entry, klass)
149
+ unless entry['table']
150
+ # Getting run twice; don't know why
151
+ if entry['field'] and entry['field'].include? "."
152
+ # This field is referenced by a table alias
153
+ entry['table'], entry['field'] = entry['field'].split(".")
154
+ else
155
+ klass = entry['class_name'].constantize if entry['class_name']
156
+ entry['table'] = klass.table_name
157
+ end
158
+ end
159
+ end
160
+
150
161
  end
151
162
  end
152
163
 
@@ -97,13 +97,13 @@ Note that your database is never changed by anything Ultrasphinx does.
97
97
  self.query_defaults ||= HashWithIndifferentAccess.new({
98
98
  :query => nil,
99
99
  :page => 1,
100
- :class_names => nil,
101
100
  :per_page => 20,
102
- :sort_by => 'created_at',
101
+ :sort_by => nil,
103
102
  :sort_mode => 'relevance',
104
- :weights => nil,
105
- :filters => nil,
106
- :facets => nil
103
+ :weights => {},
104
+ :class_names => [],
105
+ :filters => {},
106
+ :facets => []
107
107
  })
108
108
 
109
109
  cattr_accessor :excerpting_options
@@ -112,38 +112,35 @@ Note that your database is never changed by anything Ultrasphinx does.
112
112
  :chunk_separator => "...",
113
113
  :limit => 256,
114
114
  :around => 3,
115
- # results should respond to one in each group of these, in precedence order, for the excerpting to fire
115
+ # Results should respond to one in each group of these, in precedence order, for the excerpting to fire
116
116
  :content_methods => [['title', 'name'], ['body', 'description', 'content'], ['metadata']]
117
117
  })
118
118
 
119
119
  cattr_accessor :client_options
120
120
  self.client_options ||= HashWithIndifferentAccess.new({
121
121
  :with_subtotals => false,
122
+ :ignore_missing_records => false,
122
123
  :max_retries => 4,
123
- :retry_sleep_time => 3,
124
+ :retry_sleep_time => 0.5,
124
125
  :max_facets => 100,
125
126
  :finder_methods => ['get_cache', 'find']
126
127
  })
127
128
 
128
- # mode to integer mappings
129
+ # Friendly sort mode mappings
129
130
  SPHINX_CLIENT_PARAMS = HashWithIndifferentAccess.new({
130
131
  :sort_mode => HashWithIndifferentAccess.new({
131
- 'relevance' => Sphinx::Client::SPH_SORT_RELEVANCE,
132
- 'descending' => Sphinx::Client::SPH_SORT_ATTR_DESC,
133
- 'ascending' => Sphinx::Client::SPH_SORT_ATTR_ASC,
134
- 'time' => Sphinx::Client::SPH_SORT_TIME_SEGMENTS,
135
- 'extended' => Sphinx::Client::SPH_SORT_EXTENDED,
136
- 'desc' => Sphinx::Client::SPH_SORT_ATTR_DESC, # legacy compatibility
137
- 'asc' => Sphinx::Client::SPH_SORT_ATTR_ASC
132
+ 'relevance' => :relevance,
133
+ 'descending' => :attr_desc,
134
+ 'ascending' => :attr_asc,
135
+ 'time' => :time_segments,
136
+ 'extended' => :extended,
138
137
  })
139
138
  })
140
139
 
141
- LEGACY_QUERY_KEYS = ['raw_filters', 'filter', 'weight', 'class_name'] #:nodoc:
142
-
143
140
  INTERNAL_KEYS = ['parsed_query'] #:nodoc:
144
141
 
145
142
  def self.get_models_to_class_ids #:nodoc:
146
- # reading the conf file makes sure that we are in sync with the actual sphinx index,
143
+ # Reading the conf file makes sure that we are in sync with the actual Sphinx index,
147
144
  # not whatever you happened to change your models to most recently
148
145
  unless File.exist? CONF_PATH
149
146
  Ultrasphinx.say "configuration file not found for #{RAILS_ENV.inspect} environment"
@@ -199,54 +196,45 @@ Note that your database is never changed by anything Ultrasphinx does.
199
196
 
200
197
  # Returns an array of result objects.
201
198
  def results
202
- run?(true)
199
+ require_run
203
200
  @results
204
201
  end
205
202
 
206
203
  # Returns the facet map for this query, if facets were used.
207
204
  def facets
208
- run?(true)
209
205
  raise UsageError, "No facet field was configured" unless @options['facets']
206
+ require_run
210
207
  @facets
211
208
  end
212
209
 
213
210
  # Returns the raw response from the Sphinx client.
214
211
  def response
215
- run?(true)
212
+ require_run
216
213
  @response
217
214
  end
218
215
 
219
- def class_name #:nodoc:
220
- # Legacy accessor
221
- @options['class_names']
222
- end
223
-
224
216
  # Returns a hash of total result counts, scoped to each available model. This requires extra queries against the search daemon right now. Set <tt>Ultrasphinx::Search.client_options[:with_subtotals] = true</tt> to enable the extra queries. Most of the overhead is in instantiating the AR result sets, so the performance hit is not usually significant.
225
217
  def subtotals
226
- run?(true)
227
- raise UsageError, "Subtotals are not enabled" unless self.class.client_options['with_subtotals']
218
+ raise UsageError, "Subtotals are not enabled" unless Ultrasphinx::Search.client_options['with_subtotals']
219
+ require_run
228
220
  @subtotals
229
221
  end
230
222
 
231
223
  # Returns the total result count.
232
224
  def total_entries
233
- run?(true)
234
- [response['total_found'] || 0, MAX_MATCHES].min
225
+ require_run
226
+ [response[:total_found] || 0, MAX_MATCHES].min
235
227
  end
236
228
 
237
229
  # Returns the response time of the query, in milliseconds.
238
230
  def time
239
- run?(true)
240
- response['time']
231
+ require_run
232
+ response[:time]
241
233
  end
242
234
 
243
235
  # Returns whether the query has been run.
244
- def run?(should_raise = false)
245
- if @response.blank? and should_raise
246
- raise UsageError, "Search has not yet been run" unless run?
247
- else
248
- !@response.blank?
249
- end
236
+ def run?
237
+ !@response.blank?
250
238
  end
251
239
 
252
240
  # Returns the current page number of the result set. (Page indexes begin at 1.)
@@ -261,8 +249,8 @@ Note that your database is never changed by anything Ultrasphinx does.
261
249
 
262
250
  # Returns the last available page number in the result set.
263
251
  def page_count
264
- run?(true)
265
- (total_entries / per_page) + (total_entries % per_page == 0 ? 0 : 1)
252
+ require_run
253
+ (total_entries / per_page.to_f).ceil
266
254
  end
267
255
 
268
256
  # Returns the previous page number.
@@ -283,65 +271,48 @@ Note that your database is never changed by anything Ultrasphinx does.
283
271
  # Builds a new command-interface Search object.
284
272
  def initialize opts = {}
285
273
  opts = HashWithIndifferentAccess.new(opts)
286
- @options = self.class.query_defaults.merge(opts._deep_dup._coerce_basic_types)
274
+ @options = Ultrasphinx::Search.query_defaults.merge(opts._deep_dup._coerce_basic_types)
287
275
 
288
- # Legacy compatibility
289
- @options['filters'] ||= @options['filter'] || @options['raw_filters'] || {}
290
- @options['class_names'] ||= @options['class_name']
291
- @options['weights'] ||= @options['weight']
292
-
293
- @options._delete(*LEGACY_QUERY_KEYS)
294
-
295
- # Coerce some special types
296
276
  @options['query'] = @options['query'].to_s
297
277
  @options['class_names'] = Array(@options['class_names'])
278
+ @options['facets'] = Array(@options['facets'])
279
+
280
+ raise UsageError, "Weights must be a Hash" unless @options['weights'].is_a? Hash
281
+ raise UsageError, "Filters must be a Hash" unless @options['filters'].is_a? Hash
298
282
 
299
- @options['parsed_query'] = if query.blank?
300
- "@empty_searchable #{EMPTY_SEARCHABLE}"
301
- else
302
- parse(query)
303
- end
283
+ @options['parsed_query'] = parse(query)
304
284
 
305
285
  @results, @subtotals, @facets, @response = [], {}, {}, {}
306
286
 
307
- extra_keys = @options.keys - (SPHINX_CLIENT_PARAMS.merge(self.class.query_defaults).keys + LEGACY_QUERY_KEYS + INTERNAL_KEYS)
287
+ extra_keys = @options.keys - (SPHINX_CLIENT_PARAMS.merge(Ultrasphinx::Search.query_defaults).keys + INTERNAL_KEYS)
308
288
  say "discarded invalid keys: #{extra_keys * ', '}" if extra_keys.any? and RAILS_ENV != "test"
309
289
  end
310
290
 
311
291
  # Run the search, filling results with an array of ActiveRecord objects. Set the parameter to false if you only want the ids returned.
312
292
  def run(reify = true)
313
293
  @request = build_request_with_options(@options)
314
- @paginate = nil # clear cache
315
- tries = 0
316
294
 
317
295
  say "searching for #{@options.inspect}"
318
296
 
319
- begin
320
- @response = @request.Query(parsed_query)
321
- say "search returned, error #{@request.GetLastError.inspect}, warning #{@request.GetLastWarning.inspect}, returned #{total_entries}/#{response['total_found']} in #{time} seconds."
322
-
323
- @subtotals = get_subtotals(@request, parsed_query) if self.class.client_options['with_subtotals']
297
+ perform_action_with_retries do
298
+ @response = @request.query(parsed_query, UNIFIED_INDEX_NAME)
299
+ say "search returned #{total_entries}/#{response[:total_found].to_i} in #{time.to_f} seconds."
300
+
301
+ if Ultrasphinx::Search.client_options['with_subtotals']
302
+ @subtotals = get_subtotals(@request, parsed_query)
303
+ end
324
304
 
325
305
  Array(@options['facets']).each do |facet|
326
306
  @facets[facet] = get_facets(@request, parsed_query, facet)
327
- end
328
-
329
- @results = response['matches']
307
+ end
330
308
 
331
- # if you don't reify, you'll have to do the modulus reversal yourself to get record ids
309
+ @results = convert_sphinx_ids(response[:matches])
332
310
  @results = reify_results(@results) if reify
333
-
334
- rescue Sphinx::SphinxConnectError, Sphinx::SphinxResponseError, Sphinx::SphinxTemporaryError, Errno::ECONNRESET, Errno::EPIPE => e
335
- if (tries += 1) <= self.class.client_options['max_retries']
336
- say "restarting query (#{tries} attempts already) (#{e})"
337
- sleep(self.class.client_options['retry_sleep_time']) if tries == self.class.client_options['max_retries']
338
- retry
339
- else
340
- say "query failed"
341
- raise Sphinx::SphinxConnectError, e.to_s
342
- end
343
- end
344
-
311
+
312
+ say "warning; #{response[:warning]}" if response[:warning]
313
+ raise UsageError, response[:error] if response[:error]
314
+
315
+ end
345
316
  self
346
317
  end
347
318
 
@@ -350,30 +321,38 @@ Note that your database is never changed by anything Ultrasphinx does.
350
321
  # Runs run if it hasn't already been done.
351
322
  def excerpt
352
323
 
353
- run unless run?
324
+ require_run
354
325
  return if results.empty?
355
326
 
356
- # see what fields each result might respond to for our excerpting
327
+ # See what fields each result might respond to for our excerpting
357
328
  results_with_content_methods = results.map do |result|
358
- [result] << self.class.excerpting_options['content_methods'].map do |methods|
329
+ [result] << Ultrasphinx::Search.excerpting_options['content_methods'].map do |methods|
359
330
  methods.detect { |x| result.respond_to? x }
360
331
  end
361
332
  end
362
333
 
363
- # fetch the actual field contents
364
- texts = results_with_content_methods.map do |result, methods|
334
+ # Fetch the actual field contents
335
+ docs = results_with_content_methods.map do |result, methods|
365
336
  methods.map do |method|
366
337
  method and strip_bogus_characters(result.send(method)) or ""
367
338
  end
368
339
  end.flatten
369
-
370
- # ship to sphinx to highlight and excerpt
371
- responses = @request.BuildExcerpts(
372
- texts,
373
- UNIFIED_INDEX_NAME,
374
- strip_query_commands(parsed_query),
375
- self.class.excerpting_options.except('content_methods')
376
- ).in_groups_of(self.class.excerpting_options['content_methods'].size)
340
+
341
+ excerpting_options = {
342
+ :docs => docs,
343
+ :index => UNIFIED_INDEX_NAME,
344
+ :words => strip_query_commands(parsed_query)}
345
+ Ultrasphinx::Search.excerpting_options.except('content_methods').each do |key, value|
346
+ # Riddle only wants symbols
347
+ excerpting_options[key.to_sym] ||= value
348
+ end
349
+
350
+ responses = perform_action_with_retries do
351
+ # Ship to Sphinx to highlight and excerpt
352
+ @request.excerpts(excerpting_options)
353
+ end
354
+
355
+ responses = responses.in_groups_of(Ultrasphinx::Search.excerpting_options['content_methods'].size)
377
356
 
378
357
  results_with_content_methods.each_with_index do |result_and_methods, i|
379
358
  # override the individual model accessors with the excerpted data
@@ -391,7 +370,7 @@ Note that your database is never changed by anything Ultrasphinx does.
391
370
  end
392
371
 
393
372
 
394
- # Delegates enumerable methods to @results, if possible. This allows us to behave directly like a WillPaginate::Collection. Failing that, we delegate to the options hash if a key is set. This lets us use the <tt>self</tt> directly in view helpers.
373
+ # Delegates enumerable methods to @results, if possible. This allows us to behave directly like a WillPaginate::Collection. Failing that, we delegate to the options hash if a key is set. This lets us use <tt>self</tt> directly in view helpers.
395
374
  def method_missing(*args, &block)
396
375
  if @results.respond_to? args.first
397
376
  @results.send(*args, &block)
@@ -406,5 +385,11 @@ Note that your database is never changed by anything Ultrasphinx does.
406
385
  Ultrasphinx.say msg
407
386
  end
408
387
 
388
+ private
389
+
390
+ def require_run
391
+ run unless run?
392
+ end
393
+
409
394
  end
410
395
  end