ebeigarts-thinking-sphinx 1.1.22 → 1.2.10
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 +14 -0
- data/VERSION.yml +4 -0
- data/lib/thinking_sphinx.rb +60 -64
- data/lib/thinking_sphinx/active_record.rb +35 -7
- data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +3 -2
- data/lib/thinking_sphinx/attribute.rb +62 -22
- data/lib/thinking_sphinx/configuration.rb +21 -1
- data/lib/thinking_sphinx/core/array.rb +7 -0
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +3 -0
- data/lib/thinking_sphinx/deploy/capistrano.rb +26 -8
- data/lib/thinking_sphinx/excerpter.rb +22 -0
- data/lib/thinking_sphinx/facet.rb +8 -2
- data/lib/thinking_sphinx/facet_search.rb +134 -0
- data/lib/thinking_sphinx/index.rb +2 -2
- data/lib/thinking_sphinx/index/builder.rb +0 -1
- data/lib/thinking_sphinx/property.rb +2 -0
- data/lib/thinking_sphinx/rails_additions.rb +14 -0
- data/lib/thinking_sphinx/search.rb +633 -671
- data/lib/thinking_sphinx/search_methods.rb +421 -0
- data/lib/thinking_sphinx/source.rb +5 -5
- data/lib/thinking_sphinx/source/internal_properties.rb +1 -1
- data/lib/thinking_sphinx/source/sql.rb +10 -8
- data/lib/thinking_sphinx/tasks.rb +14 -9
- data/spec/{unit → lib}/thinking_sphinx/active_record/delta_spec.rb +1 -1
- 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 +44 -5
- data/spec/{unit → lib}/thinking_sphinx/association_spec.rb +0 -0
- data/spec/{unit → lib}/thinking_sphinx/attribute_spec.rb +110 -3
- data/spec/{unit → lib}/thinking_sphinx/configuration_spec.rb +87 -41
- data/spec/lib/thinking_sphinx/core/array_spec.rb +9 -0
- 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 +34 -15
- data/spec/{unit → lib}/thinking_sphinx/field_spec.rb +0 -0
- data/spec/{unit → lib}/thinking_sphinx/index/builder_spec.rb +100 -0
- data/spec/{unit → lib}/thinking_sphinx/index/faux_column_spec.rb +0 -0
- data/spec/{unit → lib}/thinking_sphinx/index_spec.rb +0 -0
- data/spec/{unit → lib}/thinking_sphinx/rails_additions_spec.rb +12 -0
- data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
- data/spec/lib/thinking_sphinx/search_spec.rb +1066 -0
- data/spec/{unit → lib}/thinking_sphinx/source_spec.rb +10 -0
- data/spec/{unit → lib}/thinking_sphinx_spec.rb +10 -0
- data/tasks/distribution.rb +20 -38
- data/tasks/testing.rb +3 -1
- data/vendor/riddle/lib/riddle.rb +1 -1
- data/vendor/riddle/lib/riddle/client.rb +3 -0
- data/vendor/riddle/lib/riddle/client/message.rb +4 -3
- data/vendor/riddle/lib/riddle/configuration/section.rb +1 -1
- data/vendor/riddle/lib/riddle/controller.rb +17 -7
- metadata +63 -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 -104
- data/spec/unit/thinking_sphinx/active_record/search_spec.rb +0 -107
- data/spec/unit/thinking_sphinx/collection_spec.rb +0 -15
- data/spec/unit/thinking_sphinx/facet_collection_spec.rb +0 -64
- data/spec/unit/thinking_sphinx/search_spec.rb +0 -228
|
@@ -159,7 +159,9 @@ module ThinkingSphinx
|
|
|
159
159
|
# messy dependencies issues).
|
|
160
160
|
#
|
|
161
161
|
def load_models
|
|
162
|
-
return if defined?(Rails) &&
|
|
162
|
+
return if defined?(Rails) &&
|
|
163
|
+
Rails.configuration.cache_classes &&
|
|
164
|
+
Rails::VERSION::STRING.to_f > 2.1
|
|
163
165
|
|
|
164
166
|
self.model_directories.each do |base|
|
|
165
167
|
Dir["#{base}**/*.rb"].each do |file|
|
|
@@ -233,6 +235,24 @@ module ThinkingSphinx
|
|
|
233
235
|
end
|
|
234
236
|
end
|
|
235
237
|
|
|
238
|
+
def client
|
|
239
|
+
client = Riddle::Client.new address, port
|
|
240
|
+
client.max_matches = configuration.searchd.max_matches || 1000
|
|
241
|
+
client
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def models_by_crc
|
|
245
|
+
@models_by_crc ||= begin
|
|
246
|
+
ThinkingSphinx.indexed_models.inject({}) do |hash, model|
|
|
247
|
+
hash[model.constantize.to_crc32] = model
|
|
248
|
+
Object.subclasses_of(model.constantize).each { |subclass|
|
|
249
|
+
hash[subclass.to_crc32] = subclass.name
|
|
250
|
+
}
|
|
251
|
+
hash
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
236
256
|
private
|
|
237
257
|
|
|
238
258
|
# Parse the config/sphinx.yml file - if it exists - then use the attribute
|
|
@@ -8,6 +8,9 @@ module ThinkingSphinx
|
|
|
8
8
|
module Deltas
|
|
9
9
|
class DelayedDelta < ThinkingSphinx::Deltas::DefaultDelta
|
|
10
10
|
def index(model, instance = nil)
|
|
11
|
+
return true unless ThinkingSphinx.updates_enabled? && ThinkingSphinx.deltas_enabled?
|
|
12
|
+
return true if instance && !toggled(instance)
|
|
13
|
+
|
|
11
14
|
ThinkingSphinx::Deltas::Job.enqueue(
|
|
12
15
|
ThinkingSphinx::Deltas::DeltaJob.new(delta_index_name(model)),
|
|
13
16
|
ThinkingSphinx::Configuration.instance.delayed_job_priority
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
Capistrano::Configuration.instance(:must_exist).load do
|
|
2
2
|
namespace :thinking_sphinx do
|
|
3
3
|
namespace :install do
|
|
4
|
-
desc
|
|
4
|
+
desc <<-DESC
|
|
5
|
+
Install Sphinx by source
|
|
6
|
+
|
|
7
|
+
If Postgres is available, Sphinx will use it.
|
|
8
|
+
|
|
9
|
+
If the variable :thinking_sphinx_configure_args is set, it will
|
|
10
|
+
be passed to the Sphinx configure script. You can use this to
|
|
11
|
+
install Sphinx in a non-standard location:
|
|
12
|
+
|
|
13
|
+
set :thinking_sphinx_configure_args, "--prefix=$HOME/software"
|
|
14
|
+
DESC
|
|
15
|
+
|
|
5
16
|
task :sphinx do
|
|
6
17
|
with_postgres = false
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
begin
|
|
19
|
+
run "which pg_config" do |channel, stream, data|
|
|
20
|
+
with_postgres = !(data.nil? || data == "")
|
|
21
|
+
end
|
|
22
|
+
rescue Capistrano::CommandError => e
|
|
23
|
+
puts "Continuing despite error: #{e.message}"
|
|
9
24
|
end
|
|
10
25
|
|
|
11
26
|
args = []
|
|
@@ -14,14 +29,15 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
|
14
29
|
args << "--with-pgsql=#{data}"
|
|
15
30
|
end
|
|
16
31
|
end
|
|
17
|
-
|
|
32
|
+
args << fetch(:thinking_sphinx_configure_args, '')
|
|
33
|
+
|
|
18
34
|
commands = <<-CMD
|
|
19
35
|
wget -q http://www.sphinxsearch.com/downloads/sphinx-0.9.8.1.tar.gz >> sphinx.log
|
|
20
36
|
tar xzvf sphinx-0.9.8.1.tar.gz
|
|
21
37
|
cd sphinx-0.9.8.1
|
|
22
38
|
./configure #{args.join(" ")}
|
|
23
39
|
make
|
|
24
|
-
|
|
40
|
+
#{try_sudo} make install
|
|
25
41
|
rm -rf sphinx-0.9.8.1 sphinx-0.9.8.1.tar.gz
|
|
26
42
|
CMD
|
|
27
43
|
run commands.split(/\n\s+/).join(" && ")
|
|
@@ -29,7 +45,7 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
|
29
45
|
|
|
30
46
|
desc "Install Thinking Sphinx as a gem from GitHub"
|
|
31
47
|
task :ts do
|
|
32
|
-
|
|
48
|
+
run "#{try_sudo} gem install freelancing-god-thinking-sphinx --source http://gems.github.com"
|
|
33
49
|
end
|
|
34
50
|
end
|
|
35
51
|
|
|
@@ -70,12 +86,14 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
|
70
86
|
|
|
71
87
|
desc "Add the shared folder for sphinx files for the production environment"
|
|
72
88
|
task :shared_sphinx_folder, :roles => :web do
|
|
73
|
-
|
|
89
|
+
run "mkdir -p #{shared_path}/db/sphinx/production"
|
|
74
90
|
end
|
|
75
91
|
|
|
76
92
|
def rake(*tasks)
|
|
93
|
+
rails_env = fetch(:rails_env, "production")
|
|
94
|
+
rake = fetch(:rake, "rake")
|
|
77
95
|
tasks.each do |t|
|
|
78
|
-
run "cd #{current_path}
|
|
96
|
+
run "cd #{current_path}; #{rake} RAILS_ENV=#{rails_env} #{t}"
|
|
79
97
|
end
|
|
80
98
|
end
|
|
81
99
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
class Excerpter
|
|
3
|
+
CoreMethods = %w( kind_of? object_id respond_to? should should_not stub! )
|
|
4
|
+
# Hide most methods, to allow them to be passed through to the instance.
|
|
5
|
+
instance_methods.select { |method|
|
|
6
|
+
method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
|
|
7
|
+
}.each { |method|
|
|
8
|
+
undef_method method
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
def initialize(search, instance)
|
|
12
|
+
@search = search
|
|
13
|
+
@instance = instance
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def method_missing(method, *args, &block)
|
|
17
|
+
string = @instance.send(method, *args, &block).to_s
|
|
18
|
+
|
|
19
|
+
@search.excerpt_for(string, @instance.class)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -72,7 +72,7 @@ module ThinkingSphinx
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def value(object, attribute_value)
|
|
75
|
-
return translate(object, attribute_value) if translate?
|
|
75
|
+
return translate(object, attribute_value) if translate? || float?
|
|
76
76
|
|
|
77
77
|
case @property.type
|
|
78
78
|
when :datetime
|
|
@@ -95,7 +95,9 @@ module ThinkingSphinx
|
|
|
95
95
|
return nil unless object = object.send(method)
|
|
96
96
|
}
|
|
97
97
|
if object.is_a?(Array)
|
|
98
|
-
object.collect { |item| item.send(column.__name) }
|
|
98
|
+
object.collect { |item| item.send(column.__name) }.detect { |item|
|
|
99
|
+
item.to_crc32 == attribute_value
|
|
100
|
+
}
|
|
99
101
|
else
|
|
100
102
|
object.send(column.__name)
|
|
101
103
|
end
|
|
@@ -104,5 +106,9 @@ module ThinkingSphinx
|
|
|
104
106
|
def column
|
|
105
107
|
@property.columns.first
|
|
106
108
|
end
|
|
109
|
+
|
|
110
|
+
def float?
|
|
111
|
+
@property.type == :float
|
|
112
|
+
end
|
|
107
113
|
end
|
|
108
114
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
class FacetSearch < Hash
|
|
3
|
+
attr_accessor :args, :options
|
|
4
|
+
|
|
5
|
+
def initialize(*args)
|
|
6
|
+
@options = args.extract_options!
|
|
7
|
+
@args = args
|
|
8
|
+
|
|
9
|
+
set_default_options
|
|
10
|
+
|
|
11
|
+
populate
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def for(hash = {})
|
|
15
|
+
for_options = {:with => {}}.merge(options)
|
|
16
|
+
|
|
17
|
+
hash.each do |key, value|
|
|
18
|
+
attrib = ThinkingSphinx::Facet.attribute_name_from_value(key, value)
|
|
19
|
+
for_options[:with][attrib] = underlying_value key, value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ThinkingSphinx.search *(args + [for_options])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def facet_names
|
|
26
|
+
@facet_names ||= begin
|
|
27
|
+
names = options[:all_facets] ?
|
|
28
|
+
facet_names_for_all_classes : facet_names_common_to_all_classes
|
|
29
|
+
|
|
30
|
+
names.delete "class_crc" unless options[:class_facet]
|
|
31
|
+
names
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def set_default_options
|
|
38
|
+
options[:all_facets] ||= false
|
|
39
|
+
if options[:class_facet].nil?
|
|
40
|
+
options[:class_facet] = ((options[:classes] || []).length != 1)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def populate
|
|
45
|
+
facet_names.each do |name|
|
|
46
|
+
search_options = facet_search_options.merge(:group_by => name)
|
|
47
|
+
add_from_results name, ThinkingSphinx.search(
|
|
48
|
+
*(args + [search_options])
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def facet_search_options
|
|
54
|
+
config = ThinkingSphinx::Configuration.instance
|
|
55
|
+
max = config.configuration.searchd.max_matches || 1000
|
|
56
|
+
|
|
57
|
+
options.merge(
|
|
58
|
+
:group_function => :attr,
|
|
59
|
+
:limit => max,
|
|
60
|
+
:max_matches => max,
|
|
61
|
+
:page => 1
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def facet_classes
|
|
66
|
+
(
|
|
67
|
+
options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
|
|
68
|
+
model.constantize
|
|
69
|
+
}
|
|
70
|
+
).select { |klass| klass.sphinx_facets.any? }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def all_facets
|
|
74
|
+
facet_classes.collect { |klass|
|
|
75
|
+
klass.sphinx_facets
|
|
76
|
+
}.flatten.select { |facet|
|
|
77
|
+
options[:facets].blank? || Array(options[:facets]).include?(facet.name)
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def facet_names_for_all_classes
|
|
82
|
+
all_facets.group_by { |facet|
|
|
83
|
+
facet.name
|
|
84
|
+
}.collect { |name, facets|
|
|
85
|
+
if facets.collect { |facet| facet.type }.uniq.length > 1
|
|
86
|
+
raise "Facet #{name} exists in more than one model with different types"
|
|
87
|
+
end
|
|
88
|
+
facets.first.attribute_name
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def facet_names_common_to_all_classes
|
|
93
|
+
facet_names_for_all_classes.select { |name|
|
|
94
|
+
facet_classes.all? { |klass|
|
|
95
|
+
klass.sphinx_facets.detect { |facet|
|
|
96
|
+
facet.attribute_name == name
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def add_from_results(facet, results)
|
|
103
|
+
name = ThinkingSphinx::Facet.name_for(facet)
|
|
104
|
+
|
|
105
|
+
self[name] ||= {}
|
|
106
|
+
|
|
107
|
+
return if results.empty?
|
|
108
|
+
|
|
109
|
+
facet = facet_from_object(results.first, facet) if facet.is_a?(String)
|
|
110
|
+
|
|
111
|
+
results.each_with_groupby_and_count { |result, group, count|
|
|
112
|
+
facet_value = facet.value(result, group)
|
|
113
|
+
|
|
114
|
+
self[name][facet_value] ||= 0
|
|
115
|
+
self[name][facet_value] += count
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def underlying_value(key, value)
|
|
120
|
+
case value
|
|
121
|
+
when Array
|
|
122
|
+
value.collect { |item| underlying_value(key, item) }
|
|
123
|
+
when String
|
|
124
|
+
value.to_crc32
|
|
125
|
+
else
|
|
126
|
+
value
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def facet_from_object(object, name)
|
|
131
|
+
object.sphinx_facets.detect { |facet| facet.attribute_name == name }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -198,7 +198,6 @@ module ThinkingSphinx
|
|
|
198
198
|
# set_property :delta => true
|
|
199
199
|
# set_property :field_weights => {"name" => 100}
|
|
200
200
|
# set_property :order => "name ASC"
|
|
201
|
-
# set_property :include => :picture
|
|
202
201
|
# set_property :select => 'name'
|
|
203
202
|
#
|
|
204
203
|
# Also, the following two properties are particularly relevant for
|
|
@@ -14,6 +14,8 @@ module ThinkingSphinx
|
|
|
14
14
|
@faceted = options[:facet]
|
|
15
15
|
@admin = options[:admin]
|
|
16
16
|
|
|
17
|
+
@alias = @alias.to_sym unless @alias.blank?
|
|
18
|
+
|
|
17
19
|
@columns.each { |col|
|
|
18
20
|
@associations[col] = association_stack(col.__stack.clone).each { |assoc|
|
|
19
21
|
assoc.join_to(source.base)
|
|
@@ -134,3 +134,17 @@ end
|
|
|
134
134
|
Class.extend(
|
|
135
135
|
ThinkingSphinx::ClassAttributeMethods
|
|
136
136
|
) unless Class.respond_to?(:cattr_reader)
|
|
137
|
+
|
|
138
|
+
module ThinkingSphinx
|
|
139
|
+
module MetaClass
|
|
140
|
+
def metaclass
|
|
141
|
+
class << self
|
|
142
|
+
self
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
unless Object.new.respond_to?(:metaclass)
|
|
149
|
+
Object.send(:include, ThinkingSphinx::MetaClass)
|
|
150
|
+
end
|
|
@@ -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,718 +8,681 @@ 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
|
|
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
|
+
# Indication of whether the request has been made to Sphinx for the search
|
|
72
|
+
# query.
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] true if the results have been requested.
|
|
75
|
+
#
|
|
76
|
+
def populated?
|
|
77
|
+
!!@populated
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The query result hash from Riddle.
|
|
81
|
+
#
|
|
82
|
+
# @return [Hash] Raw Sphinx results
|
|
83
|
+
#
|
|
84
|
+
def results
|
|
85
|
+
populate
|
|
86
|
+
@results
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def method_missing(method, *args, &block)
|
|
90
|
+
if is_scope?(method)
|
|
91
|
+
add_scope(method, *args, &block)
|
|
92
|
+
return self
|
|
93
|
+
elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
|
|
94
|
+
super
|
|
95
|
+
elsif !SafeMethods.include?(method.to_s)
|
|
96
|
+
populate
|
|
97
|
+
end
|
|
19
98
|
|
|
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)
|
|
99
|
+
if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
|
|
100
|
+
each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
|
|
101
|
+
else
|
|
102
|
+
@array.send(method, *args, &block)
|
|
35
103
|
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
|
-
# As a convenience, you can also use
|
|
226
|
-
# * <tt>:group</tt>
|
|
227
|
-
# which sets :group_by and defaults to :group_function of :attr
|
|
228
|
-
#
|
|
229
|
-
# === group_function
|
|
230
|
-
#
|
|
231
|
-
# Valid values for :group_function are
|
|
232
|
-
# * <tt>:day</tt>, <tt>:week</tt>, <tt>:month</tt>, <tt>:year</tt> - Grouping is done by the respective timeframes.
|
|
233
|
-
# * <tt>:attr</tt>, <tt>:attrpair</tt> - Grouping is done by the specified attributes(s)
|
|
234
|
-
#
|
|
235
|
-
# === group_by
|
|
236
|
-
#
|
|
237
|
-
# This parameter denotes the field by which grouping is done. Note that the
|
|
238
|
-
# specified field must be a sphinx attribute or index.
|
|
239
|
-
#
|
|
240
|
-
# === group_clause
|
|
241
|
-
#
|
|
242
|
-
# This determines the sorting order of the groups. In a grouping search,
|
|
243
|
-
# the matches within a group will sorted by the <tt>:sort_mode</tt> and <tt>:order</tt> parameters.
|
|
244
|
-
# The group matches themselves however, will be sorted by <tt>:group_clause</tt>.
|
|
245
|
-
#
|
|
246
|
-
# The syntax for this is the same as an order parameter in extended sort mode.
|
|
247
|
-
# Namely, you can specify an SQL-like sort expression with up to 5 attributes
|
|
248
|
-
# (including internal attributes), eg: "@relevance DESC, price ASC, @id DESC"
|
|
249
|
-
#
|
|
250
|
-
# === Grouping by timestamp
|
|
251
|
-
#
|
|
252
|
-
# Timestamp grouping groups off items by the day, week, month or year of the
|
|
253
|
-
# attribute given. In order to do this you need to define a timestamp attribute,
|
|
254
|
-
# which pretty much looks like the standard defintion for any attribute.
|
|
255
|
-
#
|
|
256
|
-
# define_index do
|
|
257
|
-
# #
|
|
258
|
-
# # All your other stuff
|
|
259
|
-
# #
|
|
260
|
-
# has :created_at
|
|
261
|
-
# end
|
|
262
|
-
#
|
|
263
|
-
# When you need to fire off your search, it'll go something to the tune of
|
|
264
|
-
#
|
|
265
|
-
# Fruit.search "apricot", :group_function => :day, :group_by => 'created_at'
|
|
266
|
-
#
|
|
267
|
-
# The <tt>@groupby</tt> special attribute will contain the date for that group.
|
|
268
|
-
# Depending on the <tt>:group_function</tt> parameter, the date format will be
|
|
269
|
-
#
|
|
270
|
-
# * <tt>:day</tt> - YYYYMMDD
|
|
271
|
-
# * <tt>:week</tt> - YYYYNNN (NNN is the first day of the week in question,
|
|
272
|
-
# counting from the start of the year )
|
|
273
|
-
# * <tt>:month</tt> - YYYYMM
|
|
274
|
-
# * <tt>:year</tt> - YYYY
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
# === Grouping by attribute
|
|
278
|
-
#
|
|
279
|
-
# The syntax is the same as grouping by timestamp, except for the fact that the
|
|
280
|
-
# <tt>:group_function</tt> parameter is changed
|
|
281
|
-
#
|
|
282
|
-
# Fruit.search "apricot", :group_function => :attr, :group_by => 'size'
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
# == Geo/Location Searching
|
|
286
|
-
#
|
|
287
|
-
# Sphinx - and therefore Thinking Sphinx - has the facility to search
|
|
288
|
-
# around a geographical point, using a given latitude and longitude. To
|
|
289
|
-
# take advantage of this, you will need to have both of those values in
|
|
290
|
-
# attributes. To search with that point, you can then use one of the
|
|
291
|
-
# following syntax examples:
|
|
292
|
-
#
|
|
293
|
-
# Address.search "Melbourne", :geo => [1.4, -2.217], :order => "@geodist asc"
|
|
294
|
-
# Address.search "Australia", :geo => [-0.55, 3.108], :order => "@geodist asc"
|
|
295
|
-
# :latitude_attr => "latit", :longitude_attr => "longit"
|
|
296
|
-
#
|
|
297
|
-
# The first example applies when your latitude and longitude attributes
|
|
298
|
-
# are named any of lat, latitude, lon, long or longitude. If that's not
|
|
299
|
-
# the case, you will need to explicitly state them in your search, _or_
|
|
300
|
-
# you can do so in your model:
|
|
301
|
-
#
|
|
302
|
-
# define_index do
|
|
303
|
-
# has :latit # Float column, stored in radians
|
|
304
|
-
# has :longit # Float column, stored in radians
|
|
305
|
-
#
|
|
306
|
-
# set_property :latitude_attr => "latit"
|
|
307
|
-
# set_property :longitude_attr => "longit"
|
|
308
|
-
# end
|
|
309
|
-
#
|
|
310
|
-
# Now, geo-location searching really only has an affect if you have a
|
|
311
|
-
# filter, sort or grouping clause related to it - otherwise it's just a
|
|
312
|
-
# normal search, and _will not_ return a distance value otherwise. To
|
|
313
|
-
# make use of the positioning difference, use the special attribute
|
|
314
|
-
# "@geodist" in any of your filters or sorting or grouping clauses.
|
|
315
|
-
#
|
|
316
|
-
# And don't forget - both the latitude and longitude you use in your
|
|
317
|
-
# search, and the values in your indexes, need to be stored as a float in radians,
|
|
318
|
-
# _not_ degrees. Keep in mind that if you do this conversion in SQL
|
|
319
|
-
# you will need to explicitly declare a column type of :float.
|
|
320
|
-
#
|
|
321
|
-
# define_index do
|
|
322
|
-
# has 'RADIANS(lat)', :as => :lat, :type => :float
|
|
323
|
-
# # ...
|
|
324
|
-
# end
|
|
325
|
-
#
|
|
326
|
-
# Once you've got your results set, you can access the distances as
|
|
327
|
-
# follows:
|
|
328
|
-
#
|
|
329
|
-
# @results.each_with_geodist do |result, distance|
|
|
330
|
-
# # ...
|
|
331
|
-
# end
|
|
332
|
-
#
|
|
333
|
-
# The distance value is returned as a float, representing the distance in
|
|
334
|
-
# metres.
|
|
335
|
-
#
|
|
336
|
-
# == Handling a Stale Index
|
|
337
|
-
#
|
|
338
|
-
# Especially if you don't use delta indexing, you risk having records in the
|
|
339
|
-
# Sphinx index that are no longer in the database. By default, those will simply
|
|
340
|
-
# come back as nils:
|
|
341
|
-
#
|
|
342
|
-
# >> pat_user.delete
|
|
343
|
-
# >> User.search("pat")
|
|
344
|
-
# Sphinx Result: [1,2]
|
|
345
|
-
# => [nil, <#User id: 2>]
|
|
346
|
-
#
|
|
347
|
-
# (If you search across multiple models, you'll get ActiveRecord::RecordNotFound.)
|
|
348
|
-
#
|
|
349
|
-
# You can simply Array#compact these results or handle the nils in some other way, but
|
|
350
|
-
# Sphinx will still report two results, and the missing records may upset your layout.
|
|
351
|
-
#
|
|
352
|
-
# If you pass :retry_stale => true to a single-model search, missing records will
|
|
353
|
-
# cause Thinking Sphinx to retry the query but excluding those records. Since search
|
|
354
|
-
# is paginated, the new search could potentially include missing records as well, so by
|
|
355
|
-
# default Thinking Sphinx will retry three times. Pass :retry_stale => 5 to retry five
|
|
356
|
-
# times, and so on. If there are still missing ids on the last retry, they are
|
|
357
|
-
# shown as nils.
|
|
358
|
-
#
|
|
359
|
-
def search(*args)
|
|
360
|
-
query = args.clone # an array
|
|
361
|
-
options = query.extract_options!
|
|
362
|
-
|
|
363
|
-
retry_search_on_stale_index(query, options) do
|
|
364
|
-
results, client = search_results(*(query + [options]))
|
|
365
|
-
|
|
366
|
-
log "Sphinx Error: #{results[:error]}", :error if results[:error]
|
|
367
|
-
|
|
368
|
-
klass = options[:class]
|
|
369
|
-
page = options[:page] ? options[:page].to_i : 1
|
|
104
|
+
end
|
|
370
105
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
106
|
+
# Returns true if the Search object or the underlying Array object respond
|
|
107
|
+
# to the requested method.
|
|
108
|
+
#
|
|
109
|
+
# @param [Symbol] method The method name
|
|
110
|
+
# @return [Boolean] true if either Search or Array responds to the method.
|
|
111
|
+
#
|
|
112
|
+
def respond_to?(method)
|
|
113
|
+
super || @array.respond_to?(method)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The current page number of the result set. Defaults to 1 if no page was
|
|
117
|
+
# explicitly requested.
|
|
118
|
+
#
|
|
119
|
+
# @return [Integer]
|
|
120
|
+
#
|
|
121
|
+
def current_page
|
|
122
|
+
@options[:page].blank? ? 1 : @options[:page].to_i
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The next page number of the result set. If there are no more pages
|
|
126
|
+
# available, nil is returned.
|
|
127
|
+
#
|
|
128
|
+
# @return [Integer, nil]
|
|
129
|
+
#
|
|
130
|
+
def next_page
|
|
131
|
+
current_page >= total_pages ? nil : current_page + 1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The previous page number of the result set. If this is the first page,
|
|
135
|
+
# then nil is returned.
|
|
136
|
+
#
|
|
137
|
+
# @return [Integer, nil]
|
|
138
|
+
#
|
|
139
|
+
def previous_page
|
|
140
|
+
current_page == 1 ? nil : current_page - 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The amount of records per set of paged results. Defaults to 20 unless a
|
|
144
|
+
# specific page size is requested.
|
|
145
|
+
#
|
|
146
|
+
# @return [Integer]
|
|
147
|
+
#
|
|
148
|
+
def per_page
|
|
149
|
+
@options[:limit] || @options[:per_page] || 20
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The total number of pages available if the results are paginated.
|
|
153
|
+
#
|
|
154
|
+
# @return [Integer]
|
|
155
|
+
#
|
|
156
|
+
def total_pages
|
|
157
|
+
populate
|
|
158
|
+
@total_pages ||= (@results[:total] / per_page.to_f).ceil
|
|
159
|
+
end
|
|
160
|
+
# Compatibility with older versions of will_paginate
|
|
161
|
+
alias_method :page_count, :total_pages
|
|
162
|
+
|
|
163
|
+
# The total number of search results available.
|
|
164
|
+
#
|
|
165
|
+
# @return [Integer]
|
|
166
|
+
#
|
|
167
|
+
def total_entries
|
|
168
|
+
populate
|
|
169
|
+
@total_entries ||= @results[:total_found]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The current page's offset, based on the number of records per page.
|
|
173
|
+
#
|
|
174
|
+
# @return [Integer]
|
|
175
|
+
#
|
|
176
|
+
def offset
|
|
177
|
+
(current_page - 1) * per_page
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def indexes
|
|
181
|
+
return options[:index] if options[:index]
|
|
182
|
+
return '*' if classes.empty?
|
|
374
183
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
begin
|
|
385
|
-
# Passing this in an option so Collection.create_from_results can see it.
|
|
386
|
-
# It should only raise on stale records if there are any retries left.
|
|
387
|
-
options[:raise_on_stale] = stale_retries_left > 0
|
|
388
|
-
block.call
|
|
389
|
-
# If ThinkingSphinx::Collection.create_from_results found records in Sphinx but not
|
|
390
|
-
# in the DB and the :raise_on_stale option is set, this exception is raised. We retry
|
|
391
|
-
# a limited number of times, excluding the stale ids from the search.
|
|
392
|
-
rescue StaleIdsException => e
|
|
393
|
-
stale_retries_left -= 1
|
|
394
|
-
|
|
395
|
-
stale_ids |= e.ids # For logging
|
|
396
|
-
options[:without_ids] = Array(options[:without_ids]) | e.ids # Actual exclusion
|
|
397
|
-
|
|
398
|
-
tries = stale_retries_left
|
|
399
|
-
log "Sphinx Stale Ids (%s %s left): %s" % [
|
|
400
|
-
tries, (tries==1 ? 'try' : 'tries'), stale_ids.join(', ')
|
|
401
|
-
]
|
|
402
|
-
|
|
403
|
-
retry
|
|
404
|
-
end
|
|
184
|
+
classes.collect { |klass| klass.sphinx_index_names }.flatten.join(',')
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def each_with_groupby_and_count(&block)
|
|
188
|
+
populate
|
|
189
|
+
results[:matches].each_with_index do |match, index|
|
|
190
|
+
yield self[index],
|
|
191
|
+
match[:attributes]["@groupby"],
|
|
192
|
+
match[:attributes]["@count"]
|
|
405
193
|
end
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def each_with_weighting(&block)
|
|
197
|
+
populate
|
|
198
|
+
results[:matches].each_with_index do |match, index|
|
|
199
|
+
yield self[index], match[:weight]
|
|
410
200
|
end
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
# - Index to check within
|
|
417
|
-
# - Options hash (defaults to {})
|
|
418
|
-
#
|
|
419
|
-
# Example:
|
|
420
|
-
#
|
|
421
|
-
# ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User)
|
|
422
|
-
#
|
|
423
|
-
def search_for_id(*args)
|
|
424
|
-
options = args.extract_options!
|
|
425
|
-
client = client_from_options options
|
|
426
|
-
|
|
427
|
-
query, filters = search_conditions(
|
|
428
|
-
options[:class], options[:conditions] || {}
|
|
429
|
-
)
|
|
430
|
-
client.filters += filters
|
|
431
|
-
client.match_mode = :extended unless query.empty?
|
|
432
|
-
client.id_range = args.first..args.first
|
|
433
|
-
|
|
434
|
-
begin
|
|
435
|
-
return client.query(query, args[1])[:matches].length > 0
|
|
436
|
-
rescue Errno::ECONNREFUSED => err
|
|
437
|
-
raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
|
|
438
|
-
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def excerpt_for(string, model = nil)
|
|
204
|
+
if model.nil? && one_class
|
|
205
|
+
model ||= one_class
|
|
439
206
|
end
|
|
440
207
|
|
|
441
|
-
|
|
208
|
+
populate
|
|
209
|
+
client.excerpts(
|
|
210
|
+
:docs => [string],
|
|
211
|
+
:words => results[:words].keys.join(' '),
|
|
212
|
+
:index => "#{model.source_of_sphinx_index.sphinx_name}_core"
|
|
213
|
+
).first
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def search(*args)
|
|
217
|
+
merge_search ThinkingSphinx::Search.new(*args)
|
|
218
|
+
self
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private
|
|
222
|
+
|
|
223
|
+
def config
|
|
224
|
+
ThinkingSphinx::Configuration.instance
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def populate
|
|
228
|
+
return if @populated
|
|
229
|
+
@populated = true
|
|
442
230
|
|
|
443
|
-
|
|
444
|
-
# the result hash and the client. Not super elegant, but it'll do for
|
|
445
|
-
# the moment.
|
|
446
|
-
#
|
|
447
|
-
def search_results(*args)
|
|
448
|
-
options = args.extract_options!
|
|
449
|
-
query = args.join(' ')
|
|
450
|
-
client = client_from_options options
|
|
451
|
-
|
|
452
|
-
query = star_query(query, options[:star]) if options[:star]
|
|
453
|
-
|
|
454
|
-
extra_query, filters = search_conditions(
|
|
455
|
-
options[:class], options[:conditions] || {}
|
|
456
|
-
)
|
|
457
|
-
client.filters += filters
|
|
458
|
-
client.match_mode = :extended unless extra_query.empty?
|
|
459
|
-
query = [query, extra_query].join(' ')
|
|
460
|
-
query.strip! # Because "" and " " are not equivalent
|
|
461
|
-
|
|
462
|
-
set_sort_options! client, options
|
|
463
|
-
|
|
464
|
-
client.limit = options[:per_page].to_i if options[:per_page]
|
|
465
|
-
page = options[:page] ? options[:page].to_i : 1
|
|
466
|
-
page = 1 if page <= 0
|
|
467
|
-
client.offset = (page - 1) * client.limit
|
|
468
|
-
|
|
231
|
+
retry_on_stale_index do
|
|
469
232
|
begin
|
|
470
|
-
log "Sphinx: #{query}"
|
|
471
|
-
results = client.query
|
|
472
|
-
log "Sphinx Result:"
|
|
473
|
-
log results[:matches].collect { |m|
|
|
474
|
-
m[:attributes]["sphinx_internal_id"]
|
|
475
|
-
}.inspect
|
|
233
|
+
log "Querying Sphinx: #{query}"
|
|
234
|
+
@results = client.query query, indexes, comment
|
|
476
235
|
rescue Errno::ECONNREFUSED => err
|
|
477
|
-
raise ThinkingSphinx::ConnectionError,
|
|
236
|
+
raise ThinkingSphinx::ConnectionError,
|
|
237
|
+
'Connection to Sphinx Daemon (searchd) failed.'
|
|
478
238
|
end
|
|
479
|
-
|
|
480
|
-
return results, client
|
|
481
|
-
end
|
|
482
239
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
index_options = klass ? klass.sphinx_index_options : {}
|
|
491
|
-
|
|
492
|
-
# The Riddle default is per-query max_matches=1000. If we set the
|
|
493
|
-
# per-server max to a smaller value in sphinx.yml, we need to override
|
|
494
|
-
# the Riddle default or else we get search errors like
|
|
495
|
-
# "per-query max_matches=1000 out of bounds (per-server max_matches=200)"
|
|
496
|
-
if per_server_max_matches = config.configuration.searchd.max_matches
|
|
497
|
-
options[:max_matches] ||= per_server_max_matches
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
# Turn :index_weights => { "foo" => 2, User => 1 }
|
|
501
|
-
# into :index_weights => { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
|
|
502
|
-
if iw = options[:index_weights]
|
|
503
|
-
options[:index_weights] = iw.inject({}) do |hash, (index,weight)|
|
|
504
|
-
if index.is_a?(Class)
|
|
505
|
-
name = ThinkingSphinx::Index.name(index)
|
|
506
|
-
hash["#{name}_core"] = weight
|
|
507
|
-
hash["#{name}_delta"] = weight
|
|
508
|
-
else
|
|
509
|
-
hash[index] = weight
|
|
510
|
-
end
|
|
511
|
-
hash
|
|
512
|
-
end
|
|
240
|
+
if options[:ids_only]
|
|
241
|
+
replace @results[:matches].collect { |match|
|
|
242
|
+
match[:attributes]["sphinx_internal_id"]
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
replace instances_from_matches
|
|
246
|
+
add_excerpter
|
|
513
247
|
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def add_excerpter
|
|
252
|
+
each do |object|
|
|
253
|
+
next if object.respond_to?(:excerpts)
|
|
514
254
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
options[:group_by] = options[:group].to_s
|
|
518
|
-
options[:group_function] ||= :attr
|
|
519
|
-
end
|
|
255
|
+
excerpter = ThinkingSphinx::Excerpter.new self, object
|
|
256
|
+
block = lambda { excerpter }
|
|
520
257
|
|
|
521
|
-
|
|
522
|
-
:
|
|
523
|
-
:group_by, :group_function, :group_clause, :group_distinct, :cut_off,
|
|
524
|
-
:retry_count, :retry_delay, :index_weights, :rank_mode,
|
|
525
|
-
:max_query_time, :field_weights, :filters, :anchor, :limit
|
|
526
|
-
].each do |key|
|
|
527
|
-
client.send(
|
|
528
|
-
key.to_s.concat("=").to_sym,
|
|
529
|
-
options[key] || index_options[key] || client.send(key)
|
|
530
|
-
)
|
|
258
|
+
object.metaclass.instance_eval do
|
|
259
|
+
define_method(:excerpts, &block)
|
|
531
260
|
end
|
|
532
|
-
|
|
533
|
-
options[:classes] = [klass] if klass
|
|
534
|
-
|
|
535
|
-
client.anchor = anchor_conditions(klass, options) || {} if client.anchor.empty?
|
|
536
|
-
|
|
537
|
-
client.filters << Riddle::Client::Filter.new(
|
|
538
|
-
"sphinx_deleted", [0]
|
|
539
|
-
)
|
|
540
|
-
|
|
541
|
-
# class filters
|
|
542
|
-
client.filters << Riddle::Client::Filter.new(
|
|
543
|
-
"class_crc", options[:classes].collect { |k| k.to_crc32s }.flatten
|
|
544
|
-
) if options[:classes]
|
|
545
|
-
|
|
546
|
-
# normal attribute filters
|
|
547
|
-
client.filters += options[:with].collect { |attr,val|
|
|
548
|
-
Riddle::Client::Filter.new attr.to_s, filter_value(val)
|
|
549
|
-
} if options[:with]
|
|
550
|
-
|
|
551
|
-
# exclusive attribute filters
|
|
552
|
-
client.filters += options[:without].collect { |attr,val|
|
|
553
|
-
Riddle::Client::Filter.new attr.to_s, filter_value(val), true
|
|
554
|
-
} if options[:without]
|
|
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
|
-
|
|
568
|
-
client
|
|
569
261
|
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def self.log(message, method = :debug)
|
|
265
|
+
return if ::ActiveRecord::Base.logger.nil?
|
|
266
|
+
::ActiveRecord::Base.logger.send method, message
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def log(message, method = :debug)
|
|
270
|
+
self.class.log(message, method)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def client
|
|
274
|
+
client = config.client
|
|
275
|
+
|
|
276
|
+
index_options = one_class ?
|
|
277
|
+
one_class.sphinx_indexes.first.local_options : {}
|
|
278
|
+
|
|
279
|
+
[
|
|
280
|
+
:max_matches, :group_by, :group_function, :group_clause,
|
|
281
|
+
:group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
|
|
282
|
+
:rank_mode, :max_query_time, :field_weights
|
|
283
|
+
].each do |key|
|
|
284
|
+
# puts "key: #{key}"
|
|
285
|
+
value = options[key] || index_options[key]
|
|
286
|
+
# puts "value: #{value.inspect}"
|
|
287
|
+
client.send("#{key}=", value) if value
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
client.limit = per_page
|
|
291
|
+
client.offset = offset
|
|
292
|
+
client.match_mode = match_mode
|
|
293
|
+
client.filters = filters
|
|
294
|
+
client.sort_mode = sort_mode
|
|
295
|
+
client.sort_by = sort_by
|
|
296
|
+
client.group_by = group_by if group_by
|
|
297
|
+
client.group_function = group_function if group_function
|
|
298
|
+
client.index_weights = index_weights
|
|
299
|
+
client.anchor = anchor
|
|
300
|
+
|
|
301
|
+
client
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def retry_on_stale_index(&block)
|
|
305
|
+
stale_ids = []
|
|
306
|
+
retries = stale_retries
|
|
307
|
+
|
|
308
|
+
begin
|
|
309
|
+
options[:raise_on_stale] = retries > 0
|
|
310
|
+
block.call
|
|
311
|
+
|
|
312
|
+
# If ThinkingSphinx::Search#instances_from_matches found records in
|
|
313
|
+
# Sphinx but not in the DB and the :raise_on_stale option is set, this
|
|
314
|
+
# exception is raised. We retry a limited number of times, excluding the
|
|
315
|
+
# stale ids from the search.
|
|
316
|
+
rescue StaleIdsException => err
|
|
317
|
+
retries -= 1
|
|
318
|
+
|
|
319
|
+
# For logging
|
|
320
|
+
stale_ids |= err.ids
|
|
321
|
+
# ID exclusion
|
|
322
|
+
options[:without_ids] = Array(options[:without_ids]) | err.ids
|
|
323
|
+
|
|
324
|
+
log 'Sphinx Stale Ids (%s %s left): %s' % [
|
|
325
|
+
retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
|
|
326
|
+
]
|
|
327
|
+
retry
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def classes
|
|
332
|
+
@classes ||= options[:classes] || []
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def one_class
|
|
336
|
+
@one_class ||= classes.length != 1 ? nil : classes.first
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def query
|
|
340
|
+
@query ||= begin
|
|
341
|
+
q = @args.join(' ') << conditions_as_query
|
|
342
|
+
(options[:star] ? star_query(q) : q).strip
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def conditions_as_query
|
|
347
|
+
return '' if @options[:conditions].blank?
|
|
570
348
|
|
|
571
|
-
|
|
572
|
-
|
|
349
|
+
# Soon to be deprecated.
|
|
350
|
+
keys = @options[:conditions].keys.reject { |key|
|
|
351
|
+
attributes.include?(key.to_sym)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
' ' + keys.collect { |key|
|
|
355
|
+
"@#{key} #{options[:conditions][key]}"
|
|
356
|
+
}.join(' ')
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def star_query(query)
|
|
360
|
+
token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
|
|
573
361
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
362
|
+
query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
|
|
363
|
+
pre, proper, post = $`, $&, $'
|
|
364
|
+
# E.g. "@foo", "/2", "~3", but not as part of a token
|
|
365
|
+
is_operator = pre.match(%r{(\W|^)[@~/]\Z})
|
|
366
|
+
# E.g. "foo bar", with quotes
|
|
367
|
+
is_quote = proper.starts_with?('"') && proper.ends_with?('"')
|
|
368
|
+
has_star = pre.ends_with?("*") || post.starts_with?("*")
|
|
369
|
+
if is_operator || is_quote || has_star
|
|
370
|
+
proper
|
|
371
|
+
else
|
|
372
|
+
"*#{proper}*"
|
|
584
373
|
end
|
|
585
374
|
end
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def comment
|
|
378
|
+
options[:comment] || ''
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def match_mode
|
|
382
|
+
options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def sort_mode
|
|
386
|
+
@sort_mode ||= case options[:sort_mode]
|
|
387
|
+
when :asc
|
|
388
|
+
:attr_asc
|
|
389
|
+
when :desc
|
|
390
|
+
:attr_desc
|
|
391
|
+
when nil
|
|
392
|
+
case options[:order]
|
|
393
|
+
when String
|
|
394
|
+
:extended
|
|
395
|
+
when Symbol
|
|
396
|
+
:attr_asc
|
|
593
397
|
else
|
|
594
|
-
|
|
398
|
+
:relevance
|
|
595
399
|
end
|
|
400
|
+
else
|
|
401
|
+
options[:sort_mode]
|
|
596
402
|
end
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
value.respond_to?(:in_time_zone) ? value.utc.to_i : value.to_i
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def sort_by
|
|
406
|
+
case @sort_by = (options[:sort_by] || options[:order])
|
|
407
|
+
when String
|
|
408
|
+
sorted_fields_to_attributes(@sort_by)
|
|
409
|
+
when Symbol
|
|
410
|
+
field_names.include?(@sort_by) ?
|
|
411
|
+
@sort_by.to_s.concat('_sort') : @sort_by.to_s
|
|
412
|
+
else
|
|
413
|
+
''
|
|
609
414
|
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def field_names
|
|
418
|
+
return [] unless one_class
|
|
610
419
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
420
|
+
one_class.sphinx_indexes.collect { |index|
|
|
421
|
+
index.fields.collect { |field| field.unique_name }
|
|
422
|
+
}.flatten
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def sorted_fields_to_attributes(order_string)
|
|
426
|
+
field_names.each { |field|
|
|
427
|
+
order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
|
428
|
+
match.gsub field.to_s, field.to_s.concat("_sort")
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
order_string
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
|
|
436
|
+
# { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
|
|
437
|
+
#
|
|
438
|
+
def index_weights
|
|
439
|
+
weights = options[:index_weights] || {}
|
|
440
|
+
weights.keys.inject({}) do |hash, key|
|
|
441
|
+
if key.is_a?(Class)
|
|
442
|
+
name = ThinkingSphinx::Index.name_for(key)
|
|
443
|
+
hash["#{name}_core"] = weights[key]
|
|
444
|
+
hash["#{name}_delta"] = weights[key]
|
|
445
|
+
else
|
|
446
|
+
hash[key] = weights[key]
|
|
630
447
|
end
|
|
631
448
|
|
|
632
|
-
|
|
449
|
+
hash
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def group_by
|
|
454
|
+
options[:group] ? options[:group].to_s : nil
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def group_function
|
|
458
|
+
options[:group] ? :attr : nil
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def internal_filters
|
|
462
|
+
filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
|
|
463
|
+
|
|
464
|
+
class_crcs = classes.collect { |klass|
|
|
465
|
+
klass.to_crc32s
|
|
466
|
+
}.flatten
|
|
467
|
+
|
|
468
|
+
unless class_crcs.empty?
|
|
469
|
+
filters << Riddle::Client::Filter.new('class_crc', class_crcs)
|
|
633
470
|
end
|
|
634
471
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
lat_attr ||= :lat if attributes.include?(:lat)
|
|
654
|
-
lat_attr ||= :latitude if attributes.include?(:latitude)
|
|
655
|
-
|
|
656
|
-
lon_attr = options[:longitude_attr] if options[:longitude_attr]
|
|
657
|
-
lon_attr ||= :lng if attributes.include?(:lng)
|
|
658
|
-
lon_attr ||= :lon if attributes.include?(:lon)
|
|
659
|
-
lon_attr ||= :long if attributes.include?(:long)
|
|
660
|
-
lon_attr ||= :longitude if attributes.include?(:longitude)
|
|
661
|
-
|
|
662
|
-
lat = options[:lat]
|
|
663
|
-
lon = options[:lon]
|
|
664
|
-
|
|
665
|
-
if options[:geo]
|
|
666
|
-
lat = options[:geo].first
|
|
667
|
-
lon = options[:geo].last
|
|
472
|
+
filters << Riddle::Client::Filter.new(
|
|
473
|
+
'sphinx_internal_id', filter_value(options[:without_ids]), true
|
|
474
|
+
) if options[:without_ids]
|
|
475
|
+
|
|
476
|
+
filters
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def condition_filters
|
|
480
|
+
(options[:conditions] || {}).collect { |attrib, value|
|
|
481
|
+
if attributes.include?(attrib.to_sym)
|
|
482
|
+
puts <<-MSG
|
|
483
|
+
Deprecation Warning: filters on attributes should be done using the :with
|
|
484
|
+
option, not :conditions. For example:
|
|
485
|
+
:with => {:#{attrib} => #{value.inspect}}
|
|
486
|
+
MSG
|
|
487
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
|
488
|
+
else
|
|
489
|
+
nil
|
|
668
490
|
end
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
491
|
+
}.compact
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def filters
|
|
495
|
+
internal_filters +
|
|
496
|
+
condition_filters +
|
|
497
|
+
(options[:with] || {}).collect { |attrib, value|
|
|
498
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
|
499
|
+
} +
|
|
500
|
+
(options[:without] || {}).collect { |attrib, value|
|
|
501
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
|
|
502
|
+
} +
|
|
503
|
+
(options[:with_all] || {}).collect { |attrib, values|
|
|
504
|
+
Array(values).collect { |value|
|
|
505
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
|
506
|
+
}
|
|
507
|
+
}.flatten
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# When passed a Time instance, returns the integer timestamp.
|
|
511
|
+
#
|
|
512
|
+
# If using Rails 2.1+, need to handle timezones to translate them back to
|
|
513
|
+
# UTC, as that's what datetimes will be stored as by MySQL.
|
|
514
|
+
#
|
|
515
|
+
# in_time_zone is a method that was added for the timezone support in
|
|
516
|
+
# Rails 2.1, which is why it's used for testing. I'm sure there's better
|
|
517
|
+
# ways, but this does the job.
|
|
518
|
+
#
|
|
519
|
+
def filter_value(value)
|
|
520
|
+
case value
|
|
521
|
+
when Range
|
|
522
|
+
filter_value(value.first).first..filter_value(value.last).first
|
|
523
|
+
when Array
|
|
524
|
+
value.collect { |v| filter_value(v) }.flatten
|
|
525
|
+
when Time
|
|
526
|
+
value.respond_to?(:in_time_zone) ? [value.utc.to_i] : [value.to_i]
|
|
527
|
+
when NilClass
|
|
528
|
+
0
|
|
529
|
+
else
|
|
530
|
+
Array(value)
|
|
676
531
|
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def anchor
|
|
535
|
+
return {} unless options[:geo] || (options[:lat] && options[:lng])
|
|
536
|
+
|
|
537
|
+
{
|
|
538
|
+
:latitude => options[:geo] ? options[:geo].first : options[:lat],
|
|
539
|
+
:longitude => options[:geo] ? options[:geo].last : options[:lng],
|
|
540
|
+
:latitude_attribute => latitude_attr.to_s,
|
|
541
|
+
:longitude_attribute => longitude_attr.to_s
|
|
542
|
+
}
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def latitude_attr
|
|
546
|
+
options[:latitude_attr] ||
|
|
547
|
+
index_option(:latitude_attr) ||
|
|
548
|
+
attribute(:lat, :latitude)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def longitude_attr
|
|
552
|
+
options[:longitude_attr] ||
|
|
553
|
+
index_option(:longitude_attr) ||
|
|
554
|
+
attribute(:lon, :lng, :longitude)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def index_option(key)
|
|
558
|
+
return nil unless one_class
|
|
559
|
+
|
|
560
|
+
one_class.sphinx_indexes.collect { |index|
|
|
561
|
+
index.local_options[key]
|
|
562
|
+
}.compact.first
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def attribute(*keys)
|
|
566
|
+
return nil unless one_class
|
|
567
|
+
|
|
568
|
+
keys.detect { |key|
|
|
569
|
+
attributes.include?(key)
|
|
570
|
+
}
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def attributes
|
|
574
|
+
return [] unless one_class
|
|
677
575
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
576
|
+
attributes = one_class.sphinx_indexes.collect { |index|
|
|
577
|
+
index.attributes.collect { |attrib| attrib.unique_name }
|
|
578
|
+
}.flatten
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def stale_retries
|
|
582
|
+
case options[:retry_stale]
|
|
583
|
+
when TrueClass
|
|
584
|
+
3
|
|
585
|
+
when nil, FalseClass
|
|
586
|
+
0
|
|
587
|
+
else
|
|
588
|
+
options[:retry_stale].to_i
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def instances_from_class(klass, matches)
|
|
593
|
+
index_options = klass.sphinx_index_options
|
|
687
594
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
595
|
+
ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
|
|
596
|
+
instances = ids.length > 0 ? klass.find(
|
|
597
|
+
:all,
|
|
598
|
+
:joins => options[:joins],
|
|
599
|
+
:conditions => {klass.primary_key_for_sphinx.to_sym => ids},
|
|
600
|
+
:include => (options[:include] || index_options[:include]),
|
|
601
|
+
:select => (options[:select] || index_options[:select]),
|
|
602
|
+
:order => (options[:sql_order] || index_options[:sql_order])
|
|
603
|
+
) : []
|
|
604
|
+
|
|
605
|
+
# Raise an exception if we find records in Sphinx but not in the DB, so
|
|
606
|
+
# the search method can retry without them. See
|
|
607
|
+
# ThinkingSphinx::Search.retry_search_on_stale_index.
|
|
608
|
+
if options[:raise_on_stale] && instances.length < ids.length
|
|
609
|
+
stale_ids = ids - instances.map { |i| i.id }
|
|
610
|
+
raise StaleIdsException, stale_ids
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# if the user has specified an SQL order, return the collection
|
|
614
|
+
# without rearranging it into the Sphinx order
|
|
615
|
+
return instances if (options[:sql_order] || index_options[:sql_order])
|
|
616
|
+
|
|
617
|
+
ids.collect { |obj_id|
|
|
618
|
+
instances.detect do |obj|
|
|
619
|
+
obj.primary_key_for_sphinx == obj_id
|
|
702
620
|
end
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
621
|
+
}
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Group results by class and call #find(:all) once for each group to reduce
|
|
625
|
+
# the number of #find's in multi-model searches.
|
|
626
|
+
#
|
|
627
|
+
def instances_from_matches
|
|
628
|
+
return single_class_results if one_class
|
|
629
|
+
|
|
630
|
+
groups = results[:matches].group_by { |match|
|
|
631
|
+
match[:attributes]["class_crc"]
|
|
632
|
+
}
|
|
633
|
+
groups.each do |crc, group|
|
|
634
|
+
group.replace(
|
|
635
|
+
instances_from_class(class_from_crc(crc), group)
|
|
636
|
+
)
|
|
706
637
|
end
|
|
707
638
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
|
714
|
-
match.gsub field.to_s, field.to_s.concat("_sort")
|
|
715
|
-
}
|
|
639
|
+
results[:matches].collect do |match|
|
|
640
|
+
groups.detect { |crc, group|
|
|
641
|
+
crc == match[:attributes]["class_crc"]
|
|
642
|
+
}[1].compact.detect { |obj|
|
|
643
|
+
obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
|
|
716
644
|
}
|
|
717
|
-
|
|
718
|
-
string
|
|
719
645
|
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def single_class_results
|
|
649
|
+
instances_from_class one_class, results[:matches]
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def class_from_crc(crc)
|
|
653
|
+
config.models_by_crc[crc].constantize
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def each_with_attribute(attribute, &block)
|
|
657
|
+
populate
|
|
658
|
+
results[:matches].each_with_index do |match, index|
|
|
659
|
+
yield self[index],
|
|
660
|
+
(match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def is_scope?(method)
|
|
665
|
+
one_class && one_class.sphinx_scopes.include?(method)
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def add_scope(method, *args, &block)
|
|
669
|
+
merge_search one_class.send(method, *args, &block)
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def merge_search(search)
|
|
673
|
+
search.args.each { |arg| args << arg }
|
|
720
674
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
675
|
+
search.options.keys.each do |key|
|
|
676
|
+
if HashOptions.include?(key)
|
|
677
|
+
options[key] ||= {}
|
|
678
|
+
options[key].merge! search.options[key]
|
|
679
|
+
elsif ArrayOptions.include?(key)
|
|
680
|
+
options[key] ||= []
|
|
681
|
+
options[key] += search.options[key]
|
|
682
|
+
options[key].uniq!
|
|
683
|
+
else
|
|
684
|
+
options[key] = search.options[key]
|
|
685
|
+
end
|
|
724
686
|
end
|
|
725
687
|
end
|
|
726
688
|
end
|