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
@@ -2,6 +2,9 @@
2
2
  module Ultrasphinx
3
3
  class Search
4
4
  module Internals
5
+
6
+ INFINITY = 1/0.0
7
+
5
8
  include Associations
6
9
 
7
10
  # These methods are kept stateless to ease debugging
@@ -11,6 +14,8 @@ module Ultrasphinx
11
14
  def build_request_with_options opts
12
15
 
13
16
  request = Riddle::Client.new
17
+
18
+ # Basic options
14
19
  request.instance_eval do
15
20
  @server = Ultrasphinx::CLIENT_SETTINGS['server_host']
16
21
  @port = Ultrasphinx::CLIENT_SETTINGS['server_port']
@@ -20,8 +25,32 @@ module Ultrasphinx
20
25
  @max_matches = [@offset + @limit + Ultrasphinx::Search.client_options['max_matches_offset'], MAX_MATCHES].min
21
26
  end
22
27
 
28
+ # Geosearch location
29
+ loc = opts['location']
30
+ loc.stringify_keys!
31
+ lat, long = loc['lat'], loc['long']
32
+ if lat and long
33
+ # Convert degrees to radians, if requested
34
+ if loc['units'] == 'degrees'
35
+ lat = degrees_to_radians(lat)
36
+ long = degrees_to_radians(long)
37
+ end
38
+ # Set the location/anchor point
39
+ request.set_anchor(loc['lat_attribute_name'], lat, loc['long_attribute_name'], long)
40
+ end
41
+
23
42
  # Sorting
24
43
  sort_by = opts['sort_by']
44
+ if options['location']
45
+ case sort_by
46
+ when "distance asc", "distance" then sort_by = "@geodist asc"
47
+ when "distance desc" then sort_by = "@geodist desc"
48
+ end
49
+ end
50
+
51
+ # Use the additional sortable column if it is a text type
52
+ sort_by += "_sortable" if Fields.instance.types[sort_by] == "text"
53
+
25
54
  unless sort_by.blank?
26
55
  if opts['sort_mode'].to_s == 'relevance'
27
56
  # If you're sorting by a field you don't want 'relevance' order
@@ -62,21 +91,35 @@ module Ultrasphinx
62
91
  end
63
92
 
64
93
  # Extract raw filters
65
- # XXX We should coerce based on the Field values, not on the class
94
+ # XXX This is poorly done. We should coerce based on the Field types, not the value class.
95
+ # That would also allow us to move numeric filters from the query string into the hash.
66
96
  Array(opts['filters']).each do |field, value|
67
- field = field.to_s
68
- type = Fields.instance.types[field]
69
- unless type
70
- raise UsageError, "field #{field.inspect} is invalid"
97
+
98
+ field = field.to_s
99
+ type = Fields.instance.types[field]
100
+
101
+ # Special derived attribute
102
+ if field == 'distance' and options['location']
103
+ field, type = '@geodist', 'float'
71
104
  end
105
+
106
+ raise UsageError, "field #{field.inspect} is invalid" unless type
72
107
 
108
+ exclude = false
109
+
110
+ # check for exclude flag attached to filter
111
+ if value.is_a?(Hash)
112
+ exclude = value[:exclude]
113
+ value = value[:value]
114
+ end
115
+
73
116
  begin
74
117
  case value
75
118
  when Integer, Float, BigDecimal, NilClass, Array
76
119
  # XXX Hack to force floats to be floats
77
120
  value = value.to_f if type == 'float'
78
121
  # Just bomb the filter in there
79
- request.filters << Riddle::Client::Filter.new(field, Array(value), false)
122
+ request.filters << Riddle::Client::Filter.new(field, Array(value), exclude)
80
123
  when Range
81
124
  # Make sure ranges point in the right direction
82
125
  min, max = [value.begin, value.end].map {|x| x._to_numeric }
@@ -84,7 +127,7 @@ module Ultrasphinx
84
127
  min, max = max, min if min > max
85
128
  # XXX Hack to force floats to be floats
86
129
  min, max = min.to_f, max.to_f if type == 'float'
87
- request.filters << Riddle::Client::Filter.new(field, min..max, false)
130
+ request.filters << Riddle::Client::Filter.new(field, min..max, exclude)
88
131
  when String
89
132
  # XXX Hack to move text filters into the query
90
133
  opts['parsed_query'] << " @#{field} #{value}"
@@ -92,7 +135,7 @@ module Ultrasphinx
92
135
  raise NoMethodError
93
136
  end
94
137
  rescue NoMethodError => e
95
- raise UsageError, "filter value #{value.inspect} for field #{field.inspect} is invalid"
138
+ raise UsageError, "Filter value #{value.inspect} for field #{field.inspect} is invalid"
96
139
  end
97
140
  end
98
141
 
@@ -208,12 +251,19 @@ module Ultrasphinx
208
251
  (configuration['association_sql'] or "LEFT OUTER JOIN #{association_model.table_name} AS #{table_alias} ON #{table_alias}.#{klass.to_s.downcase}_id = #{klass.table_name}.#{association_model.primary_key}")
209
252
  ]
210
253
  when 'concatenate'
211
- # Wait for someone to complain before worrying about this
212
- raise "Concatenation text facets have not been implemented"
254
+ raise "Concatenation text facets have only been implemented for when :association_sql is defined" if configuration['association_sql'].blank?
255
+
256
+ table_alias = configuration['table_alias']
257
+
258
+ [ "#{table_alias}.#{configuration['field']}",
259
+ configuration['association_sql']
260
+ ]
213
261
  end
214
262
 
215
- klass.connection.execute("SELECT #{field_string} AS value, #{SQL_FUNCTIONS[ADAPTER]['hash']._interpolate(field_string)} AS hash FROM #{klass.table_name} #{join_string} GROUP BY value").each do |value, hash|
216
- FACET_CACHE[facet][hash.to_i] = value
263
+ query = "SELECT #{field_string} AS value, #{SQL_FUNCTIONS[ADAPTER]['hash']._interpolate(field_string)} AS hash FROM #{klass.table_name} #{join_string} GROUP BY value"
264
+
265
+ klass.connection.execute(query).each do |hash|
266
+ FACET_CACHE[facet][hash[1].to_i] = hash[0]
217
267
  end
218
268
  klass
219
269
  end
@@ -226,13 +276,16 @@ module Ultrasphinx
226
276
 
227
277
  # Inverse-modulus map the Sphinx ids to the table-specific ids
228
278
  def convert_sphinx_ids(sphinx_ids)
229
- number_of_models = IDS_TO_MODELS.size
279
+
280
+ number_of_models = IDS_TO_MODELS.size
281
+ raise ConfigurationError, "No model mappings were found. Your #{RAILS_ENV}.conf file is corrupted, or your application container needs to be restarted." if number_of_models == 0
282
+
230
283
  sphinx_ids.sort_by do |item|
231
284
  item[:index]
232
285
  end.map do |item|
233
286
  class_name = IDS_TO_MODELS[item[:doc] % number_of_models]
234
287
  raise DaemonError, "Impossible Sphinx document id #{item[:doc]} in query result" unless class_name
235
- [class_name, item[:doc] / number_of_models]
288
+ [class_name, (item[:doc] / number_of_models).to_i]
236
289
  end
237
290
  end
238
291
 
@@ -254,7 +307,7 @@ module Ultrasphinx
254
307
  end or
255
308
  # XXX This default is kind of buried, but I'm not sure why you would need it to be
256
309
  # configurable, since you can use ['finder_methods'].
257
- "find_all_by_id"
310
+ "find_all_by_#{klass.primary_key}"
258
311
  )
259
312
 
260
313
  records = klass.send(finder, ids_hash[class_name])
@@ -286,6 +339,16 @@ module Ultrasphinx
286
339
  end
287
340
  end
288
341
  end
342
+
343
+ # Add an accessor for distance, if requested
344
+ if self.options['location']['lat'] and self.options['location']['long']
345
+ results.each_with_index do |result, index|
346
+ if result
347
+ distance = (response[:matches][index][:attributes]['@geodist'] or INFINITY)
348
+ result.instance_variable_get('@attributes')['distance'] = distance
349
+ end
350
+ end
351
+ end
289
352
 
290
353
  results.compact!
291
354
 
@@ -299,14 +362,10 @@ module Ultrasphinx
299
362
 
300
363
  def perform_action_with_retries
301
364
  tries = 0
365
+ exceptions = [NoMethodError, Riddle::VersionError, Riddle::ResponseError, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE]
302
366
  begin
303
367
  yield
304
- rescue NoMethodError,
305
- Riddle::VersionError,
306
- Riddle::ResponseError,
307
- Errno::ECONNREFUSED,
308
- Errno::ECONNRESET,
309
- Errno::EPIPE => e
368
+ rescue *exceptions => e
310
369
  tries += 1
311
370
  if tries <= Ultrasphinx::Search.client_options['max_retries']
312
371
  say "restarting query (#{tries} attempts already) (#{e})"
@@ -314,7 +373,9 @@ module Ultrasphinx
314
373
  retry
315
374
  else
316
375
  say "query failed"
317
- raise DaemonError, e.to_s
376
+ # Clear the rescue list, retry one last time, and let the error fail up the stack
377
+ exceptions = []
378
+ retry
318
379
  end
319
380
  end
320
381
  end
@@ -329,7 +390,11 @@ module Ultrasphinx
329
390
  # Also removes apostrophes in the middle of words so that they don't get split in two.
330
391
  s.gsub(/(^|\s)(AND|OR|NOT|\@\w+)(\s|$)/i, "").gsub(/(\w)\'(\w)/, '\1\2')
331
392
  end
393
+
394
+ def degrees_to_radians(value)
395
+ Math::PI * value / 180.0
396
+ end
332
397
 
333
398
  end
334
399
  end
335
- end
400
+ end
@@ -111,6 +111,13 @@ module Ultrasphinx
111
111
  end
112
112
  end
113
113
 
114
+ # Account for trailing operators
115
+ operators = OPERATORS.to_a.flatten
116
+ if operators.include?(token_stream.first)
117
+ token_stream.pop until !operators.include?(token_stream.last)
118
+ token_stream << '' until token_stream.size > 0 && token_stream.size.even?
119
+ end
120
+
114
121
  if token_stream.size.zero? or token_stream.size.odd?
115
122
  raise Error, "#{token_stream.inspect} is not a valid token stream"
116
123
  end
@@ -124,7 +131,14 @@ module Ultrasphinx
124
131
  # Remove some spaces
125
132
  content.gsub!(/^"\s+|\s+"$/, '"')
126
133
  # Convert fields into sphinx style, reformat the stream object
127
- if content =~ /(.*?):(.*)/
134
+ if content =~ /(^(http|https):\/\/[a-z0-9]+([-.]{1}[a-z0-9]*)+. [a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix
135
+ # XXX hack, its somewhat common to search for URLs. be sure to add
136
+ # " @, /," in the charset_type of the US config to search on all
137
+ # URLs and email addresses, and add:
138
+ # prefix_fields = url, domain
139
+ # to your US config
140
+ token_hash[nil] += [[operator, content]]
141
+ elsif content =~ /(.*?):(.*)/
128
142
  token_hash[$1] += [[operator, $2]]
129
143
  else
130
144
  token_hash[nil] += [[operator, content]]
@@ -136,4 +150,4 @@ module Ultrasphinx
136
150
 
137
151
  end
138
152
  end
139
- end
153
+ end
@@ -1,5 +1,4 @@
1
1
 
2
-
3
2
  module Ultrasphinx
4
3
 
5
4
  =begin rdoc
@@ -41,6 +40,8 @@ If <tt>@correction</tt> is not <tt>nil</tt>, go ahead and suggest it to the user
41
40
 
42
41
  def self.correct string
43
42
  return nil unless SP
43
+ return nil if string =~ /\d+/
44
+
44
45
  correction = string.gsub(/[\w\']+/) do |word|
45
46
  unless SP.check(word)
46
47
  SP.suggest(word).first
@@ -51,7 +51,9 @@ module Ultrasphinx
51
51
  }
52
52
 
53
53
  CONNECTION_DEFAULTS = {
54
- :host => 'localhost'
54
+ :host => 'localhost',
55
+ :password => '',
56
+ :username => 'root'
55
57
  }
56
58
 
57
59
  mattr_accessor :with_rake
@@ -62,53 +64,43 @@ module Ultrasphinx
62
64
 
63
65
  SQL_FUNCTIONS = {
64
66
  'mysql' => {
65
- 'group_concat' => "CAST(GROUP_CONCAT(DISTINCT ? SEPARATOR ' ') AS CHAR)",
67
+ 'group_concat' => "CAST(GROUP_CONCAT(DISTINCT ? ? SEPARATOR ' ') AS CHAR)",
66
68
  'delta' => "DATE_SUB(NOW(), INTERVAL ? SECOND)",
67
69
  'hash' => "CAST(CRC32(?) AS unsigned)",
68
- 'range_cast' => "?",
69
- 'stored_procedures' => {}
70
+ 'range_cast' => "?"
70
71
  },
71
72
  'postgresql' => {
72
73
  'group_concat' => "GROUP_CONCAT(?)",
73
74
  'delta' => "(NOW() - '? SECOND'::interval)",
74
75
  'range_cast' => "cast(coalesce(?,1) AS integer)",
75
- 'hash' => "CRC32(?)",
76
- 'stored_procedures' => Hash[*(
77
- ['hex_to_int', 'group_concat', 'concat_ws', 'unix_timestamp', 'crc32'].map do |name|
78
- [name, load_stored_procedure(name)]
79
- end.flatten
80
- )
81
- ]
76
+ 'hash' => "CRC32(?)"
82
77
  }
83
78
  }
79
+ SQL_FUNCTIONS['jdbcmysql'] = SQL_FUNCTIONS['mysql']
84
80
 
85
81
  DEFAULTS = {
86
82
  'mysql' => %(
87
- type = mysql
88
- sql_query_pre = SET SESSION group_concat_max_len = 65535
89
- sql_query_pre = SET NAMES utf8
90
- ),
83
+ type = mysql
84
+ sql_query_pre = SET SESSION group_concat_max_len = 65535
85
+ sql_query_pre = SET NAMES utf8
86
+ ),
91
87
  'postgresql' => %(
92
- type = pgsql
93
- sql_query_pre = ) + SQL_FUNCTIONS['postgresql']['stored_procedures'].values.join(' ') + %(
94
- )
95
- }
88
+ type = pgsql
89
+ sql_query_pre =
90
+ )
91
+ }
92
+ DEFAULTS['jdbcmysql'] = DEFAULTS['mysql']
96
93
 
97
94
  ADAPTER = ActiveRecord::Base.connection.instance_variable_get("@config")[:adapter] rescue 'mysql'
98
-
99
- # Install the stored procedures.
100
- # XXX This shouldn't be done at every index, say the Postgres people.
101
- SQL_FUNCTIONS[ADAPTER]['stored_procedures'].each do |key, value|
102
- ActiveRecord::Base.connection.execute(value)
103
- end
104
-
105
- # Logger.
95
+
96
+ # Warn-mode logger. Also called from rake tasks.
106
97
  def self.say msg
98
+ # XXX Method name is stupid.
107
99
  if with_rake
108
100
  puts msg[0..0].upcase + msg[1..-1]
109
101
  else
110
102
  msg = "** ultrasphinx: #{msg}"
111
- if defined? RAILS_DEFAULT_LOGGER
103
+ if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER
112
104
  RAILS_DEFAULT_LOGGER.warn msg
113
105
  else
114
106
  STDERR.puts msg
@@ -117,6 +109,16 @@ sql_query_pre = ) + SQL_FUNCTIONS['postgresql']['stored_procedures'].values.join
117
109
  nil # Explicitly return nil
118
110
  end
119
111
 
112
+ # Debug-mode logger.
113
+ def self.log msg
114
+ # XXX Method name is stupid.
115
+ if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER
116
+ RAILS_DEFAULT_LOGGER.debug msg
117
+ else
118
+ STDERR.puts msg
119
+ end
120
+ end
121
+
120
122
  # Configuration file parser.
121
123
  def self.options_for(heading, path)
122
124
  # Evaluate ERB
@@ -127,12 +129,20 @@ sql_query_pre = ) + SQL_FUNCTIONS['postgresql']['stored_procedures'].values.join
127
129
  section = contents[/^#{heading.gsub('/', '__')}\s*?\{(.*?)\}/m, 1]
128
130
 
129
131
  if section
132
+ # Strip comments and leading/trailing whitespace
133
+ section.gsub!(/^\s*(.*?)\s*(?:#.*)?$/, '\1')
134
+
130
135
  # Convert to a hash
131
- options = section.split("\n").map do |line|
132
- line =~ /\s*(.*?)\s*=\s*([^\#]*)/
133
- $1 ? [$1, $2.strip] : []
134
- end
135
- Hash[*options.flatten]
136
+ returning({}) do |options|
137
+ lines = section.split(/\n+/)
138
+ while line = lines.shift
139
+ if line =~ /(.*?)\s*=\s*(.*)/
140
+ key, value = $1, [$2]
141
+ value << (line = lines.shift) while line =~ /\\$/
142
+ options[key] = value.join("\n ")
143
+ end
144
+ end
145
+ end
136
146
  else
137
147
  # XXX Is it safe to raise here?
138
148
  Ultrasphinx.say "warning; heading #{heading} not found in #{path}; it may be corrupted. "
@@ -186,22 +196,6 @@ sql_query_pre = ) + SQL_FUNCTIONS['postgresql']['stored_procedures'].values.join
186
196
  STOPWORDS_PATH = "#{Ultrasphinx::INDEX_SETTINGS['path']}/#{DICTIONARY}-stopwords.txt"
187
197
 
188
198
  MODEL_CONFIGURATION = {}
189
-
190
- # Complain if the database names go out of sync.
191
- def self.verify_database_name
192
- if File.exist? CONF_PATH
193
- begin
194
- if options_for(
195
- "source #{MODEL_CONFIGURATION.keys.first.tableize}_#{MAIN_INDEX}",
196
- CONF_PATH
197
- )['sql_db'] != ActiveRecord::Base.connection.instance_variable_get("@config")[:database]
198
- say "warning; configured database name is out-of-date"
199
- say "please run 'rake ultrasphinx:configure'"
200
- end
201
- rescue Object
202
- end
203
- end
204
- end
205
199
 
206
200
  # See if a delta index was defined.
207
201
  def self.delta_index_present?
@@ -1,11 +1,16 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
1
2
 
2
3
  ENV['RAILS_ENV'] ||= "development"
3
4
 
5
+ module Ultrasphinx
6
+ end
7
+
4
8
  namespace :ultrasphinx do
5
9
 
6
10
  task :_environment => [:environment] do
7
11
  # We can't just chain :environment because we want to make
8
12
  # sure it's set only for known Sphinx tasks
13
+ require 'ultrasphinx'
9
14
  Ultrasphinx.with_rake = true
10
15
  end
11
16
 
@@ -31,6 +36,11 @@ namespace :ultrasphinx do
31
36
  ultrasphinx_index(Ultrasphinx::DELTA_INDEX)
32
37
  end
33
38
 
39
+ desc "Merge the delta index into the main index."
40
+ task :merge => [:_environment] do
41
+ ultrasphinx_merge
42
+ end
43
+
34
44
  end
35
45
 
36
46
  desc "Reindex and rotate all indexes."
@@ -43,7 +53,7 @@ namespace :ultrasphinx do
43
53
  task :start => [:_environment] do
44
54
  FileUtils.mkdir_p File.dirname(Ultrasphinx::DAEMON_SETTINGS["log"]) rescue nil
45
55
  raise Ultrasphinx::DaemonError, "Already running" if ultrasphinx_daemon_running?
46
- system "searchd --config '#{Ultrasphinx::CONF_PATH}'"
56
+ system "searchd --config #{Ultrasphinx::CONF_PATH}"
47
57
  sleep(4) # give daemon a chance to write the pid file
48
58
  if ultrasphinx_daemon_running?
49
59
  say "started successfully"
@@ -103,7 +113,9 @@ namespace :ultrasphinx do
103
113
  end
104
114
  end
105
115
  say "writing #{words.size} words"
106
- File.open(tmpfile, 'w').write(words.join("\n"))
116
+ File.open(tmpfile, 'w') do |f|
117
+ f.write(words.join("\n"))
118
+ end
107
119
  say "loading dictionary '#{Ultrasphinx::DICTIONARY}' into aspell"
108
120
  system("aspell --lang=en create master #{Ultrasphinx::DICTIONARY}.rws < #{tmpfile}")
109
121
  end
@@ -121,6 +133,7 @@ namespace :us do
121
133
  task :in => ["ultrasphinx:index"]
122
134
  task :main => ["ultrasphinx:index:main"]
123
135
  task :delta => ["ultrasphinx:index:delta"]
136
+ task :merge => ["ultrasphinx:index:merge"]
124
137
  task :spell => ["ultrasphinx:spelling:build"]
125
138
  task :conf => ["ultrasphinx:configure"]
126
139
  task :boot => ["ultrasphinx:bootstrap"]
@@ -133,7 +146,7 @@ def ultrasphinx_daemon_pid
133
146
  end
134
147
 
135
148
  def ultrasphinx_daemon_running?
136
- if ultrasphinx_daemon_pid and `ps #{ultrasphinx_daemon_pid} | wc`.to_i > 1
149
+ if ultrasphinx_daemon_pid and `ps -p#{ultrasphinx_daemon_pid} | wc`.to_i > 1
137
150
  true
138
151
  else
139
152
  # Remove bogus lockfiles.
@@ -144,30 +157,57 @@ end
144
157
 
145
158
  def ultrasphinx_index(index)
146
159
  rotate = ultrasphinx_daemon_running?
147
- index_path = Ultrasphinx::INDEX_SETTINGS['path']
148
- mkdir_p index_path unless File.directory? index_path
160
+ ultrasphinx_create_index_path
149
161
 
150
- cmd = "indexer --config '#{Ultrasphinx::CONF_PATH}'"
162
+ cmd = "indexer --config #{Ultrasphinx::CONF_PATH}"
151
163
  cmd << " #{ENV['OPTS']} " if ENV['OPTS']
152
164
  cmd << " --rotate" if rotate
153
165
  cmd << " #{index}"
154
166
 
167
+ say "$ #{cmd}"
168
+ system cmd
169
+
170
+ ultrasphinx_check_rotate if rotate
171
+ end
172
+
173
+ def ultrasphinx_merge
174
+ rotate = ultrasphinx_daemon_running?
175
+
176
+ indexes = [Ultrasphinx::MAIN_INDEX, Ultrasphinx::DELTA_INDEX]
177
+ indexes.each do |index|
178
+ raise "#{index} index is missing" unless File.exist? "#{Ultrasphinx::INDEX_SETTINGS['path']}/sphinx_index_#{index}.spa"
179
+ end
180
+
181
+ cmd = "indexer --config #{Ultrasphinx::CONF_PATH}"
182
+ cmd << " #{ENV['OPTS']} " if ENV['OPTS']
183
+ cmd << " --rotate" if rotate
184
+ cmd << " --merge #{indexes.join(' ')}"
185
+
155
186
  say "$ #{cmd}"
156
187
  system cmd
157
188
 
158
- if rotate
159
- sleep(4)
160
- failed = Dir[index_path + "/*.new.*"]
161
- if failed.any?
162
- say "warning; index failed to rotate! Deleting new indexes"
163
- failed.each {|f| File.delete f }
164
- else
165
- say "index rotated ok"
166
- end
189
+ ultrasphinx_check_rotate if rotate
190
+ end
191
+
192
+ def ultrasphinx_check_rotate
193
+ sleep(4)
194
+ failed = Dir[Ultrasphinx::INDEX_SETTINGS['path'] + "/*.new.*"]
195
+ if failed.any?
196
+ say "warning; index failed to rotate! Deleting new indexes"
197
+ say "try 'killall searchd' and then 'rake ultrasphinx:daemon:start'"
198
+ failed.each {|f| File.delete f }
199
+ else
200
+ say "index rotated ok"
201
+ end
202
+ end
203
+
204
+ def ultrasphinx_create_index_path
205
+ unless File.directory? Ultrasphinx::INDEX_SETTINGS['path']
206
+ mkdir_p Ultrasphinx::INDEX_SETTINGS['path']
167
207
  end
168
208
  end
169
209
 
170
210
  def say msg
171
211
  Ultrasphinx.say msg
172
212
  end
173
-
213
+