sphinx_for_dm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +10 -0
- data/lib/sphinx.rb +460 -0
- data/lib/sphinx_for_dm.rb +88 -0
- data/scripts/sphinx.sh +47 -0
- data/tasks/sphinx_for_dm_tasks.rake +43 -0
- metadata +50 -0
data/README
ADDED
data/lib/sphinx.rb
ADDED
@@ -0,0 +1,460 @@
|
|
1
|
+
require "socket"
|
2
|
+
# = sphinx.rb - Sphinx Client Library
|
3
|
+
#
|
4
|
+
# Author:: Dmytro Shteflyuk <mailto:kpumuk@kpumuk.info>.
|
5
|
+
# Copyright:: Copyright (c) 2006 Wildbit, LLC
|
6
|
+
# License:: Distributes under the same terms as Ruby
|
7
|
+
#
|
8
|
+
# This library is distributed under the terms of the Ruby license.
|
9
|
+
# You can freely distribute/modify this library.
|
10
|
+
|
11
|
+
# ==Sphinx Client Library
|
12
|
+
#
|
13
|
+
# The Sphinx Client Library is used to communicate with <tt>searchd</tt>
|
14
|
+
# daemon and get search results from Sphinx.
|
15
|
+
#
|
16
|
+
# ===Usage
|
17
|
+
#
|
18
|
+
# sphinx = Sphinx.new
|
19
|
+
# result = sphinx.query('test')
|
20
|
+
# ids = result[:matches].map { |id, value| id }.join(',')
|
21
|
+
# posts = Post.find :all, :conditions => "id IN (#{ids})"
|
22
|
+
#
|
23
|
+
# docs = posts.map { |post| post.body }
|
24
|
+
# excerpts = sphinx.build_excerpts(docs, 'index', 'test')
|
25
|
+
#
|
26
|
+
class Sphinx
|
27
|
+
|
28
|
+
# :stopdoc:
|
29
|
+
class SphinxError < StandardError; end
|
30
|
+
class SphinxConnectError < SphinxError; end
|
31
|
+
class SphinxResponseError < SphinxError; end
|
32
|
+
class SphinxInternalError < SphinxError; end
|
33
|
+
class SphinxTemporaryError < SphinxError; end
|
34
|
+
class SphinxUnknownError < SphinxError; end
|
35
|
+
# :startdoc:
|
36
|
+
|
37
|
+
# known searchd commands
|
38
|
+
SEARCHD_COMMAND_SEARCH = 0
|
39
|
+
SEARCHD_COMMAND_EXCERPT = 1
|
40
|
+
|
41
|
+
# current client-side command implementation versions
|
42
|
+
VER_COMMAND_SEARCH = 0x104
|
43
|
+
VER_COMMAND_EXCERPT = 0x100
|
44
|
+
|
45
|
+
# known searchd status codes
|
46
|
+
SEARCHD_OK = 0
|
47
|
+
SEARCHD_ERROR = 1
|
48
|
+
SEARCHD_RETRY = 2
|
49
|
+
|
50
|
+
# known match modes
|
51
|
+
SPH_MATCH_ALL = 0
|
52
|
+
SPH_MATCH_ANY = 1
|
53
|
+
SPH_MATCH_PHRASE = 2
|
54
|
+
SPH_MATCH_BOOLEAN = 3
|
55
|
+
SPH_MATCH_EXTENDED = 4
|
56
|
+
|
57
|
+
# known sort modes
|
58
|
+
SPH_SORT_RELEVANCE = 0
|
59
|
+
SPH_SORT_ATTR_DESC = 1
|
60
|
+
SPH_SORT_ATTR_ASC = 2
|
61
|
+
SPH_SORT_TIME_SEGMENTS = 3
|
62
|
+
SPH_SORT_EXTENDED = 4
|
63
|
+
|
64
|
+
# known attribute types
|
65
|
+
SPH_ATTR_INTEGER = 1
|
66
|
+
SPH_ATTR_TIMESTAMP = 2
|
67
|
+
|
68
|
+
# known grouping functions
|
69
|
+
SPH_GROUPBY_DAY = 0
|
70
|
+
SPH_GROUPBY_WEEK = 1
|
71
|
+
SPH_GROUPBY_MONTH = 2
|
72
|
+
SPH_GROUPBY_YEAR = 3
|
73
|
+
SPH_GROUPBY_ATTR = 4
|
74
|
+
|
75
|
+
# Constructs the Sphinx object and sets options to their default values.
|
76
|
+
def initialize
|
77
|
+
@host = 'localhost' # searchd host (default is "localhost")
|
78
|
+
@port = 3312 # searchd port (default is 3312)
|
79
|
+
@offset = 0 # how much records to seek from result-set start (default is 0)
|
80
|
+
@limit = 20 # how much records to return from result-set starting at offset (default is 20)
|
81
|
+
@mode = SPH_MATCH_ALL # query matching mode (default is SPH_MATCH_ALL)
|
82
|
+
@weights = [] # per-field weights (default is 1 for all fields)
|
83
|
+
@sort = SPH_SORT_RELEVANCE # match sorting mode (default is SPH_SORT_RELEVANCE)
|
84
|
+
@sortby = '' # attribute to sort by (defualt is "")
|
85
|
+
@min_id = 0 # min ID to match (default is 0)
|
86
|
+
@max_id = 0xFFFFFFFF # max ID to match (default is UINT_MAX)
|
87
|
+
@min = {} # attribute name to min-value hash (for range filters)
|
88
|
+
@max = {} # attribute name to max-value hash (for range filters)
|
89
|
+
@filter = {} # attribute name to values set hash (for values-set filters)
|
90
|
+
@groupby = '' # group-by attribute name
|
91
|
+
@groupfunc = SPH_GROUPBY_DAY # function to pre-process group-by attribute value with
|
92
|
+
@maxmatches = 1000 # max matches to retrieve
|
93
|
+
|
94
|
+
@error = '' # last error message
|
95
|
+
@warning = '' # last warning message
|
96
|
+
end
|
97
|
+
|
98
|
+
# Get last error message.
|
99
|
+
def last_error
|
100
|
+
@error
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get last warning message.
|
104
|
+
def last_warning
|
105
|
+
@warning
|
106
|
+
end
|
107
|
+
|
108
|
+
# Set searchd server.
|
109
|
+
def set_server(host, port)
|
110
|
+
@host = host
|
111
|
+
@port = port
|
112
|
+
end
|
113
|
+
|
114
|
+
# Set match offset, count, and max number to retrieve.
|
115
|
+
def set_limits(offset, limit, max = 0)
|
116
|
+
@offset = offset
|
117
|
+
@limit = limit
|
118
|
+
@maxmatches = max if max > 0
|
119
|
+
end
|
120
|
+
|
121
|
+
# Set match mode.
|
122
|
+
def set_match_mode(mode)
|
123
|
+
@mode = mode
|
124
|
+
end
|
125
|
+
|
126
|
+
# Set sort mode.
|
127
|
+
def set_sort_mode(mode, sortby = '')
|
128
|
+
@sort = mode
|
129
|
+
@sortby = sortby
|
130
|
+
end
|
131
|
+
|
132
|
+
# Set per-field weights.
|
133
|
+
def set_weights(weights)
|
134
|
+
@weights = weights
|
135
|
+
end
|
136
|
+
|
137
|
+
# Set IDs range to match.
|
138
|
+
#
|
139
|
+
# Only match those records where document ID is beetwen <tt>min_id</tt> and <tt>max_id</tt>
|
140
|
+
# (including <tt>min_id</tt> and <tt>max_id</tt>).
|
141
|
+
def set_id_range(min_id, max_id)
|
142
|
+
@min_id = min_id
|
143
|
+
@max_id = max_id
|
144
|
+
end
|
145
|
+
|
146
|
+
# Set values filter.
|
147
|
+
#
|
148
|
+
# Only match those records where <tt>attr</tt> column values
|
149
|
+
# are in specified set.
|
150
|
+
def set_filter(attr, values)
|
151
|
+
@filter[attr] = values
|
152
|
+
end
|
153
|
+
|
154
|
+
# Set range filter.
|
155
|
+
#
|
156
|
+
# Only match those records where <tt>attr</tt> column value
|
157
|
+
# is beetwen <tt>min</tt> and <tt>max</tt> (including <tt>min</tt> and <tt>max</tt>).
|
158
|
+
def set_filter_range(attr, min, max)
|
159
|
+
@min[attr] = min
|
160
|
+
@max[attr] = max
|
161
|
+
end
|
162
|
+
|
163
|
+
# Set grouping.
|
164
|
+
#
|
165
|
+
# if grouping
|
166
|
+
def set_group_by(attr, func)
|
167
|
+
@groupby = attr
|
168
|
+
@groupfunc = func
|
169
|
+
end
|
170
|
+
|
171
|
+
# Connect to searchd server and run given search query.
|
172
|
+
#
|
173
|
+
# * <tt>query</tt> -- query string
|
174
|
+
# * <tt>index</tt> -- index name to query, default is "*" which means to query all indexes
|
175
|
+
#
|
176
|
+
# returns hash which has the following keys on success:
|
177
|
+
#
|
178
|
+
# * <tt>:matches</tt> -- hash which maps found document_id to ( "weight", "group" ) hash
|
179
|
+
# * <tt>:total</tt> -- total amount of matches retrieved (upto SPH_MAX_MATCHES, see sphinx.h)
|
180
|
+
# * <tt>:total_found</tt> -- total amount of matching documents in index
|
181
|
+
# * <tt>:time</tt> -- search time
|
182
|
+
# * <tt>:words</tt> -- hash which maps query terms (stemmed!) to ( :docs, :hits ) hash
|
183
|
+
def query(query, index = '*')
|
184
|
+
sock = connect
|
185
|
+
|
186
|
+
# build request
|
187
|
+
|
188
|
+
# mode and limits
|
189
|
+
req = [@offset, @limit, @mode, @sort].pack('NNNN')
|
190
|
+
req << [@sortby.length].pack('N')
|
191
|
+
req << @sortby
|
192
|
+
# query itself
|
193
|
+
req << [query.length].pack('N')
|
194
|
+
req << query
|
195
|
+
# weights
|
196
|
+
req << [@weights.length].pack('N')
|
197
|
+
req << @weights.pack('N' * @weights.length)
|
198
|
+
# indexes
|
199
|
+
req << [index.length].pack('N')
|
200
|
+
req << index
|
201
|
+
# id range
|
202
|
+
req << [@min_id.to_i, @max_id.to_i].pack('NN')
|
203
|
+
|
204
|
+
# filters
|
205
|
+
req << [@min.length + @filter.length].pack('N')
|
206
|
+
@min.each do |attribute, min|
|
207
|
+
req << [attribute.length].pack('N')
|
208
|
+
req << attribute
|
209
|
+
req << [0, min, @max[attribute]].pack('NNN')
|
210
|
+
end
|
211
|
+
|
212
|
+
@filter.each do |attribute, values|
|
213
|
+
req << [attribute.length].pack('N')
|
214
|
+
req << attribute
|
215
|
+
req << [values.length].pack('N')
|
216
|
+
req << values.pack('N' * values.length)
|
217
|
+
end
|
218
|
+
|
219
|
+
# group-by
|
220
|
+
req << [@groupfunc, @groupby.length].pack('NN')
|
221
|
+
req << @groupby
|
222
|
+
|
223
|
+
# max matches to retrieve
|
224
|
+
req << [@maxmatches].pack('N')
|
225
|
+
|
226
|
+
# send query, get response
|
227
|
+
len = req.length
|
228
|
+
# add header
|
229
|
+
req = [SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, len].pack('nnN') + req
|
230
|
+
sock.send(req, 0)
|
231
|
+
|
232
|
+
response = get_response(sock, VER_COMMAND_SEARCH)
|
233
|
+
|
234
|
+
# parse response
|
235
|
+
result = {}
|
236
|
+
max = response.length # protection from broken response
|
237
|
+
|
238
|
+
#read schema
|
239
|
+
p = 0
|
240
|
+
fields = []
|
241
|
+
attrs = {}
|
242
|
+
|
243
|
+
nfields = response[p, 4].unpack('N*').first
|
244
|
+
p += 4
|
245
|
+
while nfields > 0 and p < max
|
246
|
+
nfields -= 1
|
247
|
+
len = response[p, 4].unpack('N*').first
|
248
|
+
p += 4
|
249
|
+
fields << response[p, len]
|
250
|
+
p += len
|
251
|
+
end
|
252
|
+
result[:fields] = fields
|
253
|
+
|
254
|
+
nattrs = response[p, 4].unpack('N*').first
|
255
|
+
p += 4
|
256
|
+
while nattrs > 0 && p < max
|
257
|
+
nattrs -= 1
|
258
|
+
len = response[p, 4].unpack('N*').first
|
259
|
+
p += 4
|
260
|
+
attr = response[p, len]
|
261
|
+
p += len
|
262
|
+
type = response[p, 4].unpack('N*').first
|
263
|
+
p += 4
|
264
|
+
attrs[attr.to_sym] = type;
|
265
|
+
end
|
266
|
+
result[:attrs] = attrs
|
267
|
+
|
268
|
+
# read match count
|
269
|
+
count = response[p, 4].unpack('N*').first
|
270
|
+
p += 4
|
271
|
+
|
272
|
+
# read matches
|
273
|
+
result[:matches] = {}
|
274
|
+
while count > 0 and p < max
|
275
|
+
count -= 1
|
276
|
+
doc, weight = response[p, 8].unpack('N*N*')
|
277
|
+
p += 8
|
278
|
+
|
279
|
+
result[:matches][doc] ||= {}
|
280
|
+
result[:matches][doc][:weight] = weight
|
281
|
+
attrs.each do |attr, type|
|
282
|
+
val = response[p, 4].unpack('N*').first
|
283
|
+
p += 4
|
284
|
+
result[:matches][doc][:attrs] ||= {}
|
285
|
+
result[:matches][doc][:attrs][attr] = val
|
286
|
+
end
|
287
|
+
end
|
288
|
+
result[:total], result[:total_found], result[:time], words = \
|
289
|
+
response[p, 16].unpack('N*N*N*N*')
|
290
|
+
result[:time] = '%.3f' % (result[:time] / 1000)
|
291
|
+
p += 16
|
292
|
+
|
293
|
+
result[:words] = {}
|
294
|
+
while words > 0 and p < max
|
295
|
+
words -= 1
|
296
|
+
len = response[p, 4].unpack('N*').first
|
297
|
+
p += 4
|
298
|
+
word = response[p, len]
|
299
|
+
p += len
|
300
|
+
docs, hits = response[p, 8].unpack('N*N*')
|
301
|
+
p += 8
|
302
|
+
result[:words][word] = {:docs => docs, :hits => hits}
|
303
|
+
end
|
304
|
+
|
305
|
+
result
|
306
|
+
end
|
307
|
+
|
308
|
+
# Connect to searchd server and generate exceprts from given documents.
|
309
|
+
#
|
310
|
+
# * <tt>index</tt> -- a string specifiying the index which settings will be used
|
311
|
+
# for stemming, lexing and case folding
|
312
|
+
# * <tt>docs</tt> -- an array of strings which represent the documents' contents
|
313
|
+
# * <tt>words</tt> -- a string which contains the words to highlight
|
314
|
+
# * <tt>opts</tt> is a hash which contains additional optional highlighting parameters.
|
315
|
+
#
|
316
|
+
# You can use following parameters:
|
317
|
+
# * <tt>:before_match</tt> -- a string to insert before a set of matching words, default is "<b>"
|
318
|
+
# * <tt>:after_match</tt> -- a string to insert after a set of matching words, default is "<b>"
|
319
|
+
# * <tt>:chunk_separator</tt> -- a string to insert between excerpts chunks, default is " ... "
|
320
|
+
# * <tt>:limit</tt> -- max excerpt size in symbols (codepoints), default is 256
|
321
|
+
# * <tt>:around</tt> -- how much words to highlight around each match, default is 5
|
322
|
+
#
|
323
|
+
# Returns an array of string excerpts on success.
|
324
|
+
def build_excerpts(docs, index, words, opts = {})
|
325
|
+
sock = connect
|
326
|
+
|
327
|
+
# fixup options
|
328
|
+
opts[:before_match] ||= '<b>';
|
329
|
+
opts[:after_match] ||= '</b>';
|
330
|
+
opts[:chunk_separator] ||= ' ... ';
|
331
|
+
opts[:limit] ||= 256;
|
332
|
+
opts[:around] ||= 5;
|
333
|
+
|
334
|
+
# build request
|
335
|
+
|
336
|
+
# v.1.0 req
|
337
|
+
req = [0, 1].pack('N2'); # mode=0, flags=1 (remove spaces)
|
338
|
+
# req index
|
339
|
+
req << [index.length].pack('N')
|
340
|
+
req << index
|
341
|
+
# req words
|
342
|
+
req << [words.length].pack('N')
|
343
|
+
req << words
|
344
|
+
|
345
|
+
# options
|
346
|
+
req << [opts[:before_match].length].pack('N')
|
347
|
+
req << opts[:before_match]
|
348
|
+
req << [opts[:after_match].length].pack('N')
|
349
|
+
req << opts[:after_match]
|
350
|
+
req << [opts[:chunk_separator].length].pack('N')
|
351
|
+
req << opts[:chunk_separator]
|
352
|
+
req << [opts[:limit].to_i, opts[:around].to_i].pack('NN')
|
353
|
+
|
354
|
+
# documents
|
355
|
+
req << [docs.size].pack('N');
|
356
|
+
docs.each do |doc|
|
357
|
+
req << [doc.length].pack('N')
|
358
|
+
req << doc
|
359
|
+
end
|
360
|
+
|
361
|
+
# send query, get response
|
362
|
+
len = req.length
|
363
|
+
# add header
|
364
|
+
req = [SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, len].pack('nnN') + req
|
365
|
+
sock.send(req, 0)
|
366
|
+
|
367
|
+
response = get_response(sock, VER_COMMAND_EXCERPT)
|
368
|
+
|
369
|
+
# parse response
|
370
|
+
p = 0
|
371
|
+
res = []
|
372
|
+
rlen = response.length
|
373
|
+
docs.each do |doc|
|
374
|
+
len = response[p, 4].unpack('N*').first;
|
375
|
+
p += 4
|
376
|
+
if p + len > rlen
|
377
|
+
@error = 'incomplete reply'
|
378
|
+
raise SphinxResponseError, @error
|
379
|
+
end
|
380
|
+
res << response[p, len]
|
381
|
+
p += len
|
382
|
+
end
|
383
|
+
return res;
|
384
|
+
end
|
385
|
+
|
386
|
+
# Connect to searchd server.
|
387
|
+
def connect
|
388
|
+
begin
|
389
|
+
sock = TCPSocket.new(@host, @port)
|
390
|
+
rescue
|
391
|
+
@error = "connection to #{@host}:#{@port} failed"
|
392
|
+
raise SphinxConnectError, @error
|
393
|
+
end
|
394
|
+
|
395
|
+
v = sock.recv(4).unpack('N*').first
|
396
|
+
if v < 1
|
397
|
+
sock.close
|
398
|
+
@error = "expected searchd protocol version 1+, got version '#{v}'"
|
399
|
+
raise SphinxConnectError, @error
|
400
|
+
end
|
401
|
+
|
402
|
+
sock.send([1].pack('N'), 0)
|
403
|
+
sock
|
404
|
+
end
|
405
|
+
private :connect
|
406
|
+
|
407
|
+
# get and check response packet from searchd server
|
408
|
+
def get_response(sock, client_version)
|
409
|
+
header = sock.recv(8)
|
410
|
+
status, ver, len = header.unpack('n2N')
|
411
|
+
response = ''
|
412
|
+
left = len
|
413
|
+
while left > 0 do
|
414
|
+
begin
|
415
|
+
chunk = sock.recv(left)
|
416
|
+
if chunk
|
417
|
+
response << chunk
|
418
|
+
left -= chunk.length
|
419
|
+
end
|
420
|
+
rescue EOFError
|
421
|
+
end
|
422
|
+
end if left
|
423
|
+
sock.close
|
424
|
+
|
425
|
+
# check response
|
426
|
+
read = response.length
|
427
|
+
if not response or read != len
|
428
|
+
@error = len \
|
429
|
+
? "failed to read searchd response (status=#{status}, ver=#{ver}, len=#{len}, read=#{read})" \
|
430
|
+
: "received zero-sized searchd response"
|
431
|
+
raise SphinxResponseError, @error
|
432
|
+
end
|
433
|
+
|
434
|
+
# check status
|
435
|
+
if status == SEARCHD_ERROR
|
436
|
+
@error = "searchd error: " + response[4,].to_s
|
437
|
+
raise SphinxInternalError, @error
|
438
|
+
end
|
439
|
+
|
440
|
+
if status == SEARCHD_RETRY
|
441
|
+
@error = "temporary searchd error: " + response[4,]
|
442
|
+
raise SphinxTemporaryError, @error
|
443
|
+
end
|
444
|
+
|
445
|
+
unless status == SEARCHD_OK
|
446
|
+
@error = "unknown status code '#{status}'"
|
447
|
+
raise SphinxUnknownError, @error
|
448
|
+
end
|
449
|
+
|
450
|
+
# check version
|
451
|
+
if ver < client_version
|
452
|
+
@warning = "searchd command v.%d.%d older than client's v.%d.%d, some options might not work" % \
|
453
|
+
ver >> 8, ver & 0xff, client_ver >> 8, client_ver & 0xff
|
454
|
+
end
|
455
|
+
|
456
|
+
return response
|
457
|
+
end
|
458
|
+
private :get_response
|
459
|
+
|
460
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "sphinx"
|
2
|
+
|
3
|
+
module SphinxForDataMapper
|
4
|
+
module ClassMethods
|
5
|
+
def acts_as_sphinx(options = {})
|
6
|
+
default_options = {:host => 'localhost', :port => 3312, :index => name.tableize}
|
7
|
+
write_inheritable_attribute 'sphinx_options', default_options.merge(options)
|
8
|
+
extend SphinxClassMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.included(receiver)
|
13
|
+
receiver.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
module SphinxClassMethods
|
17
|
+
VALID_OPTIONS = %w[mode offset page limit index weights host
|
18
|
+
port range filter filter_range group_by sort_mode].map(&:to_sym)
|
19
|
+
|
20
|
+
def sphinx_index
|
21
|
+
read_inheritable_attribute('sphinx_options')[:index]
|
22
|
+
end
|
23
|
+
|
24
|
+
def sphinx_options
|
25
|
+
read_inheritable_attribute 'sphinx_options'
|
26
|
+
end
|
27
|
+
|
28
|
+
def ask_sphinx(query, options = {})
|
29
|
+
default_options = {:offset => 0, :limit => 20}
|
30
|
+
default_options.merge! sphinx_options
|
31
|
+
default_options.merge! options
|
32
|
+
|
33
|
+
if options[:page] && options[:limit]
|
34
|
+
options[:offset] = options[:limit] * (options[:page].to_i - 1)
|
35
|
+
options[:offset] = 0 if options[:offset] < 0
|
36
|
+
end
|
37
|
+
|
38
|
+
sphinx = Sphinx.new
|
39
|
+
sphinx.set_server options[:host], options[:port]
|
40
|
+
sphinx.set_limits options[:offset], options[:limit]
|
41
|
+
sphinx.set_weights options[:weights] if options[:weights]
|
42
|
+
sphinx.set_id_range options[:range] if options[:range]
|
43
|
+
|
44
|
+
options[:filter].each do |attr, values|
|
45
|
+
sphinx.set_filter attr, [*values]
|
46
|
+
end if options[:filter]
|
47
|
+
|
48
|
+
options[:filter_range].each do |attr, (min, max)|
|
49
|
+
sphinx.set_filter_range attr, min, max
|
50
|
+
end if options[:filter_range]
|
51
|
+
|
52
|
+
options[:group_by].each do |attr, func|
|
53
|
+
funcion = Sphinx.const_get("SPH_GROUPBY_#{func.to_s.upcase}") \
|
54
|
+
rescue raise("Unknown group by function #{func}")
|
55
|
+
sphinx.set_group_by attr, funcion
|
56
|
+
end if options[:group_by]
|
57
|
+
|
58
|
+
if options[:mode]
|
59
|
+
match_mode = Sphinx.const_get("SPH_MATCH_#{options[:mode].to_s.upcase}") \
|
60
|
+
rescue raise("Unknown search mode #{options[:mode]}")
|
61
|
+
sphinx.set_match_mode match_mode
|
62
|
+
end
|
63
|
+
|
64
|
+
if options[:sort_mode]
|
65
|
+
sort_mode, sort_expr = options[:sort_mode]
|
66
|
+
sort_mode = Sphinx.const_get("SPH_SORT_#{sort_mode.to_s.upcase}") \
|
67
|
+
rescue raise("Unknown sort mode #{sort_mode}")
|
68
|
+
sphinx.set_sort_mode sort_mode, sort_expr
|
69
|
+
end
|
70
|
+
|
71
|
+
sphinx.query query, options[:index]
|
72
|
+
end
|
73
|
+
|
74
|
+
def all_with_sphinx(query, options = {})
|
75
|
+
result = ask_sphinx(query, options.delete(:sphinx) || {})
|
76
|
+
records = result[:matches].empty? ? [] : all(({ :id.in => result[:matches].keys }).merge(options))
|
77
|
+
records = records.sort_by{|r| -result[:matches][r.id][:weight] }
|
78
|
+
%w[total total_found time].map(&:to_sym).each do |method|
|
79
|
+
class << records; self end.send(:define_method, method) {result[method]}
|
80
|
+
end
|
81
|
+
records
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class DataMapper::Base
|
87
|
+
include SphinxForDataMapper
|
88
|
+
end
|
data/scripts/sphinx.sh
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
#
|
3
|
+
# start/stop searchd server.
|
4
|
+
|
5
|
+
if ! [ -x /usr/local/bin/searchd ]; then
|
6
|
+
exit 0
|
7
|
+
fi
|
8
|
+
|
9
|
+
case "$1" in
|
10
|
+
start)
|
11
|
+
echo -n "Starting sphinx searchd server:"
|
12
|
+
echo -n " searchd" ;
|
13
|
+
/sbin/start-stop-daemon --start --quiet --pidfile /var/run/searchd.pid --chdir /etc --exec /usr/local/bin/searchd
|
14
|
+
echo "."
|
15
|
+
;;
|
16
|
+
stop)
|
17
|
+
echo -n "Stopping sphinx searchd server:"
|
18
|
+
echo -n " searchd" ;
|
19
|
+
/sbin/start-stop-daemon --stop --quiet --oknodo --pidfile /var/run/searchd.pid --exec /usr/local/bin/searchd
|
20
|
+
echo "."
|
21
|
+
;;
|
22
|
+
reload)
|
23
|
+
echo -n "Reloading sphinx searchd server:"
|
24
|
+
echo -n " searchd"
|
25
|
+
/sbin/start-stop-daemon --stop --quiet --oknodo --pidfile /var/run/searchd.pid --signal 1
|
26
|
+
echo "."
|
27
|
+
;;
|
28
|
+
force-reload)
|
29
|
+
$0 reload
|
30
|
+
;;
|
31
|
+
reindex)
|
32
|
+
cd /etc
|
33
|
+
/usr/local/bin/indexer --rotate --quiet --all
|
34
|
+
;;
|
35
|
+
restart)
|
36
|
+
echo -n "Restarting sphinx searchd server:"
|
37
|
+
echo -n " searchd"
|
38
|
+
/sbin/start-stop-daemon --stop --quiet --oknodo --pidfile /var/run/searchd.pid --exec /usr/local/bin/searchd
|
39
|
+
/sbin/start-stop-daemon --start --quiet --pidfile /var/run/searchd.pid --chdir /etc --exec /usr/local/bin/searchd
|
40
|
+
echo "."
|
41
|
+
;;
|
42
|
+
*)
|
43
|
+
echo "Usage: /etc/init.d/searchd {start|stop|reload|restart|reindex}"
|
44
|
+
exit 1
|
45
|
+
;;
|
46
|
+
esac
|
47
|
+
exit 0
|
@@ -0,0 +1,43 @@
|
|
1
|
+
namespace :dm do
|
2
|
+
namespace :sphinx do
|
3
|
+
desc "Run indexer"
|
4
|
+
task :index do
|
5
|
+
cd 'config' do
|
6
|
+
system 'indexer --all'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Rotate idendexes and restart searchd server"
|
11
|
+
task :rotate do
|
12
|
+
cd 'config' do
|
13
|
+
system 'indexer --rotate --all'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Start searchd server"
|
18
|
+
task :start do
|
19
|
+
if File.exists?('/var/run/searchd.pid')
|
20
|
+
puts 'Sphinx searchd server is already started.'
|
21
|
+
else
|
22
|
+
cd 'config' do
|
23
|
+
system 'searchd'
|
24
|
+
puts 'Sphinx searchd server started.'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Stop searchd server"
|
30
|
+
task :stop do
|
31
|
+
unless File.exists?('/var/run/searchd.pid')
|
32
|
+
puts 'Sphinx searchd server is not running.'
|
33
|
+
else
|
34
|
+
pid = File.read('/var/run/searchd.pid').chomp
|
35
|
+
system "kill #{pid}"
|
36
|
+
puts 'Sphinx searchd server stopped.'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Restart searchd server"
|
41
|
+
task :restart => [:stop, :start]
|
42
|
+
end
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.4
|
3
|
+
specification_version: 1
|
4
|
+
name: sphinx_for_dm
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2008-02-24 00:00:00 -05:00
|
8
|
+
summary: Acts_as_sphinx for DataMapper
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: foysavas@gmail.com
|
12
|
+
homepage: http://www.foysavas.com
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire: sphinx_for_dm
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Foy Savas
|
31
|
+
files:
|
32
|
+
- lib/sphinx_for_dm.rb
|
33
|
+
- lib/sphinx.rb
|
34
|
+
- scripts/sphinx.sh
|
35
|
+
- tasks/sphinx_for_dm_tasks.rake
|
36
|
+
- README
|
37
|
+
test_files: []
|
38
|
+
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README
|
43
|
+
executables: []
|
44
|
+
|
45
|
+
extensions: []
|
46
|
+
|
47
|
+
requirements: []
|
48
|
+
|
49
|
+
dependencies: []
|
50
|
+
|