pixeltrix-thinking-sphinx 1.1.5 → 1.2.1
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/README.textile +147 -0
- data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
- data/lib/thinking_sphinx/active_record/delta.rb +14 -1
- data/lib/thinking_sphinx/active_record/scopes.rb +37 -0
- data/lib/thinking_sphinx/active_record.rb +46 -12
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +9 -1
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +3 -2
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +12 -5
- data/lib/thinking_sphinx/association.rb +20 -0
- data/lib/thinking_sphinx/attribute.rb +187 -116
- data/lib/thinking_sphinx/class_facet.rb +15 -0
- data/lib/thinking_sphinx/configuration.rb +46 -14
- data/lib/thinking_sphinx/core/string.rb +3 -10
- data/lib/thinking_sphinx/deltas/datetime_delta.rb +3 -3
- data/lib/thinking_sphinx/deltas/default_delta.rb +9 -6
- data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +1 -1
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
- data/lib/thinking_sphinx/deltas.rb +14 -6
- data/lib/thinking_sphinx/deploy/capistrano.rb +98 -0
- data/lib/thinking_sphinx/excerpter.rb +22 -0
- data/lib/thinking_sphinx/facet.rb +68 -18
- data/lib/thinking_sphinx/facet_search.rb +134 -0
- data/lib/thinking_sphinx/field.rb +7 -97
- data/lib/thinking_sphinx/index/builder.rb +255 -201
- data/lib/thinking_sphinx/index.rb +28 -343
- data/lib/thinking_sphinx/property.rb +160 -0
- data/lib/thinking_sphinx/rails_additions.rb +7 -4
- data/lib/thinking_sphinx/search.rb +593 -587
- data/lib/thinking_sphinx/search_methods.rb +421 -0
- data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
- data/lib/thinking_sphinx/source/sql.rb +128 -0
- data/lib/thinking_sphinx/source.rb +150 -0
- data/lib/thinking_sphinx/tasks.rb +45 -11
- data/lib/thinking_sphinx.rb +88 -14
- data/rails/init.rb +14 -0
- data/spec/{unit → lib}/thinking_sphinx/active_record/delta_spec.rb +7 -7
- data/spec/{unit → lib}/thinking_sphinx/active_record/has_many_association_spec.rb +0 -0
- data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +92 -0
- data/spec/{unit → lib}/thinking_sphinx/active_record_spec.rb +115 -42
- data/spec/{unit → lib}/thinking_sphinx/association_spec.rb +4 -5
- data/spec/lib/thinking_sphinx/attribute_spec.rb +465 -0
- data/spec/{unit → lib}/thinking_sphinx/configuration_spec.rb +118 -7
- data/spec/{unit → lib}/thinking_sphinx/core/string_spec.rb +0 -0
- data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
- data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
- data/spec/lib/thinking_sphinx/facet_spec.rb +302 -0
- data/spec/{unit → lib}/thinking_sphinx/field_spec.rb +26 -17
- data/spec/lib/thinking_sphinx/index/builder_spec.rb +355 -0
- data/spec/{unit → lib}/thinking_sphinx/index/faux_column_spec.rb +0 -0
- data/spec/{unit → lib}/thinking_sphinx/index_spec.rb +3 -12
- data/spec/lib/thinking_sphinx/rails_additions_spec.rb +191 -0
- data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
- data/spec/lib/thinking_sphinx/search_spec.rb +887 -0
- data/spec/lib/thinking_sphinx/source_spec.rb +217 -0
- data/spec/{unit → lib}/thinking_sphinx_spec.rb +30 -8
- data/tasks/distribution.rb +20 -1
- data/tasks/testing.rb +7 -15
- data/vendor/after_commit/init.rb +3 -0
- data/vendor/after_commit/lib/after_commit/active_record.rb +27 -4
- data/vendor/after_commit/lib/after_commit/connection_adapters.rb +1 -1
- data/vendor/after_commit/lib/after_commit.rb +4 -1
- data/vendor/riddle/lib/riddle/client/message.rb +4 -3
- data/vendor/riddle/lib/riddle/client.rb +3 -0
- data/vendor/riddle/lib/riddle/configuration/section.rb +8 -2
- data/vendor/riddle/lib/riddle/controller.rb +1 -1
- data/vendor/riddle/lib/riddle.rb +1 -1
- metadata +75 -39
- data/README +0 -107
- data/lib/thinking_sphinx/active_record/search.rb +0 -57
- data/lib/thinking_sphinx/collection.rb +0 -142
- data/lib/thinking_sphinx/facet_collection.rb +0 -44
- data/spec/unit/thinking_sphinx/active_record/search_spec.rb +0 -107
- data/spec/unit/thinking_sphinx/attribute_spec.rb +0 -212
- data/spec/unit/thinking_sphinx/collection_spec.rb +0 -14
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +0 -5
- data/spec/unit/thinking_sphinx/search_spec.rb +0 -59
@@ -1,3 +1,4 @@
|
|
1
|
+
# encoding: UTF-8
|
1
2
|
module ThinkingSphinx
|
2
3
|
# Once you've got those indexes in and built, this is the stuff that
|
3
4
|
# matters - how to search! This class provides a generic search
|
@@ -7,631 +8,636 @@ module ThinkingSphinx
|
|
7
8
|
# called from a model.
|
8
9
|
#
|
9
10
|
class Search
|
10
|
-
class
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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? send should
|
15
|
+
type )
|
16
|
+
SafeMethods = %w( partition private_methods protected_methods
|
17
|
+
public_methods send )
|
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]
|
26
|
+
ArrayOptions = [:classes, :without_ids]
|
27
|
+
|
28
|
+
attr_reader :args, :options, :results
|
29
|
+
|
30
|
+
# Deprecated. Use ThinkingSphinx.search
|
31
|
+
def self.search(*args)
|
32
|
+
log '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
|
+
log '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
|
+
log '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
|
+
log '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
|
+
log 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
|
57
|
+
ThinkingSphinx.facets *args
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(*args)
|
61
|
+
@array = []
|
62
|
+
@options = args.extract_options!
|
63
|
+
@args = args
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_a
|
67
|
+
populate
|
68
|
+
@array
|
69
|
+
end
|
70
|
+
|
71
|
+
def method_missing(method, *args, &block)
|
72
|
+
if is_scope?(method)
|
73
|
+
add_scope(method, *args, &block)
|
74
|
+
return self
|
75
|
+
elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
|
76
|
+
super
|
77
|
+
elsif !SafeMethods.include?(method.to_s)
|
78
|
+
populate
|
26
79
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
# expected, and the returned result set can be used by the will_paginate
|
35
|
-
# helper.
|
36
|
-
#
|
37
|
-
# == Basic Searching
|
38
|
-
#
|
39
|
-
# The simplest way of searching is straight text.
|
40
|
-
#
|
41
|
-
# ThinkingSphinx::Search.search "pat"
|
42
|
-
# ThinkingSphinx::Search.search "google"
|
43
|
-
# User.search "pat", :page => (params[:page] || 1)
|
44
|
-
# Article.search "relevant news issue of the day"
|
45
|
-
#
|
46
|
-
# If you specify :include, like in an #find call, this will be respected
|
47
|
-
# when loading the relevant models from the search results.
|
48
|
-
#
|
49
|
-
# User.search "pat", :include => :posts
|
50
|
-
#
|
51
|
-
# == Match Modes
|
52
|
-
#
|
53
|
-
# Sphinx supports 5 different matching modes. By default Thinking Sphinx
|
54
|
-
# uses :all, which unsurprisingly requires all the supplied search terms
|
55
|
-
# to match a result.
|
56
|
-
#
|
57
|
-
# Alternative modes include:
|
58
|
-
#
|
59
|
-
# User.search "pat allan", :match_mode => :any
|
60
|
-
# User.search "pat allan", :match_mode => :phrase
|
61
|
-
# User.search "pat | allan", :match_mode => :boolean
|
62
|
-
# User.search "@name pat | @username pat", :match_mode => :extended
|
63
|
-
#
|
64
|
-
# Any will find results with any of the search terms. Phrase treats the search
|
65
|
-
# terms a single phrase instead of individual words. Boolean and extended allow
|
66
|
-
# for more complex query syntax, refer to the sphinx documentation for further
|
67
|
-
# details.
|
68
|
-
#
|
69
|
-
# == Weighting
|
70
|
-
#
|
71
|
-
# Sphinx has support for weighting, where matches in one field can be considered
|
72
|
-
# more important than in another. Weights are integers, with 1 as the default.
|
73
|
-
# They can be set per-search like this:
|
74
|
-
#
|
75
|
-
# User.search "pat allan", :field_weights => { :alias => 4, :aka => 2 }
|
76
|
-
#
|
77
|
-
# If you're searching multiple models, you can set per-index weights:
|
78
|
-
#
|
79
|
-
# ThinkingSphinx::Search.search "pat", :index_weights => { User => 10 }
|
80
|
-
#
|
81
|
-
# See http://sphinxsearch.com/doc.html#weighting for further details.
|
82
|
-
#
|
83
|
-
# == Searching by Fields
|
84
|
-
#
|
85
|
-
# If you want to step it up a level, you can limit your search terms to
|
86
|
-
# specific fields:
|
87
|
-
#
|
88
|
-
# User.search :conditions => {:name => "pat"}
|
89
|
-
#
|
90
|
-
# This uses Sphinx's extended match mode, unless you specify a different
|
91
|
-
# match mode explicitly (but then this way of searching won't work). Also
|
92
|
-
# note that you don't need to put in a search string.
|
93
|
-
#
|
94
|
-
# == Searching by Attributes
|
95
|
-
#
|
96
|
-
# Also known as filters, you can limit your searches to documents that
|
97
|
-
# have specific values for their attributes. There are two ways to do
|
98
|
-
# this. The first is one that works in all scenarios - using the :with
|
99
|
-
# option.
|
100
|
-
#
|
101
|
-
# ThinkingSphinx::Search.search :with => {:parent_id => 10}
|
102
|
-
#
|
103
|
-
# The second is only viable if you're searching with a specific model
|
104
|
-
# (not multi-model searching). With a single model, Thinking Sphinx
|
105
|
-
# can figure out what attributes and fields are available, so you can
|
106
|
-
# put it all in the :conditions hash, and it will sort it out.
|
107
|
-
#
|
108
|
-
# Node.search :conditions => {:parent_id => 10}
|
109
|
-
#
|
110
|
-
# Filters can be single values, arrays of values, or ranges.
|
111
|
-
#
|
112
|
-
# Article.search "East Timor", :conditions => {:rating => 3..5}
|
113
|
-
#
|
114
|
-
# == Excluding by Attributes
|
115
|
-
#
|
116
|
-
# Sphinx also supports negative filtering - where the filters are of
|
117
|
-
# attribute values to exclude. This is done with the :without option:
|
118
|
-
#
|
119
|
-
# User.search :without => {:role_id => 1}
|
120
|
-
#
|
121
|
-
# == Excluding by Primary Key
|
122
|
-
#
|
123
|
-
# There is a shortcut to exclude records by their ActiveRecord primary key:
|
124
|
-
#
|
125
|
-
# User.search :without_ids => 1
|
126
|
-
#
|
127
|
-
# Pass an array or a single value.
|
128
|
-
#
|
129
|
-
# The primary key must be an integer as a negative filter is used. Note
|
130
|
-
# that for multi-model search, an id may occur in more than one model.
|
131
|
-
#
|
132
|
-
# == Infix (Star) Searching
|
133
|
-
#
|
134
|
-
# By default, Sphinx uses English stemming, e.g. matching "shoes" if you
|
135
|
-
# search for "shoe". It won't find "Melbourne" if you search for
|
136
|
-
# "elbourn", though.
|
137
|
-
#
|
138
|
-
# Enable infix searching by something like this in config/sphinx.yml:
|
139
|
-
#
|
140
|
-
# development:
|
141
|
-
# enable_star: 1
|
142
|
-
# min_infix_length: 2
|
143
|
-
#
|
144
|
-
# Note that this will make indexing take longer.
|
145
|
-
#
|
146
|
-
# With those settings (and after reindexing), wildcard asterisks can be used
|
147
|
-
# in queries:
|
148
|
-
#
|
149
|
-
# Location.search "*elbourn*"
|
150
|
-
#
|
151
|
-
# To automatically add asterisks around every token (but not operators),
|
152
|
-
# pass the :star option:
|
153
|
-
#
|
154
|
-
# Location.search "elbourn -ustrali", :star => true, :match_mode => :boolean
|
155
|
-
#
|
156
|
-
# This would become "*elbourn* -*ustrali*". The :star option only adds the
|
157
|
-
# asterisks. You need to make the config/sphinx.yml changes yourself.
|
158
|
-
#
|
159
|
-
# By default, the tokens are assumed to match the regular expression /\w+/u.
|
160
|
-
# If you've modified the charset_table, pass another regular expression, e.g.
|
161
|
-
#
|
162
|
-
# User.search("oo@bar.c", :star => /[\w@.]+/u)
|
163
|
-
#
|
164
|
-
# to search for "*oo@bar.c*" and not "*oo*@*bar*.*c*".
|
165
|
-
#
|
166
|
-
# == Sorting
|
167
|
-
#
|
168
|
-
# Sphinx can only sort by attributes, so generally you will need to avoid
|
169
|
-
# using field names in your :order option. However, if you're searching
|
170
|
-
# on a single model, and have specified some fields as sortable, you can
|
171
|
-
# use those field names and Thinking Sphinx will interpret accordingly.
|
172
|
-
# Remember: this will only happen for single-model searches, and only
|
173
|
-
# through the :order option.
|
174
|
-
#
|
175
|
-
# Location.search "Melbourne", :order => :state
|
176
|
-
# User.search :conditions => {:role_id => 2}, :order => "name ASC"
|
177
|
-
#
|
178
|
-
# Keep in mind that if you use a string, you *must* specify the direction
|
179
|
-
# (ASC or DESC) else Sphinx won't return any results. If you use a symbol
|
180
|
-
# then Thinking Sphinx assumes ASC, but if you wish to state otherwise,
|
181
|
-
# use the :sort_mode option:
|
182
|
-
#
|
183
|
-
# Location.search "Melbourne", :order => :state, :sort_mode => :desc
|
184
|
-
#
|
185
|
-
# Of course, there are other sort modes - check out the Sphinx
|
186
|
-
# documentation[http://sphinxsearch.com/doc.html] for that level of
|
187
|
-
# detail though.
|
188
|
-
#
|
189
|
-
# == Grouping
|
190
|
-
#
|
191
|
-
# For this you can use the group_by, group_clause and group_function
|
192
|
-
# options - which are all directly linked to Sphinx's expectations. No
|
193
|
-
# magic from Thinking Sphinx. It can get a little tricky, so make sure
|
194
|
-
# you read all the relevant
|
195
|
-
# documentation[http://sphinxsearch.com/doc.html#clustering] first.
|
196
|
-
#
|
197
|
-
# Yes this section will be expanded, but this is a start.
|
198
|
-
#
|
199
|
-
# == Geo/Location Searching
|
200
|
-
#
|
201
|
-
# Sphinx - and therefore Thinking Sphinx - has the facility to search
|
202
|
-
# around a geographical point, using a given latitude and longitude. To
|
203
|
-
# take advantage of this, you will need to have both of those values in
|
204
|
-
# attributes. To search with that point, you can then use one of the
|
205
|
-
# following syntax examples:
|
206
|
-
#
|
207
|
-
# Address.search "Melbourne", :geo => [1.4, -2.217], :order => "@geodist asc"
|
208
|
-
# Address.search "Australia", :geo => [-0.55, 3.108], :order => "@geodist asc"
|
209
|
-
# :latitude_attr => "latit", :longitude_attr => "longit"
|
210
|
-
#
|
211
|
-
# The first example applies when your latitude and longitude attributes
|
212
|
-
# are named any of lat, latitude, lon, long or longitude. If that's not
|
213
|
-
# the case, you will need to explicitly state them in your search, _or_
|
214
|
-
# you can do so in your model:
|
215
|
-
#
|
216
|
-
# define_index do
|
217
|
-
# has :latit # Float column, stored in radians
|
218
|
-
# has :longit # Float column, stored in radians
|
219
|
-
#
|
220
|
-
# set_property :latitude_attr => "latit"
|
221
|
-
# set_property :longitude_attr => "longit"
|
222
|
-
# end
|
223
|
-
#
|
224
|
-
# Now, geo-location searching really only has an affect if you have a
|
225
|
-
# filter, sort or grouping clause related to it - otherwise it's just a
|
226
|
-
# normal search, and _will not_ return a distance value otherwise. To
|
227
|
-
# make use of the positioning difference, use the special attribute
|
228
|
-
# "@geodist" in any of your filters or sorting or grouping clauses.
|
229
|
-
#
|
230
|
-
# And don't forget - both the latitude and longitude you use in your
|
231
|
-
# search, and the values in your indexes, need to be stored as a float in radians,
|
232
|
-
# _not_ degrees. Keep in mind that if you do this conversion in SQL
|
233
|
-
# you will need to explicitly declare a column type of :float.
|
234
|
-
#
|
235
|
-
# define_index do
|
236
|
-
# has 'RADIANS(lat)', :as => :lat, :type => :float
|
237
|
-
# # ...
|
238
|
-
# end
|
239
|
-
#
|
240
|
-
# Once you've got your results set, you can access the distances as
|
241
|
-
# follows:
|
242
|
-
#
|
243
|
-
# @results.each_with_geodist do |result, distance|
|
244
|
-
# # ...
|
245
|
-
# end
|
246
|
-
#
|
247
|
-
# The distance value is returned as a float, representing the distance in
|
248
|
-
# metres.
|
249
|
-
#
|
250
|
-
# == Handling a Stale Index
|
251
|
-
#
|
252
|
-
# Especially if you don't use delta indexing, you risk having records in the
|
253
|
-
# Sphinx index that are no longer in the database. By default, those will simply
|
254
|
-
# come back as nils:
|
255
|
-
#
|
256
|
-
# >> pat_user.delete
|
257
|
-
# >> User.search("pat")
|
258
|
-
# Sphinx Result: [1,2]
|
259
|
-
# => [nil, <#User id: 2>]
|
260
|
-
#
|
261
|
-
# (If you search across multiple models, you'll get ActiveRecord::RecordNotFound.)
|
262
|
-
#
|
263
|
-
# You can simply Array#compact these results or handle the nils in some other way, but
|
264
|
-
# Sphinx will still report two results, and the missing records may upset your layout.
|
265
|
-
#
|
266
|
-
# If you pass :retry_stale => true to a single-model search, missing records will
|
267
|
-
# cause Thinking Sphinx to retry the query but excluding those records. Since search
|
268
|
-
# is paginated, the new search could potentially include missing records as well, so by
|
269
|
-
# default Thinking Sphinx will retry three times. Pass :retry_stale => 5 to retry five
|
270
|
-
# times, and so on. If there are still missing ids on the last retry, they are
|
271
|
-
# shown as nils.
|
272
|
-
#
|
273
|
-
def search(*args)
|
274
|
-
query = args.clone # an array
|
275
|
-
options = query.extract_options!
|
276
|
-
|
277
|
-
retry_search_on_stale_index(query, options) do
|
278
|
-
results, client = search_results(*(query + [options]))
|
279
|
-
|
280
|
-
::ActiveRecord::Base.logger.error(
|
281
|
-
"Sphinx Error: #{results[:error]}"
|
282
|
-
) if results[:error]
|
283
|
-
|
284
|
-
klass = options[:class]
|
285
|
-
page = options[:page] ? options[:page].to_i : 1
|
80
|
+
|
81
|
+
if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
|
82
|
+
each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
|
83
|
+
else
|
84
|
+
@array.send(method, *args, &block)
|
85
|
+
end
|
86
|
+
end
|
286
87
|
|
287
|
-
|
288
|
-
|
88
|
+
# Returns true if the Search object or the underlying Array object respond
|
89
|
+
# to the requested method.
|
90
|
+
#
|
91
|
+
# @param [Symbol] method The method name
|
92
|
+
# @return [Boolean] true if either Search or Array responds to the method.
|
93
|
+
#
|
94
|
+
def respond_to?(method)
|
95
|
+
super || @array.respond_to?(method)
|
96
|
+
end
|
97
|
+
|
98
|
+
# The current page number of the result set. Defaults to 1 if no page was
|
99
|
+
# explicitly requested.
|
100
|
+
#
|
101
|
+
# @return [Integer]
|
102
|
+
#
|
103
|
+
def current_page
|
104
|
+
@options[:page].blank? ? 1 : @options[:page].to_i
|
105
|
+
end
|
106
|
+
|
107
|
+
# The next page number of the result set. If there are no more pages
|
108
|
+
# available, nil is returned.
|
109
|
+
#
|
110
|
+
# @return [Integer, nil]
|
111
|
+
#
|
112
|
+
def next_page
|
113
|
+
current_page >= total_pages ? nil : current_page + 1
|
114
|
+
end
|
115
|
+
|
116
|
+
# The previous page number of the result set. If this is the first page,
|
117
|
+
# then nil is returned.
|
118
|
+
#
|
119
|
+
# @return [Integer, nil]
|
120
|
+
#
|
121
|
+
def previous_page
|
122
|
+
current_page == 1 ? nil : current_page - 1
|
123
|
+
end
|
124
|
+
|
125
|
+
# The amount of records per set of paged results. Defaults to 20 unless a
|
126
|
+
# specific page size is requested.
|
127
|
+
#
|
128
|
+
# @return [Integer]
|
129
|
+
#
|
130
|
+
def per_page
|
131
|
+
@options[:limit] || @options[:per_page] || 20
|
132
|
+
end
|
133
|
+
|
134
|
+
# The total number of pages available if the results are paginated.
|
135
|
+
#
|
136
|
+
# @return [Integer]
|
137
|
+
#
|
138
|
+
def total_pages
|
139
|
+
@total_pages ||= (total_entries / per_page.to_f).ceil
|
140
|
+
end
|
141
|
+
# Compatibility with older versions of will_paginate
|
142
|
+
alias_method :page_count, :total_pages
|
143
|
+
|
144
|
+
# The total number of search results available.
|
145
|
+
#
|
146
|
+
# @return [Integer]
|
147
|
+
#
|
148
|
+
def total_entries
|
149
|
+
populate
|
150
|
+
@total_entries ||= @results[:total_found]
|
151
|
+
end
|
152
|
+
|
153
|
+
# The current page's offset, based on the number of records per page.
|
154
|
+
#
|
155
|
+
# @return [Integer]
|
156
|
+
#
|
157
|
+
def offset
|
158
|
+
(current_page - 1) * per_page
|
159
|
+
end
|
160
|
+
|
161
|
+
def each_with_groupby_and_count(&block)
|
162
|
+
populate
|
163
|
+
results[:matches].each_with_index do |match, index|
|
164
|
+
yield self[index],
|
165
|
+
match[:attributes]["@groupby"],
|
166
|
+
match[:attributes]["@count"]
|
289
167
|
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def each_with_weighting(&block)
|
171
|
+
populate
|
172
|
+
results[:matches].each_with_index do |match, index|
|
173
|
+
yield self[index], match[:weight]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def excerpt_for(string, model = nil)
|
178
|
+
if model.nil? && options[:classes].length == 1
|
179
|
+
model ||= options[:classes].first
|
180
|
+
end
|
181
|
+
|
182
|
+
populate
|
183
|
+
client.excerpts(
|
184
|
+
:docs => [string],
|
185
|
+
:words => results[:words].keys.join(' '),
|
186
|
+
:index => "#{model.sphinx_name}_core"
|
187
|
+
).first
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def config
|
193
|
+
ThinkingSphinx::Configuration.instance
|
194
|
+
end
|
195
|
+
|
196
|
+
def populate
|
197
|
+
return if @populated
|
198
|
+
@populated = true
|
290
199
|
|
291
|
-
|
292
|
-
stale_ids = []
|
293
|
-
stale_retries_left = case options[:retry_stale]
|
294
|
-
when true: 3 # default to three retries
|
295
|
-
when nil, false: 0 # no retries
|
296
|
-
else options[:retry_stale].to_i
|
297
|
-
end
|
200
|
+
retry_on_stale_index do
|
298
201
|
begin
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
::ActiveRecord::Base.logger.debug("Sphinx Stale Ids (%s %s left): %s" % [
|
314
|
-
tries, (tries==1 ? 'try' : 'tries'), stale_ids.join(', ')
|
315
|
-
])
|
316
|
-
|
317
|
-
retry
|
202
|
+
log "Querying Sphinx: #{query}"
|
203
|
+
@results = client.query query, index, comment
|
204
|
+
rescue Errno::ECONNREFUSED => err
|
205
|
+
raise ThinkingSphinx::ConnectionError,
|
206
|
+
'Connection to Sphinx Daemon (searchd) failed.'
|
207
|
+
end
|
208
|
+
|
209
|
+
if options[:ids_only]
|
210
|
+
replace @results[:matches].collect { |match|
|
211
|
+
match[:attributes]["sphinx_internal_id"]
|
212
|
+
}
|
213
|
+
else
|
214
|
+
replace instances_from_matches
|
215
|
+
add_excerpter
|
318
216
|
end
|
319
217
|
end
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
# Checks if a document with the given id exists within a specific index.
|
327
|
-
# Expected parameters:
|
328
|
-
#
|
329
|
-
# - ID of the document
|
330
|
-
# - Index to check within
|
331
|
-
# - Options hash (defaults to {})
|
332
|
-
#
|
333
|
-
# Example:
|
334
|
-
#
|
335
|
-
# ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User)
|
336
|
-
#
|
337
|
-
def search_for_id(*args)
|
338
|
-
options = args.extract_options!
|
339
|
-
client = client_from_options options
|
218
|
+
end
|
219
|
+
|
220
|
+
def add_excerpter
|
221
|
+
each do |object|
|
222
|
+
next if object.respond_to?(:excerpts)
|
340
223
|
|
341
|
-
|
342
|
-
|
343
|
-
)
|
344
|
-
client.filters += filters
|
345
|
-
client.match_mode = :extended unless query.empty?
|
346
|
-
client.id_range = args.first..args.first
|
224
|
+
excerpter = ThinkingSphinx::Excerpter.new self, object
|
225
|
+
block = lambda { excerpter }
|
347
226
|
|
348
|
-
|
349
|
-
|
350
|
-
rescue Errno::ECONNREFUSED => err
|
351
|
-
raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
|
227
|
+
object.metaclass.instance_eval do
|
228
|
+
define_method(:excerpts, &block)
|
352
229
|
end
|
353
230
|
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def self.log(message, method = :debug)
|
234
|
+
return if ::ActiveRecord::Base.logger.nil?
|
235
|
+
::ActiveRecord::Base.logger.send method, message
|
236
|
+
end
|
237
|
+
|
238
|
+
def log(message, method = :debug)
|
239
|
+
self.class.log(message, method)
|
240
|
+
end
|
241
|
+
|
242
|
+
def client
|
243
|
+
client = config.client
|
354
244
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
245
|
+
index_options = (options[:classes] || []).length != 1 ?
|
246
|
+
{} : options[:classes].first.sphinx_indexes.first.local_options
|
247
|
+
|
248
|
+
[
|
249
|
+
:max_matches, :group_by, :group_function, :group_clause,
|
250
|
+
:group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
|
251
|
+
:rank_mode, :max_query_time, :field_weights
|
252
|
+
].each do |key|
|
253
|
+
# puts "key: #{key}"
|
254
|
+
value = options[key] || index_options[key]
|
255
|
+
# puts "value: #{value.inspect}"
|
256
|
+
client.send("#{key}=", value) if value
|
365
257
|
end
|
366
258
|
|
367
|
-
|
259
|
+
client.limit = per_page
|
260
|
+
client.offset = offset
|
261
|
+
client.match_mode = match_mode
|
262
|
+
client.filters = filters
|
263
|
+
client.sort_mode = sort_mode
|
264
|
+
client.sort_by = sort_by
|
265
|
+
client.group_by = group_by if group_by
|
266
|
+
client.group_function = group_function if group_function
|
267
|
+
client.index_weights = index_weights
|
268
|
+
client.anchor = anchor
|
368
269
|
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
270
|
+
client
|
271
|
+
end
|
272
|
+
|
273
|
+
def retry_on_stale_index(&block)
|
274
|
+
stale_ids = []
|
275
|
+
retries = stale_retries
|
276
|
+
|
277
|
+
begin
|
278
|
+
options[:raise_on_stale] = retries > 0
|
279
|
+
block.call
|
280
|
+
|
281
|
+
# If ThinkingSphinx::Search#instances_from_matches found records in
|
282
|
+
# Sphinx but not in the DB and the :raise_on_stale option is set, this
|
283
|
+
# exception is raised. We retry a limited number of times, excluding the
|
284
|
+
# stale ids from the search.
|
285
|
+
rescue StaleIdsException => err
|
286
|
+
retries -= 1
|
287
|
+
|
288
|
+
# For logging
|
289
|
+
stale_ids |= err.ids
|
290
|
+
# ID exclusion
|
291
|
+
options[:without_ids] = Array(options[:without_ids]) | err.ids
|
292
|
+
|
293
|
+
log 'Sphinx Stale Ids (%s %s left): %s' % [
|
294
|
+
retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
|
295
|
+
]
|
296
|
+
retry
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def query
|
301
|
+
@query ||= begin
|
302
|
+
q = @args.join(' ') << conditions_as_query
|
303
|
+
(options[:star] ? star_query(q) : q).strip
|
403
304
|
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def conditions_as_query
|
308
|
+
return '' if @options[:conditions].blank?
|
309
|
+
|
310
|
+
# Soon to be deprecated.
|
311
|
+
keys = @options[:conditions].keys.reject { |key|
|
312
|
+
attributes.include?(key)
|
313
|
+
}
|
404
314
|
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
index_options = klass ? klass.sphinx_index_options : {}
|
315
|
+
' ' + keys.collect { |key|
|
316
|
+
"@#{key} #{options[:conditions][key]}"
|
317
|
+
}.join(' ')
|
318
|
+
end
|
319
|
+
|
320
|
+
def star_query(query)
|
321
|
+
token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
|
413
322
|
|
414
|
-
|
415
|
-
|
416
|
-
#
|
417
|
-
|
418
|
-
|
419
|
-
|
323
|
+
query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
|
324
|
+
pre, proper, post = $`, $&, $'
|
325
|
+
# E.g. "@foo", "/2", "~3", but not as part of a token
|
326
|
+
is_operator = pre.match(%r{(\W|^)[@~/]\Z})
|
327
|
+
# E.g. "foo bar", with quotes
|
328
|
+
is_quote = proper.starts_with?('"') && proper.ends_with?('"')
|
329
|
+
has_star = pre.ends_with?("*") || post.starts_with?("*")
|
330
|
+
if is_operator || is_quote || has_star
|
331
|
+
proper
|
332
|
+
else
|
333
|
+
"*#{proper}*"
|
420
334
|
end
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def index
|
339
|
+
options[:index] || '*'
|
340
|
+
end
|
341
|
+
|
342
|
+
def comment
|
343
|
+
options[:comment] || ''
|
344
|
+
end
|
345
|
+
|
346
|
+
def match_mode
|
347
|
+
options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
|
348
|
+
end
|
349
|
+
|
350
|
+
def sort_mode
|
351
|
+
@sort_mode ||= case options[:sort_mode]
|
352
|
+
when :asc
|
353
|
+
:attr_asc
|
354
|
+
when :desc
|
355
|
+
:attr_desc
|
356
|
+
when nil
|
357
|
+
case options[:order]
|
358
|
+
when String
|
359
|
+
:extended
|
360
|
+
when Symbol
|
361
|
+
:attr_asc
|
362
|
+
else
|
363
|
+
:relevance
|
435
364
|
end
|
436
|
-
|
437
|
-
[
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
365
|
+
else
|
366
|
+
options[:sort_mode]
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def sort_by
|
371
|
+
case @sort_by = (options[:sort_by] || options[:order])
|
372
|
+
when String
|
373
|
+
sorted_fields_to_attributes(@sort_by)
|
374
|
+
when Symbol
|
375
|
+
field_names.include?(@sort_by) ?
|
376
|
+
@sort_by.to_s.concat('_sort') : @sort_by.to_s
|
377
|
+
else
|
378
|
+
''
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def field_names
|
383
|
+
return [] if (options[:classes] || []).length != 1
|
384
|
+
|
385
|
+
options[:classes].first.sphinx_indexes.collect { |index|
|
386
|
+
index.fields.collect { |field| field.unique_name }
|
387
|
+
}.flatten
|
388
|
+
end
|
389
|
+
|
390
|
+
def sorted_fields_to_attributes(order_string)
|
391
|
+
field_names.each { |field|
|
392
|
+
order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
393
|
+
match.gsub field.to_s, field.to_s.concat("_sort")
|
394
|
+
}
|
395
|
+
}
|
396
|
+
|
397
|
+
order_string
|
398
|
+
end
|
399
|
+
|
400
|
+
# Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
|
401
|
+
# { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
|
402
|
+
#
|
403
|
+
def index_weights
|
404
|
+
weights = options[:index_weights] || {}
|
405
|
+
weights.keys.inject({}) do |hash, key|
|
406
|
+
if key.is_a?(Class)
|
407
|
+
name = ThinkingSphinx::Index.name(key)
|
408
|
+
hash["#{name}_core"] = weights[key]
|
409
|
+
hash["#{name}_delta"] = weights[key]
|
410
|
+
else
|
411
|
+
hash[key] = weights[key]
|
447
412
|
end
|
448
413
|
|
449
|
-
|
450
|
-
|
451
|
-
client.anchor = anchor_conditions(klass, options) || {} if client.anchor.empty?
|
452
|
-
|
453
|
-
client.filters << Riddle::Client::Filter.new(
|
454
|
-
"sphinx_deleted", [0]
|
455
|
-
)
|
456
|
-
|
457
|
-
# class filters
|
458
|
-
client.filters << Riddle::Client::Filter.new(
|
459
|
-
"class_crc", options[:classes].collect { |k| k.to_crc32s }.flatten
|
460
|
-
) if options[:classes]
|
461
|
-
|
462
|
-
# normal attribute filters
|
463
|
-
client.filters += options[:with].collect { |attr,val|
|
464
|
-
Riddle::Client::Filter.new attr.to_s, filter_value(val)
|
465
|
-
} if options[:with]
|
466
|
-
|
467
|
-
# exclusive attribute filters
|
468
|
-
client.filters += options[:without].collect { |attr,val|
|
469
|
-
Riddle::Client::Filter.new attr.to_s, filter_value(val), true
|
470
|
-
} if options[:without]
|
471
|
-
|
472
|
-
# every-match attribute filters
|
473
|
-
client.filters += options[:with_all].collect { |attr,vals|
|
474
|
-
Array(vals).collect { |val|
|
475
|
-
Riddle::Client::Filter.new attr.to_s, filter_value(val)
|
476
|
-
}
|
477
|
-
}.flatten if options[:with_all]
|
478
|
-
|
479
|
-
# exclusive attribute filter on primary key
|
480
|
-
client.filters += Array(options[:without_ids]).collect { |id|
|
481
|
-
Riddle::Client::Filter.new 'sphinx_internal_id', filter_value(id), true
|
482
|
-
} if options[:without_ids]
|
483
|
-
|
484
|
-
client
|
414
|
+
hash
|
485
415
|
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def group_by
|
419
|
+
options[:group] ? options[:group].to_s : nil
|
420
|
+
end
|
421
|
+
|
422
|
+
def group_function
|
423
|
+
options[:group] ? :attr : nil
|
424
|
+
end
|
425
|
+
|
426
|
+
def internal_filters
|
427
|
+
filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
|
486
428
|
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
is_quote = proper.starts_with?('"') && proper.ends_with?('"') # E.g. "foo bar", with quotes
|
494
|
-
has_star = pre.ends_with?("*") || post.starts_with?("*")
|
495
|
-
if is_operator || is_quote || has_star
|
496
|
-
proper
|
497
|
-
else
|
498
|
-
"*#{proper}*"
|
499
|
-
end
|
500
|
-
end
|
429
|
+
class_crcs = (options[:classes] || []).collect { |klass|
|
430
|
+
klass.to_crc32s
|
431
|
+
}.flatten
|
432
|
+
|
433
|
+
unless class_crcs.empty?
|
434
|
+
filters << Riddle::Client::Filter.new('class_crc', class_crcs)
|
501
435
|
end
|
502
436
|
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
437
|
+
filters << Riddle::Client::Filter.new(
|
438
|
+
'sphinx_internal_id', filter_value(options[:without_ids]), true
|
439
|
+
) if options[:without_ids]
|
440
|
+
|
441
|
+
filters
|
442
|
+
end
|
443
|
+
|
444
|
+
def condition_filters
|
445
|
+
(options[:conditions] || {}).collect { |attrib, value|
|
446
|
+
if attributes.include?(attrib)
|
447
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
509
448
|
else
|
510
|
-
|
449
|
+
nil
|
511
450
|
end
|
451
|
+
}.compact
|
452
|
+
end
|
453
|
+
|
454
|
+
def filters
|
455
|
+
internal_filters +
|
456
|
+
condition_filters +
|
457
|
+
(options[:with] || {}).collect { |attrib, value|
|
458
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
459
|
+
} +
|
460
|
+
(options[:without] || {}).collect { |attrib, value|
|
461
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
|
462
|
+
} +
|
463
|
+
(options[:with_all] || {}).collect { |attrib, values|
|
464
|
+
Array(values).collect { |value|
|
465
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
466
|
+
}
|
467
|
+
}.flatten
|
468
|
+
end
|
469
|
+
|
470
|
+
# When passed a Time instance, returns the integer timestamp.
|
471
|
+
#
|
472
|
+
# If using Rails 2.1+, need to handle timezones to translate them back to
|
473
|
+
# UTC, as that's what datetimes will be stored as by MySQL.
|
474
|
+
#
|
475
|
+
# in_time_zone is a method that was added for the timezone support in
|
476
|
+
# Rails 2.1, which is why it's used for testing. I'm sure there's better
|
477
|
+
# ways, but this does the job.
|
478
|
+
#
|
479
|
+
def filter_value(value)
|
480
|
+
case value
|
481
|
+
when Range
|
482
|
+
filter_value(value.first).first..filter_value(value.last).first
|
483
|
+
when Array
|
484
|
+
value.collect { |v| filter_value(v) }.flatten
|
485
|
+
when Time
|
486
|
+
value.respond_to?(:in_time_zone) ? [value.utc.to_i] : [value.to_i]
|
487
|
+
else
|
488
|
+
Array(value)
|
512
489
|
end
|
490
|
+
end
|
491
|
+
|
492
|
+
def anchor
|
493
|
+
return {} unless options[:geo] || (options[:lat] && options[:lng])
|
513
494
|
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
495
|
+
{
|
496
|
+
:latitude => options[:geo] ? options[:geo].first : options[:lat],
|
497
|
+
:longitude => options[:geo] ? options[:geo].last : options[:lng],
|
498
|
+
:latitude_attr => latitude_attr,
|
499
|
+
:longitude_attr => longitude_attr
|
500
|
+
}
|
501
|
+
end
|
502
|
+
|
503
|
+
def latitude_attr
|
504
|
+
options[:latitude_attr] ||
|
505
|
+
index_option(:latitude_attr) ||
|
506
|
+
attribute(:lat, :latitude)
|
507
|
+
end
|
508
|
+
|
509
|
+
def longitude_attr
|
510
|
+
options[:longitude_attr] ||
|
511
|
+
index_option(:longitude_attr) ||
|
512
|
+
attribute(:lon, :lng, :longitude)
|
513
|
+
end
|
514
|
+
|
515
|
+
def index_option(key)
|
516
|
+
return nil if options[:classes].length != 1
|
526
517
|
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
518
|
+
options[:classes].first.sphinx_indexes.collect { |index|
|
519
|
+
index.local_options[key]
|
520
|
+
}.compact.first
|
521
|
+
end
|
522
|
+
|
523
|
+
def attribute(*keys)
|
524
|
+
return nil if options[:classes].length != 1
|
525
|
+
|
526
|
+
keys.detect { |key|
|
527
|
+
attributes.include?(key)
|
528
|
+
}
|
529
|
+
end
|
530
|
+
|
531
|
+
def attributes
|
532
|
+
return [] if (options[:classes] || []).length != 1
|
533
|
+
|
534
|
+
attributes = options[:classes].first.sphinx_indexes.collect { |index|
|
535
|
+
index.attributes.collect { |attrib| attrib.unique_name }
|
536
|
+
}.flatten
|
537
|
+
end
|
538
|
+
|
539
|
+
def stale_retries
|
540
|
+
case options[:retry_stale]
|
541
|
+
when TrueClass
|
542
|
+
3
|
543
|
+
when nil, FalseClass
|
544
|
+
0
|
545
|
+
else
|
546
|
+
options[:retry_stale].to_i
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def instances_from_class(klass, matches)
|
551
|
+
index_options = klass.sphinx_index_options
|
552
|
+
|
553
|
+
ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
|
554
|
+
instances = ids.length > 0 ? klass.find(
|
555
|
+
:all,
|
556
|
+
:joins => options[:joins],
|
557
|
+
:conditions => {klass.primary_key.to_sym => ids},
|
558
|
+
:include => (options[:include] || index_options[:include]),
|
559
|
+
:select => (options[:select] || index_options[:select]),
|
560
|
+
:order => (options[:sql_order] || index_options[:sql_order])
|
561
|
+
) : []
|
562
|
+
|
563
|
+
# Raise an exception if we find records in Sphinx but not in the DB, so
|
564
|
+
# the search method can retry without them. See
|
565
|
+
# ThinkingSphinx::Search.retry_search_on_stale_index.
|
566
|
+
if options[:raise_on_stale] && instances.length < ids.length
|
567
|
+
stale_ids = ids - instances.map {|i| i.id }
|
568
|
+
raise StaleIdsException, stale_ids
|
569
|
+
end
|
570
|
+
|
571
|
+
# if the user has specified an SQL order, return the collection
|
572
|
+
# without rearranging it into the Sphinx order
|
573
|
+
return instances if options[:sql_order]
|
574
|
+
|
575
|
+
ids.collect { |obj_id|
|
576
|
+
instances.detect { |obj| obj.id == obj_id }
|
577
|
+
}
|
578
|
+
end
|
579
|
+
|
580
|
+
# Group results by class and call #find(:all) once for each group to reduce
|
581
|
+
# the number of #find's in multi-model searches.
|
582
|
+
#
|
583
|
+
def instances_from_matches
|
584
|
+
groups = results[:matches].group_by { |match|
|
585
|
+
match[:attributes]["class_crc"]
|
586
|
+
}
|
587
|
+
groups.each do |crc, group|
|
588
|
+
group.replace(
|
589
|
+
instances_from_class(class_from_crc(crc), group)
|
590
|
+
)
|
549
591
|
end
|
550
592
|
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
lat_attr = options[:latitude_attr] if options[:latitude_attr]
|
569
|
-
lat_attr ||= :lat if attributes.include?(:lat)
|
570
|
-
lat_attr ||= :latitude if attributes.include?(:latitude)
|
571
|
-
|
572
|
-
lon_attr = options[:longitude_attr] if options[:longitude_attr]
|
573
|
-
lon_attr ||= :lng if attributes.include?(:lng)
|
574
|
-
lon_attr ||= :lon if attributes.include?(:lon)
|
575
|
-
lon_attr ||= :long if attributes.include?(:long)
|
576
|
-
lon_attr ||= :longitude if attributes.include?(:longitude)
|
577
|
-
|
578
|
-
lat = options[:lat]
|
579
|
-
lon = options[:lon]
|
580
|
-
|
581
|
-
if options[:geo]
|
582
|
-
lat = options[:geo].first
|
583
|
-
lon = options[:geo].last
|
593
|
+
results[:matches].collect do |match|
|
594
|
+
groups.detect { |crc, group|
|
595
|
+
crc == match[:attributes]["class_crc"]
|
596
|
+
}[1].detect { |obj|
|
597
|
+
obj && obj.id == match[:attributes]["sphinx_internal_id"]
|
598
|
+
}
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
def class_from_crc(crc)
|
603
|
+
@models_by_crc ||= begin
|
604
|
+
ThinkingSphinx.indexed_models.inject({}) do |hash, model|
|
605
|
+
hash[model.constantize.to_crc32] = model
|
606
|
+
Object.subclasses_of(model.constantize).each { |subclass|
|
607
|
+
hash[subclass.to_crc32] = subclass.name
|
608
|
+
}
|
609
|
+
hash
|
584
610
|
end
|
585
|
-
|
586
|
-
lat && lon ? {
|
587
|
-
:latitude_attribute => lat_attr.to_s,
|
588
|
-
:latitude => lat,
|
589
|
-
:longitude_attribute => lon_attr.to_s,
|
590
|
-
:longitude => lon
|
591
|
-
} : nil
|
592
611
|
end
|
612
|
+
@models_by_crc[crc].constantize
|
613
|
+
end
|
614
|
+
|
615
|
+
def each_with_attribute(attribute, &block)
|
616
|
+
populate
|
617
|
+
results[:matches].each_with_index do |match, index|
|
618
|
+
yield self[index],
|
619
|
+
(match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
def is_scope?(method)
|
624
|
+
options[:classes] && options[:classes].length == 1 &&
|
625
|
+
options[:classes].first.sphinx_scopes.include?(method)
|
626
|
+
end
|
627
|
+
|
628
|
+
def add_scope(method, *args, &block)
|
629
|
+
search = options[:classes].first.send(method, *args, &block)
|
593
630
|
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
}.flatten : []
|
602
|
-
index_options = klass ? klass.sphinx_index_options : {}
|
603
|
-
|
604
|
-
order = options[:order] || index_options[:order]
|
605
|
-
case order
|
606
|
-
when Symbol
|
607
|
-
client.sort_mode = :attr_asc if client.sort_mode == :relevance || client.sort_mode.nil?
|
608
|
-
if fields.include?(order)
|
609
|
-
client.sort_by = order.to_s.concat("_sort")
|
610
|
-
else
|
611
|
-
client.sort_by = order.to_s
|
612
|
-
end
|
613
|
-
when String
|
614
|
-
client.sort_mode = :extended
|
615
|
-
client.sort_by = sorted_fields_to_attributes(order, fields)
|
631
|
+
search.options.keys.each do |key|
|
632
|
+
if HashOptions.include?(key)
|
633
|
+
options[key] ||= {}
|
634
|
+
options[key].merge! search.options[key]
|
635
|
+
elsif ArrayOptions.include?(key)
|
636
|
+
options[key] ||= []
|
637
|
+
options[key] += search.options[key]
|
616
638
|
else
|
617
|
-
|
639
|
+
options[key] = search.options[key]
|
618
640
|
end
|
619
|
-
|
620
|
-
client.sort_mode = :attr_asc if client.sort_mode == :asc
|
621
|
-
client.sort_mode = :attr_desc if client.sort_mode == :desc
|
622
|
-
end
|
623
|
-
|
624
|
-
# Search through a collection of fields and translate any appearances
|
625
|
-
# of them in a string to their attribute equivalent for sorting.
|
626
|
-
#
|
627
|
-
def sorted_fields_to_attributes(string, fields)
|
628
|
-
fields.each { |field|
|
629
|
-
string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
630
|
-
match.gsub field.to_s, field.to_s.concat("_sort")
|
631
|
-
}
|
632
|
-
}
|
633
|
-
|
634
|
-
string
|
635
641
|
end
|
636
642
|
end
|
637
643
|
end
|