ultrasphinx 1.8 → 1.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data.tar.gz.sig +1 -3
  2. data/CHANGELOG +4 -0
  3. data/DEPLOYMENT_NOTES +7 -3
  4. data/Manifest +10 -0
  5. data/RAKE_TASKS +4 -2
  6. data/README +35 -18
  7. data/TODO +0 -1
  8. data/examples/default.base +24 -19
  9. data/lib/ultrasphinx.rb +1 -1
  10. data/lib/ultrasphinx/configure.rb +66 -25
  11. data/lib/ultrasphinx/core_extensions.rb +10 -1
  12. data/lib/ultrasphinx/is_indexed.rb +49 -6
  13. data/lib/ultrasphinx/search.rb +81 -69
  14. data/lib/ultrasphinx/search/internals.rb +61 -38
  15. data/lib/ultrasphinx/search/parser.rb +17 -7
  16. data/lib/ultrasphinx/ultrasphinx.rb +69 -16
  17. data/tasks/ultrasphinx.rake +47 -29
  18. data/test/config/ultrasphinx/test.base +50 -21
  19. data/test/integration/app/app/models/geo/address.rb +2 -1
  20. data/test/integration/app/app/models/person/user.rb +12 -3
  21. data/test/integration/app/app/models/seller.rb +2 -1
  22. data/test/integration/app/config/environment.rb +2 -0
  23. data/test/integration/app/config/ultrasphinx/default.base +16 -8
  24. data/test/integration/app/config/ultrasphinx/development.conf +319 -0
  25. data/test/integration/app/config/ultrasphinx/development.conf.canonical +152 -24
  26. data/test/integration/app/db/schema.rb +56 -0
  27. data/test/integration/app/test/fixtures/sellers.yml +1 -1
  28. data/test/integration/app/test/fixtures/users.yml +1 -1
  29. data/test/integration/app/test/unit/country_test.rb +8 -0
  30. data/test/integration/delta_test.rb +39 -0
  31. data/test/integration/search_test.rb +60 -1
  32. data/test/integration/spell_test.rb +6 -2
  33. data/test/profile/benchmark.rb +44 -0
  34. data/test/test_helper.rb +6 -1
  35. data/test/unit/parser_test.rb +21 -2
  36. data/ultrasphinx.gemspec +4 -4
  37. data/vendor/riddle/README +2 -2
  38. data/vendor/riddle/lib/riddle.rb +1 -1
  39. data/vendor/riddle/lib/riddle/client.rb +45 -10
  40. data/vendor/riddle/spec/fixtures/data/anchor.bin +0 -0
  41. data/vendor/riddle/spec/fixtures/data/any.bin +0 -0
  42. data/vendor/riddle/spec/fixtures/data/boolean.bin +0 -0
  43. data/vendor/riddle/spec/fixtures/data/distinct.bin +0 -0
  44. data/vendor/riddle/spec/fixtures/data/field_weights.bin +0 -0
  45. data/vendor/riddle/spec/fixtures/data/filter.bin +0 -0
  46. data/vendor/riddle/spec/fixtures/data/group.bin +0 -0
  47. data/vendor/riddle/spec/fixtures/data/index.bin +0 -0
  48. data/vendor/riddle/spec/fixtures/data/index_weights.bin +0 -0
  49. data/vendor/riddle/spec/fixtures/data/phrase.bin +0 -0
  50. data/vendor/riddle/spec/fixtures/data/rank_mode.bin +0 -0
  51. data/vendor/riddle/spec/fixtures/data/simple.bin +0 -0
  52. data/vendor/riddle/spec/fixtures/data/sort.bin +0 -0
  53. data/vendor/riddle/spec/fixtures/data/update_simple.bin +0 -0
  54. data/vendor/riddle/spec/fixtures/data/weights.bin +0 -0
  55. data/vendor/riddle/spec/fixtures/data_generator.php +130 -0
  56. data/vendor/riddle/spec/fixtures/sphinxapi.php +1066 -0
  57. data/vendor/riddle/spec/spec_helper.rb +1 -0
  58. data/vendor/riddle/spec/unit/client_spec.rb +18 -4
  59. metadata +12 -2
  60. metadata.gz.sig +0 -0
@@ -27,6 +27,15 @@ class Object
27
27
  end
28
28
  end
29
29
 
30
+ #class HashWithIndifferentAccess
31
+ # # Returns a regular Hash with all string keys. Much faster
32
+ # # than HWIA#merge.
33
+ # def _fast_merge(right)
34
+ # left = Hash[self]
35
+ # left.merge!(self.class.new(right))
36
+ # end
37
+ #end
38
+
30
39
  class Hash
31
40
  def _coerce_basic_types
32
41
  # XXX To remove
@@ -89,7 +98,7 @@ class String
89
98
  end
90
99
 
91
100
  def _interpolate(value)
92
- self.sub('?', value)
101
+ self.gsub('?', value)
93
102
  end
94
103
  end
95
104
 
@@ -58,7 +58,7 @@ The keys <tt>:facet</tt>, <tt>:sortable</tt>, <tt>:class_name</tt>, <tt>:associa
58
58
 
59
59
  == Concatenating several fields within one record
60
60
 
61
- Use the <tt>:concatenate</tt> key (MySQL only).
61
+ Use the <tt>:concatenate</tt> key.
62
62
 
63
63
  Accepts an array of option hashes.
64
64
 
@@ -88,6 +88,26 @@ Also, If you want to include a model that you don't have an actual ActiveRecord
88
88
 
89
89
  Ultrasphinx is not an object-relational mapper, and the association generation is intended to stay minimal--don't be afraid of <tt>:association_sql</tt>.
90
90
 
91
+ == Enabling delta indexing
92
+
93
+ Use the <tt>:delta</tt> key.
94
+
95
+ Accepts either <tt>true</tt>, or a hash with a <tt>:field</tt> key.
96
+
97
+ If you pass <tt>true</tt>, the <tt>updated_at</tt> column will be used for choosing the delta records, if it exists. If it doesn't exist, the entire table will be reindexed at every delta. Example:
98
+
99
+ :delta => true
100
+
101
+ If you need to use a non-default column name, use a hash:
102
+
103
+ :delta => {:field => 'created_at'}
104
+
105
+ Note that the column type must be time-comparable in the DB. Also note that faceting may return higher counts than actually exist on delta-indexed tables, and that sorting by string columns will not work well. These are both limitations of Sphinx's index merge scheme. You can perhaps mitigate the issues by only searching the main index for facets or sorts:
106
+
107
+ Ultrasphinx::Search.new(:query => "query", :indexes => Ultrasphinx::MAIN_INDEX)
108
+
109
+ The date range of the delta include is set in the <tt>.base</tt> file.
110
+
91
111
  = Examples
92
112
 
93
113
  == Complex configuration
@@ -110,6 +130,7 @@ Here's an example configuration using most of the options, taken from production
110
130
  {:association_name => 'comments', :field => 'body', :as => 'comments',
111
131
  :conditions => "comments.item_type = '#{base_class}'"}
112
132
  ],
133
+ :delta => {:field => 'published_at'},
113
134
  :conditions => self.live_condition_string
114
135
  end
115
136
 
@@ -137,16 +158,38 @@ If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you
137
158
  def self.is_indexed opts = {}
138
159
  opts = HashWithIndifferentAccess.new(opts)
139
160
 
140
- opts.assert_valid_keys ['fields', 'concatenate', 'conditions', 'include']
161
+ opts.assert_valid_keys ['fields', 'concatenate', 'conditions', 'include', 'delta']
162
+
163
+ # Single options
141
164
 
142
- Array(opts['fields']).each do |entry|
165
+ if opts['conditions']
166
+ # Do nothing
167
+ end
168
+
169
+ if opts['delta']
170
+ if opts['delta'] == true
171
+ opts['delta'] = {'field' => 'updated_at'}
172
+ elsif opts['delta'].is_a? String
173
+ opts['delta'] = {'field' => opts['delta']}
174
+ end
175
+ opts['delta'].stringify_keys!
176
+ opts['delta'].assert_valid_keys ['field']
177
+ end
178
+
179
+ # Enumerable options
180
+
181
+ opts['fields'] = Array(opts['fields'])
182
+ opts['concatenate'] = Array(opts['concatenate'])
183
+ opts['include'] = Array(opts['include'])
184
+
185
+ opts['fields'].each do |entry|
143
186
  if entry.is_a? Hash
144
187
  entry.stringify_keys!
145
188
  entry.assert_valid_keys ['field', 'as', 'facet', 'function_sql', 'sortable']
146
189
  end
147
190
  end
148
191
 
149
- Array(opts['concatenate']).each do |entry|
192
+ opts['concatenate'].each do |entry|
150
193
  entry.stringify_keys!
151
194
  entry.assert_valid_keys ['class_name', 'association_name', 'conditions', 'field', 'as', 'fields', 'association_sql', 'facet', 'function_sql', 'sortable']
152
195
  raise Ultrasphinx::ConfigurationError, "You can't mix regular concat and group concats" if entry['fields'] and (entry['field'] or entry['class_name'] or entry['association_name'])
@@ -155,11 +198,11 @@ If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you
155
198
  raise Ultrasphinx::ConfigurationError, "Regular concatenations should have multiple fields" if entry['fields'] and !entry['fields'].is_a?(Array)
156
199
  end
157
200
 
158
- Array(opts['include']).each do |entry|
201
+ opts['include'].each do |entry|
159
202
  entry.stringify_keys!
160
203
  entry.assert_valid_keys ['class_name', 'association_name', 'field', 'as', 'association_sql', 'facet', 'function_sql', 'sortable']
161
204
  end
162
-
205
+
163
206
  Ultrasphinx::MODEL_CONFIGURATION[self.name] = opts
164
207
  end
165
208
  end
@@ -24,7 +24,7 @@ Now, to run the query, call its <tt>run</tt> method. Your results will be availa
24
24
  The query string supports boolean operation, parentheses, phrases, and field-specific search. Query words are stemmed and joined by an implicit <tt>AND</tt> by default.
25
25
 
26
26
  * Valid boolean operators are <tt>AND</tt>, <tt>OR</tt>, and <tt>NOT</tt>.
27
- * Field-specific searches should be formatted as <tt>fieldname:contents</tt>. (This will only work for text fields. For numeric and date fields, see the <tt>'filters</tt> parameter, below.)
27
+ * Field-specific searches should be formatted as <tt>fieldname:contents</tt>. (This will only work for text fields. For numeric and date fields, see the <tt>:filters</tt> parameter, below.)
28
28
  * Phrases must be enclosed in double quotes.
29
29
 
30
30
  A Sphinx::SphinxInternalError will be raised on invalid queries. In general, queries can only be nested to one level.
@@ -42,10 +42,11 @@ The hash lets you customize internal aspects of the search.
42
42
  <tt>:weights</tt>:: A hash. Text-field names and associated query weighting. The default weight for every field is 1.0. Example: <tt>:weights => {'title' => 2.0}</tt>
43
43
  <tt>:filters</tt>:: A hash. Names of numeric or date fields and associated values. You can use a single value, an array of values, or a range. (See the bottom of the ActiveRecord::Base page for an example.)
44
44
  <tt>:facets</tt>:: An array of fields for grouping/faceting. You can access the returned facet values and their result counts with the <tt>facets</tt> method.
45
+ <tt>:indexes</tt>:: An array of indexes to search. Currently only <tt>Ultrasphinx::MAIN_INDEX</tt> and <tt>Ultrasphinx::DELTA_INDEX</tt> are available. Defaults to both; changing this is rarely needed.
45
46
 
46
47
  Note that you can set up your own query defaults in <tt>environment.rb</tt>:
47
48
 
48
- Ultrasphinx::Search.query_defaults = HashWithIndifferentAccess.new({
49
+ self.class.query_defaults = HashWithIndifferentAccess.new({
49
50
  :per_page => 10,
50
51
  :sort_mode => 'relevance',
51
52
  :weights => {'title' => 2.0}
@@ -53,9 +54,9 @@ Note that you can set up your own query defaults in <tt>environment.rb</tt>:
53
54
 
54
55
  = Advanced features
55
56
 
56
- == Cache_fu integration
57
+ == Interlock integration
57
58
 
58
- The <tt>get_cache</tt> method will be used to instantiate records for models that respond to it. Otherwise, <tt>find</tt> is used.
59
+ Ultrasphinx uses the <tt>find_all_by_id</tt> method to instantiate records. If you set <tt>with_finders: true</tt> in {Interlock's}[http://blog.evanweaver.com/files/doc/fauna/interlock] <tt>config/memcached.yml</tt>, Interlock overrides <tt>find_all_by_id</tt> with a caching version.
59
60
 
60
61
  == Will_paginate integration
61
62
 
@@ -100,6 +101,10 @@ Note that your database is never changed by anything Ultrasphinx does.
100
101
  :per_page => 20,
101
102
  :sort_by => nil,
102
103
  :sort_mode => 'relevance',
104
+ :indexes => [
105
+ MAIN_INDEX,
106
+ (DELTA_INDEX if Ultrasphinx.delta_index_present?)
107
+ ].compact,
103
108
  :weights => {},
104
109
  :class_names => [],
105
110
  :filters => {},
@@ -112,7 +117,8 @@ Note that your database is never changed by anything Ultrasphinx does.
112
117
  :chunk_separator => "...",
113
118
  :limit => 256,
114
119
  :around => 3,
115
- # Results should respond to one in each group of these, in precedence order, for the excerpting to fire
120
+ # Results should respond to one in each group of these, in precedence order, for the
121
+ # excerpting to fire
116
122
  :content_methods => [['title', 'name'], ['body', 'description', 'content'], ['metadata']]
117
123
  })
118
124
 
@@ -120,63 +126,41 @@ Note that your database is never changed by anything Ultrasphinx does.
120
126
  self.client_options ||= HashWithIndifferentAccess.new({
121
127
  :with_subtotals => false,
122
128
  :ignore_missing_records => false,
123
- :max_missing_records => 5, # Has no effect if :ignore_missing_records => false
129
+ # Has no effect if :ignore_missing_records => false
130
+ :max_missing_records => 5,
124
131
  :max_retries => 4,
125
132
  :retry_sleep_time => 0.5,
126
- :max_facets => 100,
127
- :finder_methods => ['get_cache', 'find']
133
+ :max_facets => 1000,
134
+ :max_matches_offset => 1000,
135
+ # Whether to add an accessor to each returned result that specifies its global rank in
136
+ # the search.
137
+ :with_global_rank => false,
138
+ # Which method names to try to use for loading records. You can define your own (for
139
+ # example, with :includes) and then attach it here. Each method must accept an Array
140
+ # of ids, but do not have to preserve order. If the class does not respond_to? any
141
+ # method name in the array, :find_all_by_id will be used.
142
+ :finder_methods => []
128
143
  })
129
144
 
130
145
  # Friendly sort mode mappings
131
- SPHINX_CLIENT_PARAMS = HashWithIndifferentAccess.new({
132
- :sort_mode => HashWithIndifferentAccess.new({
146
+ SPHINX_CLIENT_PARAMS = {
147
+ 'sort_mode' => {
133
148
  'relevance' => :relevance,
134
149
  'descending' => :attr_desc,
135
150
  'ascending' => :attr_asc,
136
151
  'time' => :time_segments,
137
152
  'extended' => :extended,
138
- })
139
- })
153
+ }
154
+ }
140
155
 
141
156
  INTERNAL_KEYS = ['parsed_query'] #:nodoc:
142
157
 
143
- def self.get_models_to_class_ids #:nodoc:
144
- # Reading the conf file makes sure that we are in sync with the actual Sphinx index,
145
- # not whatever you happened to change your models to most recently
146
- unless File.exist? CONF_PATH
147
- Ultrasphinx.say "configuration file not found for #{RAILS_ENV.inspect} environment"
148
- Ultrasphinx.say "please run 'rake ultrasphinx:configure'"
149
- else
150
- begin
151
- lines = open(CONF_PATH).readlines
158
+ MODELS_TO_IDS = Ultrasphinx.get_models_to_class_ids || {}
152
159
 
153
- sources = lines.select do |line|
154
- line =~ /^source \w/
155
- end.map do |line|
156
- line[/source ([\w\d_-]*)/, 1].gsub('__', '/').classify
157
- end
158
-
159
- ids = lines.select do |line|
160
- line =~ /^sql_query /
161
- end.map do |line|
162
- line[/(\d*) AS class_id/, 1].to_i
163
- end
164
-
165
- raise unless sources.size == ids.size
166
- Hash[*sources.zip(ids).flatten]
167
-
168
- rescue
169
- Ultrasphinx.say "#{CONF_PATH} file is corrupted"
170
- Ultrasphinx.say "please run 'rake ultrasphinx:configure'"
171
- end
172
-
173
- end
174
- end
175
-
176
- MODELS_TO_IDS = get_models_to_class_ids || {}
177
-
178
- MAX_MATCHES = DAEMON_SETTINGS["max_matches"].to_i
160
+ IDS_TO_MODELS = MODELS_TO_IDS.invert #:nodoc:
179
161
 
162
+ MAX_MATCHES = DAEMON_SETTINGS["max_matches"].to_i
163
+
180
164
  FACET_CACHE = {} #:nodoc:
181
165
 
182
166
  # Returns the options hash.
@@ -214,9 +198,11 @@ Note that your database is never changed by anything Ultrasphinx does.
214
198
  @response
215
199
  end
216
200
 
217
- # 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.
201
+ # Returns a hash of total result counts, scoped to each available model. Set <tt>Ultrasphinx::Search.client_options[:with_subtotals] = true</tt> to enable.
202
+ #
203
+ # The subtotals are implemented as a special type of facet.
218
204
  def subtotals
219
- raise UsageError, "Subtotals are not enabled" unless Ultrasphinx::Search.client_options['with_subtotals']
205
+ raise UsageError, "Subtotals are not enabled" unless self.class.client_options['with_subtotals']
220
206
  require_run
221
207
  @subtotals
222
208
  end
@@ -267,16 +253,25 @@ Note that your database is never changed by anything Ultrasphinx does.
267
253
  # Returns the global index position of the first result on this page.
268
254
  def offset
269
255
  (current_page - 1) * per_page
270
- end
256
+ end
271
257
 
272
258
  # Builds a new command-interface Search object.
273
259
  def initialize opts = {}
274
- opts = HashWithIndifferentAccess.new(opts)
275
- @options = Ultrasphinx::Search.query_defaults.merge(opts._deep_dup._coerce_basic_types)
276
260
 
261
+ # Change to normal hashes with String keys for speed
262
+ opts = Hash[HashWithIndifferentAccess.new(opts._deep_dup._coerce_basic_types)]
263
+ unless self.class.query_defaults.instance_of? Hash
264
+ self.class.query_defaults = Hash[self.class.query_defaults]
265
+ self.class.client_options = Hash[self.class.client_options]
266
+ self.class.excerpting_options = Hash[self.class.excerpting_options]
267
+ self.class.excerpting_options['content_methods'].map! {|ary| ary.map {|m| m.to_s}}
268
+ end
269
+
270
+ @options = self.class.query_defaults.merge(opts)
277
271
  @options['query'] = @options['query'].to_s
278
272
  @options['class_names'] = Array(@options['class_names'])
279
273
  @options['facets'] = Array(@options['facets'])
274
+ @options['indexes'] = Array(@options['indexes']).join(" ")
280
275
 
281
276
  raise UsageError, "Weights must be a Hash" unless @options['weights'].is_a? Hash
282
277
  raise UsageError, "Filters must be a Hash" unless @options['filters'].is_a? Hash
@@ -285,22 +280,30 @@ Note that your database is never changed by anything Ultrasphinx does.
285
280
 
286
281
  @results, @subtotals, @facets, @response = [], {}, {}, {}
287
282
 
288
- extra_keys = @options.keys - (SPHINX_CLIENT_PARAMS.merge(Ultrasphinx::Search.query_defaults).keys + INTERNAL_KEYS)
283
+ extra_keys = @options.keys - (self.class.query_defaults.keys + INTERNAL_KEYS)
289
284
  say "discarded invalid keys: #{extra_keys * ', '}" if extra_keys.any? and RAILS_ENV != "test"
290
285
  end
291
286
 
292
- # Run the search, filling results with an array of ActiveRecord objects. Set the parameter to false if you only want the ids returned.
287
+ # Run the search, filling results with an array of ActiveRecord objects. Set the parameter to false
288
+ # if you only want the ids returned.
293
289
  def run(reify = true)
294
290
  @request = build_request_with_options(@options)
295
291
 
296
292
  say "searching for #{@options.inspect}"
297
293
 
298
294
  perform_action_with_retries do
299
- @response = @request.query(parsed_query, UNIFIED_INDEX_NAME)
295
+ @response = @request.query(parsed_query, @options['indexes'])
300
296
  say "search returned #{total_entries}/#{response[:total_found].to_i} in #{time.to_f} seconds."
301
297
 
302
- if Ultrasphinx::Search.client_options['with_subtotals']
298
+ if self.class.client_options['with_subtotals']
303
299
  @subtotals = get_subtotals(@request, parsed_query)
300
+
301
+ # If the original query has a filter on this class, we will use its more accurate total rather the facet's
302
+ # less accurate total.
303
+ if @options['class_names'].size == 1
304
+ @subtotals[@options['class_names'].first] = response[:total_found]
305
+ end
306
+
304
307
  end
305
308
 
306
309
  Array(@options['facets']).each do |facet|
@@ -318,19 +321,22 @@ Note that your database is never changed by anything Ultrasphinx does.
318
321
  end
319
322
 
320
323
 
321
- # Overwrite the configured content accessors with excerpted and highlighted versions of themselves.
322
- # Runs run if it hasn't already been done. Please note that this does not change the @attributes
323
- # hash in the record; only the accessor.
324
+ # Overwrite the configured content attributes with excerpted and highlighted versions of themselves.
325
+ # Runs run if it hasn't already been done.
324
326
  def excerpt
325
327
 
326
328
  require_run
327
329
  return if results.empty?
328
330
 
329
- # See what fields each result might respond to for our excerpting
331
+ # See what fields in each result might respond to our excerptable methods
330
332
  results_with_content_methods = results.map do |result|
331
- [result] << Ultrasphinx::Search.excerpting_options['content_methods'].map do |methods|
332
- methods.detect { |x| result.respond_to? x }
333
- end
333
+ [result,
334
+ self.class.excerpting_options['content_methods'].map do |methods|
335
+ methods.detect do |this|
336
+ result.respond_to? this
337
+ end
338
+ end
339
+ ]
334
340
  end
335
341
 
336
342
  # Fetch the actual field contents
@@ -341,10 +347,11 @@ Note that your database is never changed by anything Ultrasphinx does.
341
347
  end.flatten
342
348
 
343
349
  excerpting_options = {
344
- :docs => docs,
345
- :index => UNIFIED_INDEX_NAME,
346
- :words => strip_query_commands(parsed_query)}
347
- Ultrasphinx::Search.excerpting_options.except('content_methods').each do |key, value|
350
+ :docs => docs,
351
+ :index => MAIN_INDEX, # http://www.sphinxsearch.com/forum/view.html?id=100
352
+ :words => strip_query_commands(parsed_query)
353
+ }
354
+ self.class.excerpting_options.except('content_methods').each do |key, value|
348
355
  # Riddle only wants symbols
349
356
  excerpting_options[key.to_sym] ||= value
350
357
  end
@@ -354,13 +361,18 @@ Note that your database is never changed by anything Ultrasphinx does.
354
361
  @request.excerpts(excerpting_options)
355
362
  end
356
363
 
357
- responses = responses.in_groups_of(Ultrasphinx::Search.excerpting_options['content_methods'].size)
364
+ responses = responses.in_groups_of(self.class.excerpting_options['content_methods'].size)
358
365
 
359
366
  results_with_content_methods.each_with_index do |result_and_methods, i|
360
367
  # Override the individual model accessors with the excerpted data
361
368
  result, methods = result_and_methods
362
369
  methods.each_with_index do |method, j|
363
- result._metaclass.send('define_method', method) { responses[i][j] } if method
370
+ data = responses[i][j]
371
+ if method
372
+ result._metaclass.send('define_method', method) { data }
373
+ attributes = result.instance_variable_get('@attributes')
374
+ attributes[method] = data if attributes[method]
375
+ end
364
376
  end
365
377
  end
366
378
 
@@ -17,7 +17,7 @@ module Ultrasphinx
17
17
  @match_mode = :extended # Force extended query mode
18
18
  @offset = opts['per_page'] * (opts['page'] - 1)
19
19
  @limit = opts['per_page']
20
- @max_matches = [@offset + @limit, MAX_MATCHES].min
20
+ @max_matches = [@offset + @limit + Ultrasphinx::Search.client_options['max_matches_offset'], MAX_MATCHES].min
21
21
  end
22
22
 
23
23
  # Sorting
@@ -40,9 +40,13 @@ module Ultrasphinx
40
40
  weights = opts['weights']
41
41
  if weights.any?
42
42
  # Order according to the field order for Sphinx, and set the missing fields to 1.0
43
- request.weights = (Fields.instance.types.select{|n,t| t == 'text'}.map(&:first).sort.inject([]) do |array, field|
44
- array << (weights[field] || 1.0)
45
- end)
43
+ ordered_weights = []
44
+ Fields.instance.types.map do |name, type|
45
+ name if type == 'text'
46
+ end.compact.sort.each do |name|
47
+ ordered_weights << (weights[name] || 1.0)
48
+ end
49
+ request.weights = ordered_weights
46
50
  end
47
51
 
48
52
  # Class names
@@ -126,12 +130,12 @@ module Ultrasphinx
126
130
  @group_clauses = '@count desc'
127
131
  @offset = 0
128
132
  @limit = Ultrasphinx::Search.client_options['max_facets']
129
- @max_matches = [@limit, MAX_MATCHES].min
133
+ @max_matches = [@limit + Ultrasphinx::Search.client_options['max_matches_offset'], MAX_MATCHES].min
130
134
  end
131
135
 
132
136
  # Run the query
133
137
  begin
134
- matches = request.query(query, UNIFIED_INDEX_NAME)[:matches]
138
+ matches = request.query(query, options['indexes'])[:matches]
135
139
  rescue DaemonError
136
140
  raise ConfigurationError, "Index seems out of date. Run 'rake ultrasphinx:index'"
137
141
  end
@@ -171,7 +175,7 @@ module Ultrasphinx
171
175
 
172
176
  # Concatenates might not work well
173
177
  type, configuration = nil, nil
174
- MODEL_CONFIGURATION[klass.name].except('conditions').each do |_type, values|
178
+ MODEL_CONFIGURATION[klass.name].except('conditions', 'delta').each do |_type, values|
175
179
  type = _type
176
180
  configuration = values.detect { |this_field| this_field['as'] == facet }
177
181
  break if configuration
@@ -192,16 +196,16 @@ module Ultrasphinx
192
196
  [configuration['field'], ""]
193
197
  when 'include'
194
198
  # XXX Only handles the basic case. No test coverage.
195
-
199
+
196
200
  table_alias = configuration['table_alias']
197
201
  association_model = if configuration['class_name']
198
202
  configuration['class_name'].constantize
199
203
  else
200
204
  get_association_model(klass, configuration)
201
205
  end
202
-
203
- ["table_alias.#{configuration['field']}",
204
- (configuration['association_sql'] or "LEFT OUTER JOIN #{association_model.table_name} AS table_alias ON table_alias.#{association_model.primary_key} = #{klass.table_name}.#{association_model.class_name.underscore}_id")
206
+
207
+ ["#{table_alias}.#{configuration['field']}",
208
+ (configuration['association_sql'] or "LEFT OUTER JOIN #{association_model.table_name} AS #{table_alias} ON #{table_alias}.#{klass.to_s.downcase}_id = #{klass.table_name}.#{association_model.primary_key}")
205
209
  ]
206
210
  when 'concatenate'
207
211
  # Wait for someone to complain before worrying about this
@@ -222,12 +226,13 @@ module Ultrasphinx
222
226
 
223
227
  # Inverse-modulus map the Sphinx ids to the table-specific ids
224
228
  def convert_sphinx_ids(sphinx_ids)
229
+ number_of_models = IDS_TO_MODELS.size
225
230
  sphinx_ids.sort_by do |item|
226
231
  item[:index]
227
232
  end.map do |item|
228
- class_name = MODELS_TO_IDS.invert[item[:doc] % MODELS_TO_IDS.size]
233
+ class_name = IDS_TO_MODELS[item[:doc] % number_of_models]
229
234
  raise DaemonError, "Impossible Sphinx document id #{item[:doc]} in query result" unless class_name
230
- [class_name, item[:doc] / MODELS_TO_IDS.size]
235
+ [class_name, item[:doc] / number_of_models]
231
236
  end
232
237
  end
233
238
 
@@ -235,39 +240,57 @@ module Ultrasphinx
235
240
  def reify_results(ids)
236
241
  results = []
237
242
 
238
- ids.each do |klass_name, id|
243
+ ids_hash = {}
244
+ ids.each do |class_name, id|
245
+ (ids_hash[class_name] ||= []) << id
246
+ end
239
247
 
240
- # What class and class method are we using to get the record?
241
- klass = klass_name.constantize
242
- finder = Ultrasphinx::Search.client_options['finder_methods'].detect do |method_name|
243
- klass.respond_to? method_name
244
- end
248
+ ids.map {|ary| ary.first}.uniq.each do |class_name|
249
+ klass = class_name.constantize
245
250
 
246
- # Load it
247
- begin
248
- # XXX Does not use Memcached's multiget, or MySQL's, for that matter
249
- record = klass.send(finder, id)
250
- raise ActiveRecord::RecordNotFound unless record
251
- rescue ActiveRecord::RecordNotFound => e
252
- if Ultrasphinx::Search.client_options['ignore_missing_records']
253
- say "warning; #{klass}.#{finder}(#{id}) returned RecordNotFound"
254
- else
255
- raise(e)
251
+ finder = (
252
+ Ultrasphinx::Search.client_options['finder_methods'].detect do |method_name|
253
+ klass.respond_to? method_name
254
+ end or
255
+ # XXX This default is kind of buried, but I'm not sure why you would need it to be
256
+ # configurable, since you can use ['finder_methods'].
257
+ "find_all_by_id"
258
+ )
259
+
260
+ records = klass.send(finder, ids_hash[class_name])
261
+
262
+ unless Ultrasphinx::Search.client_options['ignore_missing_records']
263
+ if records.size != ids_hash[class_name].size
264
+ missed_ids = ids_hash[class_name] - records.map(&:id)
265
+ msg = if missed_ids.size == 1
266
+ "Couldn't find #{class_name} with ID=#{missed_ids.first}"
267
+ else
268
+ "Couldn't find #{class_name.pluralize} with IDs: #{missed_ids.join(',')} (found #{records.size} results, but was looking for #{ids_hash[class_name].size})"
269
+ end
270
+ raise ActiveRecord::RecordNotFound, msg
256
271
  end
257
- end
272
+ end
258
273
 
259
- # Add it to the list. Cache_fu does funny things with returned record organization.
260
- results += record.is_a?(Hash) ? record.values : Array(record)
274
+ records.each do |record|
275
+ results[ids.index([class_name, record.id])] = record
276
+ end
261
277
  end
262
-
263
- # Add an accessor for absolute search rank for each record (does anyone use this?)
264
- results.each_with_index do |result, index|
265
- i = per_page * (current_page - 1) + index
266
- result._metaclass.send('define_method', 'result_index') { i }
278
+
279
+ # Add an accessor for global search rank for each record, if requested
280
+ if self.class.client_options['with_global_rank']
281
+ # XXX Nobody uses this
282
+ results.each_with_index do |result, index|
283
+ if result
284
+ global_index = per_page * (current_page - 1) + index
285
+ result.instance_variable_get('@attributes')['result_index'] = global_index
286
+ end
287
+ end
267
288
  end
268
289
 
290
+ results.compact!
291
+
269
292
  if ids.size - results.size > Ultrasphinx::Search.client_options['max_missing_records']
270
- # Never reached if Ultrasphinx::Search.client_options['ignore_missing_records'] is false
293
+ # Never reached if Ultrasphinx::Search.client_options['ignore_missing_records'] is false due to raise
271
294
  raise ConfigurationError, "Too many results for this query returned ActiveRecord::RecordNotFound. The index is probably out of date"
272
295
  end
273
296