ultrasphinx 1.9 → 1.11
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/CHANGELOG +6 -0
- data/DEPLOYMENT_NOTES +3 -1
- data/Manifest +10 -2
- data/RAKE_TASKS +2 -1
- data/README +6 -6
- data/Rakefile +27 -0
- data/examples/default.base +7 -2
- data/init.rb +1 -0
- data/lib/ultrasphinx/autoload.rb +0 -1
- data/lib/ultrasphinx/configure.rb +64 -36
- data/lib/ultrasphinx/core_extensions.rb +17 -2
- data/lib/ultrasphinx/fields.rb +33 -13
- data/lib/ultrasphinx/is_indexed.rb +32 -14
- data/lib/ultrasphinx/postgresql/concat_ws.sql +3 -3
- data/lib/ultrasphinx/postgresql/crc32.sql +1 -1
- data/lib/ultrasphinx/postgresql/group_concat.sql +1 -3
- data/lib/ultrasphinx/postgresql/hex_to_int.sql +1 -1
- data/lib/ultrasphinx/postgresql/unix_timestamp.sql +1 -1
- data/lib/ultrasphinx/search.rb +56 -6
- data/lib/ultrasphinx/search/internals.rb +88 -23
- data/lib/ultrasphinx/search/parser.rb +16 -2
- data/lib/ultrasphinx/spell.rb +2 -1
- data/lib/ultrasphinx/ultrasphinx.rb +43 -49
- data/tasks/ultrasphinx.rake +56 -16
- data/test/config/ultrasphinx/test.base +7 -2
- data/test/integration/app/app/controllers/addresses_controller.rb +9 -9
- data/test/integration/app/app/controllers/states_controller.rb +9 -9
- data/test/integration/app/app/models/category.rb +5 -0
- data/test/integration/app/app/models/geo/address.rb +1 -1
- data/test/integration/app/app/models/seller.rb +3 -3
- data/test/integration/app/app/views/addresses/edit.html.erb +2 -2
- data/test/integration/app/app/views/addresses/index.html.erb +2 -2
- data/test/integration/app/app/views/addresses/new.html.erb +1 -1
- data/test/integration/app/app/views/states/edit.html.erb +2 -2
- data/test/integration/app/app/views/states/index.html.erb +2 -2
- data/test/integration/app/app/views/states/new.html.erb +1 -1
- data/test/integration/app/app/views/users/index.html.erb +3 -3
- data/test/integration/app/config/environment.rb +1 -0
- data/test/integration/app/config/ultrasphinx/default.base +2 -2
- data/test/integration/app/config/ultrasphinx/development.conf.canonical +74 -50
- data/test/integration/app/db/migrate/007_add_lat_and_long_to_address.rb +3 -3
- data/test/integration/app/db/migrate/010_create_categories.rb +14 -0
- data/test/integration/app/db/migrate/011_categories_sellers.rb +15 -0
- data/test/integration/app/public/dispatch.cgi +0 -0
- data/test/integration/app/public/dispatch.fcgi +0 -0
- data/test/integration/app/public/dispatch.rb +0 -0
- data/test/integration/app/script/about +0 -0
- data/test/integration/app/script/breakpointer +0 -0
- data/test/integration/app/script/console +0 -0
- data/test/integration/app/script/destroy +0 -0
- data/test/integration/app/script/generate +0 -0
- data/test/integration/app/script/performance/benchmarker +0 -0
- data/test/integration/app/script/performance/profiler +0 -0
- data/test/integration/app/script/plugin +0 -0
- data/test/integration/app/script/process/inspector +0 -0
- data/test/integration/app/script/process/reaper +0 -0
- data/test/integration/app/script/process/spawner +0 -0
- data/test/integration/app/script/runner +0 -0
- data/test/integration/app/script/server +0 -0
- data/test/integration/app/test/fixtures/addresses.yml +77 -6
- data/test/integration/app/test/fixtures/categories.yml +101 -0
- data/test/integration/app/test/fixtures/categories_sellers.yml +29 -0
- data/test/integration/app/test/functional/addresses_controller_test.rb +9 -4
- data/test/integration/app/test/functional/sellers_controller_test.rb +9 -2
- data/test/integration/app/test/functional/states_controller_test.rb +10 -4
- data/test/integration/app/test/functional/users_controller_test.rb +7 -2
- data/test/integration/app/test/unit/address_test.rb +1 -1
- data/test/integration/app/test/unit/category_test.rb +8 -0
- data/test/integration/app/test/unit/country_test.rb +1 -1
- data/test/integration/app/test/unit/state_test.rb +1 -1
- data/test/integration/app/test/unit/user_test.rb +1 -1
- data/test/integration/delta_test.rb +13 -0
- data/test/integration/search_test.rb +80 -8
- data/test/profile/benchmark.rb +0 -0
- data/test/setup.rb +25 -7
- data/test/teardown.rb +13 -0
- data/test/test_helper.rb +3 -3
- data/ultrasphinx.gemspec +22 -43
- data/vendor/riddle/README +18 -4
- data/vendor/riddle/Rakefile +1 -0
- data/vendor/riddle/lib/riddle.rb +11 -5
- data/vendor/riddle/lib/riddle/client.rb +65 -20
- data/vendor/riddle/lib/riddle/client/response.rb +10 -0
- data/vendor/riddle/spec/fixtures/data/anchor.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/any.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/boolean.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/comment.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/distinct.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/field_weights.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/filter.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/group.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/index.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/index_weights.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/keywords_with_hits.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/keywords_without_hits.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/phrase.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/rank_mode.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/simple.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/sort.bin +0 -0
- data/vendor/riddle/spec/fixtures/data/weights.bin +0 -0
- data/vendor/riddle/spec/fixtures/data_generator.php +15 -0
- data/vendor/riddle/spec/fixtures/sphinx/configuration.erb +4 -4
- data/vendor/riddle/spec/fixtures/sphinxapi.php +118 -7
- data/vendor/riddle/spec/functional/keywords_spec.rb +40 -0
- data/vendor/riddle/spec/spec_helper.rb +1 -0
- data/vendor/riddle/spec/unit/client_spec.rb +26 -0
- metadata +38 -11
- metadata.gz.sig +0 -0
- data/test/integration/app/config/ultrasphinx/development.conf +0 -319
- data/test/integration/app/db/schema.rb +0 -56
@@ -51,6 +51,15 @@ class Hash
|
|
51
51
|
end._flatten_once]
|
52
52
|
end
|
53
53
|
|
54
|
+
def _stringify_all!(*skip)
|
55
|
+
# Stringifies all keys, and stringifies all values except those slotted for keys in 'skip'
|
56
|
+
stringify_keys!
|
57
|
+
self.except(*skip).each do |key, value|
|
58
|
+
self[key] = value.to_s
|
59
|
+
end
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
54
63
|
# Delete by multiple keys
|
55
64
|
def _delete(*args)
|
56
65
|
args.map do |key|
|
@@ -97,9 +106,15 @@ class String
|
|
97
106
|
end
|
98
107
|
end
|
99
108
|
|
100
|
-
|
101
|
-
|
109
|
+
# Interpolate SQL field names into functions
|
110
|
+
def _interpolate(*values)
|
111
|
+
if values.size == 1
|
112
|
+
self.gsub('?', values.first)
|
113
|
+
else
|
114
|
+
values.inject(self) { |result, value| result.sub('?', value.to_s) }
|
115
|
+
end
|
102
116
|
end
|
117
|
+
|
103
118
|
end
|
104
119
|
|
105
120
|
module Ultrasphinx::NumericSelf
|
data/lib/ultrasphinx/fields.rb
CHANGED
@@ -6,6 +6,8 @@ This is a special singleton configuration class that stores the index field conf
|
|
6
6
|
=end
|
7
7
|
|
8
8
|
class Fields
|
9
|
+
# XXX Class needs a big refactoring; one of the worst parts of Ultrasphinx
|
10
|
+
|
9
11
|
include Singleton
|
10
12
|
include Associations
|
11
13
|
|
@@ -17,6 +19,7 @@ This is a special singleton configuration class that stores the index field conf
|
|
17
19
|
'datetime' => 'date',
|
18
20
|
'timestamp' => 'date',
|
19
21
|
'float' => 'float',
|
22
|
+
'decimal' => 'integer', # this ain't right, but float doesn't work.
|
20
23
|
'boolean' => 'bool'
|
21
24
|
}
|
22
25
|
|
@@ -28,19 +31,22 @@ This is a special singleton configuration class that stores the index field conf
|
|
28
31
|
@groups = []
|
29
32
|
end
|
30
33
|
|
34
|
+
|
31
35
|
def groups
|
32
36
|
@groups.compact.sort_by do |string|
|
33
37
|
string[/= (.*)/, 1]
|
34
38
|
end
|
35
39
|
end
|
36
40
|
|
37
|
-
|
41
|
+
|
42
|
+
def save_and_verify_type(field, new_type, string_sortable, klass, msg = nil)
|
38
43
|
# Smoosh fields together based on their name in the Sphinx query schema
|
39
44
|
field, new_type = field.to_s, TYPE_MAP[new_type.to_s]
|
40
45
|
|
41
46
|
if types[field]
|
42
47
|
# Existing field name; verify its type
|
43
|
-
|
48
|
+
msg ||= "Column type mismatch for #{field.inspect}; was already #{types[field].inspect}, but is now #{new_type.inspect}."
|
49
|
+
raise ConfigurationError, msg unless types[field] == new_type
|
44
50
|
classes[field] = (classes[field] + [klass]).uniq
|
45
51
|
|
46
52
|
else
|
@@ -63,6 +69,7 @@ This is a special singleton configuration class that stores the index field conf
|
|
63
69
|
end
|
64
70
|
end
|
65
71
|
|
72
|
+
|
66
73
|
def cast(source_string, field)
|
67
74
|
if types[field] == "date"
|
68
75
|
"UNIX_TIMESTAMP(#{source_string})"
|
@@ -73,6 +80,7 @@ This is a special singleton configuration class that stores the index field conf
|
|
73
80
|
end + " AS #{field}"
|
74
81
|
end
|
75
82
|
|
83
|
+
|
76
84
|
def null(field)
|
77
85
|
case types[field]
|
78
86
|
when 'text'
|
@@ -88,6 +96,7 @@ This is a special singleton configuration class that stores the index field conf
|
|
88
96
|
end + " AS #{field}"
|
89
97
|
end
|
90
98
|
|
99
|
+
|
91
100
|
def configure(configuration)
|
92
101
|
|
93
102
|
configuration.each do |model, options|
|
@@ -99,10 +108,7 @@ This is a special singleton configuration class that stores the index field conf
|
|
99
108
|
begin
|
100
109
|
|
101
110
|
# Fields are from the model
|
102
|
-
# We destructively canonicize them back onto the configuration hash
|
103
111
|
options['fields'] = options['fields'].to_a.map do |entry|
|
104
|
-
entry = {'field' => entry} unless entry.is_a? Hash
|
105
|
-
|
106
112
|
extract_table_alias!(entry, klass)
|
107
113
|
extract_field_alias!(entry, klass)
|
108
114
|
|
@@ -110,8 +116,8 @@ This is a special singleton configuration class that stores the index field conf
|
|
110
116
|
# XXX I think this is here for migrations
|
111
117
|
Ultrasphinx.say "warning: field #{entry['field']} is not present in #{model}"
|
112
118
|
else
|
113
|
-
save_and_verify_type(entry['as'], klass.columns_hash[entry['field']].type,
|
114
|
-
|
119
|
+
save_and_verify_type(entry['as'], klass.columns_hash[entry['field']].type, nil, klass)
|
120
|
+
install_duplicate_fields!(entry, klass)
|
115
121
|
end
|
116
122
|
end
|
117
123
|
|
@@ -122,15 +128,15 @@ This is a special singleton configuration class that stores the index field conf
|
|
122
128
|
|
123
129
|
association_model = get_association_model(klass, entry)
|
124
130
|
|
125
|
-
save_and_verify_type(entry['as'] || entry['field'], association_model.columns_hash[entry['field']].type,
|
126
|
-
|
131
|
+
save_and_verify_type(entry['as'] || entry['field'], association_model.columns_hash[entry['field']].type, nil, klass)
|
132
|
+
install_duplicate_fields!(entry, klass)
|
127
133
|
end
|
128
134
|
|
129
135
|
# Regular concats are CHAR, group_concats are BLOB and need to be cast to CHAR
|
130
136
|
options['concatenate'].to_a.each do |entry|
|
131
137
|
extract_table_alias!(entry, klass)
|
132
|
-
save_and_verify_type(entry['as'], 'text',
|
133
|
-
|
138
|
+
save_and_verify_type(entry['as'], 'text', nil, klass)
|
139
|
+
install_duplicate_fields!(entry, klass)
|
134
140
|
end
|
135
141
|
|
136
142
|
rescue ActiveRecord::StatementInvalid
|
@@ -141,20 +147,34 @@ This is a special singleton configuration class that stores the index field conf
|
|
141
147
|
self
|
142
148
|
end
|
143
149
|
|
144
|
-
|
150
|
+
|
151
|
+
def install_duplicate_fields!(entry, klass)
|
145
152
|
if entry['facet']
|
146
|
-
|
153
|
+
# Source must be a string
|
154
|
+
save_and_verify_type(entry['as'], 'text', nil, klass,
|
155
|
+
"#{klass}##{entry['as']}: 'facet' option is only valid for text fields; numeric fields are enabled by default")
|
156
|
+
# Install facet column
|
147
157
|
save_and_verify_type("#{entry['as']}_facet", 'integer', nil, klass)
|
148
158
|
end
|
159
|
+
|
160
|
+
if entry['sortable']
|
161
|
+
# Source must be a string
|
162
|
+
save_and_verify_type(entry['as'], 'text', nil, klass,
|
163
|
+
"#{klass}##{entry['as']}: 'sortable' option is only valid for text columns; numeric fields are enabled by default")
|
164
|
+
# Install sortable column
|
165
|
+
save_and_verify_type("#{entry['as']}_sortable", 'text', true, klass)
|
166
|
+
end
|
149
167
|
entry
|
150
168
|
end
|
151
169
|
|
170
|
+
|
152
171
|
def extract_field_alias!(entry, klass)
|
153
172
|
unless entry['as']
|
154
173
|
entry['as'] = entry['field']
|
155
174
|
end
|
156
175
|
end
|
157
176
|
|
177
|
+
|
158
178
|
def extract_table_alias!(entry, klass)
|
159
179
|
unless entry['table_alias']
|
160
180
|
entry['table_alias'] = if entry['field'] and entry['field'].include? "." and entry['association_sql']
|
@@ -42,6 +42,16 @@ SQL conditions, to scope which records are selected for indexing. Accepts a stri
|
|
42
42
|
|
43
43
|
The <tt>:conditions</tt> key is especially useful if you delete records by marking them deleted rather than removing them from the database.
|
44
44
|
|
45
|
+
== Ordering subgroups
|
46
|
+
|
47
|
+
Use the <tt>:order</tt> key.
|
48
|
+
|
49
|
+
An SQL order string.
|
50
|
+
|
51
|
+
:order => 'posts.id ASC'
|
52
|
+
|
53
|
+
|
54
|
+
|
45
55
|
== Including a field from an association
|
46
56
|
|
47
57
|
Use the <tt>:include</tt> key.
|
@@ -76,9 +86,9 @@ Also use the <tt>:concatenate</tt> key.
|
|
76
86
|
To concatenate one field from a set of associated records as a combined field in the parent record, use a group (or vertical) concatenation. A group concatenation should contain an <tt>:association_name</tt> key (the association name for the included model), a <tt>:field</tt> key (the field on the included model to concatenate), and an optional <tt>:as</tt> key (also the name of the result of the concatenation). For example, to concatenate all <tt>Post#body</tt> contents into the parent's <tt>responses</tt> field:
|
77
87
|
:concatenate => [{:association_name => 'posts', :field => 'body', :as => 'responses'}]
|
78
88
|
|
79
|
-
The keys <tt>:facet</tt>, <tt>:sortable</tt>, <tt>:conditions</tt>, <tt>:function_sql</tt>, <tt>:class_name</tt>, and <tt>:association_sql</tt>, are also recognized.
|
89
|
+
The keys <tt>:facet</tt>, <tt>:sortable</tt>, <tt>:order</tt>, <tt>:conditions</tt>, <tt>:function_sql</tt>, <tt>:class_name</tt>, and <tt>:association_sql</tt>, are also recognized.
|
80
90
|
|
81
|
-
Vertical concatenations are implemented with GROUP_CONCAT on MySQL and with an aggregate and a stored procedure on PostgreSQL.
|
91
|
+
Vertical concatenations are implemented with GROUP_CONCAT on MySQL and with an aggregate and a stored procedure on PostgreSQL. Note that <tt>:order</tt> is useful if you need to order the grouping so that proximity search works correctly, and <tt>:conditions</tt> are currently ignored if you have <tt>:association_sql</tt> defined.
|
82
92
|
|
83
93
|
== Custom joins
|
84
94
|
|
@@ -156,9 +166,8 @@ If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you
|
|
156
166
|
=end
|
157
167
|
|
158
168
|
def self.is_indexed opts = {}
|
159
|
-
opts
|
160
|
-
|
161
|
-
opts.assert_valid_keys ['fields', 'concatenate', 'conditions', 'include', 'delta']
|
169
|
+
opts.stringify_keys!
|
170
|
+
opts.assert_valid_keys ['fields', 'concatenate', 'conditions', 'include', 'delta', 'order']
|
162
171
|
|
163
172
|
# Single options
|
164
173
|
|
@@ -172,7 +181,8 @@ If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you
|
|
172
181
|
elsif opts['delta'].is_a? String
|
173
182
|
opts['delta'] = {'field' => opts['delta']}
|
174
183
|
end
|
175
|
-
|
184
|
+
|
185
|
+
opts['delta']._stringify_all!
|
176
186
|
opts['delta'].assert_valid_keys ['field']
|
177
187
|
end
|
178
188
|
|
@@ -182,27 +192,35 @@ If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you
|
|
182
192
|
opts['concatenate'] = Array(opts['concatenate'])
|
183
193
|
opts['include'] = Array(opts['include'])
|
184
194
|
|
185
|
-
opts['fields'].
|
195
|
+
opts['fields'].map! do |entry|
|
186
196
|
if entry.is_a? Hash
|
187
|
-
entry.
|
188
|
-
entry.assert_valid_keys ['field', 'as', 'facet', 'function_sql', 'sortable']
|
197
|
+
entry._stringify_all!('sortable', 'facet')
|
198
|
+
entry.assert_valid_keys ['field', 'as', 'facet', 'function_sql', 'sortable', 'table_alias']
|
199
|
+
entry
|
200
|
+
else
|
201
|
+
# Single strings
|
202
|
+
{'field' => entry.to_s}
|
189
203
|
end
|
190
204
|
end
|
191
205
|
|
192
206
|
opts['concatenate'].each do |entry|
|
193
|
-
entry.
|
194
|
-
|
207
|
+
entry._stringify_all!('fields', 'sortable', 'facet')
|
208
|
+
|
209
|
+
entry.assert_valid_keys ['class_name', 'association_name', 'conditions', 'field', 'as', 'fields', 'association_sql', 'facet', 'function_sql', 'sortable', 'order', 'table_alias']
|
195
210
|
raise Ultrasphinx::ConfigurationError, "You can't mix regular concat and group concats" if entry['fields'] and (entry['field'] or entry['class_name'] or entry['association_name'])
|
196
211
|
raise Ultrasphinx::ConfigurationError, "Concatenations must specify an :as key" unless entry['as']
|
197
212
|
raise Ultrasphinx::ConfigurationError, "Group concatenations must not have multiple fields" if entry['field'].is_a? Array
|
198
213
|
raise Ultrasphinx::ConfigurationError, "Regular concatenations should have multiple fields" if entry['fields'] and !entry['fields'].is_a?(Array)
|
214
|
+
raise Ultrasphinx::ConfigurationError, "Regular concatenations can't specify an order" if entry['fields'] and entry['order']
|
215
|
+
|
216
|
+
entry['fields'].map!(&:to_s) if entry['fields'] # Stringify fields array
|
199
217
|
end
|
200
218
|
|
201
219
|
opts['include'].each do |entry|
|
202
|
-
entry.
|
203
|
-
entry.assert_valid_keys ['class_name', 'association_name', 'field', 'as', 'association_sql', 'facet', 'function_sql', 'sortable']
|
220
|
+
entry._stringify_all!('sortable', 'facet')
|
221
|
+
entry.assert_valid_keys ['class_name', 'association_name', 'field', 'as', 'association_sql', 'facet', 'function_sql', 'sortable', 'table_alias', 'dont_group_by']
|
204
222
|
end
|
205
|
-
|
223
|
+
|
206
224
|
Ultrasphinx::MODEL_CONFIGURATION[self.name] = opts
|
207
225
|
end
|
208
226
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
|
2
2
|
/* http://osdir.com/ml/db.postgresql.admIN/2003-08/msg00057.html */
|
3
3
|
|
4
|
-
CREATE
|
4
|
+
CREATE FUNCTION MAKE_CONCAT_WS() RETURNS text AS '
|
5
5
|
declare
|
6
6
|
v_args int := 32;
|
7
|
-
v_first text := ''CREATE
|
8
|
-
v_part1 text := ''CREATE
|
7
|
+
v_first text := ''CREATE FUNCTION CONCAT_WS(text,text,text) RETURNS text AS ''''SELECT CASE WHEN $1 IS NULL THEN NULL WHEN $3 IS NULL THEN $2 ELSE $2 || $1 || $3 END'''' LANGUAGE sql IMMUTABLE'';
|
8
|
+
v_part1 text := ''CREATE FUNCTION CONCAT_WS(text,text'';
|
9
9
|
v_part2 text := '') RETURNS text AS ''''SELECT CONCAT_WS($1,CONCAT_WS($1,$2'';
|
10
10
|
v_part3 text := '')'''' LANGUAGE sql IMMUTABLE'';
|
11
11
|
v_sql text;
|
@@ -6,7 +6,7 @@
|
|
6
6
|
Note: For DISTINCT and ORDER BY a subquery is required
|
7
7
|
*/
|
8
8
|
|
9
|
-
CREATE
|
9
|
+
CREATE FUNCTION _group_concat(text, text)
|
10
10
|
RETURNS text AS $$
|
11
11
|
SELECT CASE
|
12
12
|
WHEN $2 IS NULL THEN $1
|
@@ -15,8 +15,6 @@ RETURNS text AS $$
|
|
15
15
|
END
|
16
16
|
$$ IMMUTABLE LANGUAGE SQL;
|
17
17
|
|
18
|
-
DROP AGGREGATE IF EXISTS group_concat(text);
|
19
|
-
|
20
18
|
CREATE AGGREGATE group_concat (
|
21
19
|
BASETYPE = text,
|
22
20
|
SFUNC = _group_concat,
|
data/lib/ultrasphinx/search.rb
CHANGED
@@ -42,8 +42,11 @@ The hash lets you customize internal aspects of the search.
|
|
42
42
|
<tt>:weights</tt>:: A hash. Text-field names and associated query weighting. The default weight for every field is 1.0. Example: <tt>:weights => {'title' => 2.0}</tt>
|
43
43
|
<tt>:filters</tt>:: A hash. Names of numeric or date fields and associated values. You can use a single value, an array of values, or a range. (See the bottom of the ActiveRecord::Base page for an example.)
|
44
44
|
<tt>:facets</tt>:: An array of fields for grouping/faceting. You can access the returned facet values and their result counts with the <tt>facets</tt> method.
|
45
|
+
<tt>:location</tt>:: A hash. Specify the names of your latititude and longitude attributes as declared in your is_indexed calls. To sort the results by distance, set <tt>:sort_mode => 'extended'</tt> and <tt>:sort_by => 'distance asc'.</tt>
|
45
46
|
<tt>:indexes</tt>:: An array of indexes to search. Currently only <tt>Ultrasphinx::MAIN_INDEX</tt> and <tt>Ultrasphinx::DELTA_INDEX</tt> are available. Defaults to both; changing this is rarely needed.
|
46
47
|
|
48
|
+
== Query Defaults
|
49
|
+
|
47
50
|
Note that you can set up your own query defaults in <tt>environment.rb</tt>:
|
48
51
|
|
49
52
|
self.class.query_defaults = HashWithIndifferentAccess.new({
|
@@ -54,6 +57,34 @@ Note that you can set up your own query defaults in <tt>environment.rb</tt>:
|
|
54
57
|
|
55
58
|
= Advanced features
|
56
59
|
|
60
|
+
== Geographic distance
|
61
|
+
|
62
|
+
If you pass a <tt>:location</tt> Hash, distance from the location in meters will be available in your result records via the <tt>distance</tt> accessor:
|
63
|
+
|
64
|
+
@search = Ultrasphinx::Search.new(:class_names => 'Point',
|
65
|
+
:query => 'pizza',
|
66
|
+
:sort_mode => 'extended',
|
67
|
+
:sort_by => 'distance',
|
68
|
+
:location => {
|
69
|
+
:lat => 40.3,
|
70
|
+
:long => -73.6
|
71
|
+
})
|
72
|
+
|
73
|
+
@search.run.first.distance #=> 1402.4
|
74
|
+
|
75
|
+
Note that Sphinx expects lat/long to be indexed as radians. If you have degrees in your database, do the conversion in the <tt>is_indexed</tt> as so:
|
76
|
+
|
77
|
+
is_indexed 'fields' => [
|
78
|
+
'name',
|
79
|
+
'description',
|
80
|
+
{:field => 'lat', :function_sql => "RADIANS(?)"},
|
81
|
+
{:field => 'lng', :function_sql => "RADIANS(?)"}
|
82
|
+
]
|
83
|
+
|
84
|
+
Then, set <tt>Ultrasphinx::Search.client_options[:location][:units] = 'degrees'</tt>.
|
85
|
+
|
86
|
+
The MySQL <tt>:double</tt> column type is recommended for storing location data. For Postgres, use <tt>:float</tt.
|
87
|
+
|
57
88
|
== Interlock integration
|
58
89
|
|
59
90
|
Ultrasphinx uses the <tt>find_all_by_id</tt> method to instantiate records. If you set <tt>with_finders: true</tt> in {Interlock's}[http://blog.evanweaver.com/files/doc/fauna/interlock] <tt>config/memcached.yml</tt>, Interlock overrides <tt>find_all_by_id</tt> with a caching version.
|
@@ -108,7 +139,12 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
108
139
|
:weights => {},
|
109
140
|
:class_names => [],
|
110
141
|
:filters => {},
|
111
|
-
:facets => []
|
142
|
+
:facets => [],
|
143
|
+
:location => HashWithIndifferentAccess.new({
|
144
|
+
:lat_attribute_name => 'lat',
|
145
|
+
:long_attribute_name => 'lng',
|
146
|
+
:units => 'radians'
|
147
|
+
})
|
112
148
|
})
|
113
149
|
|
114
150
|
cattr_accessor :excerpting_options
|
@@ -239,6 +275,9 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
239
275
|
require_run
|
240
276
|
(total_entries / per_page.to_f).ceil
|
241
277
|
end
|
278
|
+
|
279
|
+
# add alias for total_pages as expected by latest will_paginate
|
280
|
+
alias total_pages page_count
|
242
281
|
|
243
282
|
# Returns the previous page number.
|
244
283
|
def previous_page
|
@@ -262,12 +301,19 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
262
301
|
opts = Hash[HashWithIndifferentAccess.new(opts._deep_dup._coerce_basic_types)]
|
263
302
|
unless self.class.query_defaults.instance_of? Hash
|
264
303
|
self.class.query_defaults = Hash[self.class.query_defaults]
|
304
|
+
self.class.query_defaults['location'] = Hash[self.class.query_defaults['location']]
|
305
|
+
|
265
306
|
self.class.client_options = Hash[self.class.client_options]
|
266
307
|
self.class.excerpting_options = Hash[self.class.excerpting_options]
|
267
308
|
self.class.excerpting_options['content_methods'].map! {|ary| ary.map {|m| m.to_s}}
|
268
309
|
end
|
310
|
+
|
311
|
+
# We need an annoying deep merge on the :location parameter
|
312
|
+
opts['location'].reverse_merge!(self.class.query_defaults['location']) if opts['location']
|
313
|
+
|
314
|
+
# Merge the rest of the defaults
|
315
|
+
@options = self.class.query_defaults.merge(opts)
|
269
316
|
|
270
|
-
@options = self.class.query_defaults.merge(opts)
|
271
317
|
@options['query'] = @options['query'].to_s
|
272
318
|
@options['class_names'] = Array(@options['class_names'])
|
273
319
|
@options['facets'] = Array(@options['facets'])
|
@@ -281,7 +327,7 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
281
327
|
@results, @subtotals, @facets, @response = [], {}, {}, {}
|
282
328
|
|
283
329
|
extra_keys = @options.keys - (self.class.query_defaults.keys + INTERNAL_KEYS)
|
284
|
-
|
330
|
+
log "discarded invalid keys: #{extra_keys * ', '}" if extra_keys.any? and RAILS_ENV != "test"
|
285
331
|
end
|
286
332
|
|
287
333
|
# Run the search, filling results with an array of ActiveRecord objects. Set the parameter to false
|
@@ -289,11 +335,11 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
289
335
|
def run(reify = true)
|
290
336
|
@request = build_request_with_options(@options)
|
291
337
|
|
292
|
-
|
338
|
+
log "searching for #{@options.inspect}"
|
293
339
|
|
294
340
|
perform_action_with_retries do
|
295
341
|
@response = @request.query(parsed_query, @options['indexes'])
|
296
|
-
|
342
|
+
log "search returned #{total_entries}/#{response[:total_found].to_i} in #{time.to_f} seconds."
|
297
343
|
|
298
344
|
if self.class.client_options['with_subtotals']
|
299
345
|
@subtotals = get_subtotals(@request, parsed_query)
|
@@ -342,7 +388,7 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
342
388
|
# Fetch the actual field contents
|
343
389
|
docs = results_with_content_methods.map do |result, methods|
|
344
390
|
methods.map do |method|
|
345
|
-
method and strip_bogus_characters(result.send(method)) or ""
|
391
|
+
method and strip_bogus_characters(result.send(method).to_s) or ""
|
346
392
|
end
|
347
393
|
end.flatten
|
348
394
|
|
@@ -395,6 +441,10 @@ Note that your database is never changed by anything Ultrasphinx does.
|
|
395
441
|
end
|
396
442
|
end
|
397
443
|
|
444
|
+
def log msg #:nodoc:
|
445
|
+
Ultrasphinx.log msg
|
446
|
+
end
|
447
|
+
|
398
448
|
def say msg #:nodoc:
|
399
449
|
Ultrasphinx.say msg
|
400
450
|
end
|