ultrasphinx 1.9 → 1.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. data.tar.gz.sig +0 -0
  2. data/CHANGELOG +6 -0
  3. data/DEPLOYMENT_NOTES +3 -1
  4. data/Manifest +10 -2
  5. data/RAKE_TASKS +2 -1
  6. data/README +6 -6
  7. data/Rakefile +27 -0
  8. data/examples/default.base +7 -2
  9. data/init.rb +1 -0
  10. data/lib/ultrasphinx/autoload.rb +0 -1
  11. data/lib/ultrasphinx/configure.rb +64 -36
  12. data/lib/ultrasphinx/core_extensions.rb +17 -2
  13. data/lib/ultrasphinx/fields.rb +33 -13
  14. data/lib/ultrasphinx/is_indexed.rb +32 -14
  15. data/lib/ultrasphinx/postgresql/concat_ws.sql +3 -3
  16. data/lib/ultrasphinx/postgresql/crc32.sql +1 -1
  17. data/lib/ultrasphinx/postgresql/group_concat.sql +1 -3
  18. data/lib/ultrasphinx/postgresql/hex_to_int.sql +1 -1
  19. data/lib/ultrasphinx/postgresql/unix_timestamp.sql +1 -1
  20. data/lib/ultrasphinx/search.rb +56 -6
  21. data/lib/ultrasphinx/search/internals.rb +88 -23
  22. data/lib/ultrasphinx/search/parser.rb +16 -2
  23. data/lib/ultrasphinx/spell.rb +2 -1
  24. data/lib/ultrasphinx/ultrasphinx.rb +43 -49
  25. data/tasks/ultrasphinx.rake +56 -16
  26. data/test/config/ultrasphinx/test.base +7 -2
  27. data/test/integration/app/app/controllers/addresses_controller.rb +9 -9
  28. data/test/integration/app/app/controllers/states_controller.rb +9 -9
  29. data/test/integration/app/app/models/category.rb +5 -0
  30. data/test/integration/app/app/models/geo/address.rb +1 -1
  31. data/test/integration/app/app/models/seller.rb +3 -3
  32. data/test/integration/app/app/views/addresses/edit.html.erb +2 -2
  33. data/test/integration/app/app/views/addresses/index.html.erb +2 -2
  34. data/test/integration/app/app/views/addresses/new.html.erb +1 -1
  35. data/test/integration/app/app/views/states/edit.html.erb +2 -2
  36. data/test/integration/app/app/views/states/index.html.erb +2 -2
  37. data/test/integration/app/app/views/states/new.html.erb +1 -1
  38. data/test/integration/app/app/views/users/index.html.erb +3 -3
  39. data/test/integration/app/config/environment.rb +1 -0
  40. data/test/integration/app/config/ultrasphinx/default.base +2 -2
  41. data/test/integration/app/config/ultrasphinx/development.conf.canonical +74 -50
  42. data/test/integration/app/db/migrate/007_add_lat_and_long_to_address.rb +3 -3
  43. data/test/integration/app/db/migrate/010_create_categories.rb +14 -0
  44. data/test/integration/app/db/migrate/011_categories_sellers.rb +15 -0
  45. data/test/integration/app/public/dispatch.cgi +0 -0
  46. data/test/integration/app/public/dispatch.fcgi +0 -0
  47. data/test/integration/app/public/dispatch.rb +0 -0
  48. data/test/integration/app/script/about +0 -0
  49. data/test/integration/app/script/breakpointer +0 -0
  50. data/test/integration/app/script/console +0 -0
  51. data/test/integration/app/script/destroy +0 -0
  52. data/test/integration/app/script/generate +0 -0
  53. data/test/integration/app/script/performance/benchmarker +0 -0
  54. data/test/integration/app/script/performance/profiler +0 -0
  55. data/test/integration/app/script/plugin +0 -0
  56. data/test/integration/app/script/process/inspector +0 -0
  57. data/test/integration/app/script/process/reaper +0 -0
  58. data/test/integration/app/script/process/spawner +0 -0
  59. data/test/integration/app/script/runner +0 -0
  60. data/test/integration/app/script/server +0 -0
  61. data/test/integration/app/test/fixtures/addresses.yml +77 -6
  62. data/test/integration/app/test/fixtures/categories.yml +101 -0
  63. data/test/integration/app/test/fixtures/categories_sellers.yml +29 -0
  64. data/test/integration/app/test/functional/addresses_controller_test.rb +9 -4
  65. data/test/integration/app/test/functional/sellers_controller_test.rb +9 -2
  66. data/test/integration/app/test/functional/states_controller_test.rb +10 -4
  67. data/test/integration/app/test/functional/users_controller_test.rb +7 -2
  68. data/test/integration/app/test/unit/address_test.rb +1 -1
  69. data/test/integration/app/test/unit/category_test.rb +8 -0
  70. data/test/integration/app/test/unit/country_test.rb +1 -1
  71. data/test/integration/app/test/unit/state_test.rb +1 -1
  72. data/test/integration/app/test/unit/user_test.rb +1 -1
  73. data/test/integration/delta_test.rb +13 -0
  74. data/test/integration/search_test.rb +80 -8
  75. data/test/profile/benchmark.rb +0 -0
  76. data/test/setup.rb +25 -7
  77. data/test/teardown.rb +13 -0
  78. data/test/test_helper.rb +3 -3
  79. data/ultrasphinx.gemspec +22 -43
  80. data/vendor/riddle/README +18 -4
  81. data/vendor/riddle/Rakefile +1 -0
  82. data/vendor/riddle/lib/riddle.rb +11 -5
  83. data/vendor/riddle/lib/riddle/client.rb +65 -20
  84. data/vendor/riddle/lib/riddle/client/response.rb +10 -0
  85. data/vendor/riddle/spec/fixtures/data/anchor.bin +0 -0
  86. data/vendor/riddle/spec/fixtures/data/any.bin +0 -0
  87. data/vendor/riddle/spec/fixtures/data/boolean.bin +0 -0
  88. data/vendor/riddle/spec/fixtures/data/comment.bin +0 -0
  89. data/vendor/riddle/spec/fixtures/data/distinct.bin +0 -0
  90. data/vendor/riddle/spec/fixtures/data/field_weights.bin +0 -0
  91. data/vendor/riddle/spec/fixtures/data/filter.bin +0 -0
  92. data/vendor/riddle/spec/fixtures/data/group.bin +0 -0
  93. data/vendor/riddle/spec/fixtures/data/index.bin +0 -0
  94. data/vendor/riddle/spec/fixtures/data/index_weights.bin +0 -0
  95. data/vendor/riddle/spec/fixtures/data/keywords_with_hits.bin +0 -0
  96. data/vendor/riddle/spec/fixtures/data/keywords_without_hits.bin +0 -0
  97. data/vendor/riddle/spec/fixtures/data/phrase.bin +0 -0
  98. data/vendor/riddle/spec/fixtures/data/rank_mode.bin +0 -0
  99. data/vendor/riddle/spec/fixtures/data/simple.bin +0 -0
  100. data/vendor/riddle/spec/fixtures/data/sort.bin +0 -0
  101. data/vendor/riddle/spec/fixtures/data/weights.bin +0 -0
  102. data/vendor/riddle/spec/fixtures/data_generator.php +15 -0
  103. data/vendor/riddle/spec/fixtures/sphinx/configuration.erb +4 -4
  104. data/vendor/riddle/spec/fixtures/sphinxapi.php +118 -7
  105. data/vendor/riddle/spec/functional/keywords_spec.rb +40 -0
  106. data/vendor/riddle/spec/spec_helper.rb +1 -0
  107. data/vendor/riddle/spec/unit/client_spec.rb +26 -0
  108. metadata +38 -11
  109. metadata.gz.sig +0 -0
  110. data/test/integration/app/config/ultrasphinx/development.conf +0 -319
  111. 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
- def _interpolate(value)
101
- self.gsub('?', value)
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
@@ -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
- def save_and_verify_type(field, new_type, string_sortable, klass)
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
- raise ConfigurationError, "Column type mismatch for #{field.inspect}; was already #{types[field].inspect}, but is now #{new_type.inspect}." unless types[field] == new_type
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, entry['sortable'], klass)
114
- install_facets!(entry, klass)
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, entry['sortable'], klass)
126
- install_facets!(entry, klass)
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', entry['sortable'], klass)
133
- install_facets!(entry, klass)
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
- def install_facets!(entry, klass)
150
+
151
+ def install_duplicate_fields!(entry, klass)
145
152
  if entry['facet']
146
- save_and_verify_type(entry['as'], 'text', nil, klass) # source must be a string
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 = HashWithIndifferentAccess.new(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
- opts['delta'].stringify_keys!
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'].each do |entry|
195
+ opts['fields'].map! do |entry|
186
196
  if entry.is_a? Hash
187
- entry.stringify_keys!
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.stringify_keys!
194
- entry.assert_valid_keys ['class_name', 'association_name', 'conditions', 'field', 'as', 'fields', 'association_sql', 'facet', 'function_sql', 'sortable']
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.stringify_keys!
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 OR REPLACE FUNCTION MAKE_CONCAT_WS() RETURNS text AS '
4
+ CREATE FUNCTION MAKE_CONCAT_WS() RETURNS text AS '
5
5
  declare
6
6
  v_args int := 32;
7
- v_first text := ''CREATE OR REPLACE 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 OR REPLACE FUNCTION CONCAT_WS(text,text'';
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;
@@ -1,7 +1,7 @@
1
1
 
2
2
  /* Fake CRC32 */
3
3
 
4
- CREATE OR REPLACE FUNCTION crc32(text)
4
+ CREATE FUNCTION crc32(text)
5
5
  RETURNS bigint AS $$
6
6
  DECLARE
7
7
  tmp bigint;
@@ -6,7 +6,7 @@
6
6
  Note: For DISTINCT and ORDER BY a subquery is required
7
7
  */
8
8
 
9
- CREATE OR REPLACE FUNCTION _group_concat(text, text)
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,
@@ -1,5 +1,5 @@
1
1
 
2
- CREATE OR REPLACE FUNCTION hex_to_int(varchar) RETURNS int4 AS '
2
+ CREATE FUNCTION hex_to_int(varchar) RETURNS int4 AS '
3
3
  DECLARE
4
4
  h alias for $1;
5
5
  exec varchar;
@@ -6,7 +6,7 @@
6
6
  UNIX_TIMESTAMP(date)
7
7
  */
8
8
 
9
- CREATE OR REPLACE FUNCTION unix_timestamp(timestamp without time zone)
9
+ CREATE FUNCTION unix_timestamp(timestamp without time zone)
10
10
  RETURNS bigint AS $$
11
11
  SELECT EXTRACT(EPOCH FROM $1)::bigint
12
12
  $$ VOLATILE LANGUAGE SQL;
@@ -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
- say "discarded invalid keys: #{extra_keys * ', '}" if extra_keys.any? and RAILS_ENV != "test"
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
- say "searching for #{@options.inspect}"
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
- say "search returned #{total_entries}/#{response[:total_found].to_i} in #{time.to_f} seconds."
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