DrMark-thinking-sphinx 0.9.9 → 1.1.6
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 +64 -2
- data/lib/thinking_sphinx.rb +88 -11
- data/lib/thinking_sphinx/active_record.rb +136 -21
- data/lib/thinking_sphinx/active_record/delta.rb +43 -62
- data/lib/thinking_sphinx/active_record/has_many_association.rb +1 -1
- data/lib/thinking_sphinx/active_record/search.rb +7 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +130 -0
- data/lib/thinking_sphinx/association.rb +17 -0
- data/lib/thinking_sphinx/attribute.rb +171 -97
- data/lib/thinking_sphinx/collection.rb +126 -2
- data/lib/thinking_sphinx/configuration.rb +120 -171
- data/lib/thinking_sphinx/core/string.rb +15 -0
- data/lib/thinking_sphinx/deltas.rb +27 -0
- data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +67 -0
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
- data/lib/thinking_sphinx/facet.rb +58 -0
- data/lib/thinking_sphinx/facet_collection.rb +60 -0
- data/lib/thinking_sphinx/field.rb +18 -52
- data/lib/thinking_sphinx/index.rb +246 -199
- data/lib/thinking_sphinx/index/builder.rb +85 -16
- data/lib/thinking_sphinx/rails_additions.rb +85 -5
- data/lib/thinking_sphinx/search.rb +459 -190
- data/lib/thinking_sphinx/tasks.rb +128 -0
- data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +53 -124
- data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +2 -2
- data/spec/unit/thinking_sphinx/active_record_spec.rb +110 -30
- data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -149
- data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
- data/spec/unit/thinking_sphinx/configuration_spec.rb +54 -412
- data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
- data/spec/unit/thinking_sphinx/field_spec.rb +0 -79
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +1 -29
- data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +1 -39
- data/spec/unit/thinking_sphinx/index_spec.rb +78 -226
- data/spec/unit/thinking_sphinx/search_spec.rb +29 -228
- data/spec/unit/thinking_sphinx_spec.rb +23 -19
- data/tasks/distribution.rb +48 -0
- data/tasks/rails.rake +1 -0
- data/tasks/testing.rb +86 -0
- data/vendor/after_commit/LICENSE +20 -0
- data/vendor/after_commit/README +16 -0
- data/vendor/after_commit/Rakefile +22 -0
- data/vendor/after_commit/init.rb +8 -0
- data/vendor/after_commit/lib/after_commit.rb +45 -0
- data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
- data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
- data/vendor/after_commit/test/after_commit_test.rb +53 -0
- data/vendor/delayed_job/lib/delayed/job.rb +251 -0
- data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
- data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
- data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
- data/{lib → vendor/riddle/lib}/riddle.rb +9 -5
- data/{lib → vendor/riddle/lib}/riddle/client.rb +6 -26
- data/{lib → vendor/riddle/lib}/riddle/client/filter.rb +10 -1
- data/{lib → vendor/riddle/lib}/riddle/client/message.rb +0 -0
- data/{lib → vendor/riddle/lib}/riddle/client/response.rb +0 -0
- data/vendor/riddle/lib/riddle/configuration.rb +33 -0
- data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
- data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
- data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
- data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
- data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
- data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
- data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
- data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
- data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
- data/vendor/riddle/lib/riddle/controller.rb +44 -0
- metadata +63 -10
- data/lib/test.rb +0 -46
- data/tasks/thinking_sphinx_tasks.rake +0 -1
- data/tasks/thinking_sphinx_tasks.rb +0 -86
@@ -18,9 +18,14 @@ module ThinkingSphinx
|
|
18
18
|
# rails documentation. It's not needed though, so it gets undef'd.
|
19
19
|
# Hopefully the list of methods that get in the way doesn't get too
|
20
20
|
# long.
|
21
|
-
|
21
|
+
HiddenMethods = [:parent, :name, :id, :type].each { |method|
|
22
|
+
define_method(method) {
|
23
|
+
caller.grep(/irb.completion/).empty? ? method_missing(method) : super
|
24
|
+
}
|
25
|
+
}
|
22
26
|
|
23
|
-
attr_accessor :fields, :attributes, :properties, :conditions
|
27
|
+
attr_accessor :fields, :attributes, :properties, :conditions,
|
28
|
+
:groupings
|
24
29
|
|
25
30
|
# Set up all the collections. Consider this the equivalent of an
|
26
31
|
# instance's initialize method.
|
@@ -30,6 +35,7 @@ module ThinkingSphinx
|
|
30
35
|
@attributes = []
|
31
36
|
@properties = {}
|
32
37
|
@conditions = []
|
38
|
+
@groupings = []
|
33
39
|
end
|
34
40
|
|
35
41
|
# This is how you add fields - the strings Sphinx looks at - to your
|
@@ -81,22 +87,16 @@ module ThinkingSphinx
|
|
81
87
|
def indexes(*args)
|
82
88
|
options = args.extract_options!
|
83
89
|
args.each do |columns|
|
84
|
-
|
90
|
+
field = Field.new(FauxColumn.coerce(columns), options)
|
91
|
+
fields << field
|
85
92
|
|
86
|
-
if
|
87
|
-
|
88
|
-
fields.last.columns.collect { |col| col.clone },
|
89
|
-
options.merge(
|
90
|
-
:type => :string,
|
91
|
-
:as => fields.last.unique_name.to_s.concat("_sort").to_sym
|
92
|
-
)
|
93
|
-
)
|
94
|
-
end
|
93
|
+
add_sort_attribute field, options if field.sortable
|
94
|
+
add_facet_attribute field, options if field.faceted
|
95
95
|
end
|
96
96
|
end
|
97
97
|
alias_method :field, :indexes
|
98
98
|
alias_method :includes, :indexes
|
99
|
-
|
99
|
+
|
100
100
|
# This is the method to add attributes to your index (hence why it is
|
101
101
|
# aliased as 'attribute'). The syntax is the same as #indexes, so use
|
102
102
|
# that as starting point, but keep in mind the following points.
|
@@ -128,7 +128,7 @@ module ThinkingSphinx
|
|
128
128
|
# when you would like to index a calculated value. Don't forget to set
|
129
129
|
# the type of the attribute though:
|
130
130
|
#
|
131
|
-
#
|
131
|
+
# has "age < 18", :as => :minor, :type => :boolean
|
132
132
|
#
|
133
133
|
# If you're creating attributes for latitude and longitude, don't
|
134
134
|
# forget that Sphinx expects these values to be in radians.
|
@@ -136,11 +136,26 @@ module ThinkingSphinx
|
|
136
136
|
def has(*args)
|
137
137
|
options = args.extract_options!
|
138
138
|
args.each do |columns|
|
139
|
-
|
139
|
+
attribute = Attribute.new(FauxColumn.coerce(columns), options)
|
140
|
+
attributes << attribute
|
141
|
+
|
142
|
+
add_facet_attribute attribute, options if attribute.faceted
|
140
143
|
end
|
141
144
|
end
|
142
145
|
alias_method :attribute, :has
|
143
146
|
|
147
|
+
def facet(*args)
|
148
|
+
options = args.extract_options!
|
149
|
+
options[:facet] = true
|
150
|
+
|
151
|
+
args.each do |columns|
|
152
|
+
attribute = Attribute.new(FauxColumn.coerce(columns), options)
|
153
|
+
attributes << attribute
|
154
|
+
|
155
|
+
add_facet_attribute attribute, options
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
144
159
|
# Use this method to add some manual SQL conditions for your index
|
145
160
|
# request. You can pass in as many strings as you like, they'll get
|
146
161
|
# joined together with ANDs later on.
|
@@ -152,6 +167,16 @@ module ThinkingSphinx
|
|
152
167
|
@conditions += args
|
153
168
|
end
|
154
169
|
|
170
|
+
# Use this method to add some manual SQL strings to the GROUP BY
|
171
|
+
# clause. You can pass in as many strings as you'd like, they'll get
|
172
|
+
# joined together with commas later on.
|
173
|
+
#
|
174
|
+
# group_by "lat", "lng"
|
175
|
+
#
|
176
|
+
def group_by(*args)
|
177
|
+
@groupings += args
|
178
|
+
end
|
179
|
+
|
155
180
|
# This is what to use to set properties on the index. Chief amongst
|
156
181
|
# those is the delta property - to allow automatic updates to your
|
157
182
|
# indexes as new models are added and edited - but also you can
|
@@ -160,6 +185,9 @@ module ThinkingSphinx
|
|
160
185
|
#
|
161
186
|
# set_property :delta => true
|
162
187
|
# set_property :field_weights => {"name" => 100}
|
188
|
+
# set_property :order => "name ASC"
|
189
|
+
# set_property :include => :picture
|
190
|
+
# set_property :select => 'name'
|
163
191
|
#
|
164
192
|
# Also, the following two properties are particularly relevant for
|
165
193
|
# geo-location searching - latitude_attr and longitude_attr. If your
|
@@ -168,11 +196,21 @@ module ThinkingSphinx
|
|
168
196
|
# when defining the index, so you don't need to specify them for every
|
169
197
|
# geo-related search.
|
170
198
|
#
|
171
|
-
# set_property :latitude_attr => "lt", :
|
199
|
+
# set_property :latitude_attr => "lt", :longitude_attr => "lg"
|
172
200
|
#
|
173
201
|
# Please don't forget to add a boolean field named 'delta' to your
|
174
202
|
# model's database table if enabling the delta index for it.
|
203
|
+
# Valid options for the delta property are:
|
204
|
+
#
|
205
|
+
# true
|
206
|
+
# false
|
207
|
+
# :default
|
208
|
+
# :delayed
|
209
|
+
# :datetime
|
175
210
|
#
|
211
|
+
# You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
|
212
|
+
# your own handling for delta indexing.
|
213
|
+
|
176
214
|
def set_property(*args)
|
177
215
|
options = args.extract_options!
|
178
216
|
if options.empty?
|
@@ -189,6 +227,37 @@ module ThinkingSphinx
|
|
189
227
|
def method_missing(method, *args)
|
190
228
|
FauxColumn.new(method, *args)
|
191
229
|
end
|
230
|
+
|
231
|
+
# A method to allow adding fields from associations which have names
|
232
|
+
# that clash with method names in the Builder class (ie: properties,
|
233
|
+
# fields, attributes).
|
234
|
+
#
|
235
|
+
# Example: indexes assoc(:properties).column
|
236
|
+
#
|
237
|
+
def assoc(assoc, *args)
|
238
|
+
FauxColumn.new(assoc, *args)
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
def add_sort_attribute(field, options)
|
244
|
+
add_internal_attribute field, options, "_sort"
|
245
|
+
end
|
246
|
+
|
247
|
+
def add_facet_attribute(resource, options)
|
248
|
+
add_internal_attribute resource, options, "_facet", true
|
249
|
+
end
|
250
|
+
|
251
|
+
def add_internal_attribute(resource, options, suffix, crc = false)
|
252
|
+
@attributes << Attribute.new(
|
253
|
+
resource.columns.collect { |col| col.clone },
|
254
|
+
options.merge(
|
255
|
+
:type => resource.is_a?(Field) ? :string : nil,
|
256
|
+
:as => resource.unique_name.to_s.concat(suffix).to_sym,
|
257
|
+
:crc => crc
|
258
|
+
).except(:facet)
|
259
|
+
)
|
260
|
+
end
|
192
261
|
end
|
193
262
|
end
|
194
263
|
end
|
@@ -29,6 +29,18 @@ Array.send(
|
|
29
29
|
:include, ThinkingSphinx::ArrayExtractOptions
|
30
30
|
) unless Array.instance_methods.include?("extract_options!")
|
31
31
|
|
32
|
+
module ThinkingSphinx
|
33
|
+
module AbstractQuotedTableName
|
34
|
+
def quote_table_name(name)
|
35
|
+
quote_column_name(name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.send(
|
41
|
+
:include, ThinkingSphinx::AbstractQuotedTableName
|
42
|
+
) unless ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_methods.include?("quote_table_name")
|
43
|
+
|
32
44
|
module ThinkingSphinx
|
33
45
|
module MysqlQuotedTableName
|
34
46
|
def quote_table_name(name) #:nodoc:
|
@@ -37,10 +49,13 @@ module ThinkingSphinx
|
|
37
49
|
end
|
38
50
|
end
|
39
51
|
|
40
|
-
if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter")
|
41
|
-
ActiveRecord::ConnectionAdapters
|
42
|
-
:
|
43
|
-
)
|
52
|
+
if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter") or ActiveRecord::Base.respond_to?(:jdbcmysql_connection)
|
53
|
+
adapter = ActiveRecord::ConnectionAdapters.const_get(
|
54
|
+
defined?(JRUBY_VERSION) ? :JdbcAdapter : :MysqlAdapter
|
55
|
+
)
|
56
|
+
unless adapter.instance_methods.include?("quote_table_name")
|
57
|
+
adapter.send(:include, ThinkingSphinx::MysqlQuotedTableName)
|
58
|
+
end
|
44
59
|
end
|
45
60
|
|
46
61
|
module ThinkingSphinx
|
@@ -53,4 +68,69 @@ end
|
|
53
68
|
|
54
69
|
ActiveRecord::Base.extend(
|
55
70
|
ThinkingSphinx::ActiveRecordQuotedName
|
56
|
-
) unless ActiveRecord::Base.respond_to?("quoted_table_name")
|
71
|
+
) unless ActiveRecord::Base.respond_to?("quoted_table_name")
|
72
|
+
|
73
|
+
module ThinkingSphinx
|
74
|
+
module ActiveRecordStoreFullSTIClass
|
75
|
+
def store_full_sti_class
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
ActiveRecord::Base.extend(
|
82
|
+
ThinkingSphinx::ActiveRecordStoreFullSTIClass
|
83
|
+
) unless ActiveRecord::Base.respond_to?(:store_full_sti_class)
|
84
|
+
|
85
|
+
module ThinkingSphinx
|
86
|
+
module ClassAttributeMethods
|
87
|
+
def cattr_reader(*syms)
|
88
|
+
syms.flatten.each do |sym|
|
89
|
+
next if sym.is_a?(Hash)
|
90
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
91
|
+
unless defined? @@#{sym}
|
92
|
+
@@#{sym} = nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.#{sym}
|
96
|
+
@@#{sym}
|
97
|
+
end
|
98
|
+
|
99
|
+
def #{sym}
|
100
|
+
@@#{sym}
|
101
|
+
end
|
102
|
+
EOS
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def cattr_writer(*syms)
|
107
|
+
options = syms.extract_options!
|
108
|
+
syms.flatten.each do |sym|
|
109
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
110
|
+
unless defined? @@#{sym}
|
111
|
+
@@#{sym} = nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.#{sym}=(obj)
|
115
|
+
@@#{sym} = obj
|
116
|
+
end
|
117
|
+
|
118
|
+
#{"
|
119
|
+
def #{sym}=(obj)
|
120
|
+
@@#{sym} = obj
|
121
|
+
end
|
122
|
+
" unless options[:instance_writer] == false }
|
123
|
+
EOS
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def cattr_accessor(*syms)
|
128
|
+
cattr_reader(*syms)
|
129
|
+
cattr_writer(*syms)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
Class.extend(
|
135
|
+
ThinkingSphinx::ClassAttributeMethods
|
136
|
+
) unless Class.respond_to?(:cattr_reader)
|
@@ -5,35 +5,29 @@ module ThinkingSphinx
|
|
5
5
|
# Most times, you will just want a specific model's results - to search and
|
6
6
|
# search_for_ids methods will do the job in exactly the same manner when
|
7
7
|
# called from a model.
|
8
|
-
#
|
8
|
+
#
|
9
9
|
class Search
|
10
|
+
GlobalFacetOptions = {
|
11
|
+
:all_attributes => false,
|
12
|
+
:class_facet => true
|
13
|
+
}
|
14
|
+
|
10
15
|
class << self
|
11
16
|
# Searches for results that match the parameters provided. Will only
|
12
17
|
# return the ids for the matching objects. See #search for syntax
|
13
18
|
# examples.
|
14
19
|
#
|
20
|
+
# Note that this only searches the Sphinx index, with no ActiveRecord
|
21
|
+
# queries. Thus, if your index is not in sync with the database, this
|
22
|
+
# method may return ids that no longer exist there.
|
23
|
+
#
|
15
24
|
def search_for_ids(*args)
|
16
25
|
results, client = search_results(*args.clone)
|
17
|
-
|
26
|
+
|
18
27
|
options = args.extract_options!
|
19
28
|
page = options[:page] ? options[:page].to_i : 1
|
20
|
-
|
21
|
-
|
22
|
-
pager = WillPaginate::Collection.create(page,
|
23
|
-
client.limit, results[:total_found] || 0) do |collection|
|
24
|
-
collection.replace results[:matches].collect { |match|
|
25
|
-
match[:attributes]["sphinx_internal_id"]
|
26
|
-
}
|
27
|
-
collection.instance_variable_set :@total_entries, results[:total_found]
|
28
|
-
end
|
29
|
-
return (options[:include_raw] ? [pager, results] : pager)
|
30
|
-
rescue
|
31
|
-
if options[:include_raw]
|
32
|
-
results[:matches].collect { |match| match[:attributes]["sphinx_internal_id"] }, results
|
33
|
-
else
|
34
|
-
results[:matches].collect { |match| match[:attributes]["sphinx_internal_id"] }
|
35
|
-
end
|
36
|
-
end
|
29
|
+
|
30
|
+
ThinkingSphinx::Collection.ids_from_results(results, page, client.limit, options)
|
37
31
|
end
|
38
32
|
|
39
33
|
# Searches through the Sphinx indexes for relevant matches. There's
|
@@ -44,11 +38,11 @@ module ThinkingSphinx
|
|
44
38
|
# just like paginate. The same parameters - :page and :per_page - work as
|
45
39
|
# expected, and the returned result set can be used by the will_paginate
|
46
40
|
# helper.
|
47
|
-
#
|
41
|
+
#
|
48
42
|
# == Basic Searching
|
49
43
|
#
|
50
44
|
# The simplest way of searching is straight text.
|
51
|
-
#
|
45
|
+
#
|
52
46
|
# ThinkingSphinx::Search.search "pat"
|
53
47
|
# ThinkingSphinx::Search.search "google"
|
54
48
|
# User.search "pat", :page => (params[:page] || 1)
|
@@ -56,10 +50,10 @@ module ThinkingSphinx
|
|
56
50
|
#
|
57
51
|
# If you specify :include, like in an #find call, this will be respected
|
58
52
|
# when loading the relevant models from the search results.
|
59
|
-
#
|
53
|
+
#
|
60
54
|
# User.search "pat", :include => :posts
|
61
55
|
#
|
62
|
-
# ==
|
56
|
+
# == Match Modes
|
63
57
|
#
|
64
58
|
# Sphinx supports 5 different matching modes. By default Thinking Sphinx
|
65
59
|
# uses :all, which unsurprisingly requires all the supplied search terms
|
@@ -77,11 +71,25 @@ module ThinkingSphinx
|
|
77
71
|
# for more complex query syntax, refer to the sphinx documentation for further
|
78
72
|
# details.
|
79
73
|
#
|
80
|
-
# ==
|
74
|
+
# == Weighting
|
75
|
+
#
|
76
|
+
# Sphinx has support for weighting, where matches in one field can be considered
|
77
|
+
# more important than in another. Weights are integers, with 1 as the default.
|
78
|
+
# They can be set per-search like this:
|
79
|
+
#
|
80
|
+
# User.search "pat allan", :field_weights => { :alias => 4, :aka => 2 }
|
81
|
+
#
|
82
|
+
# If you're searching multiple models, you can set per-index weights:
|
83
|
+
#
|
84
|
+
# ThinkingSphinx::Search.search "pat", :index_weights => { User => 10 }
|
85
|
+
#
|
86
|
+
# See http://sphinxsearch.com/doc.html#weighting for further details.
|
81
87
|
#
|
88
|
+
# == Searching by Fields
|
89
|
+
#
|
82
90
|
# If you want to step it up a level, you can limit your search terms to
|
83
91
|
# specific fields:
|
84
|
-
#
|
92
|
+
#
|
85
93
|
# User.search :conditions => {:name => "pat"}
|
86
94
|
#
|
87
95
|
# This uses Sphinx's extended match mode, unless you specify a different
|
@@ -91,21 +99,29 @@ module ThinkingSphinx
|
|
91
99
|
# == Searching by Attributes
|
92
100
|
#
|
93
101
|
# Also known as filters, you can limit your searches to documents that
|
94
|
-
# have specific values for their attributes. There are
|
95
|
-
# this. The first
|
96
|
-
#
|
97
|
-
#
|
98
|
-
# ThinkingSphinx::Search.search :with => {:
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
102
|
+
# have specific values for their attributes. There are three ways to do
|
103
|
+
# this. The first two techniques work in all scenarios - using the :with
|
104
|
+
# or :with_all options.
|
105
|
+
#
|
106
|
+
# ThinkingSphinx::Search.search :with => {:tag_ids => 10}
|
107
|
+
# ThinkingSphinx::Search.search :with => {:tag_ids => [10,12]}
|
108
|
+
# ThinkingSphinx::Search.search :with_all => {:tag_ids => [10,12]}
|
109
|
+
#
|
110
|
+
# The first :with search will match records with a tag_id attribute of 10.
|
111
|
+
# The second :with will match records with a tag_id attribute of 10 OR 12.
|
112
|
+
# If you need to find records that are tagged with ids 10 AND 12, you
|
113
|
+
# will need to use the :with_all search parameter. This is particuarly
|
114
|
+
# useful in conjunction with Multi Value Attributes (MVAs).
|
115
|
+
#
|
116
|
+
# The third filtering technique is only viable if you're searching with a
|
117
|
+
# specific model (not multi-model searching). With a single model,
|
118
|
+
# Thinking Sphinx can figure out what attributes and fields are available,
|
119
|
+
# so you can put it all in the :conditions hash, and it will sort it out.
|
120
|
+
#
|
105
121
|
# Node.search :conditions => {:parent_id => 10}
|
106
|
-
#
|
122
|
+
#
|
107
123
|
# Filters can be single values, arrays of values, or ranges.
|
108
|
-
#
|
124
|
+
#
|
109
125
|
# Article.search "East Timor", :conditions => {:rating => 3..5}
|
110
126
|
#
|
111
127
|
# == Excluding by Attributes
|
@@ -115,6 +131,51 @@ module ThinkingSphinx
|
|
115
131
|
#
|
116
132
|
# User.search :without => {:role_id => 1}
|
117
133
|
#
|
134
|
+
# == Excluding by Primary Key
|
135
|
+
#
|
136
|
+
# There is a shortcut to exclude records by their ActiveRecord primary key:
|
137
|
+
#
|
138
|
+
# User.search :without_ids => 1
|
139
|
+
#
|
140
|
+
# Pass an array or a single value.
|
141
|
+
#
|
142
|
+
# The primary key must be an integer as a negative filter is used. Note
|
143
|
+
# that for multi-model search, an id may occur in more than one model.
|
144
|
+
#
|
145
|
+
# == Infix (Star) Searching
|
146
|
+
#
|
147
|
+
# By default, Sphinx uses English stemming, e.g. matching "shoes" if you
|
148
|
+
# search for "shoe". It won't find "Melbourne" if you search for
|
149
|
+
# "elbourn", though.
|
150
|
+
#
|
151
|
+
# Enable infix searching by something like this in config/sphinx.yml:
|
152
|
+
#
|
153
|
+
# development:
|
154
|
+
# enable_star: 1
|
155
|
+
# min_infix_length: 2
|
156
|
+
#
|
157
|
+
# Note that this will make indexing take longer.
|
158
|
+
#
|
159
|
+
# With those settings (and after reindexing), wildcard asterisks can be used
|
160
|
+
# in queries:
|
161
|
+
#
|
162
|
+
# Location.search "*elbourn*"
|
163
|
+
#
|
164
|
+
# To automatically add asterisks around every token (but not operators),
|
165
|
+
# pass the :star option:
|
166
|
+
#
|
167
|
+
# Location.search "elbourn -ustrali", :star => true, :match_mode => :boolean
|
168
|
+
#
|
169
|
+
# This would become "*elbourn* -*ustrali*". The :star option only adds the
|
170
|
+
# asterisks. You need to make the config/sphinx.yml changes yourself.
|
171
|
+
#
|
172
|
+
# By default, the tokens are assumed to match the regular expression /\w+/u.
|
173
|
+
# If you've modified the charset_table, pass another regular expression, e.g.
|
174
|
+
#
|
175
|
+
# User.search("oo@bar.c", :star => /[\w@.]+/u)
|
176
|
+
#
|
177
|
+
# to search for "*oo@bar.c*" and not "*oo*@*bar*.*c*".
|
178
|
+
#
|
118
179
|
# == Sorting
|
119
180
|
#
|
120
181
|
# Sphinx can only sort by attributes, so generally you will need to avoid
|
@@ -138,15 +199,80 @@ module ThinkingSphinx
|
|
138
199
|
# documentation[http://sphinxsearch.com/doc.html] for that level of
|
139
200
|
# detail though.
|
140
201
|
#
|
141
|
-
#
|
202
|
+
# If desired, you can sort by a column in your model instead of a sphinx
|
203
|
+
# field or attribute. This sort only applies to the current page, so is
|
204
|
+
# most useful when performing a search with a single page of results.
|
142
205
|
#
|
206
|
+
# User.search("pat", :sql_order => "name")
|
207
|
+
#
|
208
|
+
# == Grouping
|
209
|
+
#
|
143
210
|
# For this you can use the group_by, group_clause and group_function
|
144
211
|
# options - which are all directly linked to Sphinx's expectations. No
|
145
212
|
# magic from Thinking Sphinx. It can get a little tricky, so make sure
|
146
213
|
# you read all the relevant
|
147
214
|
# documentation[http://sphinxsearch.com/doc.html#clustering] first.
|
215
|
+
#
|
216
|
+
# Grouping is done via three parameters within the options hash
|
217
|
+
# * <tt>:group_function</tt> determines the way grouping is done
|
218
|
+
# * <tt>:group_by</tt> determines the field which is used for grouping
|
219
|
+
# * <tt>:group_clause</tt> determines the sorting order
|
220
|
+
#
|
221
|
+
# === group_function
|
222
|
+
#
|
223
|
+
# Valid values for :group_function are
|
224
|
+
# * <tt>:day</tt>, <tt>:week</tt>, <tt>:month</tt>, <tt>:year</tt> - Grouping is done by the respective timeframes.
|
225
|
+
# * <tt>:attr</tt>, <tt>:attrpair</tt> - Grouping is done by the specified attributes(s)
|
226
|
+
#
|
227
|
+
# === group_by
|
148
228
|
#
|
149
|
-
#
|
229
|
+
# This parameter denotes the field by which grouping is done. Note that the
|
230
|
+
# specified field must be a sphinx attribute or index.
|
231
|
+
#
|
232
|
+
# === group_clause
|
233
|
+
#
|
234
|
+
# This determines the sorting order of the groups. In a grouping search,
|
235
|
+
# the matches within a group will sorted by the <tt>:sort_mode</tt> and <tt>:order</tt> parameters.
|
236
|
+
# The group matches themselves however, will be sorted by <tt>:group_clause</tt>.
|
237
|
+
#
|
238
|
+
# The syntax for this is the same as an order parameter in extended sort mode.
|
239
|
+
# Namely, you can specify an SQL-like sort expression with up to 5 attributes
|
240
|
+
# (including internal attributes), eg: "@relevance DESC, price ASC, @id DESC"
|
241
|
+
#
|
242
|
+
# === Grouping by timestamp
|
243
|
+
#
|
244
|
+
# Timestamp grouping groups off items by the day, week, month or year of the
|
245
|
+
# attribute given. In order to do this you need to define a timestamp attribute,
|
246
|
+
# which pretty much looks like the standard defintion for any attribute.
|
247
|
+
#
|
248
|
+
# define_index do
|
249
|
+
# #
|
250
|
+
# # All your other stuff
|
251
|
+
# #
|
252
|
+
# has :created_at
|
253
|
+
# end
|
254
|
+
#
|
255
|
+
# When you need to fire off your search, it'll go something to the tune of
|
256
|
+
#
|
257
|
+
# Fruit.search "apricot", :group_function => :day, :group_by => 'created_at'
|
258
|
+
#
|
259
|
+
# The <tt>@groupby</tt> special attribute will contain the date for that group.
|
260
|
+
# Depending on the <tt>:group_function</tt> parameter, the date format will be
|
261
|
+
#
|
262
|
+
# * <tt>:day</tt> - YYYYMMDD
|
263
|
+
# * <tt>:week</tt> - YYYYNNN (NNN is the first day of the week in question,
|
264
|
+
# counting from the start of the year )
|
265
|
+
# * <tt>:month</tt> - YYYYMM
|
266
|
+
# * <tt>:year</tt> - YYYY
|
267
|
+
#
|
268
|
+
#
|
269
|
+
# === Grouping by attribute
|
270
|
+
#
|
271
|
+
# The syntax is the same as grouping by timestamp, except for the fact that the
|
272
|
+
# <tt>:group_function</tt> parameter is changed
|
273
|
+
#
|
274
|
+
# Fruit.search "apricot", :group_function => :attr, :group_by => 'size'
|
275
|
+
#
|
150
276
|
#
|
151
277
|
# == Geo/Location Searching
|
152
278
|
#
|
@@ -155,11 +281,11 @@ module ThinkingSphinx
|
|
155
281
|
# take advantage of this, you will need to have both of those values in
|
156
282
|
# attributes. To search with that point, you can then use one of the
|
157
283
|
# following syntax examples:
|
158
|
-
#
|
159
|
-
# Address.search "Melbourne", :geo => [1.4, -2.217]
|
160
|
-
# Address.search "Australia", :geo => [-0.55, 3.108],
|
284
|
+
#
|
285
|
+
# Address.search "Melbourne", :geo => [1.4, -2.217], :order => "@geodist asc"
|
286
|
+
# Address.search "Australia", :geo => [-0.55, 3.108], :order => "@geodist asc"
|
161
287
|
# :latitude_attr => "latit", :longitude_attr => "longit"
|
162
|
-
#
|
288
|
+
#
|
163
289
|
# The first example applies when your latitude and longitude attributes
|
164
290
|
# are named any of lat, latitude, lon, long or longitude. If that's not
|
165
291
|
# the case, you will need to explicitly state them in your search, _or_
|
@@ -168,17 +294,17 @@ module ThinkingSphinx
|
|
168
294
|
# define_index do
|
169
295
|
# has :latit # Float column, stored in radians
|
170
296
|
# has :longit # Float column, stored in radians
|
171
|
-
#
|
297
|
+
#
|
172
298
|
# set_property :latitude_attr => "latit"
|
173
299
|
# set_property :longitude_attr => "longit"
|
174
300
|
# end
|
175
|
-
#
|
301
|
+
#
|
176
302
|
# Now, geo-location searching really only has an affect if you have a
|
177
303
|
# filter, sort or grouping clause related to it - otherwise it's just a
|
178
|
-
# normal search
|
179
|
-
#
|
180
|
-
# clauses.
|
181
|
-
#
|
304
|
+
# normal search, and _will not_ return a distance value otherwise. To
|
305
|
+
# make use of the positioning difference, use the special attribute
|
306
|
+
# "@geodist" in any of your filters or sorting or grouping clauses.
|
307
|
+
#
|
182
308
|
# And don't forget - both the latitude and longitude you use in your
|
183
309
|
# search, and the values in your indexes, need to be stored as a float in radians,
|
184
310
|
# _not_ degrees. Keep in mind that if you do this conversion in SQL
|
@@ -188,40 +314,93 @@ module ThinkingSphinx
|
|
188
314
|
# has 'RADIANS(lat)', :as => :lat, :type => :float
|
189
315
|
# # ...
|
190
316
|
# end
|
317
|
+
#
|
318
|
+
# Once you've got your results set, you can access the distances as
|
319
|
+
# follows:
|
320
|
+
#
|
321
|
+
# @results.each_with_geodist do |result, distance|
|
322
|
+
# # ...
|
323
|
+
# end
|
324
|
+
#
|
325
|
+
# The distance value is returned as a float, representing the distance in
|
326
|
+
# metres.
|
327
|
+
#
|
328
|
+
# == Handling a Stale Index
|
329
|
+
#
|
330
|
+
# Especially if you don't use delta indexing, you risk having records in the
|
331
|
+
# Sphinx index that are no longer in the database. By default, those will simply
|
332
|
+
# come back as nils:
|
191
333
|
#
|
334
|
+
# >> pat_user.delete
|
335
|
+
# >> User.search("pat")
|
336
|
+
# Sphinx Result: [1,2]
|
337
|
+
# => [nil, <#User id: 2>]
|
338
|
+
#
|
339
|
+
# (If you search across multiple models, you'll get ActiveRecord::RecordNotFound.)
|
340
|
+
#
|
341
|
+
# You can simply Array#compact these results or handle the nils in some other way, but
|
342
|
+
# Sphinx will still report two results, and the missing records may upset your layout.
|
343
|
+
#
|
344
|
+
# If you pass :retry_stale => true to a single-model search, missing records will
|
345
|
+
# cause Thinking Sphinx to retry the query but excluding those records. Since search
|
346
|
+
# is paginated, the new search could potentially include missing records as well, so by
|
347
|
+
# default Thinking Sphinx will retry three times. Pass :retry_stale => 5 to retry five
|
348
|
+
# times, and so on. If there are still missing ids on the last retry, they are
|
349
|
+
# shown as nils.
|
350
|
+
#
|
192
351
|
def search(*args)
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
352
|
+
query = args.clone # an array
|
353
|
+
options = query.extract_options!
|
354
|
+
|
355
|
+
retry_search_on_stale_index(query, options) do
|
356
|
+
results, client = search_results(*(query + [options]))
|
357
|
+
|
358
|
+
::ActiveRecord::Base.logger.error(
|
359
|
+
"Sphinx Error: #{results[:error]}"
|
360
|
+
) if results[:error]
|
361
|
+
|
362
|
+
klass = options[:class]
|
363
|
+
page = options[:page] ? options[:page].to_i : 1
|
364
|
+
|
365
|
+
ThinkingSphinx::Collection.create_from_results(results, page, client.limit, options)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def retry_search_on_stale_index(query, options, &block)
|
370
|
+
stale_ids = []
|
371
|
+
stale_retries_left = case options[:retry_stale]
|
372
|
+
when true
|
373
|
+
3 # default to three retries
|
374
|
+
when nil, false
|
375
|
+
0 # no retries
|
376
|
+
else options[:retry_stale].to_i
|
377
|
+
end
|
378
|
+
begin
|
379
|
+
# Passing this in an option so Collection.create_from_results can see it.
|
380
|
+
# It should only raise on stale records if there are any retries left.
|
381
|
+
options[:raise_on_stale] = stale_retries_left > 0
|
382
|
+
block.call
|
383
|
+
# If ThinkingSphinx::Collection.create_from_results found records in Sphinx but not
|
384
|
+
# in the DB and the :raise_on_stale option is set, this exception is raised. We retry
|
385
|
+
# a limited number of times, excluding the stale ids from the search.
|
386
|
+
rescue StaleIdsException => e
|
387
|
+
stale_retries_left -= 1
|
388
|
+
|
389
|
+
stale_ids |= e.ids # For logging
|
390
|
+
options[:without_ids] = Array(options[:without_ids]) | e.ids # Actual exclusion
|
391
|
+
|
392
|
+
tries = stale_retries_left
|
393
|
+
::ActiveRecord::Base.logger.debug("Sphinx Stale Ids (%s %s left): %s" % [
|
394
|
+
tries, (tries==1 ? 'try' : 'tries'), stale_ids.join(', ')
|
395
|
+
])
|
396
|
+
|
397
|
+
retry
|
219
398
|
end
|
220
399
|
end
|
221
400
|
|
222
401
|
def count(*args)
|
223
402
|
results, client = search_results(*args.clone)
|
224
|
-
results[:
|
403
|
+
results[:total_found] || 0
|
225
404
|
end
|
226
405
|
|
227
406
|
# Checks if a document with the given id exists within a specific index.
|
@@ -230,50 +409,70 @@ module ThinkingSphinx
|
|
230
409
|
# - ID of the document
|
231
410
|
# - Index to check within
|
232
411
|
# - Options hash (defaults to {})
|
233
|
-
#
|
412
|
+
#
|
234
413
|
# Example:
|
235
|
-
#
|
414
|
+
#
|
236
415
|
# ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User)
|
237
|
-
#
|
416
|
+
#
|
238
417
|
def search_for_id(*args)
|
239
418
|
options = args.extract_options!
|
240
419
|
client = client_from_options options
|
241
|
-
|
420
|
+
|
242
421
|
query, filters = search_conditions(
|
243
422
|
options[:class], options[:conditions] || {}
|
244
423
|
)
|
245
424
|
client.filters += filters
|
246
425
|
client.match_mode = :extended unless query.empty?
|
247
426
|
client.id_range = args.first..args.first
|
248
|
-
|
427
|
+
|
249
428
|
begin
|
250
429
|
return client.query(query, args[1])[:matches].length > 0
|
251
430
|
rescue Errno::ECONNREFUSED => err
|
252
431
|
raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
|
253
432
|
end
|
254
433
|
end
|
255
|
-
|
434
|
+
|
435
|
+
# Model.facets *args
|
436
|
+
# ThinkingSphinx::Search.facets *args
|
437
|
+
# ThinkingSphinx::Search.facets *args, :all_attributes => true
|
438
|
+
# ThinkingSphinx::Search.facets *args, :class_facet => false
|
439
|
+
#
|
440
|
+
def facets(*args)
|
441
|
+
options = args.extract_options!
|
442
|
+
|
443
|
+
if options[:class]
|
444
|
+
facets_for_model options[:class], args, options
|
445
|
+
else
|
446
|
+
facets_for_all_models args, options
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
256
450
|
private
|
257
|
-
|
451
|
+
|
258
452
|
# This method handles the common search functionality, and returns both
|
259
453
|
# the result hash and the client. Not super elegant, but it'll do for
|
260
454
|
# the moment.
|
261
|
-
#
|
455
|
+
#
|
262
456
|
def search_results(*args)
|
263
457
|
options = args.extract_options!
|
458
|
+
query = args.join(' ')
|
264
459
|
client = client_from_options options
|
265
|
-
|
266
|
-
query
|
460
|
+
|
461
|
+
query = star_query(query, options[:star]) if options[:star]
|
462
|
+
|
463
|
+
extra_query, filters = search_conditions(
|
267
464
|
options[:class], options[:conditions] || {}
|
268
465
|
)
|
269
466
|
client.filters += filters
|
270
|
-
client.match_mode = :extended unless
|
271
|
-
query =
|
272
|
-
|
467
|
+
client.match_mode = :extended unless extra_query.empty?
|
468
|
+
query = [query, extra_query].join(' ')
|
469
|
+
query.strip! # Because "" and " " are not equivalent
|
470
|
+
|
273
471
|
set_sort_options! client, options
|
274
|
-
|
472
|
+
|
275
473
|
client.limit = options[:per_page].to_i if options[:per_page]
|
276
474
|
page = options[:page] ? options[:page].to_i : 1
|
475
|
+
page = 1 if page <= 0
|
277
476
|
client.offset = (page - 1) * client.limit
|
278
477
|
|
279
478
|
begin
|
@@ -283,74 +482,42 @@ module ThinkingSphinx
|
|
283
482
|
rescue Errno::ECONNREFUSED => err
|
284
483
|
raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
|
285
484
|
end
|
286
|
-
|
485
|
+
|
287
486
|
return results, client
|
288
487
|
end
|
289
|
-
|
290
|
-
# This function loops over the records and appends a 'distance' variable to each one with
|
291
|
-
# the value from Sphinx
|
292
|
-
def append_distances(instances, results, distance_name)
|
293
|
-
instances.each_with_index do |record, index|
|
294
|
-
if record
|
295
|
-
distance = (results[index][:attributes]['@geodist'] or nil)
|
296
|
-
record.instance_variable_get('@attributes')["#{distance_name}"] = distance
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
def instances_from_results(results, options = {}, klass = nil)
|
302
|
-
if klass.nil?
|
303
|
-
results.collect { |result| instance_from_result result, options }
|
304
|
-
else
|
305
|
-
ids = results.collect { |result| result[:attributes]["sphinx_internal_id"] }
|
306
|
-
instances = ids.length > 0 ? klass.find(
|
307
|
-
:all,
|
308
|
-
:conditions => {klass.primary_key.to_sym => ids},
|
309
|
-
:include => options[:include],
|
310
|
-
:select => options[:select]
|
311
|
-
) : []
|
312
|
-
final_instances = ids.collect { |obj_id| instances.detect { |obj| obj.id == obj_id } }
|
313
|
-
|
314
|
-
final_instances = append_distances(final_instances, results, options[:distance_name]) if options[:distance_name] && (results.collect { |result| result[:attributes]['@geodist'] }.length > 0)
|
315
|
-
|
316
|
-
return final_instances
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
# Either use the provided class to instantiate a result from a model, or
|
321
|
-
# get the result's CRC value and determine the class from that.
|
322
|
-
#
|
323
|
-
def instance_from_result(result, options)
|
324
|
-
class_from_crc(result[:attributes]["class_crc"]).find(
|
325
|
-
result[:attributes]["sphinx_internal_id"],
|
326
|
-
:include => options[:include], :select => options[:select]
|
327
|
-
)
|
328
|
-
end
|
329
|
-
|
330
|
-
# Convert a CRC value to the corresponding class.
|
331
|
-
#
|
332
|
-
def class_from_crc(crc)
|
333
|
-
unless @models_by_crc
|
334
|
-
Configuration.new.load_models
|
335
|
-
|
336
|
-
@models_by_crc = ThinkingSphinx.indexed_models.inject({}) do |hash, model|
|
337
|
-
hash[model.constantize.to_crc32] = model
|
338
|
-
hash
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
@models_by_crc[crc].constantize
|
343
|
-
end
|
344
|
-
|
488
|
+
|
345
489
|
# Set all the appropriate settings for the client, using the provided
|
346
490
|
# options hash.
|
347
491
|
#
|
348
492
|
def client_from_options(options = {})
|
349
|
-
config = ThinkingSphinx::Configuration.
|
493
|
+
config = ThinkingSphinx::Configuration.instance
|
350
494
|
client = Riddle::Client.new config.address, config.port
|
351
495
|
klass = options[:class]
|
352
|
-
index_options = klass ? klass.
|
353
|
-
|
496
|
+
index_options = klass ? klass.sphinx_index_options : {}
|
497
|
+
|
498
|
+
# The Riddle default is per-query max_matches=1000. If we set the
|
499
|
+
# per-server max to a smaller value in sphinx.yml, we need to override
|
500
|
+
# the Riddle default or else we get search errors like
|
501
|
+
# "per-query max_matches=1000 out of bounds (per-server max_matches=200)"
|
502
|
+
if per_server_max_matches = config.configuration.searchd.max_matches
|
503
|
+
options[:max_matches] ||= per_server_max_matches
|
504
|
+
end
|
505
|
+
|
506
|
+
# Turn :index_weights => { "foo" => 2, User => 1 }
|
507
|
+
# into :index_weights => { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
|
508
|
+
if iw = options[:index_weights]
|
509
|
+
options[:index_weights] = iw.inject({}) do |hash, (index,weight)|
|
510
|
+
if index.is_a?(Class)
|
511
|
+
name = ThinkingSphinx::Index.name(index)
|
512
|
+
hash["#{name}_core"] = weight
|
513
|
+
hash["#{name}_delta"] = weight
|
514
|
+
else
|
515
|
+
hash[index] = weight
|
516
|
+
end
|
517
|
+
hash
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
354
521
|
[
|
355
522
|
:max_matches, :match_mode, :sort_mode, :sort_by, :id_range,
|
356
523
|
:group_by, :group_function, :group_clause, :group_distinct, :cut_off,
|
@@ -366,116 +533,160 @@ module ThinkingSphinx
|
|
366
533
|
options[:classes] = [klass] if klass
|
367
534
|
|
368
535
|
client.anchor = anchor_conditions(klass, options) || {} if client.anchor.empty?
|
369
|
-
|
536
|
+
|
370
537
|
client.filters << Riddle::Client::Filter.new(
|
371
538
|
"sphinx_deleted", [0]
|
372
539
|
)
|
373
540
|
|
374
541
|
# class filters
|
375
542
|
client.filters << Riddle::Client::Filter.new(
|
376
|
-
"
|
543
|
+
"class_crc", options[:classes].collect { |k| k.to_crc32s }.flatten
|
377
544
|
) if options[:classes]
|
378
|
-
|
545
|
+
|
379
546
|
# normal attribute filters
|
380
547
|
client.filters += options[:with].collect { |attr,val|
|
381
548
|
Riddle::Client::Filter.new attr.to_s, filter_value(val)
|
382
549
|
} if options[:with]
|
383
|
-
|
550
|
+
|
384
551
|
# exclusive attribute filters
|
385
552
|
client.filters += options[:without].collect { |attr,val|
|
386
553
|
Riddle::Client::Filter.new attr.to_s, filter_value(val), true
|
387
554
|
} if options[:without]
|
388
|
-
|
555
|
+
|
556
|
+
# every-match attribute filters
|
557
|
+
client.filters += options[:with_all].collect { |attr,vals|
|
558
|
+
Array(vals).collect { |val|
|
559
|
+
Riddle::Client::Filter.new attr.to_s, filter_value(val)
|
560
|
+
}
|
561
|
+
}.flatten if options[:with_all]
|
562
|
+
|
563
|
+
# exclusive attribute filter on primary key
|
564
|
+
client.filters += Array(options[:without_ids]).collect { |id|
|
565
|
+
Riddle::Client::Filter.new 'sphinx_internal_id', filter_value(id), true
|
566
|
+
} if options[:without_ids]
|
567
|
+
|
389
568
|
client
|
390
569
|
end
|
391
|
-
|
570
|
+
|
571
|
+
def star_query(query, custom_token = nil)
|
572
|
+
token = custom_token.is_a?(Regexp) ? custom_token : /\w+/u
|
573
|
+
|
574
|
+
query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
|
575
|
+
pre, proper, post = $`, $&, $'
|
576
|
+
is_operator = pre.match(%r{(\W|^)[@~/]\Z}) # E.g. "@foo", "/2", "~3", but not as part of a token
|
577
|
+
is_quote = proper.starts_with?('"') && proper.ends_with?('"') # E.g. "foo bar", with quotes
|
578
|
+
has_star = pre.ends_with?("*") || post.starts_with?("*")
|
579
|
+
if is_operator || is_quote || has_star
|
580
|
+
proper
|
581
|
+
else
|
582
|
+
"*#{proper}*"
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
392
587
|
def filter_value(value)
|
393
588
|
case value
|
394
589
|
when Range
|
395
|
-
value.first.is_a?(Time) ? value.first
|
590
|
+
value.first.is_a?(Time) ? timestamp(value.first)..timestamp(value.last) : value
|
396
591
|
when Array
|
397
|
-
value.collect { |val| val.is_a?(Time) ? val
|
592
|
+
value.collect { |val| val.is_a?(Time) ? timestamp(val) : val }
|
398
593
|
else
|
399
594
|
Array(value)
|
400
595
|
end
|
401
596
|
end
|
402
|
-
|
597
|
+
|
598
|
+
# Returns the integer timestamp for a Time object.
|
599
|
+
#
|
600
|
+
# If using Rails 2.1+, need to handle timezones to translate them back to
|
601
|
+
# UTC, as that's what datetimes will be stored as by MySQL.
|
602
|
+
#
|
603
|
+
# in_time_zone is a method that was added for the timezone support in
|
604
|
+
# Rails 2.1, which is why it's used for testing. I'm sure there's better
|
605
|
+
# ways, but this does the job.
|
606
|
+
#
|
607
|
+
def timestamp(value)
|
608
|
+
value.respond_to?(:in_time_zone) ? value.utc.to_i : value.to_i
|
609
|
+
end
|
610
|
+
|
403
611
|
# Translate field and attribute conditions to the relevant search string
|
404
612
|
# and filters.
|
405
|
-
#
|
613
|
+
#
|
406
614
|
def search_conditions(klass, conditions={})
|
407
|
-
attributes = klass ? klass.
|
615
|
+
attributes = klass ? klass.sphinx_indexes.collect { |index|
|
408
616
|
index.attributes.collect { |attrib| attrib.unique_name }
|
409
617
|
}.flatten : []
|
410
|
-
|
411
|
-
search_string =
|
618
|
+
|
619
|
+
search_string = []
|
412
620
|
filters = []
|
413
|
-
|
621
|
+
|
414
622
|
conditions.each do |key,val|
|
415
623
|
if attributes.include?(key.to_sym)
|
416
624
|
filters << Riddle::Client::Filter.new(
|
417
625
|
key.to_s, filter_value(val)
|
418
626
|
)
|
419
627
|
else
|
420
|
-
search_string << "@#{key} #{val}
|
628
|
+
search_string << "@#{key} #{val}"
|
421
629
|
end
|
422
630
|
end
|
423
631
|
|
424
|
-
return search_string, filters
|
632
|
+
return search_string.join(' '), filters
|
425
633
|
end
|
426
|
-
|
634
|
+
|
427
635
|
# Return the appropriate latitude and longitude values, depending on
|
428
636
|
# whether the relevant attributes have been defined, and also whether
|
429
637
|
# there's actually any values.
|
430
|
-
#
|
638
|
+
#
|
431
639
|
def anchor_conditions(klass, options)
|
432
|
-
attributes = klass ? klass.
|
640
|
+
attributes = klass ? klass.sphinx_indexes.collect { |index|
|
433
641
|
index.attributes.collect { |attrib| attrib.unique_name }
|
434
642
|
}.flatten : []
|
435
|
-
|
436
|
-
lat_attr = klass ? klass.
|
643
|
+
|
644
|
+
lat_attr = klass ? klass.sphinx_indexes.collect { |index|
|
437
645
|
index.options[:latitude_attr]
|
438
646
|
}.compact.first : nil
|
439
|
-
|
440
|
-
lon_attr = klass ? klass.
|
647
|
+
|
648
|
+
lon_attr = klass ? klass.sphinx_indexes.collect { |index|
|
441
649
|
index.options[:longitude_attr]
|
442
650
|
}.compact.first : nil
|
443
|
-
|
651
|
+
|
444
652
|
lat_attr = options[:latitude_attr] if options[:latitude_attr]
|
445
653
|
lat_attr ||= :lat if attributes.include?(:lat)
|
446
654
|
lat_attr ||= :latitude if attributes.include?(:latitude)
|
447
|
-
|
655
|
+
|
448
656
|
lon_attr = options[:longitude_attr] if options[:longitude_attr]
|
657
|
+
lon_attr ||= :lng if attributes.include?(:lng)
|
449
658
|
lon_attr ||= :lon if attributes.include?(:lon)
|
450
659
|
lon_attr ||= :long if attributes.include?(:long)
|
451
660
|
lon_attr ||= :longitude if attributes.include?(:longitude)
|
452
|
-
|
661
|
+
|
453
662
|
lat = options[:lat]
|
454
663
|
lon = options[:lon]
|
455
|
-
|
664
|
+
|
456
665
|
if options[:geo]
|
457
666
|
lat = options[:geo].first
|
458
667
|
lon = options[:geo].last
|
459
668
|
end
|
460
|
-
|
669
|
+
|
461
670
|
lat && lon ? {
|
462
|
-
:latitude_attribute => lat_attr,
|
671
|
+
:latitude_attribute => lat_attr.to_s,
|
463
672
|
:latitude => lat,
|
464
|
-
:longitude_attribute => lon_attr,
|
673
|
+
:longitude_attribute => lon_attr.to_s,
|
465
674
|
:longitude => lon
|
466
675
|
} : nil
|
467
676
|
end
|
468
|
-
|
677
|
+
|
469
678
|
# Set the sort options using the :order key as well as the appropriate
|
470
679
|
# Riddle settings.
|
471
|
-
#
|
680
|
+
#
|
472
681
|
def set_sort_options!(client, options)
|
473
682
|
klass = options[:class]
|
474
|
-
fields = klass ? klass.
|
683
|
+
fields = klass ? klass.sphinx_indexes.collect { |index|
|
475
684
|
index.fields.collect { |field| field.unique_name }
|
476
685
|
}.flatten : []
|
686
|
+
index_options = klass ? klass.sphinx_index_options : {}
|
477
687
|
|
478
|
-
|
688
|
+
order = options[:order] || index_options[:order]
|
689
|
+
case order
|
479
690
|
when Symbol
|
480
691
|
client.sort_mode = :attr_asc if client.sort_mode == :relevance || client.sort_mode.nil?
|
481
692
|
if fields.include?(order)
|
@@ -489,23 +700,81 @@ module ThinkingSphinx
|
|
489
700
|
else
|
490
701
|
# do nothing
|
491
702
|
end
|
492
|
-
|
703
|
+
|
493
704
|
client.sort_mode = :attr_asc if client.sort_mode == :asc
|
494
705
|
client.sort_mode = :attr_desc if client.sort_mode == :desc
|
495
706
|
end
|
496
|
-
|
707
|
+
|
497
708
|
# Search through a collection of fields and translate any appearances
|
498
709
|
# of them in a string to their attribute equivalent for sorting.
|
499
|
-
#
|
710
|
+
#
|
500
711
|
def sorted_fields_to_attributes(string, fields)
|
501
712
|
fields.each { |field|
|
502
713
|
string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
503
714
|
match.gsub field.to_s, field.to_s.concat("_sort")
|
504
715
|
}
|
505
716
|
}
|
506
|
-
|
717
|
+
|
507
718
|
string
|
508
719
|
end
|
720
|
+
|
721
|
+
def facets_for_model(klass, args, options)
|
722
|
+
hash = ThinkingSphinx::FacetCollection.new args + [options]
|
723
|
+
options = options.clone.merge! :group_function => :attr
|
724
|
+
|
725
|
+
klass.sphinx_facets.inject(hash) do |hash, facet|
|
726
|
+
unless facet.name == :class && !options[:class_facet]
|
727
|
+
options[:group_by] = facet.attribute_name
|
728
|
+
hash.add_from_results facet, search(*(args + [options]))
|
729
|
+
end
|
730
|
+
|
731
|
+
hash
|
732
|
+
end
|
733
|
+
end
|
734
|
+
|
735
|
+
def facets_for_all_models(args, options)
|
736
|
+
options = GlobalFacetOptions.merge(options)
|
737
|
+
hash = ThinkingSphinx::FacetCollection.new args + [options]
|
738
|
+
options = options.merge! :group_function => :attr
|
739
|
+
|
740
|
+
facet_names(options).inject(hash) do |hash, name|
|
741
|
+
options[:group_by] = name
|
742
|
+
hash.add_from_results name, search(*(args + [options]))
|
743
|
+
hash
|
744
|
+
end
|
745
|
+
end
|
746
|
+
|
747
|
+
def facet_classes(options)
|
748
|
+
options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
|
749
|
+
model.constantize
|
750
|
+
}
|
751
|
+
end
|
752
|
+
|
753
|
+
def facet_names(options)
|
754
|
+
classes = facet_classes(options)
|
755
|
+
names = options[:all_attributes] ?
|
756
|
+
facet_names_for_all_classes(classes) :
|
757
|
+
facet_names_common_to_all_classes(classes)
|
758
|
+
|
759
|
+
names.delete "class_crc" unless options[:class_facet]
|
760
|
+
names
|
761
|
+
end
|
762
|
+
|
763
|
+
def facet_names_for_all_classes(classes)
|
764
|
+
classes.collect { |klass|
|
765
|
+
klass.sphinx_facets.collect { |facet| facet.attribute_name }
|
766
|
+
}.flatten.uniq
|
767
|
+
end
|
768
|
+
|
769
|
+
def facet_names_common_to_all_classes(classes)
|
770
|
+
facet_names_for_all_classes(classes).select { |name|
|
771
|
+
classes.all? { |klass|
|
772
|
+
klass.sphinx_facets.detect { |facet|
|
773
|
+
facet.attribute_name == name
|
774
|
+
}
|
775
|
+
}
|
776
|
+
}
|
777
|
+
end
|
509
778
|
end
|
510
779
|
end
|
511
|
-
end
|
780
|
+
end
|