sphinx_for_dm 0.0.1
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/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
|
+
|