This repository has been archived by the owner on Jun 5, 2018. It is now read-only.
forked from pat/thinking-sphinx
/
client.rb
852 lines (733 loc) · 26.6 KB
/
client.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
require 'riddle/client/filter'
require 'riddle/client/message'
require 'riddle/client/response'
module Riddle
class VersionError < StandardError; end
class ResponseError < StandardError; end
class OutOfBoundsError < StandardError; end
# This class was heavily based on the existing Client API by Dmytro Shteflyuk
# and Alexy Kovyrin. Their code worked fine, I just wanted something a bit
# more Ruby-ish (ie. lowercase and underscored method names). I also have
# used a few helper classes, just to neaten things up.
#
# Feel free to use it wherever. Send bug reports, patches, comments and
# suggestions to pat at freelancing-gods dot com.
#
# Most properties of the client are accessible through attribute accessors,
# and where relevant use symboles instead of the long constants common in
# other clients.
# Some examples:
#
# client.sort_mode = :extended
# client.sort_by = "birthday DESC"
# client.match_mode = :extended
#
# To add a filter, you will need to create a Filter object:
#
# client.filters << Riddle::Client::Filter.new("birthday",
# Time.at(1975, 1, 1).to_i..Time.at(1985, 1, 1).to_i, false)
#
class Client
Commands = {
:search => 0, # SEARCHD_COMMAND_SEARCH
:excerpt => 1, # SEARCHD_COMMAND_EXCERPT
:update => 2, # SEARCHD_COMMAND_UPDATE
:keywords => 3, # SEARCHD_COMMAND_KEYWORDS
:persist => 4, # SEARCHD_COMMAND_PERSIST
:status => 5, # SEARCHD_COMMAND_STATUS
:query => 6, # SEARCHD_COMMAND_QUERY
:flushattrs => 7 # SEARCHD_COMMAND_FLUSHATTRS
}
Versions = {
:search => 0x113, # VER_COMMAND_SEARCH
:excerpt => 0x100, # VER_COMMAND_EXCERPT
:update => 0x101, # VER_COMMAND_UPDATE
:keywords => 0x100, # VER_COMMAND_KEYWORDS
:status => 0x100, # VER_COMMAND_STATUS
:query => 0x100, # VER_COMMAND_QUERY
:flushattrs => 0x100 # VER_COMMAND_FLUSHATTRS
}
Statuses = {
:ok => 0, # SEARCHD_OK
:error => 1, # SEARCHD_ERROR
:retry => 2, # SEARCHD_RETRY
:warning => 3 # SEARCHD_WARNING
}
MatchModes = {
:all => 0, # SPH_MATCH_ALL
:any => 1, # SPH_MATCH_ANY
:phrase => 2, # SPH_MATCH_PHRASE
:boolean => 3, # SPH_MATCH_BOOLEAN
:extended => 4, # SPH_MATCH_EXTENDED
:fullscan => 5, # SPH_MATCH_FULLSCAN
:extended2 => 6 # SPH_MATCH_EXTENDED2
}
RankModes = {
:proximity_bm25 => 0, # SPH_RANK_PROXIMITY_BM25
:bm25 => 1, # SPH_RANK_BM25
:none => 2, # SPH_RANK_NONE
:wordcount => 3, # SPH_RANK_WORDCOUNT
:proximity => 4, # SPH_RANK_PROXIMITY
:match_any => 5, # SPH_RANK_MATCHANY
:fieldmask => 6, # SPH_RANK_FIELDMASK
:sph04 => 7, # SPH_RANK_SPH04
:total => 8 # SPH_RANK_TOTAL
}
SortModes = {
:relevance => 0, # SPH_SORT_RELEVANCE
:attr_desc => 1, # SPH_SORT_ATTR_DESC
:attr_asc => 2, # SPH_SORT_ATTR_ASC
:time_segments => 3, # SPH_SORT_TIME_SEGMENTS
:extended => 4, # SPH_SORT_EXTENDED
:expr => 5 # SPH_SORT_EXPR
}
AttributeTypes = {
:integer => 1, # SPH_ATTR_INTEGER
:timestamp => 2, # SPH_ATTR_TIMESTAMP
:ordinal => 3, # SPH_ATTR_ORDINAL
:bool => 4, # SPH_ATTR_BOOL
:float => 5, # SPH_ATTR_FLOAT
:bigint => 6, # SPH_ATTR_BIGINT
:string => 7, # SPH_ATTR_STRING
:multi => 0x40000000 # SPH_ATTR_MULTI
}
GroupFunctions = {
:day => 0, # SPH_GROUPBY_DAY
:week => 1, # SPH_GROUPBY_WEEK
:month => 2, # SPH_GROUPBY_MONTH
:year => 3, # SPH_GROUPBY_YEAR
:attr => 4, # SPH_GROUPBY_ATTR
:attrpair => 5 # SPH_GROUPBY_ATTRPAIR
}
FilterTypes = {
:values => 0, # SPH_FILTER_VALUES
:range => 1, # SPH_FILTER_RANGE
:float_range => 2 # SPH_FILTER_FLOATRANGE
}
attr_accessor :servers, :port, :offset, :limit, :max_matches,
:match_mode, :sort_mode, :sort_by, :weights, :id_range, :filters,
:group_by, :group_function, :group_clause, :group_distinct, :cut_off,
:retry_count, :retry_delay, :anchor, :index_weights, :rank_mode,
:rank_expr, :max_query_time, :field_weights, :timeout, :overrides,
:select, :connection, :key
attr_reader :queue
@@connection = nil
def self.connection=(value)
Riddle.mutex.synchronize do
@@connection = value
end
end
def self.connection
@@connection
end
# Can instantiate with a specific server and port - otherwise it assumes
# defaults of localhost and 3312 respectively. All other settings can be
# accessed and changed via the attribute accessors.
def initialize(servers = nil, port = nil, key = nil)
Riddle.version_warning
@servers = Array(servers || "localhost")
@port = port || 9312
@socket = nil
@key = key
reset
@queue = []
end
# Reset attributes and settings to defaults.
def reset
# defaults
@offset = 0
@limit = 20
@max_matches = 1000
@match_mode = :all
@sort_mode = :relevance
@sort_by = ''
@weights = []
@id_range = 0..0
@filters = []
@group_by = ''
@group_function = :day
@group_clause = '@group desc'
@group_distinct = ''
@cut_off = 0
@retry_count = 0
@retry_delay = 0
@anchor = {}
# string keys are index names, integer values are weightings
@index_weights = {}
@rank_mode = :proximity_bm25
@rank_expr = ''
@max_query_time = 0
# string keys are field names, integer values are weightings
@field_weights = {}
@timeout = 0
@overrides = {}
@select = "*"
end
# The searchd server to query. Servers are removed from @server after a
# Timeout::Error is hit to allow for fail-over.
def server
@servers.first
end
# Backwards compatible writer to the @servers array.
def server=(server)
@servers = server.to_a
end
# Set the geo-anchor point - with the names of the attributes that contain
# the latitude and longitude (in radians), and the reference position.
# Note that for geocoding to work properly, you must also set
# match_mode to :extended. To sort results by distance, you will
# need to set sort_by to '@geodist asc', and sort_mode to extended (as an
# example). Sphinx expects latitude and longitude to be returned from you
# SQL source in radians.
#
# Example:
# client.set_anchor('lat', -0.6591741, 'long', 2.530770)
#
def set_anchor(lat_attr, lat, long_attr, long)
@anchor = {
:latitude_attribute => lat_attr,
:latitude => lat,
:longitude_attribute => long_attr,
:longitude => long
}
end
# Append a query to the queue. This uses the same parameters as the query
# method.
def append_query(search, index = '*', comments = '')
@queue << query_message(search, index, comments)
end
# Run all the queries currently in the queue. This will return an array of
# results hashes.
def run
response = Response.new request(:search, @queue)
results = @queue.collect do
result = {
:matches => [],
:fields => [],
:attributes => {},
:attribute_names => [],
:words => {}
}
result[:status] = response.next_int
case result[:status]
when Statuses[:warning]
result[:warning] = response.next
when Statuses[:error]
result[:error] = response.next
next result
end
result[:fields] = response.next_array
attributes = response.next_int
attributes.times do
attribute_name = response.next
type = response.next_int
result[:attributes][attribute_name] = type
result[:attribute_names] << attribute_name
end
result_attribute_names_and_types = result[:attribute_names].
inject([]) { |array, attr| array.push([ attr, result[:attributes][attr] ]) }
matches = response.next_int
is_64_bit = response.next_int
result[:matches] = (0...matches).map do |i|
doc = is_64_bit > 0 ? response.next_64bit_int : response.next_int
weight = response.next_int
current_match_attributes = {}
result_attribute_names_and_types.each do |attr, type|
current_match_attributes[attr] = attribute_from_type(type, response)
end
{:doc => doc, :weight => weight, :index => i, :attributes => current_match_attributes}
end
result[:total] = response.next_int.to_i || 0
result[:total_found] = response.next_int.to_i || 0
result[:time] = ('%.3f' % (response.next_int / 1000.0)).to_f || 0.0
words = response.next_int
words.times do
word = response.next
docs = response.next_int
hits = response.next_int
result[:words][word] = {:docs => docs, :hits => hits}
end
result
end
@queue.clear
results
end
# Query the Sphinx daemon - defaulting to all indices, but you can specify
# a specific one if you wish. The search parameter should be a string
# following Sphinx's expectations.
#
# The object returned from this method is a hash with the following keys:
#
# * :matches
# * :fields
# * :attributes
# * :attribute_names
# * :words
# * :total
# * :total_found
# * :time
# * :status
# * :warning (if appropriate)
# * :error (if appropriate)
#
# The key <tt>:matches</tt> returns an array of hashes - the actual search
# results. Each hash has the document id (<tt>:doc</tt>), the result
# weighting (<tt>:weight</tt>), and a hash of the attributes for the
# document (<tt>:attributes</tt>).
#
# The <tt>:fields</tt> and <tt>:attribute_names</tt> keys return list of
# fields and attributes for the documents. The key <tt>:attributes</tt>
# will return a hash of attribute name and type pairs, and <tt>:words</tt>
# returns a hash of hashes representing the words from the search, with the
# number of documents and hits for each, along the lines of:
#
# results[:words]["Pat"] #=> {:docs => 12, :hits => 15}
#
# <tt>:total</tt>, <tt>:total_found</tt> and <tt>:time</tt> return the
# number of matches available, the total number of matches (which may be
# greater than the maximum available, depending on the number of matches
# and your sphinx configuration), and the time in milliseconds that the
# query took to run.
#
# <tt>:status</tt> is the error code for the query - and if there was a
# related warning, it will be under the <tt>:warning</tt> key. Fatal errors
# will be described under <tt>:error</tt>.
#
def query(search, index = '*', comments = '')
@queue << query_message(search, index, comments)
self.run.first
end
# Build excerpts from search terms (the +words+) and the text of documents. Excerpts are bodies of text that have the +words+ highlighted.
# They may also be abbreviated to fit within a word limit.
#
# As part of the options hash, you will need to
# define:
# * :docs
# * :words
# * :index
#
# Optional settings include:
# * :before_match (defaults to <span class="match">)
# * :after_match (defaults to </span>)
# * :chunk_separator (defaults to ' … ' - which is an HTML ellipsis)
# * :limit (defaults to 256)
# * :around (defaults to 5)
# * :exact_phrase (defaults to false)
# * :single_passage (defaults to false)
#
# The defaults differ from the official PHP client, as I've opted for
# semantic HTML markup.
#
# Example:
#
# client.excerpts(:docs => ["Pat Allan, Pat Cash"], :words => 'Pat', :index => 'pats')
# #=> ["<span class=\"match\">Pat</span> Allan, <span class=\"match\">Pat</span> Cash"]
#
# lorem_lipsum = "Lorem ipsum dolor..."
#
# client.excerpts(:docs => ["Pat Allan, #{lorem_lipsum} Pat Cash"], :words => 'Pat', :index => 'pats')
# #=> ["<span class=\"match\">Pat</span> Allan, Lorem ipsum dolor sit amet, consectetur adipisicing
# elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua … . Excepteur
# sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
# laborum. <span class=\"match\">Pat</span> Cash"]
#
# Workflow:
#
# Excerpt creation is completely isolated from searching the index. The nominated index is only used to
# discover encoding and charset information.
#
# Therefore, the workflow goes:
#
# 1. Do the sphinx query.
# 2. Fetch the documents found by sphinx from their repositories.
# 3. Pass the documents' text to +excerpts+ for marking up of matched terms.
#
def excerpts(options = {})
options[:index] ||= '*'
options[:before_match] ||= '<span class="match">'
options[:after_match] ||= '</span>'
options[:chunk_separator] ||= ' … ' # ellipsis
options[:limit] ||= 256
options[:limit_passages] ||= 0
options[:limit_words] ||= 0
options[:around] ||= 5
options[:exact_phrase] ||= false
options[:single_passage] ||= false
options[:query_mode] ||= false
options[:force_all_words] ||= false
options[:start_passage_id] ||= 1
options[:load_files] ||= false
options[:html_strip_mode] ||= 'index'
options[:allow_empty] ||= false
options[:passage_boundary] ||= 'none'
options[:emit_zones] ||= false
options[:load_files_scattered] ||= false
response = Response.new request(:excerpt, excerpts_message(options))
options[:docs].collect { response.next }
end
# Update attributes - first parameter is the relevant index, second is an
# array of attributes to be updated, and the third is a hash, where the
# keys are the document ids, and the values are arrays with the attribute
# values - in the same order as the second parameter.
#
# Example:
#
# client.update('people', ['birthday'], {1 => [Time.at(1982, 20, 8).to_i]})
#
def update(index, attributes, values_by_doc)
response = Response.new request(
:update,
update_message(index, attributes, values_by_doc)
)
response.next_int
end
# Generates a keyword list for a given query. Each keyword is represented
# by a hash, with keys :tokenised and :normalised. If return_hits is set to
# true it will also report on the number of hits and documents for each
# keyword (see :hits and :docs keys respectively).
def keywords(query, index, return_hits = false)
response = Response.new request(
:keywords,
keywords_message(query, index, return_hits)
)
(0...response.next_int).collect do
hash = {}
hash[:tokenised] = response.next
hash[:normalised] = response.next
if return_hits
hash[:docs] = response.next_int
hash[:hits] = response.next_int
end
hash
end
end
def status
response = Response.new request(
:status, Message.new
)
rows, cols = response.next_int, response.next_int
(0...rows).inject({}) do |hash, row|
hash[response.next.to_sym] = response.next
hash
end
end
def flush_attributes
response = Response.new request(
:flushattrs, Message.new
)
response.next_int
end
def add_override(attribute, type, values)
@overrides[attribute] = {:type => type, :values => values}
end
def open
open_socket
return if Versions[:search] < 0x116
@socket.send [
Commands[:persist], 0, 4, 1
].pack("nnNN"), 0
end
def close
close_socket
end
private
def open_socket
raise "Already Connected" unless @socket.nil?
if @timeout == 0
@socket = initialise_connection
else
begin
Timeout.timeout(@timeout) { @socket = initialise_connection }
rescue Timeout::Error, Riddle::ConnectionError => e
failed_servers ||= []
failed_servers << servers.shift
retry if !servers.empty?
case e
when Timeout::Error
raise Riddle::ConnectionError,
"Connection to #{failed_servers.inspect} on #{@port} timed out after #{@timeout} seconds"
else
raise e
end
end
end
true
end
def close_socket
raise "Not Connected" if @socket.nil?
@socket.close
@socket = nil
true
end
# If there's an active connection to the Sphinx daemon, this will yield the
# socket. If there's no active connection, then it will connect, yield the
# new socket, then close it.
def connect(&block)
if @socket.nil? || @socket.closed?
@socket = nil
open_socket
begin
yield @socket
ensure
close_socket
end
else
yield @socket
end
end
def initialise_connection
socket = initialise_socket
# Checking version
version = socket.recv(4).unpack('N*').first
if version < 1
socket.close
raise VersionError, "Can only connect to searchd version 1.0 or better, not version #{version}"
end
# Send version
socket.send [1].pack('N'), 0
socket
end
def initialise_socket
tries = 0
begin
socket = if self.connection
self.connection.call(self)
elsif self.class.connection
self.class.connection.call(self)
elsif server && server.index('/') == 0
UNIXSocket.new server
elsif server
TCPSocket.new server, @port
else
raise "Server not set."
end
rescue Errno::ETIMEDOUT, Errno::ECONNRESET, Errno::ECONNREFUSED => e
retry if (tries += 1) < 5
raise Riddle::ConnectionError,
"Connection to #{server} on #{@port} failed. #{e.message}"
end
socket
end
def request_header(command, length = 0)
length += key_message.length if key
core_header = case command
when :search
# Message length is +4/+8 to account for the following count value for
# the number of messages.
if Versions[command] >= 0x118
[Commands[command], Versions[command], 8 + length].pack("nnN")
else
[Commands[command], Versions[command], 4 + length].pack("nnN")
end
when :status
[Commands[command], Versions[command], 4, 1].pack("nnNN")
else
[Commands[command], Versions[command], length].pack("nnN")
end
key ? core_header + key_message : core_header
end
def key_message
@key_message ||= begin
message = Message.new
message.append_string key
message.to_s
end
end
# Send a collection of messages, for a command type (eg, search, excerpts,
# update), to the Sphinx daemon.
def request(command, messages)
response = ""
status = -1
version = 0
length = 0
message = Riddle.encode(Array(messages).join(""), 'ASCII-8BIT')
connect do |socket|
case command
when :search
if Versions[command] >= 0x118
socket.send request_header(command, message.length) +
[0, messages.length].pack('NN') + message, 0
else
socket.send request_header(command, message.length) +
[messages.length].pack('N') + message, 0
end
when :status
socket.send request_header(command, message.length), 0
else
socket.send request_header(command, message.length) + message, 0
end
header = socket.recv(8)
status, version, length = header.unpack('n2N')
while response.length < (length || 0)
part = socket.recv(length - response.length)
# will return 0 bytes if remote side closed TCP connection, e.g, searchd segfaulted.
break if part.length == 0 && socket.is_a?(TCPSocket)
response << part if part
end
end
if response.empty? || response.length != length
raise ResponseError, "No response from searchd (status: #{status}, version: #{version})"
end
case status
when Statuses[:ok]
if version < Versions[command]
puts format("searchd command v.%d.%d older than client (v.%d.%d)",
version >> 8, version & 0xff,
Versions[command] >> 8, Versions[command] & 0xff)
end
response
when Statuses[:warning]
length = response[0, 4].unpack('N*').first
puts response[4, length]
response[4 + length, response.length - 4 - length]
when Statuses[:error], Statuses[:retry]
message = response[4, response.length - 4]
klass = message[/out of bounds/] ? OutOfBoundsError : ResponseError
raise klass, "searchd error (status: #{status}): #{message}"
else
raise ResponseError, "Unknown searchd error (status: #{status})"
end
end
# Generation of the message to send to Sphinx for a search.
def query_message(search, index, comments = '')
message = Message.new
# Mode, Limits
message.append_ints @offset, @limit, MatchModes[@match_mode]
# Ranking
message.append_int RankModes[@rank_mode]
message.append_string @rank_expr if @rank_mode == :expr
# Sort Mode
message.append_int SortModes[@sort_mode]
message.append_string @sort_by
# Query
message.append_string search
# Weights
message.append_int @weights.length
message.append_ints *@weights
# Index
message.append_string index
# ID Range
message.append_int 1
message.append_64bit_ints @id_range.first, @id_range.last
# Filters
message.append_int @filters.length
@filters.each { |filter| message.append filter.query_message }
# Grouping
message.append_int GroupFunctions[@group_function]
message.append_string @group_by
message.append_int @max_matches
message.append_string @group_clause
message.append_ints @cut_off, @retry_count, @retry_delay
message.append_string @group_distinct
# Anchor Point
if @anchor.empty?
message.append_int 0
else
message.append_int 1
message.append_string @anchor[:latitude_attribute]
message.append_string @anchor[:longitude_attribute]
message.append_floats @anchor[:latitude], @anchor[:longitude]
end
# Per Index Weights
message.append_int @index_weights.length
@index_weights.each do |key,val|
message.append_string key.to_s
message.append_int val
end
# Max Query Time
message.append_int @max_query_time
# Per Field Weights
message.append_int @field_weights.length
@field_weights.each do |key,val|
message.append_string key.to_s
message.append_int val
end
message.append_string comments
return message.to_s if Versions[:search] < 0x116
# Overrides
message.append_int @overrides.length
@overrides.each do |key,val|
message.append_string key.to_s
message.append_int AttributeTypes[val[:type]]
message.append_int val[:values].length
val[:values].each do |id,map|
message.append_64bit_int id
method = case val[:type]
when :float
:append_float
when :bigint
:append_64bit_int
else
:append_int
end
message.send method, map
end
end
message.append_string @select
message.to_s
end
# Generation of the message to send to Sphinx for an excerpts request.
def excerpts_message(options)
message = Message.new
message.append [0, excerpt_flags(options)].pack('N2') # 0 = mode
message.append_string options[:index]
message.append_string options[:words]
# options
message.append_string options[:before_match]
message.append_string options[:after_match]
message.append_string options[:chunk_separator]
message.append_ints options[:limit], options[:around]
message.append_array options[:docs]
message.to_s
end
# Generation of the message to send to Sphinx to update attributes of a
# document.
def update_message(index, attributes, values_by_doc)
message = Message.new
message.append_string index
message.append_array attributes
message.append_int values_by_doc.length
values_by_doc.each do |key,values|
message.append_64bit_int key # document ID
message.append_ints *values # array of new values (integers)
end
message.to_s
end
# Generates the simple message to send to the daemon for a keywords request.
def keywords_message(query, index, return_hits)
message = Message.new
message.append_string query
message.append_string index
message.append_int return_hits ? 1 : 0
message.to_s
end
AttributeHandlers = {
AttributeTypes[:integer] => :next_int,
AttributeTypes[:timestamp] => :next_int,
AttributeTypes[:ordinal] => :next_int,
AttributeTypes[:bool] => :next_int,
AttributeTypes[:float] => :next_float,
AttributeTypes[:bigint] => :next_64bit_int,
AttributeTypes[:string] => :next,
AttributeTypes[:multi] + AttributeTypes[:integer] => :next_int_array
}
def attribute_from_type(type, response)
handler = AttributeHandlers[type]
response.send handler
end
def excerpt_flags(options)
flags = 1
flags |= 2 if options[:exact_phrase]
flags |= 4 if options[:single_passage]
flags |= 8 if options[:use_boundaries]
flags |= 16 if options[:weight_order]
flags |= 32 if options[:query_mode]
flags |= 64 if options[:force_all_words]
flags |= 128 if options[:load_files]
flags |= 256 if options[:allow_empty]
flags |= 512 if options[:emit_zones]
flags |= 1024 if options[:load_files_scattered]
flags
end
end
end