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