ultrasphinx 1.8 → 1.9
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +1 -3
- data/CHANGELOG +4 -0
- data/DEPLOYMENT_NOTES +7 -3
- data/Manifest +10 -0
- data/RAKE_TASKS +4 -2
- data/README +35 -18
- data/TODO +0 -1
- data/examples/default.base +24 -19
- data/lib/ultrasphinx.rb +1 -1
- data/lib/ultrasphinx/configure.rb +66 -25
- data/lib/ultrasphinx/core_extensions.rb +10 -1
- data/lib/ultrasphinx/is_indexed.rb +49 -6
- data/lib/ultrasphinx/search.rb +81 -69
- data/lib/ultrasphinx/search/internals.rb +61 -38
- data/lib/ultrasphinx/search/parser.rb +17 -7
- data/lib/ultrasphinx/ultrasphinx.rb +69 -16
- data/tasks/ultrasphinx.rake +47 -29
- data/test/config/ultrasphinx/test.base +50 -21
- data/test/integration/app/app/models/geo/address.rb +2 -1
- data/test/integration/app/app/models/person/user.rb +12 -3
- data/test/integration/app/app/models/seller.rb +2 -1
- data/test/integration/app/config/environment.rb +2 -0
- data/test/integration/app/config/ultrasphinx/default.base +16 -8
- data/test/integration/app/config/ultrasphinx/development.conf +319 -0
- data/test/integration/app/config/ultrasphinx/development.conf.canonical +152 -24
- data/test/integration/app/db/schema.rb +56 -0
- data/test/integration/app/test/fixtures/sellers.yml +1 -1
- data/test/integration/app/test/fixtures/users.yml +1 -1
- data/test/integration/app/test/unit/country_test.rb +8 -0
- data/test/integration/delta_test.rb +39 -0
- data/test/integration/search_test.rb +60 -1
- data/test/integration/spell_test.rb +6 -2
- data/test/profile/benchmark.rb +44 -0
- data/test/test_helper.rb +6 -1
- data/test/unit/parser_test.rb +21 -2
- data/ultrasphinx.gemspec +4 -4
- data/vendor/riddle/README +2 -2
- data/vendor/riddle/lib/riddle.rb +1 -1
- data/vendor/riddle/lib/riddle/client.rb +45 -10
- data/vendor/riddle/spec/fixtures/data/anchor.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/any.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/boolean.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/distinct.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/field_weights.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/filter.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/group.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/index.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/index_weights.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/phrase.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/rank_mode.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/simple.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/sort.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/update_simple.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/weights.bin +0 -0
- data/vendor/riddle/spec/fixtures/data_generator.php +130 -0
- data/vendor/riddle/spec/fixtures/sphinxapi.php +1066 -0
- data/vendor/riddle/spec/spec_helper.rb +1 -0
- data/vendor/riddle/spec/unit/client_spec.rb +18 -4
- metadata +12 -2
- 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.
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/ultrasphinx/search.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
==
|
57
|
+
== Interlock integration
|
57
58
|
|
58
|
-
|
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
|
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
|
-
|
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 =>
|
127
|
-
:
|
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 =
|
132
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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 - (
|
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
|
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,
|
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
|
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
|
322
|
-
# Runs run if it hasn't already been done.
|
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
|
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
|
332
|
-
|
333
|
-
|
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 =>
|
346
|
-
:words => strip_query_commands(parsed_query)
|
347
|
-
|
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(
|
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
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
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,
|
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.#{
|
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 =
|
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] /
|
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
|
-
|
243
|
+
ids_hash = {}
|
244
|
+
ids.each do |class_name, id|
|
245
|
+
(ids_hash[class_name] ||= []) << id
|
246
|
+
end
|
239
247
|
|
240
|
-
|
241
|
-
klass =
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
-
|
260
|
-
|
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
|
264
|
-
|
265
|
-
|
266
|
-
|
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
|
|