ultrasphinx 1.5 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ v1.5.1. Various bugfixes.
3
+
2
4
  v1.5. API change. Change layout of base files to allow overriding of more options, see examples/default.base. Allow sorting on text fields (use the 'sortable' key).
3
5
 
4
6
  v1.4. New is_indexed 'fields' => {'function_sql'} key for custom field mangling; support setting textual keys in the 'filters'.
data/README CHANGED
@@ -7,6 +7,8 @@ Ruby on Rails configurator and client to the Sphinx full text search engine.
7
7
 
8
8
  Copyright 2007 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file. Some portions copyright Dmytro Shteflyuk and Alexey Kovyrin, distributed under the Ruby License, and used with permission. Some portions copyright PJ Hyett and Mislav Marohnić, distributed under the MIT license, and used with permission.
9
9
 
10
+ The public certificate for the gem is at http://rubyforge.org/frs/download.php/25331/evan_weaver-original-public_cert.pem.
11
+
10
12
  == Requirements
11
13
 
12
14
  * MySQL (or Postgres, experimental)
@@ -96,7 +98,7 @@ See Ultrasphinx::Spell.
96
98
 
97
99
  == Available Rake tasks
98
100
 
99
- These rake tasks are made available to your Rails app:
101
+ These Rake tasks are made available to your Rails app:
100
102
 
101
103
  <tt>ultrasphinx:configure</tt>:: Rebuild the configuration file for this particular environment.
102
104
  <tt>ultrasphinx:index</tt>:: Reindex the database and send an update signal to the search daemon.
@@ -8,13 +8,15 @@ module Ultrasphinx
8
8
 
9
9
  Dir["#{RAILS_ROOT}/app/models/**/*.rb"].each do |filename|
10
10
  next if filename =~ /\/(\.svn|CVS|\.bzr)\//
11
- begin
12
- open(filename) {|file| load filename if file.grep(/is_indexed/).any?}
13
- rescue Object => e
14
- say "warning; possibly critical autoload error on #{filename}"
15
- say e.inspect
16
- end
17
- end
11
+ open(filename) do |file|
12
+ begin
13
+ load filename if file.grep(/is_indexed/).any?
14
+ rescue Object => e
15
+ say "warning; possibly critical autoload error on #{filename}"
16
+ say e.inspect
17
+ end
18
+ end
19
+ end
18
20
 
19
21
  # Build the field-to-type mappings.
20
22
  Fields.instance.configure(MODEL_CONFIGURATION)
@@ -26,7 +28,7 @@ module Ultrasphinx
26
28
 
27
29
  load_constants
28
30
 
29
- puts "Rebuilding Ultrasphinx configurations for #{ENV['RAILS_ENV']} environment"
31
+ puts "Rebuilding Ultrasphinx configurations for #{RAILS_ENV} environment"
30
32
  puts "Available models are #{MODEL_CONFIGURATION.keys.to_sentence}"
31
33
  File.open(CONF_PATH, "w") do |conf|
32
34
 
@@ -64,7 +66,7 @@ module Ultrasphinx
64
66
  # Tentatively supporting Postgres now
65
67
  connection_settings = klass.connection.instance_variable_get("@config")
66
68
 
67
- adapter_defaults = ADAPTER_DEFAULTS[connection_settings[:adapter]]
69
+ adapter_defaults = ADAPTER_DEFAULTS[ADAPTER]
68
70
  raise ConfigurationError, "Unsupported database adapter" unless adapter_defaults
69
71
 
70
72
  conf = [adapter_defaults]
@@ -127,8 +129,6 @@ module Ultrasphinx
127
129
 
128
130
  def build_query(klass, column_strings, join_strings, condition_strings)
129
131
 
130
- connection_settings = klass.connection.instance_variable_get("@config")
131
-
132
132
  ["sql_query =",
133
133
  "SELECT",
134
134
  column_strings.sort_by do |string|
@@ -141,7 +141,7 @@ module Ultrasphinx
141
141
  condition_strings.uniq.map do |condition|
142
142
  "AND #{condition}"
143
143
  end,
144
- ("GROUP BY id" if connection_settings[:adapter] == 'mysql') # XXX should be somewhere more obvious
144
+ ADAPTER_SQL_FUNCTIONS[ADAPTER]['group_by']
145
145
  ].flatten.join(" ")
146
146
  end
147
147
 
@@ -50,34 +50,17 @@ class Hash
50
50
  end._flatten_once]
51
51
  end
52
52
 
53
+ def _delete(*args)
54
+ args.map do |key|
55
+ self.delete key
56
+ end
57
+ end
58
+
53
59
  def _to_conf_string(section = nil)
54
60
  inner = self.map do |key, value|
55
61
  " #{key} = #{value}"
56
62
  end.join("\n")
57
63
  section ? "#{section} {\n#{inner}\n}\n" : inner
58
64
  end
59
-
60
- def _deep_stringify_keys
61
- Hash[*(self.map do |key, value|
62
- # puts "#{key.inspect}, #{value.inspect}"
63
- z = [key.to_s,
64
- case value
65
- when Hash
66
- value._deep_stringify_keys
67
- when Array
68
- value.map do |subvalue|
69
- if subvalue.is_a? Hash or subvalue.is_a? Array
70
- subvalue._deep_stringify_keys
71
- else
72
- subvalue
73
- end
74
- end
75
- else
76
- value
77
- end
78
- ]
79
- # p z
80
- # z
81
- end._flatten_once)]
82
- end
65
+
83
66
  end
@@ -55,7 +55,7 @@ module Ultrasphinx
55
55
 
56
56
  def cast(source_string, field)
57
57
  if types[field] == "date"
58
- "UNIX_TIMESTAMP(#{source_string})"
58
+ "#{ADAPTER_SQL_FUNCTIONS[ADAPTER]['timestamp']}#{source_string})"
59
59
  elsif source_string =~ /GROUP_CONCAT/
60
60
  "CAST(#{source_string} AS CHAR)"
61
61
  else
@@ -93,7 +93,7 @@ module Ultrasphinx
93
93
  entry['as'] = entry['field'] unless entry['as']
94
94
 
95
95
  unless klass.columns_hash[entry['field']]
96
- ActiveRecord::Base.logger.warn "ultrasphinx: WARNING: field #{entry['field']} is not present in #{model}"
96
+ Ultrasphinx.say "WARNING: field #{entry['field']} is not present in #{model}"
97
97
  else
98
98
  save_and_verify_type(entry['as'], klass.columns_hash[entry['field']].type, entry['sortable'], klass)
99
99
  end
@@ -116,7 +116,7 @@ module Ultrasphinx
116
116
  save_and_verify_type(entry['as'], 'text', entry['sortable'], klass)
117
117
  end
118
118
  rescue ActiveRecord::StatementInvalid
119
- ActiveRecord::Base.logger.warn "ultrasphinx: WARNING: model #{model} does not exist in the database yet"
119
+ Ultrasphinx.say "WARNING: model #{model} does not exist in the database yet"
120
120
  end
121
121
  end
122
122
 
@@ -120,23 +120,28 @@ If the associations weren't just <tt>has_many</tt> and <tt>belongs_to</tt>, you
120
120
  =end
121
121
 
122
122
  def self.is_indexed opts = {}
123
- opts = opts._deep_stringify_keys
123
+ opts = HashWithIndifferentAccess.new(opts)
124
124
 
125
125
  opts.assert_valid_keys ['fields', 'concatenate', 'conditions', 'include']
126
126
 
127
- Array(opts[:fields]).each do |field|
128
- field.assert_valid_keys ['field', 'as', 'facet', 'function_sql', 'sortable'] if field.is_a? Hash
127
+ Array(opts['fields']).each do |entry|
128
+ if entry.is_a? Hash
129
+ entry.stringify_keys!
130
+ entry.assert_valid_keys ['field', 'as', 'facet', 'function_sql', 'sortable']
131
+ end
129
132
  end
130
133
 
131
- Array(opts[:concatenate]).each do |concat|
132
- concat.assert_valid_keys ['class_name', 'conditions', 'field', 'as', 'fields', 'association_name', 'association_sql', 'facet', 'function_sql', 'sortable']
133
- raise Ultrasphinx::ConfigurationError, "You can't mix regular concat and group concats" if concat['fields'] and (concat['field'] or concat['class_name'] or concat['association_name'])
134
- raise Ultrasphinx::ConfigurationError, "Group concats must not have multiple fields" if concat['field'].is_a? Array
135
- raise Ultrasphinx::ConfigurationError, "Regular concats should have multiple fields" if concat['fields'] and !concat['fields'].is_a?(Array)
134
+ Array(opts['concatenate']).each do |entry|
135
+ entry.stringify_keys!
136
+ entry.assert_valid_keys ['class_name', 'conditions', 'field', 'as', 'fields', 'association_name', 'association_sql', 'facet', 'function_sql', 'sortable']
137
+ 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'])
138
+ raise Ultrasphinx::ConfigurationError, "Group concats must not have multiple fields" if entry['field'].is_a? Array
139
+ raise Ultrasphinx::ConfigurationError, "Regular concats should have multiple fields" if entry['fields'] and !entry['fields'].is_a?(Array)
136
140
  end
137
141
 
138
- Array(opts[:include]).each do |inc|
139
- inc.assert_valid_keys ['class_name', 'field', 'as', 'association_sql', 'facet', 'function_sql', 'sortable']
142
+ Array(opts['include']).each do |entry|
143
+ entry.stringify_keys!
144
+ entry.assert_valid_keys ['class_name', 'field', 'as', 'association_sql', 'facet', 'function_sql', 'sortable']
140
145
  end
141
146
 
142
147
  Ultrasphinx::MODEL_CONFIGURATION[self.name] = opts
@@ -6,11 +6,11 @@ Command-interface Search object.
6
6
 
7
7
  == Basic usage
8
8
 
9
- To set up a search, instantiate an Ultrasphinx::Search object with a hash of parameters. Only the <tt>'query'</tt> key is mandatory.
9
+ To set up a search, instantiate an Ultrasphinx::Search object with a hash of parameters. Only the <tt>:query</tt> key is mandatory.
10
10
  @search = Ultrasphinx::Search.new(
11
- 'query' => @query,
12
- 'sort_mode' => 'descending',
13
- 'sort_by' => 'created_at'
11
+ :query => @query,
12
+ :sort_mode => 'descending',
13
+ :sort_by => 'created_at'
14
14
  )
15
15
 
16
16
  Now, to run the query, call its <tt>run</tt> method. Your results will be available as ActiveRecord instances via the <tt>results</tt> method. Example:
@@ -34,21 +34,21 @@ A Sphinx::SphinxInternalError will be raised on invalid queries. In general, que
34
34
 
35
35
  The hash lets you customize internal aspects of the search.
36
36
 
37
- <tt>'per_page'</tt>:: An integer. How many results per page.
38
- <tt>'page'</tt>:: An integer. Which page of the results to return.
39
- <tt>'class_name'</tt>:: An array or string. The class name of the model you want to search, an array of model names to search, or <tt>nil</tt> for all available models.
40
- <tt>'sort_mode'</tt>:: 'relevance' or 'ascending' or 'descending'. How to order the result set. Note that 'time' and 'extended' modes are available, but not tested.
41
- <tt>'sort_by'</tt>:: A field name. What field to order by for 'ascending' or 'descending' mode. Has no effect for 'relevance'.
42
- <tt>'weight'</tt>:: A hash. Text-field names and associated query weighting. The default weight for every field is 1.0. Example: <tt>'weight' => {'title' => 2.0}</tt>
43
- <tt>'filter'</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
- <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.
37
+ <tt>:per_page</tt>:: An integer. How many results per page.
38
+ <tt>:page</tt>:: An integer. Which page of the results to return.
39
+ <tt>:class_names</tt>:: An array or string. The class name of the model you want to search, an array of model names to search, or <tt>nil</tt> for all available models.
40
+ <tt>:sort_mode</tt>:: <tt>'relevance'</tt> or <tt>'ascending'</tt> or <tt>'descending'</tt>. How to order the result set. Note that <tt>'time'</tt> and <tt>'extended'</tt> modes are available, but not tested.
41
+ <tt>:sort_by</tt>:: A field name. What field to order by for <tt>'ascending'</tt> or <tt>'descending'</tt> mode. Has no effect for <tt>'relevance'</tt>.
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
+ <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
+ <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
45
 
46
46
  Note that you can set up your own query defaults in <tt>environment.rb</tt>:
47
47
 
48
48
  Ultrasphinx::Search.query_defaults = {
49
- 'per_page' => 10,
50
- 'sort_mode' => 'relevance',
51
- 'weight' => {'title' => 2.0}
49
+ :per_page => 10,
50
+ :sort_mode => 'relevance',
51
+ :weights => {'title' => 2.0}
52
52
  }
53
53
 
54
54
  = Advanced features
@@ -76,12 +76,12 @@ You need to set the <tt>content_methods</tt> key on Ultrasphinx::Search.excerpti
76
76
  There are some other keys you can set, such as excerpt size, HTML tags to highlight with, and number of words on either side of each excerpt chunk. Example (in <tt>environment.rb</tt>):
77
77
 
78
78
  Ultrasphinx::Search.excerpting_options = {
79
- 'before_match' => '<strong>',
80
- 'after_match' => '</strong>',
81
- 'chunk_separator' => "...",
82
- 'limit' => 256,
83
- 'around' => 3,
84
- 'content_methods' => [['title'], ['body', 'description', 'content'], ['metadata']]
79
+ :before_match => '<strong>',
80
+ :after_match => '</strong>',
81
+ :chunk_separator => "...",
82
+ :limit => 256,
83
+ :around => 3,
84
+ :content_methods => [['title'], ['body', 'description', 'content'], ['metadata']]
85
85
  }
86
86
 
87
87
  Note that your database is never changed by anything Ultrasphinx does.
@@ -94,40 +94,40 @@ Note that your database is never changed by anything Ultrasphinx does.
94
94
  include Parser
95
95
 
96
96
  cattr_accessor :query_defaults
97
- self.query_defaults ||= {
98
- 'query' => nil,
99
- 'page' => 1,
100
- 'class_name' => nil,
101
- 'per_page' => 20,
102
- 'sort_by' => 'created_at',
103
- 'sort_mode' => 'relevance',
104
- 'weight' => nil,
105
- 'filter' => nil,
106
- 'facets' => nil
107
- }
97
+ self.query_defaults ||= HashWithIndifferentAccess.new({
98
+ :query => nil,
99
+ :page => 1,
100
+ :class_names => nil,
101
+ :per_page => 20,
102
+ :sort_by => 'created_at',
103
+ :sort_mode => 'relevance',
104
+ :weights => nil,
105
+ :filters => nil,
106
+ :facets => nil
107
+ })
108
108
 
109
109
  cattr_accessor :excerpting_options
110
- self.excerpting_options ||= {
111
- 'before_match' => "<strong>", 'after_match' => "</strong>",
112
- 'chunk_separator' => "...",
113
- 'limit' => 256,
114
- 'around' => 3,
110
+ self.excerpting_options ||= HashWithIndifferentAccess.new({
111
+ :before_match => "<strong>", :after_match => "</strong>",
112
+ :chunk_separator => "...",
113
+ :limit => 256,
114
+ :around => 3,
115
115
  # results should respond to one in each group of these, in precedence order, for the excerpting to fire
116
- 'content_methods' => [['title', 'name'], ['body', 'description', 'content'], ['metadata']]
117
- }
116
+ :content_methods => [['title', 'name'], ['body', 'description', 'content'], ['metadata']]
117
+ })
118
118
 
119
119
  cattr_accessor :client_options
120
- self.client_options ||= {
121
- 'with_subtotals' => false,
122
- 'max_retries' => 4,
123
- 'retry_sleep_time' => 3,
124
- 'max_facets' => 100,
125
- 'finder_methods' => ['get_cache', 'find']
126
- }
120
+ self.client_options ||= HashWithIndifferentAccess.new({
121
+ :with_subtotals => false,
122
+ :max_retries => 4,
123
+ :retry_sleep_time => 3,
124
+ :max_facets => 100,
125
+ :finder_methods => ['get_cache', 'find']
126
+ })
127
127
 
128
128
  # mode to integer mappings
129
- SPHINX_CLIENT_PARAMS = {
130
- 'sort_mode' => {
129
+ SPHINX_CLIENT_PARAMS = HashWithIndifferentAccess.new({
130
+ :sort_mode => HashWithIndifferentAccess.new({
131
131
  'relevance' => Sphinx::Client::SPH_SORT_RELEVANCE,
132
132
  'descending' => Sphinx::Client::SPH_SORT_ATTR_DESC,
133
133
  'ascending' => Sphinx::Client::SPH_SORT_ATTR_ASC,
@@ -135,10 +135,10 @@ Note that your database is never changed by anything Ultrasphinx does.
135
135
  'extended' => Sphinx::Client::SPH_SORT_EXTENDED,
136
136
  'desc' => Sphinx::Client::SPH_SORT_ATTR_DESC, # legacy compatibility
137
137
  'asc' => Sphinx::Client::SPH_SORT_ATTR_ASC
138
- }
139
- }
138
+ })
139
+ })
140
140
 
141
- LEGACY_QUERY_KEYS = ['raw_filters'] #:nodoc:
141
+ LEGACY_QUERY_KEYS = ['raw_filters', 'filter', 'weight', 'class_name'] #:nodoc:
142
142
 
143
143
  INTERNAL_KEYS = ['parsed_query'] #:nodoc:
144
144
 
@@ -146,7 +146,7 @@ Note that your database is never changed by anything Ultrasphinx does.
146
146
  # reading the conf file makes sure that we are in sync with the actual sphinx index,
147
147
  # not whatever you happened to change your models to most recently
148
148
  unless File.exist? CONF_PATH
149
- Ultrasphinx.say "configuration file not found for #{ENV['RAILS_ENV'].inspect} environment"
149
+ Ultrasphinx.say "configuration file not found for #{RAILS_ENV.inspect} environment"
150
150
  Ultrasphinx.say "please run 'rake ultrasphinx:configure'"
151
151
  else
152
152
  begin
@@ -178,12 +178,12 @@ Note that your database is never changed by anything Ultrasphinx does.
178
178
 
179
179
  # Returns the query string used.
180
180
  def query
181
- # redundant with method_missing
181
+ # Redundant with method_missing
182
182
  @options['query']
183
183
  end
184
184
 
185
185
  def parsed_query #:nodoc:
186
- # redundant with method_missing
186
+ # Redundant with method_missing
187
187
  @options['parsed_query']
188
188
  end
189
189
 
@@ -193,19 +193,24 @@ Note that your database is never changed by anything Ultrasphinx does.
193
193
  @results
194
194
  end
195
195
 
196
+ # Returns the facet map for this query, if facets were used.
196
197
  def facets
197
198
  raise UsageError, "No facet field was configured" unless @options['facets']
198
199
  run?(true)
199
200
  @facets
200
201
  end
201
-
202
-
202
+
203
203
  # Returns the raw response from the Sphinx client.
204
204
  def response
205
205
  @response
206
206
  end
207
207
 
208
- # Returns a hash of total result counts, scoped to each available model. This requires extra queries against the search daemon right now. Set <tt>Ultrasphinx::Search.client_options['with_subtotals'] = true</tt> to enable the extra queries. Most of the overhead is in instantiating the AR result sets, so the performance hit is not usually significant.
208
+ def class_name #:nodoc:
209
+ # Legacy accessor
210
+ @options['class_name']
211
+ end
212
+
213
+ # Returns a hash of total result counts, scoped to each available model. This requires extra queries against the search daemon right now. Set <tt>Ultrasphinx::Search.client_options[:with_subtotals] = true</tt> to enable the extra queries. Most of the overhead is in instantiating the AR result sets, so the performance hit is not usually significant.
209
214
  def subtotals
210
215
  raise UsageError, "Subtotals are not enabled" unless self.class.client_options['with_subtotals']
211
216
  @subtotals
@@ -239,14 +244,7 @@ Note that your database is never changed by anything Ultrasphinx does.
239
244
  def per_page
240
245
  @options['per_page']
241
246
  end
242
-
243
- # Clear the associated facet caches. They will be rebuilt on your next <tt>run</tt> or <tt>excerpt</tt>.
244
- def clear_facet_caches
245
- Array(@options['facets']).each do |facet|
246
- FACET_CACHE.delete(facet)
247
- end
248
- end
249
-
247
+
250
248
  # Returns the last available page number in the result set.
251
249
  def page_count
252
250
  (total_entries / per_page) + (total_entries % per_page == 0 ? 0 : 1)
@@ -268,16 +266,20 @@ Note that your database is never changed by anything Ultrasphinx does.
268
266
  end
269
267
 
270
268
  # Builds a new command-interface Search object.
271
- def initialize opts = {}
272
-
273
- opts = opts._deep_stringify_keys
274
-
269
+ def initialize opts = {}
270
+ opts = HashWithIndifferentAccess.new(opts)
275
271
  @options = self.class.query_defaults.merge(opts._deep_dup._coerce_basic_types)
276
272
 
277
- @options['filter'] ||= @options['raw_filters'] || {} # XXX legacy name
278
-
273
+ # Legacy compatibility
274
+ @options['filters'] ||= @options['filter'] || @options['raw_filters'] || {}
275
+ @options['class_names'] ||= @options['class_name']
276
+ @options['weights'] ||= @options['weight']
277
+
278
+ @options._delete(*LEGACY_QUERY_KEYS)
279
+
280
+ # Coerce some special types
279
281
  @options['query'] = @options['query'].to_s
280
- @options['class_name'] = Array(@options['class_name'])
282
+ @options['class_names'] = Array(@options['class_names'])
281
283
 
282
284
  @options['parsed_query'] = if query.blank?
283
285
  "@empty_searchable #{EMPTY_SEARCHABLE}"
@@ -288,7 +290,7 @@ Note that your database is never changed by anything Ultrasphinx does.
288
290
  @results, @subtotals, @facets, @response = [], {}, {}, {}
289
291
 
290
292
  extra_keys = @options.keys - (SPHINX_CLIENT_PARAMS.merge(self.class.query_defaults).keys + LEGACY_QUERY_KEYS + INTERNAL_KEYS)
291
- logger.warn "Discarded invalid keys: #{extra_keys * ', '}" if extra_keys.any?
293
+ say "discarded invalid keys: #{extra_keys * ', '}" if extra_keys.any? and RAILS_ENV != "test"
292
294
  end
293
295
 
294
296
  # Run the search, filling results with an array of ActiveRecord objects. Set the parameter to false if you only want the ids returned.
@@ -297,12 +299,12 @@ Note that your database is never changed by anything Ultrasphinx does.
297
299
  @paginate = nil # clear cache
298
300
  tries = 0
299
301
 
300
- logger.info "** ultrasphinx: searching for #{@options.inspect}"
302
+ say "searching for #{@options.inspect}"
301
303
 
302
304
  begin
303
305
 
304
306
  @response = @request.Query(parsed_query)
305
- logger.info "** ultrasphinx: search returned, error #{@request.GetLastError.inspect}, warning #{@request.GetLastWarning.inspect}, returned #{total_entries}/#{response['total_found']} in #{time} seconds."
307
+ say "search returned, error #{@request.GetLastError.inspect}, warning #{@request.GetLastWarning.inspect}, returned #{total_entries}/#{response['total_found']} in #{time} seconds."
306
308
 
307
309
  @subtotals = get_subtotals(@request, parsed_query) if self.class.client_options['with_subtotals']
308
310
 
@@ -317,11 +319,11 @@ Note that your database is never changed by anything Ultrasphinx does.
317
319
 
318
320
  rescue Sphinx::SphinxResponseError, Sphinx::SphinxTemporaryError, Errno::EPIPE => e
319
321
  if (tries += 1) <= self.class.client_options['max_retries']
320
- logger.warn "** ultrasphinx: restarting query (#{tries} attempts already) (#{e})"
322
+ say "restarting query (#{tries} attempts already) (#{e})"
321
323
  sleep(self.class.client_options['retry_sleep_time']) if tries == self.class.client_options['max_retries']
322
324
  retry
323
325
  else
324
- logger.warn "** ultrasphinx: query failed"
326
+ say "query failed"
325
327
  raise e
326
328
  end
327
329
  end
@@ -386,8 +388,8 @@ Note that your database is never changed by anything Ultrasphinx does.
386
388
  end
387
389
  end
388
390
 
389
- def logger #:nodoc:
390
- RAILS_DEFAULT_LOGGER
391
+ def say msg #:nodoc:
392
+ Ultrasphinx.say msg
391
393
  end
392
394
 
393
395
  end
@@ -24,20 +24,20 @@ module Ultrasphinx
24
24
  request.SetLimits offset, limit, [offset + limit, MAX_MATCHES].min
25
25
  request.SetSortMode SPHINX_CLIENT_PARAMS['sort_mode'][opts['sort_mode']], opts['sort_by'].to_s
26
26
 
27
- if weights = opts['weight']
27
+ if weights = opts['weights']
28
28
  # Order the weights hash according to the field order for Sphinx, and set the missing fields to 1.0
29
29
  request.SetWeights(Fields.instance.types.select{|n,t| t == 'text'}.map(&:first).sort.inject([]) do |array, field|
30
30
  array << (weights[field] || 1.0)
31
31
  end)
32
32
  end
33
33
 
34
- unless opts['class_name'].compact.empty?
35
- request.SetFilter 'class_id', opts['class_name'].map{|m| MODELS_TO_IDS[m.to_s]}
34
+ unless opts['class_names'].compact.empty?
35
+ request.SetFilter 'class_id', opts['class_names'].map{|m| MODELS_TO_IDS[m.to_s]}
36
36
  end
37
37
 
38
38
  # Extract ranged raw filters
39
39
  # Some of this mangling might not be necessary
40
- opts['filter'].each do |field, value|
40
+ opts['filters'].each do |field, value|
41
41
  begin
42
42
  case value
43
43
  when Fixnum, Float, BigDecimal, NilClass, Array
@@ -104,31 +104,33 @@ module Ultrasphinx
104
104
  facets = facets.dup
105
105
 
106
106
  if Fields.instance.types[facet] == 'text'
107
- unless FACET_CACHE[facet]
108
- # Cache the reverse CRC map for the textual facet if it hasn't been done yet
109
- # XXX not necessarily optimal since it requires a direct DB hit once per mongrel
110
- Ultrasphinx.say "caching crc reverse map for text facet #{facet}"
111
-
112
- Fields.instance.classes[facet].each do |klass|
113
- # you can only use a facet from your own self right now; no includes allowed
114
- field = (MODEL_CONFIGURATION[klass.name]['fields'].detect do |field_hash|
115
- field_hash['as'] == facet
116
- end)['field']
117
-
118
- klass.connection.execute("SELECT #{field} AS value, CRC32(#{field}) AS crc FROM #{klass.table_name} GROUP BY #{field}").each_hash do |hash|
119
- (FACET_CACHE[facet] ||= {})[hash['crc'].to_i] = hash['value']
120
- end
121
- end
122
- end
123
-
124
- # Apply the map
107
+ # Apply the map, rebuilding if the cache is missing or out-of-date
125
108
  facets = Hash[*(facets.map do |crc, value|
109
+ rebuild_facet_cache(facet) unless FACET_CACHE[facet] and FACET_CACHE[facet].has_key?(crc)
126
110
  [FACET_CACHE[facet][crc], value]
127
111
  end.flatten)]
128
112
  end
129
113
 
130
114
  facets
131
115
  end
116
+
117
+ def rebuild_facet_cache(facet)
118
+ # Cache the reverse CRC map for the textual facet if it hasn't been done yet
119
+ # XXX not necessarily optimal since it requires a direct DB hit once per mongrel
120
+ Ultrasphinx.say "caching CRC reverse map for text facet #{facet}"
121
+
122
+ Fields.instance.classes[facet].each do |klass|
123
+ # you can only use a facet from your own self right now; no includes allowed
124
+ field = (MODEL_CONFIGURATION[klass.name]['fields'].detect do |field_hash|
125
+ field_hash['as'] == facet
126
+ end)['field']
127
+
128
+ klass.connection.execute("SELECT #{field} AS value, CRC32(#{field}) AS crc FROM #{klass.table_name} GROUP BY #{field}").each_hash do |hash|
129
+ (FACET_CACHE[facet] ||= {})[hash['crc'].to_i] = hash['value']
130
+ end
131
+ end
132
+ FACET_CACHE[facet]
133
+ end
132
134
 
133
135
  def reify_results(sphinx_ids)
134
136
 
@@ -153,7 +155,7 @@ module Ultrasphinx
153
155
  klass.respond_to? method_name
154
156
  end
155
157
 
156
- logger.debug "** ultrasphinx: using #{klass.name}.#{finder} as finder method"
158
+ # Ultrasphinx.say "using #{klass.name}.#{finder} as finder method"
157
159
 
158
160
  begin
159
161
  # XXX Does not use Memcached's multiget
@@ -173,8 +175,13 @@ module Ultrasphinx
173
175
  # Put them back in order
174
176
  results.sort_by do |r|
175
177
  raise Sphinx::SphinxResponseError, "Bogus ActiveRecord id for #{r.class}:#{r.id}" unless r.id
176
- index = (sphinx_ids.index(sphinx_id = r.id * MODELS_TO_IDS.size + MODELS_TO_IDS[r.class.base_class.name]))
178
+
179
+ model_index = MODELS_TO_IDS[r.class.base_class.name]
180
+ raise UsageError, "#{r.class.base_class} is not an indexed class. Maybe you indexed an STI child class instead of the base class?" unless model_index
181
+
182
+ index = (sphinx_ids.index(sphinx_id = r.id * MODELS_TO_IDS.size + model_index))
177
183
  raise Sphinx::SphinxResponseError, "Bogus reverse id for #{r.class}:#{r.id} (Sphinx:#{sphinx_id})" unless index
184
+
178
185
  index / sphinx_ids.size.to_f
179
186
  end
180
187
 
@@ -58,6 +58,7 @@ module Ultrasphinx
58
58
 
59
59
  def query_to_token_stream(query)
60
60
  # First, split query on spaces that are not inside sets of quotes or parens
61
+
61
62
  query = query.scan(/[^"() ]*["(][^")]*[")]|[^"() ]+/)
62
63
 
63
64
  token_stream = []
@@ -68,7 +69,17 @@ module Ultrasphinx
68
69
  # recurse for parens, if necessary
69
70
  if subtoken =~ /^(.*?)\((.*)\)(.*?$)/
70
71
  subtoken = query[index] = "#{$1}(#{parse $2})#{$3}"
71
- end
72
+ end
73
+
74
+ # reappend missing closing quotes
75
+ if subtoken =~ /(^|\:)\"/
76
+ subtoken = subtoken.chomp('"') + '"'
77
+ end
78
+
79
+ # strip parentheses within quoted strings
80
+ if subtoken =~ /\"(.*)\"/
81
+ subtoken.sub!($1, $1.gsub(/[()]/, ''))
82
+ end
72
83
 
73
84
  # add to the stream, converting the operator
74
85
  if !has_operator
@@ -59,10 +59,28 @@ sql_query_pre = SET NAMES utf8
59
59
  'postgresql' => %(
60
60
  type = pgsql
61
61
  )}
62
+
63
+ ADAPTER_SQL_FUNCTIONS = {
64
+ 'mysql' => {
65
+ 'group_by' => 'GROUP BY id',
66
+ 'timestamp' => 'UNIX_TIMESTAMP('
67
+ },
68
+ 'postgresql' => {
69
+ 'group_by' => '',
70
+ 'timestamp' => 'EXTRACT(EPOCH FROM '
71
+ }
72
+ }
73
+
74
+ ADAPTER = ActiveRecord::Base.connection.instance_variable_get("@config")[:adapter]
62
75
 
63
76
  # Logger.
64
77
  def self.say msg
65
- STDERR.puts "** ultrasphinx: #{msg}"
78
+ msg = "** ultrasphinx: #{msg}"
79
+ if defined? RAILS_DEFAULT_LOGGER
80
+ RAILS_DEFAULT_LOGGER.warn msg
81
+ else
82
+ STDERR.puts msg
83
+ end
66
84
  end
67
85
 
68
86
  # Configuration file parser.
@@ -100,13 +118,16 @@ type = pgsql
100
118
  # Complain if the database names go out of sync.
101
119
  def self.verify_database_name
102
120
  if File.exist? CONF_PATH
103
- if options_for(
104
- "source #{MODEL_CONFIGURATION.keys.first.tableize}",
105
- CONF_PATH
106
- )['sql_db'] != ActiveRecord::Base.connection.instance_variable_get("@config")[:database]
107
- say "warning; configured database name is out-of-date"
108
- say "please run 'rake ultrasphinx:configure'"
109
- end rescue nil
121
+ begin
122
+ if options_for(
123
+ "source #{MODEL_CONFIGURATION.keys.first.tableize}",
124
+ CONF_PATH
125
+ )['sql_db'] != ActiveRecord::Base.connection.instance_variable_get("@config")[:database]
126
+ say "warning; configured database name is out-of-date"
127
+ say "please run 'rake ultrasphinx:configure'"
128
+ end
129
+ rescue Object
130
+ end
110
131
  end
111
132
  end
112
133
 
@@ -1,4 +1,6 @@
1
1
 
2
+ RAILS_ENV = "test"
3
+
2
4
  def silently
3
5
  old_stdout, $stdout = $stdout, StringIO.new
4
6
  yield
@@ -8,8 +10,6 @@ end
8
10
  RAILS_ROOT = File.dirname(__FILE__)
9
11
  $LOAD_PATH << "#{RAILS_ROOT}/../lib" << RAILS_ROOT
10
12
 
11
- RAILS_ENV = "test"
12
-
13
13
  require 'rubygems'
14
14
  require 'initializer'
15
15
  require 'active_support'
@@ -81,7 +81,13 @@ describe "parser" do
81
81
  '(800) 555-LOVE',
82
82
 
83
83
  'Bend, OR',
84
- 'Bend, OR'
84
+ 'Bend, OR',
85
+
86
+ '"(traditional)"',
87
+ '"traditional"',
88
+
89
+ 'cuisine:"american (traditional"',
90
+ '@cuisine "american traditional"'
85
91
 
86
92
  ].in_groups_of(2).each do |query, result|
87
93
  it "should parse" do
@@ -1,14 +1,14 @@
1
1
 
2
- # Gem::Specification for Ultrasphinx-1.5
2
+ # Gem::Specification for Ultrasphinx-1.5.1
3
3
  # Originally generated by Echoe
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = %q{ultrasphinx}
7
- s.version = "1.5"
8
- s.date = %q{2007-09-19}
7
+ s.version = "1.5.1"
8
+ s.date = %q{2007-09-25}
9
9
  s.summary = %q{Ruby on Rails configurator and client to the Sphinx fulltext search engine.}
10
10
  s.email = %q{}
11
- s.homepage = %q{http://blog.evanweaver.com/pages/code#ultrasphinx}
11
+ s.homepage = %q{http://blog.evanweaver.com/files/doc/fauna/ultrasphinx/}
12
12
  s.rubyforge_project = %q{fauna}
13
13
  s.description = %q{Ruby on Rails configurator and client to the Sphinx fulltext search engine.}
14
14
  s.has_rdoc = true
@@ -28,7 +28,7 @@ end
28
28
  # Echoe.new("ultrasphinx") do |p|
29
29
  # p.project = "fauna"
30
30
  # p.summary = "Ruby on Rails configurator and client to the Sphinx fulltext search engine."
31
- # p.url = "http://blog.evanweaver.com/pages/code#ultrasphinx"
31
+ # p.url = "http://blog.evanweaver.com/files/doc/fauna/ultrasphinx/"
32
32
  # p.docs_host = "blog.evanweaver.com:~/www/bax/public/files/doc/"
33
33
  # p.rdoc_pattern = /is_indexed.rb|search.rb|spell.rb|ultrasphinx.rb|^README|TODO|CHANGELOG|^LICENSE/
34
34
  # p.dependencies = "chronic"
metadata CHANGED
@@ -3,13 +3,13 @@ rubygems_version: 0.9.4
3
3
  specification_version: 1
4
4
  name: ultrasphinx
5
5
  version: !ruby/object:Gem::Version
6
- version: "1.5"
7
- date: 2007-09-19 00:00:00 -04:00
6
+ version: 1.5.1
7
+ date: 2007-09-25 00:00:00 -04:00
8
8
  summary: Ruby on Rails configurator and client to the Sphinx fulltext search engine.
9
9
  require_paths:
10
10
  - lib
11
11
  email: ""
12
- homepage: http://blog.evanweaver.com/pages/code#ultrasphinx
12
+ homepage: http://blog.evanweaver.com/files/doc/fauna/ultrasphinx/
13
13
  rubyforge_project: fauna
14
14
  description: Ruby on Rails configurator and client to the Sphinx fulltext search engine.
15
15
  autorequire:
metadata.gz.sig CHANGED
Binary file