ryanb-thinking_sphinx 0.9.8
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/LICENCE +20 -0
- data/README +60 -0
- data/lib/riddle.rb +26 -0
- data/lib/riddle/client.rb +639 -0
- data/lib/riddle/client/filter.rb +44 -0
- data/lib/riddle/client/message.rb +65 -0
- data/lib/riddle/client/response.rb +84 -0
- data/lib/test.rb +46 -0
- data/lib/thinking_sphinx.rb +102 -0
- data/lib/thinking_sphinx/active_record.rb +141 -0
- data/lib/thinking_sphinx/active_record/delta.rb +97 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
- data/lib/thinking_sphinx/active_record/search.rb +50 -0
- data/lib/thinking_sphinx/association.rb +144 -0
- data/lib/thinking_sphinx/attribute.rb +284 -0
- data/lib/thinking_sphinx/configuration.rb +283 -0
- data/lib/thinking_sphinx/field.rb +200 -0
- data/lib/thinking_sphinx/index.rb +340 -0
- data/lib/thinking_sphinx/index/builder.rb +195 -0
- data/lib/thinking_sphinx/index/faux_column.rb +110 -0
- data/lib/thinking_sphinx/rails_additions.rb +56 -0
- data/lib/thinking_sphinx/search.rb +482 -0
- data/lib/thinking_sphinx/tasks.rb +86 -0
- data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +207 -0
- data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
- data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
- data/spec/unit/thinking_sphinx/active_record_spec.rb +236 -0
- data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
- data/spec/unit/thinking_sphinx/attribute_spec.rb +360 -0
- data/spec/unit/thinking_sphinx/configuration_spec.rb +493 -0
- data/spec/unit/thinking_sphinx/field_spec.rb +219 -0
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +33 -0
- data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +68 -0
- data/spec/unit/thinking_sphinx/index_spec.rb +277 -0
- data/spec/unit/thinking_sphinx/search_spec.rb +190 -0
- data/spec/unit/thinking_sphinx_spec.rb +129 -0
- data/tasks/thinking_sphinx_tasks.rake +1 -0
- metadata +103 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
class Index
|
3
|
+
# Instances of this class represent database columns and the stack of
|
4
|
+
# associations that lead from the base model to them.
|
5
|
+
#
|
6
|
+
# The name and stack are accessible through methods starting with __ to
|
7
|
+
# avoid conflicting with the method_missing calls that build the stack.
|
8
|
+
#
|
9
|
+
class FauxColumn
|
10
|
+
# Create a new column with a pre-defined stack. The top element in the
|
11
|
+
# stack will get shifted to be the name value.
|
12
|
+
#
|
13
|
+
def initialize(*stack)
|
14
|
+
@name = stack.pop
|
15
|
+
@stack = stack
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.coerce(columns)
|
19
|
+
case columns
|
20
|
+
when Symbol, String
|
21
|
+
FauxColumn.new(columns)
|
22
|
+
when Array
|
23
|
+
columns.collect { |col| FauxColumn.coerce(col) }
|
24
|
+
when FauxColumn
|
25
|
+
columns
|
26
|
+
else
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Can't use normal method name, as that could be an association or
|
32
|
+
# column name.
|
33
|
+
#
|
34
|
+
def __name
|
35
|
+
@name
|
36
|
+
end
|
37
|
+
|
38
|
+
# Can't use normal method name, as that could be an association or
|
39
|
+
# column name.
|
40
|
+
#
|
41
|
+
def __stack
|
42
|
+
@stack
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns true if the stack is empty *and* if the name is a string -
|
46
|
+
# which is an indication that of raw SQL, as opposed to a value from a
|
47
|
+
# table's column.
|
48
|
+
#
|
49
|
+
def is_string?
|
50
|
+
@name.is_a?(String) && @stack.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
# This handles any 'invalid' method calls and sets them as the name,
|
54
|
+
# and pushing the previous name into the stack. The object returns
|
55
|
+
# itself.
|
56
|
+
#
|
57
|
+
# If there's a single argument, it becomes the name, and the method
|
58
|
+
# symbol goes into the stack as well. Multiple arguments means new
|
59
|
+
# columns with the original stack and new names (from each argument) gets
|
60
|
+
# returned.
|
61
|
+
#
|
62
|
+
# Easier to explain with examples:
|
63
|
+
#
|
64
|
+
# col = FauxColumn.new :a, :b, :c
|
65
|
+
# col.__name #=> :c
|
66
|
+
# col.__stack #=> [:a, :b]
|
67
|
+
#
|
68
|
+
# col.whatever #=> col
|
69
|
+
# col.__name #=> :whatever
|
70
|
+
# col.__stack #=> [:a, :b, :c]
|
71
|
+
#
|
72
|
+
# col.something(:id) #=> col
|
73
|
+
# col.__name #=> :id
|
74
|
+
# col.__stack #=> [:a, :b, :c, :whatever, :something]
|
75
|
+
#
|
76
|
+
# cols = col.short(:x, :y, :z)
|
77
|
+
# cols[0].__name #=> :x
|
78
|
+
# cols[0].__stack #=> [:a, :b, :c, :whatever, :something, :short]
|
79
|
+
# cols[1].__name #=> :y
|
80
|
+
# cols[1].__stack #=> [:a, :b, :c, :whatever, :something, :short]
|
81
|
+
# cols[2].__name #=> :z
|
82
|
+
# cols[2].__stack #=> [:a, :b, :c, :whatever, :something, :short]
|
83
|
+
#
|
84
|
+
# Also, this allows method chaining to build up a relevant stack:
|
85
|
+
#
|
86
|
+
# col = FauxColumn.new :a, :b
|
87
|
+
# col.__name #=> :b
|
88
|
+
# col.__stack #=> [:a]
|
89
|
+
#
|
90
|
+
# col.one.two.three #=> col
|
91
|
+
# col.__name #=> :three
|
92
|
+
# col.__stack #=> [:a, :b, :one, :two]
|
93
|
+
#
|
94
|
+
def method_missing(method, *args)
|
95
|
+
@stack << @name
|
96
|
+
@name = method
|
97
|
+
|
98
|
+
if (args.empty?)
|
99
|
+
self
|
100
|
+
elsif (args.length == 1)
|
101
|
+
method_missing(args.first)
|
102
|
+
else
|
103
|
+
args.collect { |arg|
|
104
|
+
FauxColumn.new(@stack + [@name, arg])
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module HashExcept
|
3
|
+
# Returns a new hash without the given keys.
|
4
|
+
def except(*keys)
|
5
|
+
rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
|
6
|
+
reject { |key,| rejected.include?(key) }
|
7
|
+
end
|
8
|
+
|
9
|
+
# Replaces the hash without only the given keys.
|
10
|
+
def except!(*keys)
|
11
|
+
replace(except(*keys))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Hash.send(
|
17
|
+
:include, ThinkingSphinx::HashExcept
|
18
|
+
) unless Hash.instance_methods.include?("except")
|
19
|
+
|
20
|
+
module ThinkingSphinx
|
21
|
+
module ArrayExtractOptions
|
22
|
+
def extract_options!
|
23
|
+
last.is_a?(::Hash) ? pop : {}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Array.send(
|
29
|
+
:include, ThinkingSphinx::ArrayExtractOptions
|
30
|
+
) unless Array.instance_methods.include?("extract_options!")
|
31
|
+
|
32
|
+
module ThinkingSphinx
|
33
|
+
module MysqlQuotedTableName
|
34
|
+
def quote_table_name(name) #:nodoc:
|
35
|
+
quote_column_name(name).gsub('.', '`.`')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter")
|
41
|
+
ActiveRecord::ConnectionAdapters::MysqlAdapter.send(
|
42
|
+
:include, ThinkingSphinx::MysqlQuotedTableName
|
43
|
+
) unless ActiveRecord::ConnectionAdapters::MysqlAdapter.instance_methods.include?("quote_table_name")
|
44
|
+
end
|
45
|
+
|
46
|
+
module ThinkingSphinx
|
47
|
+
module ActiveRecordQuotedName
|
48
|
+
def quoted_table_name
|
49
|
+
self.connection.quote_table_name(self.table_name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
ActiveRecord::Base.extend(
|
55
|
+
ThinkingSphinx::ActiveRecordQuotedName
|
56
|
+
) unless ActiveRecord::Base.respond_to?("quoted_table_name")
|
@@ -0,0 +1,482 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
# Once you've got those indexes in and built, this is the stuff that
|
3
|
+
# matters - how to search! This class provides a generic search
|
4
|
+
# interface - which you can use to search all your indexed models at once.
|
5
|
+
# Most times, you will just want a specific model's results - to search and
|
6
|
+
# search_for_ids methods will do the job in exactly the same manner when
|
7
|
+
# called from a model.
|
8
|
+
#
|
9
|
+
class Search
|
10
|
+
class << self
|
11
|
+
# Searches for results that match the parameters provided. Will only
|
12
|
+
# return the ids for the matching objects. See #search for syntax
|
13
|
+
# examples.
|
14
|
+
#
|
15
|
+
def search_for_ids(*args)
|
16
|
+
results, client = search_results(*args.clone)
|
17
|
+
|
18
|
+
options = args.extract_options!
|
19
|
+
page = options[:page] ? options[:page].to_i : 1
|
20
|
+
|
21
|
+
begin
|
22
|
+
pager = WillPaginate::Collection.create(page,
|
23
|
+
client.limit, results[:total_found] || 0) do |collection|
|
24
|
+
collection.replace results[:matches].collect { |match| match[:doc] }
|
25
|
+
collection.instance_variable_set :@total_entries, results[:total_found]
|
26
|
+
end
|
27
|
+
rescue
|
28
|
+
results[:matches].collect { |match| match[:doc] }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Searches through the Sphinx indexes for relevant matches. There's
|
33
|
+
# various ways to search, sort, group and filter - which are covered
|
34
|
+
# below.
|
35
|
+
#
|
36
|
+
# Also, if you have WillPaginate installed, the search method can be used
|
37
|
+
# just like paginate. The same parameters - :page and :per_page - work as
|
38
|
+
# expected, and the returned result set can be used by the will_paginate
|
39
|
+
# helper.
|
40
|
+
#
|
41
|
+
# == Basic Searching
|
42
|
+
#
|
43
|
+
# The simplest way of searching is straight text.
|
44
|
+
#
|
45
|
+
# ThinkingSphinx::Search.search "pat"
|
46
|
+
# ThinkingSphinx::Search.search "google"
|
47
|
+
# User.search "pat", :page => (params[:page] || 1)
|
48
|
+
# Article.search "relevant news issue of the day"
|
49
|
+
#
|
50
|
+
# If you specify :include, like in an #find call, this will be respected
|
51
|
+
# when loading the relevant models from the search results.
|
52
|
+
#
|
53
|
+
# User.search "pat", :include => :posts
|
54
|
+
#
|
55
|
+
# == Advanced Searching
|
56
|
+
#
|
57
|
+
# Sphinx supports 5 different matching modes. By default Thinking Sphinx
|
58
|
+
# uses :all, which unsurprisingly requires all the supplied search terms
|
59
|
+
# to match a result.
|
60
|
+
#
|
61
|
+
# Alternative modes include:
|
62
|
+
#
|
63
|
+
# User.search "pat allan", :match_mode => :any
|
64
|
+
# User.search "pat allan", :match_mode => :phrase
|
65
|
+
# User.search "pat | allan", :match_mode => :boolean
|
66
|
+
# User.search "@name pat | @username pat", :match_mode => :extended
|
67
|
+
#
|
68
|
+
# Any will find results with any of the search terms. Phrase treats the search
|
69
|
+
# terms a single phrase instead of individual words. Boolean and extended allow
|
70
|
+
# for more complex query syntax, refer to the sphinx documentation for further
|
71
|
+
# details.
|
72
|
+
#
|
73
|
+
# == Searching by Fields
|
74
|
+
#
|
75
|
+
# If you want to step it up a level, you can limit your search terms to
|
76
|
+
# specific fields:
|
77
|
+
#
|
78
|
+
# User.search :conditions => {:name => "pat"}
|
79
|
+
#
|
80
|
+
# This uses Sphinx's extended match mode, unless you specify a different
|
81
|
+
# match mode explicitly (but then this way of searching won't work). Also
|
82
|
+
# note that you don't need to put in a search string.
|
83
|
+
#
|
84
|
+
# == Searching by Attributes
|
85
|
+
#
|
86
|
+
# Also known as filters, you can limit your searches to documents that
|
87
|
+
# have specific values for their attributes. There are two ways to do
|
88
|
+
# this. The first is one that works in all scenarios - using the :with
|
89
|
+
# option.
|
90
|
+
#
|
91
|
+
# ThinkingSphinx::Search.search :with => {:parent_id => 10}
|
92
|
+
#
|
93
|
+
# The second is only viable if you're searching with a specific model
|
94
|
+
# (not multi-model searching). With a single model, Thinking Sphinx
|
95
|
+
# can figure out what attributes and fields are available, so you can
|
96
|
+
# put it all in the :conditions hash, and it will sort it out.
|
97
|
+
#
|
98
|
+
# Node.search :conditions => {:parent_id => 10}
|
99
|
+
#
|
100
|
+
# Filters can be single values, arrays of values, or ranges.
|
101
|
+
#
|
102
|
+
# Article.search "East Timor", :conditions => {:rating => 3..5}
|
103
|
+
#
|
104
|
+
# == Excluding by Attributes
|
105
|
+
#
|
106
|
+
# Sphinx also supports negative filtering - where the filters are of
|
107
|
+
# attribute values to exclude. This is done with the :without option:
|
108
|
+
#
|
109
|
+
# User.search :without => {:role_id => 1}
|
110
|
+
#
|
111
|
+
# == Sorting
|
112
|
+
#
|
113
|
+
# Sphinx can only sort by attributes, so generally you will need to avoid
|
114
|
+
# using field names in your :order option. However, if you're searching
|
115
|
+
# on a single model, and have specified some fields as sortable, you can
|
116
|
+
# use those field names and Thinking Sphinx will interpret accordingly.
|
117
|
+
# Remember: this will only happen for single-model searches, and only
|
118
|
+
# through the :order option.
|
119
|
+
#
|
120
|
+
# Location.search "Melbourne", :order => :state
|
121
|
+
# User.search :conditions => {:role_id => 2}, :order => "name ASC"
|
122
|
+
#
|
123
|
+
# Keep in mind that if you use a string, you *must* specify the direction
|
124
|
+
# (ASC or DESC) else Sphinx won't return any results. If you use a symbol
|
125
|
+
# then Thinking Sphinx assumes ASC, but if you wish to state otherwise,
|
126
|
+
# use the :sort_mode option:
|
127
|
+
#
|
128
|
+
# Location.search "Melbourne", :order => :state, :sort_mode => :desc
|
129
|
+
#
|
130
|
+
# Of course, there are other sort modes - check out the Sphinx
|
131
|
+
# documentation[http://sphinxsearch.com/doc.html] for that level of
|
132
|
+
# detail though.
|
133
|
+
#
|
134
|
+
# == Grouping
|
135
|
+
#
|
136
|
+
# For this you can use the group_by, group_clause and group_function
|
137
|
+
# options - which are all directly linked to Sphinx's expectations. No
|
138
|
+
# magic from Thinking Sphinx. It can get a little tricky, so make sure
|
139
|
+
# you read all the relevant
|
140
|
+
# documentation[http://sphinxsearch.com/doc.html#clustering] first.
|
141
|
+
#
|
142
|
+
# Yes this section will be expanded, but this is a start.
|
143
|
+
#
|
144
|
+
# == Geo/Location Searching
|
145
|
+
#
|
146
|
+
# Sphinx - and therefore Thinking Sphinx - has the facility to search
|
147
|
+
# around a geographical point, using a given latitude and longitude. To
|
148
|
+
# take advantage of this, you will need to have both of those values in
|
149
|
+
# attributes. To search with that point, you can then use one of the
|
150
|
+
# following syntax examples:
|
151
|
+
#
|
152
|
+
# Address.search "Melbourne", :geo => [1.4, -2.217]
|
153
|
+
# Address.search "Australia", :geo => [-0.55, 3.108],
|
154
|
+
# :latitude_attr => "latit", :longitude_attr => "longit"
|
155
|
+
#
|
156
|
+
# The first example applies when your latitude and longitude attributes
|
157
|
+
# are named any of lat, latitude, lon, long or longitude. If that's not
|
158
|
+
# the case, you will need to explicitly state them in your search, _or_
|
159
|
+
# you can do so in your model:
|
160
|
+
#
|
161
|
+
# define_index do
|
162
|
+
# has :latit # Float column, stored in radians
|
163
|
+
# has :longit # Float column, stored in radians
|
164
|
+
#
|
165
|
+
# set_property :latitude_attr => "latit"
|
166
|
+
# set_property :longitude_attr => "longit"
|
167
|
+
# end
|
168
|
+
#
|
169
|
+
# Now, geo-location searching really only has an affect if you have a
|
170
|
+
# filter, sort or grouping clause related to it - otherwise it's just a
|
171
|
+
# normal search. To make use of the positioning difference, use the
|
172
|
+
# special attribute "@geodist" in any of your filters or sorting or grouping
|
173
|
+
# clauses.
|
174
|
+
#
|
175
|
+
# And don't forget - both the latitude and longitude you use in your
|
176
|
+
# search, and the values in your indexes, need to be stored as a float in radians,
|
177
|
+
# _not_ degrees. Keep in mind that if you do this conversion in SQL
|
178
|
+
# you will need to explicitly declare a column type of :float.
|
179
|
+
#
|
180
|
+
# define_index do
|
181
|
+
# has 'RADIANS(lat)', :as => :lat, :type => :float
|
182
|
+
# # ...
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
def search(*args)
|
186
|
+
results, client = search_results(*args.clone)
|
187
|
+
|
188
|
+
::ActiveRecord::Base.logger.error(
|
189
|
+
"Sphinx Error: #{results[:error]}"
|
190
|
+
) if results[:error]
|
191
|
+
|
192
|
+
options = args.extract_options!
|
193
|
+
klass = options[:class]
|
194
|
+
page = options[:page] ? options[:page].to_i : 1
|
195
|
+
|
196
|
+
begin
|
197
|
+
pager = WillPaginate::Collection.create(page,
|
198
|
+
client.limit, results[:total] || 0) do |collection|
|
199
|
+
collection.replace instances_from_results(results[:matches], options, klass)
|
200
|
+
collection.instance_variable_set :@total_entries, results[:total_found]
|
201
|
+
end
|
202
|
+
rescue StandardError => err
|
203
|
+
instances_from_results(results[:matches], options, klass)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def count(*args)
|
208
|
+
results, client = search_results(*args.clone)
|
209
|
+
results[:total] || 0
|
210
|
+
end
|
211
|
+
|
212
|
+
# Checks if a document with the given id exists within a specific index.
|
213
|
+
# Expected parameters:
|
214
|
+
#
|
215
|
+
# - ID of the document
|
216
|
+
# - Index to check within
|
217
|
+
# - Options hash (defaults to {})
|
218
|
+
#
|
219
|
+
# Example:
|
220
|
+
#
|
221
|
+
# ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User)
|
222
|
+
#
|
223
|
+
def search_for_id(*args)
|
224
|
+
options = args.extract_options!
|
225
|
+
client = client_from_options options
|
226
|
+
|
227
|
+
query, filters = search_conditions(
|
228
|
+
options[:class], options[:conditions] || {}
|
229
|
+
)
|
230
|
+
client.filters += filters
|
231
|
+
client.match_mode = :extended unless query.empty?
|
232
|
+
client.id_range = args.first..args.first
|
233
|
+
|
234
|
+
begin
|
235
|
+
return client.query(query, args[1])[:matches].length > 0
|
236
|
+
rescue Errno::ECONNREFUSED => err
|
237
|
+
raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
# This method handles the common search functionality, and returns both
|
244
|
+
# the result hash and the client. Not super elegant, but it'll do for
|
245
|
+
# the moment.
|
246
|
+
#
|
247
|
+
def search_results(*args)
|
248
|
+
options = args.extract_options!
|
249
|
+
client = client_from_options options
|
250
|
+
|
251
|
+
query, filters = search_conditions(
|
252
|
+
options[:class], options[:conditions] || {}
|
253
|
+
)
|
254
|
+
client.filters += filters
|
255
|
+
client.match_mode = :extended unless query.empty?
|
256
|
+
query = args.join(" ") + query
|
257
|
+
|
258
|
+
set_sort_options! client, options
|
259
|
+
|
260
|
+
client.limit = options[:per_page].to_i if options[:per_page]
|
261
|
+
page = options[:page] ? options[:page].to_i : 1
|
262
|
+
client.offset = (page - 1) * client.limit
|
263
|
+
|
264
|
+
begin
|
265
|
+
::ActiveRecord::Base.logger.debug "Sphinx: #{query}"
|
266
|
+
results = client.query query
|
267
|
+
::ActiveRecord::Base.logger.debug "Sphinx Result: #{results[:matches].collect{|m| m[:doc]}.inspect}"
|
268
|
+
rescue Errno::ECONNREFUSED => err
|
269
|
+
raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
|
270
|
+
end
|
271
|
+
|
272
|
+
return results, client
|
273
|
+
end
|
274
|
+
|
275
|
+
def instances_from_results(results, options = {}, klass = nil)
|
276
|
+
if klass.nil?
|
277
|
+
results.collect { |result| instance_from_result result, options }
|
278
|
+
else
|
279
|
+
ids = results.collect { |result| result[:doc] }
|
280
|
+
instances = klass.find(
|
281
|
+
:all,
|
282
|
+
:conditions => {klass.primary_key.to_sym => ids},
|
283
|
+
:include => options[:include],
|
284
|
+
:select => options[:select]
|
285
|
+
)
|
286
|
+
ids.collect { |obj_id| instances.detect { |obj| obj.id == obj_id } }
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Either use the provided class to instantiate a result from a model, or
|
291
|
+
# get the result's CRC value and determine the class from that.
|
292
|
+
#
|
293
|
+
def instance_from_result(result, options)
|
294
|
+
class_from_crc(result[:attributes]["class_crc"]).find(
|
295
|
+
result[:doc], :include => options[:include], :select => options[:select]
|
296
|
+
)
|
297
|
+
end
|
298
|
+
|
299
|
+
# Convert a CRC value to the corresponding class.
|
300
|
+
#
|
301
|
+
def class_from_crc(crc)
|
302
|
+
unless @models_by_crc
|
303
|
+
Configuration.new.load_models
|
304
|
+
|
305
|
+
@models_by_crc = ThinkingSphinx.indexed_models.inject({}) do |hash, model|
|
306
|
+
hash[model.constantize.to_crc32] = model
|
307
|
+
hash
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
@models_by_crc[crc].constantize
|
312
|
+
end
|
313
|
+
|
314
|
+
# Set all the appropriate settings for the client, using the provided
|
315
|
+
# options hash.
|
316
|
+
#
|
317
|
+
def client_from_options(options = {})
|
318
|
+
config = ThinkingSphinx::Configuration.new
|
319
|
+
client = Riddle::Client.new config.address, config.port
|
320
|
+
klass = options[:class]
|
321
|
+
index_options = klass ? klass.indexes.last.options : {}
|
322
|
+
|
323
|
+
[
|
324
|
+
:max_matches, :match_mode, :sort_mode, :sort_by, :id_range,
|
325
|
+
:group_by, :group_function, :group_clause, :group_distinct, :cut_off,
|
326
|
+
:retry_count, :retry_delay, :index_weights, :rank_mode,
|
327
|
+
:max_query_time, :field_weights, :filters, :anchor, :limit
|
328
|
+
].each do |key|
|
329
|
+
client.send(
|
330
|
+
key.to_s.concat("=").to_sym,
|
331
|
+
options[key] || index_options[key] || client.send(key)
|
332
|
+
)
|
333
|
+
end
|
334
|
+
|
335
|
+
client.anchor = anchor_conditions(klass, options) || {} if client.anchor.empty?
|
336
|
+
|
337
|
+
client.filters << Riddle::Client::Filter.new(
|
338
|
+
"sphinx_deleted", [0]
|
339
|
+
)
|
340
|
+
# class filters
|
341
|
+
client.filters << Riddle::Client::Filter.new(
|
342
|
+
"class_crc", options[:classes].collect { |klass| klass.to_crc32 }
|
343
|
+
) if options[:classes]
|
344
|
+
|
345
|
+
# normal attribute filters
|
346
|
+
client.filters += options[:with].collect { |attr,val|
|
347
|
+
Riddle::Client::Filter.new attr.to_s, filter_value(val)
|
348
|
+
} if options[:with]
|
349
|
+
|
350
|
+
# exclusive attribute filters
|
351
|
+
client.filters += options[:without].collect { |attr,val|
|
352
|
+
Riddle::Client::Filter.new attr.to_s, filter_value(val), true
|
353
|
+
} if options[:without]
|
354
|
+
|
355
|
+
client
|
356
|
+
end
|
357
|
+
|
358
|
+
def filter_value(value)
|
359
|
+
case value
|
360
|
+
when Range
|
361
|
+
value.first.is_a?(Time) ? value.first.to_i..value.last.to_i : value
|
362
|
+
when Array
|
363
|
+
value.collect { |val| val.is_a?(Time) ? val.to_i : val }
|
364
|
+
else
|
365
|
+
Array(value)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Translate field and attribute conditions to the relevant search string
|
370
|
+
# and filters.
|
371
|
+
#
|
372
|
+
def search_conditions(klass, conditions={})
|
373
|
+
attributes = klass ? klass.indexes.collect { |index|
|
374
|
+
index.attributes.collect { |attrib| attrib.unique_name }
|
375
|
+
}.flatten : []
|
376
|
+
|
377
|
+
search_string = ""
|
378
|
+
filters = []
|
379
|
+
|
380
|
+
conditions.each do |key,val|
|
381
|
+
if attributes.include?(key.to_sym)
|
382
|
+
filters << Riddle::Client::Filter.new(
|
383
|
+
key.to_s,
|
384
|
+
val.is_a?(Range) ? val : Array(val)
|
385
|
+
)
|
386
|
+
else
|
387
|
+
search_string << "@#{key} #{val} "
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
filters << Riddle::Client::Filter.new(
|
392
|
+
"class_crc", [klass.to_crc32]
|
393
|
+
) if klass
|
394
|
+
|
395
|
+
return search_string, filters
|
396
|
+
end
|
397
|
+
|
398
|
+
# Return the appropriate latitude and longitude values, depending on
|
399
|
+
# whether the relevant attributes have been defined, and also whether
|
400
|
+
# there's actually any values.
|
401
|
+
#
|
402
|
+
def anchor_conditions(klass, options)
|
403
|
+
attributes = klass ? klass.indexes.collect { |index|
|
404
|
+
index.attributes.collect { |attrib| attrib.unique_name }
|
405
|
+
}.flatten : []
|
406
|
+
|
407
|
+
lat_attr = klass ? klass.indexes.collect { |index|
|
408
|
+
index.options[:latitude_attr]
|
409
|
+
}.compact.first : nil
|
410
|
+
|
411
|
+
lon_attr = klass ? klass.indexes.collect { |index|
|
412
|
+
index.options[:longitude_attr]
|
413
|
+
}.compact.first : nil
|
414
|
+
|
415
|
+
lat_attr = options[:latitude_attr] if options[:latitude_attr]
|
416
|
+
lat_attr ||= :lat if attributes.include?(:lat)
|
417
|
+
lat_attr ||= :latitude if attributes.include?(:latitude)
|
418
|
+
|
419
|
+
lon_attr = options[:longitude_attr] if options[:longitude_attr]
|
420
|
+
lon_attr ||= :lon if attributes.include?(:lon)
|
421
|
+
lon_attr ||= :long if attributes.include?(:long)
|
422
|
+
lon_attr ||= :longitude if attributes.include?(:longitude)
|
423
|
+
|
424
|
+
lat = options[:lat]
|
425
|
+
lon = options[:lon]
|
426
|
+
|
427
|
+
if options[:geo]
|
428
|
+
lat = options[:geo].first
|
429
|
+
lon = options[:geo].last
|
430
|
+
end
|
431
|
+
|
432
|
+
lat && lon ? {
|
433
|
+
:latitude_attribute => lat_attr,
|
434
|
+
:latitude => lat,
|
435
|
+
:longitude_attribute => lon_attr,
|
436
|
+
:longitude => lon
|
437
|
+
} : nil
|
438
|
+
end
|
439
|
+
|
440
|
+
# Set the sort options using the :order key as well as the appropriate
|
441
|
+
# Riddle settings.
|
442
|
+
#
|
443
|
+
def set_sort_options!(client, options)
|
444
|
+
klass = options[:class]
|
445
|
+
fields = klass ? klass.indexes.collect { |index|
|
446
|
+
index.fields.collect { |field| field.unique_name }
|
447
|
+
}.flatten : []
|
448
|
+
|
449
|
+
case order = options[:order]
|
450
|
+
when Symbol
|
451
|
+
client.sort_mode = :attr_asc if client.sort_mode == :relevance || client.sort_mode.nil?
|
452
|
+
if fields.include?(order)
|
453
|
+
client.sort_by = order.to_s.concat("_sort")
|
454
|
+
else
|
455
|
+
client.sort_by = order.to_s
|
456
|
+
end
|
457
|
+
when String
|
458
|
+
client.sort_mode = :extended
|
459
|
+
client.sort_by = sorted_fields_to_attributes(order, fields)
|
460
|
+
else
|
461
|
+
# do nothing
|
462
|
+
end
|
463
|
+
|
464
|
+
client.sort_mode = :attr_asc if client.sort_mode == :asc
|
465
|
+
client.sort_mode = :attr_desc if client.sort_mode == :desc
|
466
|
+
end
|
467
|
+
|
468
|
+
# Search through a collection of fields and translate any appearances
|
469
|
+
# of them in a string to their attribute equivalent for sorting.
|
470
|
+
#
|
471
|
+
def sorted_fields_to_attributes(string, fields)
|
472
|
+
fields.each { |field|
|
473
|
+
string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
474
|
+
match.gsub field.to_s, field.to_s.concat("_sort")
|
475
|
+
}
|
476
|
+
}
|
477
|
+
|
478
|
+
string
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|