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
@@ -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
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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),
|
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,
|
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, "
|
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
|
-
|
212
|
-
|
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
|
-
|
216
|
-
|
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
|
-
|
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
|
-
"
|
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
|
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
|
-
|
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
|
data/lib/ultrasphinx/spell.rb
CHANGED
@@ -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 =
|
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
|
-
#
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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?
|
data/tasks/ultrasphinx.rake
CHANGED
@@ -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
|
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')
|
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
|
-
|
148
|
-
mkdir_p index_path unless File.directory? index_path
|
160
|
+
ultrasphinx_create_index_path
|
149
161
|
|
150
|
-
cmd = "indexer --config
|
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
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
+
|