thinking-sphinx 2.0.6 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/HISTORY +157 -0
- data/lib/cucumber/thinking_sphinx/external_world.rb +12 -0
- data/lib/cucumber/thinking_sphinx/internal_world.rb +127 -0
- data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
- data/lib/thinking-sphinx.rb +1 -0
- data/lib/thinking_sphinx/action_controller.rb +31 -0
- data/lib/thinking_sphinx/active_record/attribute_updates.rb +53 -0
- data/lib/thinking_sphinx/active_record/collection_proxy.rb +40 -0
- data/lib/thinking_sphinx/active_record/collection_proxy_with_scopes.rb +27 -0
- data/lib/thinking_sphinx/active_record/delta.rb +65 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +37 -0
- data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +21 -0
- data/lib/thinking_sphinx/active_record/log_subscriber.rb +61 -0
- data/lib/thinking_sphinx/active_record/scopes.rb +110 -0
- data/lib/thinking_sphinx/active_record.rb +383 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +87 -0
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +62 -0
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +171 -0
- data/lib/thinking_sphinx/association.rb +229 -0
- data/lib/thinking_sphinx/attribute.rb +407 -0
- data/lib/thinking_sphinx/auto_version.rb +38 -0
- data/lib/thinking_sphinx/bundled_search.rb +44 -0
- data/lib/thinking_sphinx/class_facet.rb +20 -0
- data/lib/thinking_sphinx/configuration.rb +335 -0
- data/lib/thinking_sphinx/context.rb +77 -0
- data/lib/thinking_sphinx/core/string.rb +15 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
- data/lib/thinking_sphinx/deltas.rb +28 -0
- data/lib/thinking_sphinx/deploy/capistrano.rb +99 -0
- data/lib/thinking_sphinx/excerpter.rb +23 -0
- data/lib/thinking_sphinx/facet.rb +128 -0
- data/lib/thinking_sphinx/facet_search.rb +170 -0
- data/lib/thinking_sphinx/field.rb +98 -0
- data/lib/thinking_sphinx/index/builder.rb +312 -0
- data/lib/thinking_sphinx/index/faux_column.rb +118 -0
- data/lib/thinking_sphinx/index.rb +157 -0
- data/lib/thinking_sphinx/join.rb +37 -0
- data/lib/thinking_sphinx/property.rb +185 -0
- data/lib/thinking_sphinx/railtie.rb +46 -0
- data/lib/thinking_sphinx/search.rb +995 -0
- data/lib/thinking_sphinx/search_methods.rb +439 -0
- data/lib/thinking_sphinx/sinatra.rb +7 -0
- data/lib/thinking_sphinx/source/internal_properties.rb +51 -0
- data/lib/thinking_sphinx/source/sql.rb +157 -0
- data/lib/thinking_sphinx/source.rb +194 -0
- data/lib/thinking_sphinx/tasks.rb +132 -0
- data/lib/thinking_sphinx/test.rb +55 -0
- data/lib/thinking_sphinx/version.rb +3 -0
- data/lib/thinking_sphinx.rb +296 -0
- metadata +53 -4
@@ -0,0 +1,995 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module ThinkingSphinx
|
3
|
+
# Once you've got those indexes in and built, this is the stuff that
|
4
|
+
# matters - how to search! This class provides a generic search
|
5
|
+
# interface - which you can use to search all your indexed models at once.
|
6
|
+
# Most times, you will just want a specific model's results - to search and
|
7
|
+
# search_for_ids methods will do the job in exactly the same manner when
|
8
|
+
# called from a model.
|
9
|
+
#
|
10
|
+
class Search < Array
|
11
|
+
CoreMethods = %w( == class class_eval extend frozen? id instance_eval
|
12
|
+
instance_of? instance_values instance_variable_defined?
|
13
|
+
instance_variable_get instance_variable_set instance_variables is_a?
|
14
|
+
kind_of? member? method methods nil? object_id respond_to?
|
15
|
+
respond_to_missing? send should type )
|
16
|
+
SafeMethods = %w( partition private_methods protected_methods
|
17
|
+
public_methods send class )
|
18
|
+
|
19
|
+
instance_methods.select { |method|
|
20
|
+
method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
|
21
|
+
}.each { |method|
|
22
|
+
undef_method method
|
23
|
+
}
|
24
|
+
|
25
|
+
HashOptions = [:conditions, :with, :without, :with_all, :without_any]
|
26
|
+
ArrayOptions = [:classes, :without_ids]
|
27
|
+
|
28
|
+
attr_reader :args, :options
|
29
|
+
|
30
|
+
# Deprecated. Use ThinkingSphinx.search
|
31
|
+
def self.search(*args)
|
32
|
+
warn 'ThinkingSphinx::Search.search is deprecated. Please use ThinkingSphinx.search instead.'
|
33
|
+
ThinkingSphinx.search(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Deprecated. Use ThinkingSphinx.search_for_ids
|
37
|
+
def self.search_for_ids(*args)
|
38
|
+
warn 'ThinkingSphinx::Search.search_for_ids is deprecated. Please use ThinkingSphinx.search_for_ids instead.'
|
39
|
+
ThinkingSphinx.search_for_ids(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Deprecated. Use ThinkingSphinx.search_for_ids
|
43
|
+
def self.search_for_id(*args)
|
44
|
+
warn 'ThinkingSphinx::Search.search_for_id is deprecated. Please use ThinkingSphinx.search_for_id instead.'
|
45
|
+
ThinkingSphinx.search_for_id(*args)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Deprecated. Use ThinkingSphinx.count
|
49
|
+
def self.count(*args)
|
50
|
+
warn 'ThinkingSphinx::Search.count is deprecated. Please use ThinkingSphinx.count instead.'
|
51
|
+
ThinkingSphinx.count(*args)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Deprecated. Use ThinkingSphinx.facets
|
55
|
+
def self.facets(*args)
|
56
|
+
warn 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
|
57
|
+
ThinkingSphinx.facets(*args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.warn(message)
|
61
|
+
::ActiveSupport::Deprecation.warn message
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.bundle_searches(enum = nil)
|
65
|
+
bundle = ThinkingSphinx::BundledSearch.new
|
66
|
+
|
67
|
+
if enum.nil?
|
68
|
+
yield bundle
|
69
|
+
else
|
70
|
+
enum.each { |item| yield bundle, item }
|
71
|
+
end
|
72
|
+
|
73
|
+
bundle.searches
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.matching_fields(fields, bitmask)
|
77
|
+
matches = []
|
78
|
+
bitstring = bitmask.to_s(2).rjust(32, '0').reverse
|
79
|
+
|
80
|
+
fields.each_with_index do |field, index|
|
81
|
+
matches << field if bitstring[index, 1] == '1'
|
82
|
+
end
|
83
|
+
matches
|
84
|
+
end
|
85
|
+
|
86
|
+
def initialize(*args)
|
87
|
+
ThinkingSphinx.context.define_indexes
|
88
|
+
|
89
|
+
@array = []
|
90
|
+
@options = args.extract_options!
|
91
|
+
@args = args
|
92
|
+
|
93
|
+
add_default_scope unless options[:ignore_default]
|
94
|
+
|
95
|
+
populate if @options[:populate]
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_a
|
99
|
+
populate
|
100
|
+
@array
|
101
|
+
end
|
102
|
+
|
103
|
+
def freeze
|
104
|
+
populate
|
105
|
+
@array.freeze
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def as_json(*args)
|
110
|
+
populate
|
111
|
+
@array.as_json(*args)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Indication of whether the request has been made to Sphinx for the search
|
115
|
+
# query.
|
116
|
+
#
|
117
|
+
# @return [Boolean] true if the results have been requested.
|
118
|
+
#
|
119
|
+
def populated?
|
120
|
+
!!@populated
|
121
|
+
end
|
122
|
+
|
123
|
+
# Indication of whether the request resulted in an error from Sphinx.
|
124
|
+
#
|
125
|
+
# @return [Boolean] true if Sphinx reports query error
|
126
|
+
#
|
127
|
+
def error?
|
128
|
+
!!error
|
129
|
+
end
|
130
|
+
|
131
|
+
# The Sphinx-reported error, if any.
|
132
|
+
#
|
133
|
+
# @return [String, nil]
|
134
|
+
#
|
135
|
+
def error
|
136
|
+
populate
|
137
|
+
@results[:error]
|
138
|
+
end
|
139
|
+
|
140
|
+
# Indication of whether the request resulted in a warning from Sphinx.
|
141
|
+
#
|
142
|
+
# @return [Boolean] true if Sphinx reports query warning
|
143
|
+
#
|
144
|
+
def warning?
|
145
|
+
!!warning
|
146
|
+
end
|
147
|
+
|
148
|
+
# The Sphinx-reported warning, if any.
|
149
|
+
#
|
150
|
+
# @return [String, nil]
|
151
|
+
#
|
152
|
+
def warning
|
153
|
+
populate
|
154
|
+
@results[:warning]
|
155
|
+
end
|
156
|
+
|
157
|
+
# The query result hash from Riddle.
|
158
|
+
#
|
159
|
+
# @return [Hash] Raw Sphinx results
|
160
|
+
#
|
161
|
+
def results
|
162
|
+
populate
|
163
|
+
@results
|
164
|
+
end
|
165
|
+
|
166
|
+
def method_missing(method, *args, &block)
|
167
|
+
if is_scope?(method)
|
168
|
+
add_scope(method, *args, &block)
|
169
|
+
return self
|
170
|
+
elsif method == :search_count
|
171
|
+
merge_search one_class.search(*args), self.args, options
|
172
|
+
return scoped_count
|
173
|
+
elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
|
174
|
+
super
|
175
|
+
elsif !SafeMethods.include?(method.to_s)
|
176
|
+
populate
|
177
|
+
end
|
178
|
+
|
179
|
+
if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
|
180
|
+
each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
|
181
|
+
else
|
182
|
+
@array.send(method, *args, &block)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns true if the Search object or the underlying Array object respond
|
187
|
+
# to the requested method.
|
188
|
+
#
|
189
|
+
# @param [Symbol] method The method name
|
190
|
+
# @return [Boolean] true if either Search or Array responds to the method.
|
191
|
+
#
|
192
|
+
def respond_to?(method, include_private = false)
|
193
|
+
super || @array.respond_to?(method, include_private)
|
194
|
+
end
|
195
|
+
|
196
|
+
# The current page number of the result set. Defaults to 1 if no page was
|
197
|
+
# explicitly requested.
|
198
|
+
#
|
199
|
+
# @return [Integer]
|
200
|
+
#
|
201
|
+
def current_page
|
202
|
+
@options[:page].blank? ? 1 : @options[:page].to_i
|
203
|
+
end
|
204
|
+
|
205
|
+
def first_page?
|
206
|
+
current_page == 1
|
207
|
+
end
|
208
|
+
|
209
|
+
# Kaminari support
|
210
|
+
def page(page_number)
|
211
|
+
@options[:page] = page_number
|
212
|
+
self
|
213
|
+
end
|
214
|
+
|
215
|
+
# The next page number of the result set. If there are no more pages
|
216
|
+
# available, nil is returned.
|
217
|
+
#
|
218
|
+
# @return [Integer, nil]
|
219
|
+
#
|
220
|
+
def next_page
|
221
|
+
current_page >= total_pages ? nil : current_page + 1
|
222
|
+
end
|
223
|
+
|
224
|
+
def next_page?
|
225
|
+
!next_page.nil?
|
226
|
+
end
|
227
|
+
|
228
|
+
# The previous page number of the result set. If this is the first page,
|
229
|
+
# then nil is returned.
|
230
|
+
#
|
231
|
+
# @return [Integer, nil]
|
232
|
+
#
|
233
|
+
def previous_page
|
234
|
+
current_page == 1 ? nil : current_page - 1
|
235
|
+
end
|
236
|
+
|
237
|
+
# The amount of records per set of paged results. Defaults to 20 unless a
|
238
|
+
# specific page size is requested.
|
239
|
+
#
|
240
|
+
# @return [Integer]
|
241
|
+
#
|
242
|
+
def per_page
|
243
|
+
@options[:limit] ||= @options[:per_page]
|
244
|
+
@options[:limit] ||= 20
|
245
|
+
@options[:limit].to_i
|
246
|
+
end
|
247
|
+
# Kaminari support
|
248
|
+
alias_method :limit_value, :per_page
|
249
|
+
|
250
|
+
# Kaminari support
|
251
|
+
def per(limit)
|
252
|
+
@options[:limit] = limit
|
253
|
+
self
|
254
|
+
end
|
255
|
+
|
256
|
+
# The total number of pages available if the results are paginated.
|
257
|
+
#
|
258
|
+
# @return [Integer]
|
259
|
+
#
|
260
|
+
def total_pages
|
261
|
+
populate
|
262
|
+
return 0 if @results[:total].nil?
|
263
|
+
|
264
|
+
@total_pages ||= (@results[:total] / per_page.to_f).ceil
|
265
|
+
end
|
266
|
+
# Compatibility with kaminari and older versions of will_paginate
|
267
|
+
alias_method :page_count, :total_pages
|
268
|
+
alias_method :num_pages, :total_pages
|
269
|
+
|
270
|
+
# Query time taken
|
271
|
+
#
|
272
|
+
# @return [Integer]
|
273
|
+
#
|
274
|
+
def query_time
|
275
|
+
populate
|
276
|
+
return 0 if @results[:time].nil?
|
277
|
+
|
278
|
+
@query_time ||= @results[:time]
|
279
|
+
end
|
280
|
+
|
281
|
+
# The total number of search results available.
|
282
|
+
#
|
283
|
+
# @return [Integer]
|
284
|
+
#
|
285
|
+
def total_entries
|
286
|
+
populate
|
287
|
+
return 0 if @results[:total_found].nil?
|
288
|
+
|
289
|
+
@total_entries ||= @results[:total_found]
|
290
|
+
end
|
291
|
+
|
292
|
+
# Compatibility with kaminari
|
293
|
+
alias_method :total_count, :total_entries
|
294
|
+
|
295
|
+
# The current page's offset, based on the number of records per page.
|
296
|
+
# Or explicit :offset if given.
|
297
|
+
#
|
298
|
+
# @return [Integer]
|
299
|
+
#
|
300
|
+
def offset
|
301
|
+
@options[:offset] || ((current_page - 1) * per_page)
|
302
|
+
end
|
303
|
+
|
304
|
+
def indexes
|
305
|
+
return options[:index] if options[:index]
|
306
|
+
return '*' if classes.empty?
|
307
|
+
|
308
|
+
classes.collect { |klass|
|
309
|
+
klass.sphinx_index_names
|
310
|
+
}.flatten.uniq.join(',')
|
311
|
+
end
|
312
|
+
|
313
|
+
def each_with_groupby_and_count(&block)
|
314
|
+
populate
|
315
|
+
results[:matches].each_with_index do |match, index|
|
316
|
+
yield self[index],
|
317
|
+
match[:attributes]["@groupby"],
|
318
|
+
match[:attributes]["@count"]
|
319
|
+
end
|
320
|
+
end
|
321
|
+
alias_method :each_with_group_and_count, :each_with_groupby_and_count
|
322
|
+
|
323
|
+
def each_with_weighting(&block)
|
324
|
+
populate
|
325
|
+
results[:matches].each_with_index do |match, index|
|
326
|
+
yield self[index], match[:weight]
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def each_with_match(&block)
|
331
|
+
populate
|
332
|
+
results[:matches].each_with_index do |match, index|
|
333
|
+
yield self[index], match
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def excerpt_for(string, model = nil)
|
338
|
+
if model.nil? && one_class
|
339
|
+
model ||= one_class
|
340
|
+
end
|
341
|
+
|
342
|
+
populate
|
343
|
+
|
344
|
+
index = options[:index] || "#{model.core_index_names.first}"
|
345
|
+
client.excerpts(
|
346
|
+
{
|
347
|
+
:docs => [string.to_s],
|
348
|
+
:words => results[:words].keys.join(' '),
|
349
|
+
:index => index.split(',').first.strip
|
350
|
+
}.merge(options[:excerpt_options] || {})
|
351
|
+
).first
|
352
|
+
end
|
353
|
+
|
354
|
+
def search(*args)
|
355
|
+
args << args.extract_options!.merge(:ignore_default => true)
|
356
|
+
merge_search ThinkingSphinx::Search.new(*args), self.args, options
|
357
|
+
self
|
358
|
+
end
|
359
|
+
|
360
|
+
def search_for_ids(*args)
|
361
|
+
args << args.extract_options!.merge(
|
362
|
+
:ignore_default => true,
|
363
|
+
:ids_only => true
|
364
|
+
)
|
365
|
+
merge_search ThinkingSphinx::Search.new(*args), self.args, options
|
366
|
+
self
|
367
|
+
end
|
368
|
+
|
369
|
+
def facets(*args)
|
370
|
+
options = args.extract_options!
|
371
|
+
merge_search self, args, options
|
372
|
+
args << options
|
373
|
+
|
374
|
+
ThinkingSphinx::FacetSearch.new(*args)
|
375
|
+
end
|
376
|
+
|
377
|
+
def client
|
378
|
+
client = options[:client] || config.client
|
379
|
+
|
380
|
+
prepare client
|
381
|
+
end
|
382
|
+
|
383
|
+
def append_to(client)
|
384
|
+
prepare client
|
385
|
+
client.append_query query, indexes, comment
|
386
|
+
client.reset
|
387
|
+
end
|
388
|
+
|
389
|
+
def populate_from_queue(results)
|
390
|
+
return if @populated
|
391
|
+
@populated = true
|
392
|
+
@results = results
|
393
|
+
|
394
|
+
compose_results
|
395
|
+
end
|
396
|
+
|
397
|
+
private
|
398
|
+
|
399
|
+
def config
|
400
|
+
ThinkingSphinx::Configuration.instance
|
401
|
+
end
|
402
|
+
|
403
|
+
def populate
|
404
|
+
return if @populated
|
405
|
+
@populated = true
|
406
|
+
|
407
|
+
retry_on_stale_index do
|
408
|
+
begin
|
409
|
+
log query do
|
410
|
+
@results = client.query query, indexes, comment
|
411
|
+
end
|
412
|
+
total = @results[:total_found].to_i
|
413
|
+
log "Found #{total} result#{'s' unless total == 1}"
|
414
|
+
|
415
|
+
log "Sphinx Daemon returned warning: #{warning}" if warning?
|
416
|
+
|
417
|
+
if error?
|
418
|
+
log "Sphinx Daemon returned error: #{error}"
|
419
|
+
raise SphinxError.new(error, @results) unless options[:ignore_errors]
|
420
|
+
end
|
421
|
+
rescue Errno::ECONNREFUSED => err
|
422
|
+
raise ThinkingSphinx::ConnectionError,
|
423
|
+
'Connection to Sphinx Daemon (searchd) failed.'
|
424
|
+
end
|
425
|
+
|
426
|
+
compose_results
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def compose_results
|
431
|
+
if options[:ids_only]
|
432
|
+
compose_ids_results
|
433
|
+
elsif options[:only]
|
434
|
+
compose_only_results
|
435
|
+
else
|
436
|
+
replace instances_from_matches
|
437
|
+
add_excerpter
|
438
|
+
add_sphinx_attributes
|
439
|
+
add_matching_fields if client.rank_mode == :fieldmask
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def compose_ids_results
|
444
|
+
replace @results[:matches].collect { |match|
|
445
|
+
match[:attributes]['sphinx_internal_id']
|
446
|
+
}
|
447
|
+
end
|
448
|
+
|
449
|
+
def compose_only_results
|
450
|
+
replace @results[:matches].collect { |match|
|
451
|
+
case only = options[:only]
|
452
|
+
when String, Symbol
|
453
|
+
match[:attributes][only.to_s]
|
454
|
+
when Array
|
455
|
+
only.inject({}) do |hash, attribute|
|
456
|
+
hash[attribute.to_sym] = match[:attributes][attribute.to_s]
|
457
|
+
hash
|
458
|
+
end
|
459
|
+
else
|
460
|
+
raise "Unexpected object for :only argument. String or Array is expected, #{only.class} was received."
|
461
|
+
end
|
462
|
+
}
|
463
|
+
end
|
464
|
+
|
465
|
+
def add_excerpter
|
466
|
+
each do |object|
|
467
|
+
next if object.nil?
|
468
|
+
|
469
|
+
object.excerpts = ThinkingSphinx::Excerpter.new self, object
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
def add_sphinx_attributes
|
474
|
+
each do |object|
|
475
|
+
next if object.nil?
|
476
|
+
|
477
|
+
match = match_hash object
|
478
|
+
next if match.nil?
|
479
|
+
|
480
|
+
object.sphinx_attributes = match[:attributes]
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
def add_matching_fields
|
485
|
+
each do |object|
|
486
|
+
next if object.nil?
|
487
|
+
|
488
|
+
match = match_hash object
|
489
|
+
next if match.nil?
|
490
|
+
object.matching_fields = ThinkingSphinx::Search.matching_fields(
|
491
|
+
@results[:fields], match[:weight]
|
492
|
+
)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
def match_hash(object)
|
497
|
+
@results[:matches].detect { |match|
|
498
|
+
class_crc = object.class.name
|
499
|
+
class_crc = object.class.to_crc32 if Riddle.loaded_version.to_i < 2
|
500
|
+
|
501
|
+
match[:attributes]['sphinx_internal_id'] == object.
|
502
|
+
primary_key_for_sphinx &&
|
503
|
+
match[:attributes][crc_attribute] == class_crc
|
504
|
+
}
|
505
|
+
end
|
506
|
+
|
507
|
+
def self.log(message, &block)
|
508
|
+
if ThinkingSphinx::ActiveRecord::LogSubscriber.logger.nil?
|
509
|
+
yield if block_given?
|
510
|
+
return
|
511
|
+
end
|
512
|
+
|
513
|
+
if block_given?
|
514
|
+
::ActiveSupport::Notifications.
|
515
|
+
instrument('query.thinking_sphinx', :query => message, &block)
|
516
|
+
else
|
517
|
+
::ActiveSupport::Notifications.
|
518
|
+
instrument('message.thinking_sphinx', :message => message)
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
def log(query, &block)
|
523
|
+
self.class.log(query, &block)
|
524
|
+
end
|
525
|
+
|
526
|
+
def prepare(client)
|
527
|
+
index_options = {}
|
528
|
+
if one_class && one_class.sphinx_indexes && one_class.sphinx_indexes.first
|
529
|
+
index_options = one_class.sphinx_indexes.first.local_options
|
530
|
+
end
|
531
|
+
|
532
|
+
[
|
533
|
+
:max_matches, :group_by, :group_function, :group_clause,
|
534
|
+
:group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
|
535
|
+
:rank_mode, :max_query_time, :field_weights
|
536
|
+
].each do |key|
|
537
|
+
value = options[key] || index_options[key]
|
538
|
+
client.send("#{key}=", value) if value
|
539
|
+
end
|
540
|
+
|
541
|
+
# treated non-standard as :select is already used for AR queries
|
542
|
+
client.select = options[:sphinx_select] || '*'
|
543
|
+
|
544
|
+
client.limit = per_page
|
545
|
+
client.offset = offset
|
546
|
+
client.match_mode = match_mode
|
547
|
+
client.filters = filters
|
548
|
+
client.sort_mode = sort_mode
|
549
|
+
client.sort_by = sort_by
|
550
|
+
client.group_by = group_by if group_by
|
551
|
+
client.group_function = group_function if group_function
|
552
|
+
client.index_weights = index_weights
|
553
|
+
client.anchor = anchor
|
554
|
+
|
555
|
+
client
|
556
|
+
end
|
557
|
+
|
558
|
+
def retry_on_stale_index(&block)
|
559
|
+
stale_ids = []
|
560
|
+
retries = stale_retries
|
561
|
+
|
562
|
+
begin
|
563
|
+
options[:raise_on_stale] = retries > 0
|
564
|
+
block.call
|
565
|
+
|
566
|
+
# If ThinkingSphinx::Search#instances_from_matches found records in
|
567
|
+
# Sphinx but not in the DB and the :raise_on_stale option is set, this
|
568
|
+
# exception is raised. We retry a limited number of times, excluding the
|
569
|
+
# stale ids from the search.
|
570
|
+
rescue StaleIdsException => err
|
571
|
+
retries -= 1
|
572
|
+
|
573
|
+
# For logging
|
574
|
+
stale_ids |= err.ids
|
575
|
+
# ID exclusion
|
576
|
+
options[:without_ids] = Array(options[:without_ids]) | err.ids
|
577
|
+
|
578
|
+
log 'Stale Ids (%s %s left): %s' % [
|
579
|
+
retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
|
580
|
+
]
|
581
|
+
retry
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
def classes
|
586
|
+
@classes ||= options[:classes] || []
|
587
|
+
end
|
588
|
+
|
589
|
+
def one_class
|
590
|
+
@one_class ||= classes.length != 1 ? nil : classes.first
|
591
|
+
end
|
592
|
+
|
593
|
+
def query
|
594
|
+
@query ||= begin
|
595
|
+
q = @args.join(' ') << conditions_as_query
|
596
|
+
(options[:star] ? star_query(q) : q).strip
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
def conditions_as_query
|
601
|
+
return '' if @options[:conditions].blank?
|
602
|
+
|
603
|
+
' ' + @options[:conditions].keys.collect { |key|
|
604
|
+
"@#{key} #{options[:conditions][key]}"
|
605
|
+
}.join(' ')
|
606
|
+
end
|
607
|
+
|
608
|
+
def star_query(query)
|
609
|
+
token = options[:star].is_a?(Regexp) ? options[:star] : default_star_token
|
610
|
+
|
611
|
+
query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
|
612
|
+
pre, proper, post = $`, $&, $'
|
613
|
+
# E.g. "@foo", "/2", "~3", but not as part of a token
|
614
|
+
is_operator = pre.match(%r{(\W|^)[@~/]\Z}) ||
|
615
|
+
pre.match(%r{(\W|^)@\([^\)]*$})
|
616
|
+
# E.g. "foo bar", with quotes
|
617
|
+
is_quote = proper.starts_with?('"') && proper.ends_with?('"')
|
618
|
+
has_star = pre.ends_with?("*") || post.starts_with?("*")
|
619
|
+
if is_operator || is_quote || has_star
|
620
|
+
proper
|
621
|
+
else
|
622
|
+
"*#{proper}*"
|
623
|
+
end
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
if Regexp.instance_methods.include?(:encoding)
|
628
|
+
DefaultStarToken = Regexp.new('\p{Word}+')
|
629
|
+
else
|
630
|
+
DefaultStarToken = Regexp.new('\w+', nil, 'u')
|
631
|
+
end
|
632
|
+
|
633
|
+
def default_star_token
|
634
|
+
DefaultStarToken
|
635
|
+
end
|
636
|
+
|
637
|
+
def comment
|
638
|
+
options[:comment] || ''
|
639
|
+
end
|
640
|
+
|
641
|
+
def match_mode
|
642
|
+
options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
|
643
|
+
end
|
644
|
+
|
645
|
+
def sort_mode
|
646
|
+
@sort_mode ||= case options[:sort_mode]
|
647
|
+
when :asc
|
648
|
+
:attr_asc
|
649
|
+
when :desc
|
650
|
+
:attr_desc
|
651
|
+
when nil
|
652
|
+
case options[:order]
|
653
|
+
when String
|
654
|
+
:extended
|
655
|
+
when Symbol
|
656
|
+
:attr_asc
|
657
|
+
else
|
658
|
+
:relevance
|
659
|
+
end
|
660
|
+
else
|
661
|
+
options[:sort_mode]
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
def sort_by
|
666
|
+
case @sort_by = (options[:sort_by] || options[:order])
|
667
|
+
when String
|
668
|
+
sorted_fields_to_attributes(@sort_by.clone)
|
669
|
+
when Symbol
|
670
|
+
field_names.include?(@sort_by) ?
|
671
|
+
@sort_by.to_s.concat('_sort') : @sort_by.to_s
|
672
|
+
else
|
673
|
+
''
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
def field_names
|
678
|
+
return [] unless one_class
|
679
|
+
|
680
|
+
one_class.sphinx_indexes.collect { |index|
|
681
|
+
index.fields.collect { |field| field.unique_name }
|
682
|
+
}.flatten
|
683
|
+
end
|
684
|
+
|
685
|
+
def sorted_fields_to_attributes(order_string)
|
686
|
+
field_names.each { |field|
|
687
|
+
order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
688
|
+
match.gsub field.to_s, field.to_s.concat("_sort")
|
689
|
+
}
|
690
|
+
}
|
691
|
+
|
692
|
+
order_string
|
693
|
+
end
|
694
|
+
|
695
|
+
# Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
|
696
|
+
# { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
|
697
|
+
#
|
698
|
+
def index_weights
|
699
|
+
weights = options[:index_weights] || {}
|
700
|
+
weights.keys.inject({}) do |hash, key|
|
701
|
+
if key.is_a?(Class)
|
702
|
+
name = ThinkingSphinx::Index.name_for(key)
|
703
|
+
hash["#{name}_core"] = weights[key]
|
704
|
+
hash["#{name}_delta"] = weights[key]
|
705
|
+
else
|
706
|
+
hash[key] = weights[key]
|
707
|
+
end
|
708
|
+
|
709
|
+
hash
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
def group_by
|
714
|
+
options[:group] ? options[:group].to_s : nil
|
715
|
+
end
|
716
|
+
|
717
|
+
def group_function
|
718
|
+
options[:group] ? :attr : nil
|
719
|
+
end
|
720
|
+
|
721
|
+
def internal_filters
|
722
|
+
filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
|
723
|
+
|
724
|
+
class_crcs = classes.collect { |klass|
|
725
|
+
klass.to_crc32s
|
726
|
+
}.flatten
|
727
|
+
|
728
|
+
unless class_crcs.empty?
|
729
|
+
filters << Riddle::Client::Filter.new('class_crc', class_crcs)
|
730
|
+
end
|
731
|
+
|
732
|
+
filters << Riddle::Client::Filter.new(
|
733
|
+
'sphinx_internal_id', filter_value(options[:without_ids]), true
|
734
|
+
) unless options[:without_ids].nil? || options[:without_ids].empty?
|
735
|
+
|
736
|
+
filters
|
737
|
+
end
|
738
|
+
|
739
|
+
def filters
|
740
|
+
internal_filters +
|
741
|
+
(options[:with] || {}).collect { |attrib, value|
|
742
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
743
|
+
} +
|
744
|
+
(options[:without] || {}).collect { |attrib, value|
|
745
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
|
746
|
+
} +
|
747
|
+
(options[:with_all] || {}).collect { |attrib, values|
|
748
|
+
Array(values).collect { |value|
|
749
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
750
|
+
}
|
751
|
+
}.flatten +
|
752
|
+
(options[:without_any] || {}).collect { |attrib, values|
|
753
|
+
Array(values).collect { |value|
|
754
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
|
755
|
+
}
|
756
|
+
}.flatten
|
757
|
+
end
|
758
|
+
|
759
|
+
# When passed a Time instance, returns the integer timestamp.
|
760
|
+
def filter_value(value)
|
761
|
+
case value
|
762
|
+
when Range
|
763
|
+
filter_value(value.first).first..filter_value(value.last).first
|
764
|
+
when Array
|
765
|
+
value.collect { |v| filter_value(v) }.flatten
|
766
|
+
when Time
|
767
|
+
[value.to_i]
|
768
|
+
when NilClass
|
769
|
+
0
|
770
|
+
else
|
771
|
+
Array(value)
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
def anchor
|
776
|
+
return {} unless options[:geo] || (options[:lat] && options[:lng])
|
777
|
+
|
778
|
+
{
|
779
|
+
:latitude => options[:geo] ? options[:geo].first : options[:lat],
|
780
|
+
:longitude => options[:geo] ? options[:geo].last : options[:lng],
|
781
|
+
:latitude_attribute => latitude_attr.to_s,
|
782
|
+
:longitude_attribute => longitude_attr.to_s
|
783
|
+
}
|
784
|
+
end
|
785
|
+
|
786
|
+
def latitude_attr
|
787
|
+
options[:latitude_attr] ||
|
788
|
+
index_option(:latitude_attr) ||
|
789
|
+
attribute(:lat, :latitude)
|
790
|
+
end
|
791
|
+
|
792
|
+
def longitude_attr
|
793
|
+
options[:longitude_attr] ||
|
794
|
+
index_option(:longitude_attr) ||
|
795
|
+
attribute(:lon, :lng, :longitude)
|
796
|
+
end
|
797
|
+
|
798
|
+
def index_option(key)
|
799
|
+
return nil unless one_class
|
800
|
+
|
801
|
+
one_class.sphinx_indexes.collect { |index|
|
802
|
+
index.local_options[key]
|
803
|
+
}.compact.first
|
804
|
+
end
|
805
|
+
|
806
|
+
def attribute(*keys)
|
807
|
+
return nil unless one_class
|
808
|
+
|
809
|
+
keys.detect { |key|
|
810
|
+
attributes.include?(key)
|
811
|
+
}
|
812
|
+
end
|
813
|
+
|
814
|
+
def attributes
|
815
|
+
return [] unless one_class
|
816
|
+
|
817
|
+
attributes = one_class.sphinx_indexes.collect { |index|
|
818
|
+
index.attributes.collect { |attrib| attrib.unique_name }
|
819
|
+
}.flatten
|
820
|
+
end
|
821
|
+
|
822
|
+
def stale_retries
|
823
|
+
case options[:retry_stale]
|
824
|
+
when TrueClass
|
825
|
+
3
|
826
|
+
when nil, FalseClass
|
827
|
+
0
|
828
|
+
else
|
829
|
+
options[:retry_stale].to_i
|
830
|
+
end
|
831
|
+
end
|
832
|
+
|
833
|
+
def include_for_class(klass)
|
834
|
+
includes = options[:include] || klass.sphinx_index_options[:include]
|
835
|
+
|
836
|
+
case includes
|
837
|
+
when NilClass
|
838
|
+
nil
|
839
|
+
when Array
|
840
|
+
include_from_array includes, klass
|
841
|
+
when Symbol
|
842
|
+
klass.reflections[includes].nil? ? nil : includes
|
843
|
+
when Hash
|
844
|
+
include_from_hash includes, klass
|
845
|
+
else
|
846
|
+
includes
|
847
|
+
end
|
848
|
+
end
|
849
|
+
|
850
|
+
def include_from_array(array, klass)
|
851
|
+
scoped_array = []
|
852
|
+
array.each do |value|
|
853
|
+
case value
|
854
|
+
when Hash
|
855
|
+
scoped_hash = include_from_hash(value, klass)
|
856
|
+
scoped_array << scoped_hash unless scoped_hash.nil?
|
857
|
+
else
|
858
|
+
scoped_array << value unless klass.reflections[value].nil?
|
859
|
+
end
|
860
|
+
end
|
861
|
+
scoped_array.empty? ? nil : scoped_array
|
862
|
+
end
|
863
|
+
|
864
|
+
def include_from_hash(hash, klass)
|
865
|
+
scoped_hash = {}
|
866
|
+
hash.keys.each do |key|
|
867
|
+
scoped_hash[key] = hash[key] unless klass.reflections[key].nil?
|
868
|
+
end
|
869
|
+
scoped_hash.empty? ? nil : scoped_hash
|
870
|
+
end
|
871
|
+
|
872
|
+
def instances_from_class(klass, matches)
|
873
|
+
index_options = klass.sphinx_index_options
|
874
|
+
|
875
|
+
ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
|
876
|
+
instances = ids.length > 0 ? klass.find(
|
877
|
+
:all,
|
878
|
+
:joins => options[:joins],
|
879
|
+
:conditions => {klass.primary_key_for_sphinx.to_sym => ids},
|
880
|
+
:include => include_for_class(klass),
|
881
|
+
:select => (options[:select] || index_options[:select]),
|
882
|
+
:order => (options[:sql_order] || index_options[:sql_order])
|
883
|
+
) : []
|
884
|
+
|
885
|
+
# Raise an exception if we find records in Sphinx but not in the DB, so
|
886
|
+
# the search method can retry without them. See
|
887
|
+
# ThinkingSphinx::Search.retry_search_on_stale_index.
|
888
|
+
if options[:raise_on_stale] && instances.length < ids.length
|
889
|
+
stale_ids = ids - instances.map { |i| i.id }
|
890
|
+
raise StaleIdsException, stale_ids
|
891
|
+
end
|
892
|
+
|
893
|
+
# if the user has specified an SQL order, return the collection
|
894
|
+
# without rearranging it into the Sphinx order
|
895
|
+
return instances if (options[:sql_order] || index_options[:sql_order])
|
896
|
+
|
897
|
+
ids.collect { |obj_id|
|
898
|
+
instances.detect do |obj|
|
899
|
+
obj.primary_key_for_sphinx == obj_id
|
900
|
+
end
|
901
|
+
}
|
902
|
+
end
|
903
|
+
|
904
|
+
# Group results by class and call #find(:all) once for each group to reduce
|
905
|
+
# the number of #find's in multi-model searches.
|
906
|
+
#
|
907
|
+
def instances_from_matches
|
908
|
+
return single_class_results if one_class
|
909
|
+
|
910
|
+
groups = results[:matches].group_by { |match|
|
911
|
+
match[:attributes][crc_attribute]
|
912
|
+
}
|
913
|
+
groups.each do |crc, group|
|
914
|
+
group.replace(
|
915
|
+
instances_from_class(class_from_crc(crc), group)
|
916
|
+
)
|
917
|
+
end
|
918
|
+
|
919
|
+
results[:matches].collect do |match|
|
920
|
+
groups.detect { |crc, group|
|
921
|
+
crc == match[:attributes][crc_attribute]
|
922
|
+
}[1].compact.detect { |obj|
|
923
|
+
obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
|
924
|
+
}
|
925
|
+
end
|
926
|
+
end
|
927
|
+
|
928
|
+
def single_class_results
|
929
|
+
instances_from_class one_class, results[:matches]
|
930
|
+
end
|
931
|
+
|
932
|
+
def class_from_crc(crc)
|
933
|
+
if Riddle.loaded_version.to_i < 2
|
934
|
+
config.models_by_crc[crc].constantize
|
935
|
+
else
|
936
|
+
crc.constantize
|
937
|
+
end
|
938
|
+
end
|
939
|
+
|
940
|
+
def each_with_attribute(attribute, &block)
|
941
|
+
populate
|
942
|
+
results[:matches].each_with_index do |match, index|
|
943
|
+
yield self[index],
|
944
|
+
(match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
|
945
|
+
end
|
946
|
+
end
|
947
|
+
|
948
|
+
def is_scope?(method)
|
949
|
+
one_class && one_class.sphinx_scopes.include?(method)
|
950
|
+
end
|
951
|
+
|
952
|
+
# Adds the default_sphinx_scope if set.
|
953
|
+
def add_default_scope
|
954
|
+
return unless one_class && one_class.has_default_sphinx_scope?
|
955
|
+
add_scope(one_class.get_default_sphinx_scope.to_sym)
|
956
|
+
end
|
957
|
+
|
958
|
+
def add_scope(method, *args, &block)
|
959
|
+
method = "#{method}_without_default".to_sym
|
960
|
+
merge_search one_class.send(method, *args, &block), self.args, options
|
961
|
+
end
|
962
|
+
|
963
|
+
def merge_search(search, args, options)
|
964
|
+
search.args.each { |arg| args << arg }
|
965
|
+
|
966
|
+
search.options.keys.each do |key|
|
967
|
+
if HashOptions.include?(key)
|
968
|
+
options[key] ||= {}
|
969
|
+
options[key].merge! search.options[key]
|
970
|
+
elsif ArrayOptions.include?(key)
|
971
|
+
options[key] ||= []
|
972
|
+
options[key] += search.options[key]
|
973
|
+
options[key].uniq!
|
974
|
+
else
|
975
|
+
options[key] = search.options[key]
|
976
|
+
end
|
977
|
+
end
|
978
|
+
end
|
979
|
+
|
980
|
+
def scoped_count
|
981
|
+
return self.total_entries if(@options[:ids_only] || @options[:only])
|
982
|
+
|
983
|
+
@options[:ids_only] = true
|
984
|
+
results_count = self.total_entries
|
985
|
+
@options[:ids_only] = false
|
986
|
+
@populated = false
|
987
|
+
|
988
|
+
results_count
|
989
|
+
end
|
990
|
+
|
991
|
+
def crc_attribute
|
992
|
+
Riddle.loaded_version.to_i < 2 ? 'class_crc' : 'sphinx_internal_class'
|
993
|
+
end
|
994
|
+
end
|
995
|
+
end
|