pager-ultrasphinx 1.0.20080510
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +184 -0
- data/README +140 -0
- data/Rakefile +27 -0
- data/lib/ultrasphinx/associations.rb +26 -0
- data/lib/ultrasphinx/autoload.rb +12 -0
- data/lib/ultrasphinx/configure.rb +367 -0
- data/lib/ultrasphinx/core_extensions.rb +132 -0
- data/lib/ultrasphinx/fields.rb +198 -0
- data/lib/ultrasphinx/is_indexed.rb +227 -0
- data/lib/ultrasphinx/postgresql/concat_ws.sql +35 -0
- data/lib/ultrasphinx/postgresql/crc32.sql +15 -0
- data/lib/ultrasphinx/postgresql/group_concat.sql +23 -0
- data/lib/ultrasphinx/postgresql/hex_to_int.sql +15 -0
- data/lib/ultrasphinx/postgresql/language.sql +1 -0
- data/lib/ultrasphinx/postgresql/unix_timestamp.sql +12 -0
- data/lib/ultrasphinx/search/internals.rb +385 -0
- data/lib/ultrasphinx/search/parser.rb +139 -0
- data/lib/ultrasphinx/search.rb +456 -0
- data/lib/ultrasphinx/spell.rb +57 -0
- data/lib/ultrasphinx/ultrasphinx.rb +199 -0
- data/lib/ultrasphinx.rb +36 -0
- data/rails/init.rb +2 -0
- data/tasks/ultrasphinx.rake +206 -0
- data/vendor/riddle/MIT-LICENCE +20 -0
- data/vendor/riddle/README +74 -0
- data/vendor/riddle/Rakefile +117 -0
- data/vendor/riddle/lib/riddle/client/filter.rb +44 -0
- data/vendor/riddle/lib/riddle/client/message.rb +65 -0
- data/vendor/riddle/lib/riddle/client/response.rb +84 -0
- data/vendor/riddle/lib/riddle/client.rb +593 -0
- data/vendor/riddle/lib/riddle.rb +25 -0
- data/vendor/will_paginate/LICENSE +18 -0
- metadata +84 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
|
2
|
+
module Ultrasphinx
|
3
|
+
|
4
|
+
class Error < ::StandardError #:nodoc:
|
5
|
+
end
|
6
|
+
class ConfigurationError < Error #:nodoc:
|
7
|
+
end
|
8
|
+
class DaemonError < Error #:nodoc:
|
9
|
+
end
|
10
|
+
class UsageError < Error #:nodoc:
|
11
|
+
end
|
12
|
+
|
13
|
+
# Internal file paths
|
14
|
+
|
15
|
+
SUBDIR = "config/ultrasphinx"
|
16
|
+
|
17
|
+
DIR = "#{RAILS_ROOT}/#{SUBDIR}"
|
18
|
+
|
19
|
+
THIS_DIR = File.expand_path(File.dirname(__FILE__))
|
20
|
+
|
21
|
+
CONF_PATH = "#{DIR}/#{RAILS_ENV}.conf"
|
22
|
+
|
23
|
+
ENV_BASE_PATH = "#{DIR}/#{RAILS_ENV}.base"
|
24
|
+
|
25
|
+
GENERIC_BASE_PATH = "#{DIR}/default.base"
|
26
|
+
|
27
|
+
BASE_PATH = (File.exist?(ENV_BASE_PATH) ? ENV_BASE_PATH : GENERIC_BASE_PATH)
|
28
|
+
|
29
|
+
raise ConfigurationError, "Please create a '#{SUBDIR}/#{RAILS_ENV}.base' or '#{SUBDIR}/default.base' file in order to use Ultrasphinx in your #{RAILS_ENV} environment." unless File.exist? BASE_PATH # XXX lame
|
30
|
+
|
31
|
+
# Some miscellaneous constants
|
32
|
+
|
33
|
+
MAX_INT = 2**32-1
|
34
|
+
|
35
|
+
MAX_WORDS = 2**16 # The maximum number of stopwords built
|
36
|
+
|
37
|
+
MAIN_INDEX = "main"
|
38
|
+
|
39
|
+
DELTA_INDEX = "delta"
|
40
|
+
|
41
|
+
INDEXES = [MAIN_INDEX, DELTA_INDEX]
|
42
|
+
|
43
|
+
CONFIG_MAP = {
|
44
|
+
# These must be symbols for key mapping against Rails itself.
|
45
|
+
:username => 'sql_user',
|
46
|
+
:password => 'sql_pass',
|
47
|
+
:host => 'sql_host',
|
48
|
+
:database => 'sql_db',
|
49
|
+
:port => 'sql_port',
|
50
|
+
:socket => 'sql_sock'
|
51
|
+
}
|
52
|
+
|
53
|
+
CONNECTION_DEFAULTS = {
|
54
|
+
:host => 'localhost',
|
55
|
+
:password => '',
|
56
|
+
:username => 'root'
|
57
|
+
}
|
58
|
+
|
59
|
+
mattr_accessor :with_rake
|
60
|
+
|
61
|
+
def self.load_stored_procedure(name)
|
62
|
+
open("#{THIS_DIR}/postgresql/#{name}.sql").read.gsub(/\s+/, ' ')
|
63
|
+
end
|
64
|
+
|
65
|
+
SQL_FUNCTIONS = {
|
66
|
+
'mysql' => {
|
67
|
+
'group_concat' => "CAST(GROUP_CONCAT(DISTINCT ? ? SEPARATOR ' ') AS CHAR)",
|
68
|
+
'delta' => "DATE_SUB(NOW(), INTERVAL ? SECOND)",
|
69
|
+
'hash' => "CAST(CRC32(?) AS unsigned)",
|
70
|
+
'range_cast' => "?"
|
71
|
+
},
|
72
|
+
'postgresql' => {
|
73
|
+
'group_concat' => "GROUP_CONCAT(?)",
|
74
|
+
'delta' => "(NOW() - '? SECOND'::interval)",
|
75
|
+
'range_cast' => "cast(coalesce(?,1) AS integer)",
|
76
|
+
'hash' => "CRC32(?)"
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
DEFAULTS = {
|
81
|
+
'mysql' => %(
|
82
|
+
type = mysql
|
83
|
+
sql_query_pre = SET SESSION group_concat_max_len = 65535
|
84
|
+
sql_query_pre = SET NAMES utf8
|
85
|
+
),
|
86
|
+
'postgresql' => %(
|
87
|
+
type = pgsql
|
88
|
+
sql_query_pre =
|
89
|
+
)
|
90
|
+
}
|
91
|
+
|
92
|
+
ADAPTER = ActiveRecord::Base.connection.instance_variable_get("@config")[:adapter] rescue 'mysql'
|
93
|
+
|
94
|
+
# Warn-mode logger. Also called from rake tasks.
|
95
|
+
def self.say msg
|
96
|
+
# XXX Method name is stupid.
|
97
|
+
if with_rake
|
98
|
+
puts msg[0..0].upcase + msg[1..-1]
|
99
|
+
else
|
100
|
+
msg = "** ultrasphinx: #{msg}"
|
101
|
+
if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER
|
102
|
+
RAILS_DEFAULT_LOGGER.warn msg
|
103
|
+
else
|
104
|
+
STDERR.puts msg
|
105
|
+
end
|
106
|
+
end
|
107
|
+
nil # Explicitly return nil
|
108
|
+
end
|
109
|
+
|
110
|
+
# Debug-mode logger.
|
111
|
+
def self.log msg
|
112
|
+
# XXX Method name is stupid.
|
113
|
+
if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER
|
114
|
+
RAILS_DEFAULT_LOGGER.debug msg
|
115
|
+
else
|
116
|
+
STDERR.puts msg
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Configuration file parser.
|
121
|
+
def self.options_for(heading, path)
|
122
|
+
# Evaluate ERB
|
123
|
+
template = ERB.new(File.open(path) {|f| f.read})
|
124
|
+
contents = template.result(binding)
|
125
|
+
|
126
|
+
# Find the correct heading.
|
127
|
+
section = contents[/^#{heading.gsub('/', '__')}\s*?\{(.*?)\}/m, 1]
|
128
|
+
|
129
|
+
if section
|
130
|
+
# 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
|
+
else
|
137
|
+
# XXX Is it safe to raise here?
|
138
|
+
Ultrasphinx.say "warning; heading #{heading} not found in #{path}; it may be corrupted. "
|
139
|
+
{}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.get_models_to_class_ids #:nodoc:
|
144
|
+
# Reading the conf file makes sure that we are in sync with the actual Sphinx index, not
|
145
|
+
# whatever you happened to change your models to most recently.
|
146
|
+
if File.exist? CONF_PATH
|
147
|
+
lines, hash = open(CONF_PATH).readlines, {}
|
148
|
+
msg = "#{CONF_PATH} file is corrupted. Please run 'rake ultrasphinx:configure'."
|
149
|
+
|
150
|
+
lines.each_with_index do |line, index|
|
151
|
+
# Find the main sources
|
152
|
+
if line =~ /^source ([\w\d_-]*)_#{MAIN_INDEX}/
|
153
|
+
# Derive the model name
|
154
|
+
model = $1.gsub('__', '/').classify
|
155
|
+
|
156
|
+
# Get the id modulus out of the adjacent sql_query
|
157
|
+
query = lines[index..-1].detect do |query_line|
|
158
|
+
query_line =~ /^sql_query /
|
159
|
+
end
|
160
|
+
raise ConfigurationError, msg unless query
|
161
|
+
hash[model] = query[/(\d*) AS class_id/, 1].to_i
|
162
|
+
end
|
163
|
+
end
|
164
|
+
raise ConfigurationError, msg unless hash.values.size == hash.values.uniq.size
|
165
|
+
hash
|
166
|
+
else
|
167
|
+
# We can't raise here because you may be generating the configuration for the first time
|
168
|
+
Ultrasphinx.say "configuration file not found for #{RAILS_ENV.inspect} environment"
|
169
|
+
Ultrasphinx.say "please run 'rake ultrasphinx:configure'"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Introspect on the existing generated conf files.
|
174
|
+
INDEXER_SETTINGS = options_for('indexer', BASE_PATH)
|
175
|
+
CLIENT_SETTINGS = options_for('client', BASE_PATH)
|
176
|
+
DAEMON_SETTINGS = options_for('searchd', BASE_PATH)
|
177
|
+
SOURCE_SETTINGS = options_for('source', BASE_PATH)
|
178
|
+
INDEX_SETTINGS = options_for('index', BASE_PATH)
|
179
|
+
|
180
|
+
# Make sure there's a trailing slash.
|
181
|
+
INDEX_SETTINGS['path'] = INDEX_SETTINGS['path'].chomp("/") + "/"
|
182
|
+
|
183
|
+
DICTIONARY = CLIENT_SETTINGS['dictionary_name'] || 'ap'
|
184
|
+
raise ConfigurationError, "Aspell does not support dictionary names longer than two letters" if DICTIONARY.size > 2
|
185
|
+
|
186
|
+
STOPWORDS_PATH = "#{Ultrasphinx::INDEX_SETTINGS['path']}/#{DICTIONARY}-stopwords.txt"
|
187
|
+
|
188
|
+
MODEL_CONFIGURATION = {}
|
189
|
+
|
190
|
+
# See if a delta index was defined.
|
191
|
+
def self.delta_index_present?
|
192
|
+
if File.exist?(CONF_PATH)
|
193
|
+
File.open(CONF_PATH).readlines.detect do |line|
|
194
|
+
line =~ /^index delta/
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
data/lib/ultrasphinx.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
require 'fileutils'
|
3
|
+
require 'chronic'
|
4
|
+
require 'singleton'
|
5
|
+
|
6
|
+
if defined? RAILS_ENV and RAILS_ENV == "development"
|
7
|
+
if ENV['USER'] == 'eweaver'
|
8
|
+
require 'ruby-debug'
|
9
|
+
Debugger.start
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
$LOAD_PATH << "#{File.dirname(__FILE__)}/../vendor/riddle/lib"
|
14
|
+
require 'riddle'
|
15
|
+
require 'ultrasphinx/ultrasphinx'
|
16
|
+
require 'ultrasphinx/associations'
|
17
|
+
require 'ultrasphinx/core_extensions'
|
18
|
+
require 'ultrasphinx/is_indexed'
|
19
|
+
|
20
|
+
if (ActiveRecord::Base.connection rescue nil) # XXX Not sure why this needed to be wrapped.
|
21
|
+
require 'ultrasphinx/configure'
|
22
|
+
require 'ultrasphinx/autoload'
|
23
|
+
require 'ultrasphinx/fields'
|
24
|
+
|
25
|
+
require 'ultrasphinx/search/internals'
|
26
|
+
require 'ultrasphinx/search/parser'
|
27
|
+
require 'ultrasphinx/search'
|
28
|
+
|
29
|
+
begin
|
30
|
+
require 'raspell'
|
31
|
+
rescue Object => e
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'ultrasphinx/spell'
|
35
|
+
end
|
36
|
+
|
data/rails/init.rb
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
|
2
|
+
ENV['RAILS_ENV'] ||= "development"
|
3
|
+
|
4
|
+
namespace :ultrasphinx do
|
5
|
+
|
6
|
+
task :_environment => [:environment] do
|
7
|
+
# We can't just chain :environment because we want to make
|
8
|
+
# sure it's set only for known Sphinx tasks
|
9
|
+
Ultrasphinx.with_rake = true
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Bootstrap a full Sphinx environment"
|
13
|
+
task :bootstrap => [:_environment, :configure, :index, :"daemon:restart"] do
|
14
|
+
say "done"
|
15
|
+
say "please restart your application containers"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Rebuild the configuration file for this particular environment."
|
19
|
+
task :configure => [:_environment] do
|
20
|
+
Ultrasphinx::Configure.run
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :index do
|
24
|
+
desc "Reindex and rotate the main index."
|
25
|
+
task :main => [:_environment] do
|
26
|
+
ultrasphinx_index(Ultrasphinx::MAIN_INDEX)
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Reindex and rotate the delta index."
|
30
|
+
task :delta => [:_environment] do
|
31
|
+
ultrasphinx_index(Ultrasphinx::DELTA_INDEX)
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "Merge the delta index into the main index."
|
35
|
+
task :merge => [:_environment] do
|
36
|
+
ultrasphinx_merge
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "Reindex and rotate all indexes."
|
42
|
+
task :index => [:_environment] do
|
43
|
+
ultrasphinx_index("--all")
|
44
|
+
end
|
45
|
+
|
46
|
+
namespace :daemon do
|
47
|
+
desc "Start the search daemon"
|
48
|
+
task :start => [:_environment] do
|
49
|
+
FileUtils.mkdir_p File.dirname(Ultrasphinx::DAEMON_SETTINGS["log"]) rescue nil
|
50
|
+
raise Ultrasphinx::DaemonError, "Already running" if ultrasphinx_daemon_running?
|
51
|
+
system "searchd --config '#{Ultrasphinx::CONF_PATH}'"
|
52
|
+
sleep(4) # give daemon a chance to write the pid file
|
53
|
+
if ultrasphinx_daemon_running?
|
54
|
+
say "started successfully"
|
55
|
+
else
|
56
|
+
say "failed to start"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "Stop the search daemon"
|
61
|
+
task :stop => [:_environment] do
|
62
|
+
raise Ultrasphinx::DaemonError, "Doesn't seem to be running" unless ultrasphinx_daemon_running?
|
63
|
+
system "kill #{pid = ultrasphinx_daemon_pid}"
|
64
|
+
sleep(1)
|
65
|
+
if ultrasphinx_daemon_running?
|
66
|
+
system "kill -9 #{pid}"
|
67
|
+
sleep(1)
|
68
|
+
end
|
69
|
+
if ultrasphinx_daemon_running?
|
70
|
+
say "#{pid} could not be stopped"
|
71
|
+
else
|
72
|
+
say "stopped #{pid}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "Restart the search daemon"
|
77
|
+
task :restart => [:_environment] do
|
78
|
+
Rake::Task["ultrasphinx:daemon:stop"].invoke if ultrasphinx_daemon_running?
|
79
|
+
sleep(3)
|
80
|
+
Rake::Task["ultrasphinx:daemon:start"].invoke
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "Check if the search daemon is running"
|
84
|
+
task :status => [:_environment] do
|
85
|
+
if ultrasphinx_daemon_running?
|
86
|
+
say "daemon is running."
|
87
|
+
else
|
88
|
+
say "daemon is stopped."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
namespace :spelling do
|
95
|
+
desc "Rebuild the custom spelling dictionary. You may need to use 'sudo' if your Aspell folder is not writable by the app user."
|
96
|
+
task :build => [:_environment] do
|
97
|
+
ENV['OPTS'] = "--buildstops #{Ultrasphinx::STOPWORDS_PATH} #{Ultrasphinx::MAX_WORDS} --buildfreqs"
|
98
|
+
Rake::Task["ultrasphinx:index"].invoke
|
99
|
+
tmpfile = "/tmp/ultrasphinx-stopwords.txt"
|
100
|
+
words = []
|
101
|
+
say "filtering"
|
102
|
+
File.open(Ultrasphinx::STOPWORDS_PATH).each do |line|
|
103
|
+
if line =~ /^([^\s\d_]{4,}) (\d+)/
|
104
|
+
# XXX should be configurable
|
105
|
+
words << $1 if $2.to_i > 40
|
106
|
+
# ideally we would also skip words within X edit distance of a correction
|
107
|
+
# by aspell-en, in order to not add typos to the dictionary
|
108
|
+
end
|
109
|
+
end
|
110
|
+
say "writing #{words.size} words"
|
111
|
+
File.open(tmpfile, 'w').write(words.join("\n"))
|
112
|
+
say "loading dictionary '#{Ultrasphinx::DICTIONARY}' into aspell"
|
113
|
+
system("aspell --lang=en create master #{Ultrasphinx::DICTIONARY}.rws < #{tmpfile}")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
# task shortcuts
|
120
|
+
namespace :us do
|
121
|
+
task :start => ["ultrasphinx:daemon:start"]
|
122
|
+
task :restart => ["ultrasphinx:daemon:restart"]
|
123
|
+
task :stop => ["ultrasphinx:daemon:stop"]
|
124
|
+
task :stat => ["ultrasphinx:daemon:status"]
|
125
|
+
task :index => ["ultrasphinx:index"]
|
126
|
+
task :in => ["ultrasphinx:index"]
|
127
|
+
task :main => ["ultrasphinx:index:main"]
|
128
|
+
task :delta => ["ultrasphinx:index:delta"]
|
129
|
+
task :merge => ["ultrasphinx:index:merge"]
|
130
|
+
task :spell => ["ultrasphinx:spelling:build"]
|
131
|
+
task :conf => ["ultrasphinx:configure"]
|
132
|
+
task :boot => ["ultrasphinx:bootstrap"]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Support methods
|
136
|
+
|
137
|
+
def ultrasphinx_daemon_pid
|
138
|
+
open(Ultrasphinx::DAEMON_SETTINGS['pid_file']).readline.chomp rescue nil
|
139
|
+
end
|
140
|
+
|
141
|
+
def ultrasphinx_daemon_running?
|
142
|
+
if ultrasphinx_daemon_pid and `ps -p#{ultrasphinx_daemon_pid} | wc`.to_i > 1
|
143
|
+
true
|
144
|
+
else
|
145
|
+
# Remove bogus lockfiles.
|
146
|
+
Dir[Ultrasphinx::INDEX_SETTINGS["path"] + "*spl"].each {|file| File.delete(file)}
|
147
|
+
false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def ultrasphinx_index(index)
|
152
|
+
rotate = ultrasphinx_daemon_running?
|
153
|
+
ultrasphinx_create_index_path
|
154
|
+
|
155
|
+
cmd = "indexer --config '#{Ultrasphinx::CONF_PATH}'"
|
156
|
+
cmd << " #{ENV['OPTS']} " if ENV['OPTS']
|
157
|
+
cmd << " --rotate" if rotate
|
158
|
+
cmd << " #{index}"
|
159
|
+
|
160
|
+
say "$ #{cmd}"
|
161
|
+
system cmd
|
162
|
+
|
163
|
+
ultrasphinx_check_rotate if rotate
|
164
|
+
end
|
165
|
+
|
166
|
+
def ultrasphinx_merge
|
167
|
+
rotate = ultrasphinx_daemon_running?
|
168
|
+
|
169
|
+
indexes = [Ultrasphinx::MAIN_INDEX, Ultrasphinx::DELTA_INDEX]
|
170
|
+
indexes.each do |index|
|
171
|
+
raise "#{index} index is missing" unless File.exist? "#{Ultrasphinx::INDEX_SETTINGS['path']}/sphinx_index_#{index}.spa"
|
172
|
+
end
|
173
|
+
|
174
|
+
cmd = "indexer --config '#{Ultrasphinx::CONF_PATH}'"
|
175
|
+
cmd << " #{ENV['OPTS']} " if ENV['OPTS']
|
176
|
+
cmd << " --rotate" if rotate
|
177
|
+
cmd << " --merge #{indexes.join(' ')}"
|
178
|
+
|
179
|
+
say "$ #{cmd}"
|
180
|
+
system cmd
|
181
|
+
|
182
|
+
ultrasphinx_check_rotate if rotate
|
183
|
+
end
|
184
|
+
|
185
|
+
def ultrasphinx_check_rotate
|
186
|
+
sleep(4)
|
187
|
+
failed = Dir[Ultrasphinx::INDEX_SETTINGS['path'] + "/*.new.*"]
|
188
|
+
if failed.any?
|
189
|
+
say "warning; index failed to rotate! Deleting new indexes"
|
190
|
+
say "try 'killall searchd' and then 'rake ultrasphinx:daemon:start'"
|
191
|
+
failed.each {|f| File.delete f }
|
192
|
+
else
|
193
|
+
say "index rotated ok"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def ultrasphinx_create_index_path
|
198
|
+
unless File.directory? Ultrasphinx::INDEX_SETTINGS['path']
|
199
|
+
mkdir_p Ultrasphinx::INDEX_SETTINGS['path']
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def say msg
|
204
|
+
Ultrasphinx.say msg
|
205
|
+
end
|
206
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007 Pat Allan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,74 @@
|
|
1
|
+
This client has been written to interface with Sphinx[http://sphinxsearch.com/]. It is written by
|
2
|
+
{Pat Allan}[http://freelancing-gods.com], and has been influenced by both Dmytro Shteflyuk's Ruby
|
3
|
+
client and the original PHP client - credit where credit's due, after all.
|
4
|
+
|
5
|
+
It does not follow the same syntax as those two, though (not much point writing this otherwise) -
|
6
|
+
opting for a more Ruby-like structure.
|
7
|
+
|
8
|
+
The easiest way to install is to grab the gem (available since 0.9.8r1112 only):
|
9
|
+
|
10
|
+
sudo gem install riddle
|
11
|
+
|
12
|
+
However, if you're so inclined, you can grab sourcecode via subversion. If you
|
13
|
+
are after a specific release, use the tag as follows:
|
14
|
+
|
15
|
+
svn co http://rails-oceania.googlecode.com/svn/patallan/riddle/tags/0.9.8-rc2 riddle
|
16
|
+
|
17
|
+
Or for the most current, just use trunk:
|
18
|
+
|
19
|
+
svn co http://rails-oceania.googlecode.com/svn/patallan/riddle/trunk riddle
|
20
|
+
|
21
|
+
Please note that at the time of writing, the following versions are supported (if you get the appropriate tag):
|
22
|
+
|
23
|
+
* 0.9.8-r871
|
24
|
+
* 0.9.8-r909
|
25
|
+
* 0.9.8-r985
|
26
|
+
* 0.9.8-r1065
|
27
|
+
* 0.9.8-r1112
|
28
|
+
* 0.9.8-rc1 (gem version: 0.9.8.1198)
|
29
|
+
* 0.9.8-rc2 (gem version: 0.9.8.1231)
|
30
|
+
|
31
|
+
To get started, just instantiate a Client object:
|
32
|
+
|
33
|
+
client = Riddle::Client.new # defaults to localhost and port 3312
|
34
|
+
client = Riddle::Client.new "sphinxserver.domain.tld", 3333 # custom settings
|
35
|
+
|
36
|
+
And then set the parameters to what you want, before running a query:
|
37
|
+
|
38
|
+
client.match_mode = :extended
|
39
|
+
client.query "Pat Allan @state Victoria"
|
40
|
+
|
41
|
+
The results from a query are similar to the other clients - but here's the details. It's a hash with
|
42
|
+
the following keys:
|
43
|
+
|
44
|
+
* :matches
|
45
|
+
* :fields
|
46
|
+
* :attributes
|
47
|
+
* :attribute_names
|
48
|
+
* :words
|
49
|
+
* :total
|
50
|
+
* :total_found
|
51
|
+
* :time
|
52
|
+
* :status
|
53
|
+
* :warning (if appropriate)
|
54
|
+
* :error (if appropriate)
|
55
|
+
|
56
|
+
The key <tt>:matches</tt> returns an array of hashes - the actual search results. Each hash has the
|
57
|
+
document id (<tt>:doc</tt>), the result weighting (<tt>:weight</tt>), and a hash of the attributes for
|
58
|
+
the document (<tt>:attributes</tt>).
|
59
|
+
|
60
|
+
The <tt>:fields</tt> and <tt>:attribute_names</tt> keys return list of fields and attributes for the
|
61
|
+
documents. The key <tt>:attributes</tt> will return a hash of attribute name and type pairs, and
|
62
|
+
<tt>:words</tt> returns a hash of hashes representing the words from the search, with the number of
|
63
|
+
documents and hits for each, along the lines of:
|
64
|
+
|
65
|
+
results[:words]["Pat"] #=> {:docs => 12, :hits => 15}
|
66
|
+
|
67
|
+
<tt>:total</tt>, <tt>:total_found</tt> and <tt>:time</tt> return the number of matches available, the
|
68
|
+
total number of matches (which may be greater than the maximum available), and the time in milliseconds
|
69
|
+
that the query took to run.
|
70
|
+
|
71
|
+
<tt>:status</tt> is the error code for the query - and if there was a related warning, it will be under
|
72
|
+
the <tt>:warning</tt> key. Fatal errors will be described under <tt>:error</tt>.
|
73
|
+
|
74
|
+
If you've installed the gem and wondering why there's no tests - check out the svn version. I've kept the specs out of the gem as I have a decent amount of test data in there, which really isn't needed unless you want to submit patches.
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/packagetask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
require 'rdoc/rdoc'
|
6
|
+
require 'rdoc/generators/html_generator'
|
7
|
+
require 'rdoc/generators/template/html/html'
|
8
|
+
|
9
|
+
module Generators
|
10
|
+
class HtmlFile < ContextUser
|
11
|
+
alias_method :core_attribute_values, :file_attribute_values
|
12
|
+
|
13
|
+
def file_attribute_values
|
14
|
+
core_attribute_values
|
15
|
+
|
16
|
+
@values["analytics"] = @options.analytics if @options.analytics
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class HtmlClass < ContextUser
|
21
|
+
alias_method :core_attribute_values, :class_attribute_values
|
22
|
+
|
23
|
+
def class_attribute_values
|
24
|
+
core_attribute_values
|
25
|
+
|
26
|
+
@values["analytics"] = @options.analytics if @options.analytics
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Options
|
32
|
+
attr_accessor :analytics
|
33
|
+
|
34
|
+
module OptionList
|
35
|
+
OPTION_LIST << [
|
36
|
+
"--analytics", "-y", "code", "Google Analytics Code"
|
37
|
+
]
|
38
|
+
end
|
39
|
+
|
40
|
+
alias_method :core_parse, :parse
|
41
|
+
|
42
|
+
def parse(argv, generators)
|
43
|
+
core_parse(argv, generators)
|
44
|
+
|
45
|
+
old_args = ARGV.dup
|
46
|
+
ARGV.replace(argv)
|
47
|
+
|
48
|
+
go = GetoptLong.new(*OptionList.options)
|
49
|
+
go.quiet = true
|
50
|
+
|
51
|
+
go.each do |opt, arg|
|
52
|
+
case opt
|
53
|
+
when "--analytics"
|
54
|
+
@analytics = arg.strip
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
ARGV.replace(old_args)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
module RDoc
|
64
|
+
module Page
|
65
|
+
remove_const :FOOTER
|
66
|
+
const_set :FOOTER, %{
|
67
|
+
<div id="validator-badges">
|
68
|
+
<p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
|
69
|
+
</div>
|
70
|
+
<script src="http://www.google-analytics.com/urchin.js" type="text/javascript">
|
71
|
+
</script>
|
72
|
+
<script type="text/javascript">
|
73
|
+
_uacct = "%analytics%";
|
74
|
+
urchinTracker();
|
75
|
+
</script>
|
76
|
+
|
77
|
+
</body>
|
78
|
+
</html>
|
79
|
+
}
|
80
|
+
|
81
|
+
remove_const :BODY
|
82
|
+
const_set :BODY, HEADER + %{
|
83
|
+
|
84
|
+
!INCLUDE! <!-- banner header -->
|
85
|
+
|
86
|
+
<div id="bodyContent">
|
87
|
+
|
88
|
+
} + METHOD_LIST + %{
|
89
|
+
|
90
|
+
</div>
|
91
|
+
|
92
|
+
} + FOOTER
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
desc 'Generate documentation'
|
97
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
98
|
+
rdoc.rdoc_dir = 'rdoc'
|
99
|
+
rdoc.title = 'Riddle - Ruby Sphinx Client'
|
100
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
101
|
+
rdoc.options << '-y US-2475317-6'
|
102
|
+
rdoc.rdoc_files.include('README')
|
103
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
104
|
+
end
|
105
|
+
|
106
|
+
desc "Run Riddle's specs"
|
107
|
+
Spec::Rake::SpecTask.new do |t|
|
108
|
+
t.libs << 'lib'
|
109
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
110
|
+
end
|
111
|
+
|
112
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
113
|
+
t.libs << 'lib'
|
114
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
115
|
+
t.rcov = true
|
116
|
+
t.rcov_opts = ['--exclude', 'spec', '--exclude', 'gems']
|
117
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Riddle
|
2
|
+
class Client
|
3
|
+
# Used for querying Sphinx.
|
4
|
+
class Filter
|
5
|
+
attr_accessor :attribute, :values, :exclude
|
6
|
+
|
7
|
+
# Attribute name, values (which can be an array or a range), and whether
|
8
|
+
# the filter should be exclusive.
|
9
|
+
def initialize(attribute, values, exclude=false)
|
10
|
+
@attribute, @values, @exclude = attribute, values, exclude
|
11
|
+
end
|
12
|
+
|
13
|
+
def exclude?
|
14
|
+
self.exclude
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the message for this filter to send to the Sphinx service
|
18
|
+
def query_message
|
19
|
+
message = Message.new
|
20
|
+
|
21
|
+
message.append_string self.attribute
|
22
|
+
case self.values
|
23
|
+
when Range
|
24
|
+
if self.values.first.is_a?(Float) && self.values.last.is_a?(Float)
|
25
|
+
message.append_int FilterTypes[:float_range]
|
26
|
+
message.append_floats self.values.first, self.values.last
|
27
|
+
else
|
28
|
+
message.append_int FilterTypes[:range]
|
29
|
+
message.append_ints self.values.first, self.values.last
|
30
|
+
end
|
31
|
+
when Array
|
32
|
+
message.append_int FilterTypes[:values]
|
33
|
+
message.append_int self.values.length
|
34
|
+
# using to_f is a hack from the php client - to workaround 32bit
|
35
|
+
# signed ints on x32 platforms
|
36
|
+
message.append_ints *self.values.collect { |val| val.to_f }
|
37
|
+
end
|
38
|
+
message.append_int self.exclude? ? 1 : 0
|
39
|
+
|
40
|
+
message.to_s
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|