sphinx 0.9.10 → 0.9.10.2043

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/.gitignore CHANGED
@@ -1,2 +1,2 @@
1
- doc
2
- pkg
1
+ rdoc
2
+ pkg
data/Rakefile CHANGED
@@ -31,6 +31,6 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
31
31
  rdoc.rdoc_dir = 'rdoc'
32
32
  rdoc.title = 'Sphinx Client API'
33
33
  rdoc.options << '--line-numbers' << '--inline-source'
34
- rdoc.rdoc_files.include('README')
34
+ rdoc.rdoc_files.include('README.rdoc')
35
35
  rdoc.rdoc_files.include('lib/**/*.rb')
36
36
  end
data/VERSION.yml CHANGED
@@ -2,3 +2,4 @@
2
2
  :major: 0
3
3
  :minor: 9
4
4
  :patch: 10
5
+ :build: 2043
data/lib/sphinx.rb CHANGED
@@ -1,6 +1,12 @@
1
+ require 'socket'
2
+ require 'net/protocol'
3
+
4
+ module Sphinx
5
+ end
6
+
1
7
  require File.dirname(__FILE__) + '/sphinx/request'
2
8
  require File.dirname(__FILE__) + '/sphinx/response'
9
+ require File.dirname(__FILE__) + '/sphinx/timeout'
10
+ require File.dirname(__FILE__) + '/sphinx/buffered_io'
11
+ require File.dirname(__FILE__) + '/sphinx/server'
3
12
  require File.dirname(__FILE__) + '/sphinx/client'
4
-
5
- module Sphinx
6
- end
@@ -0,0 +1,22 @@
1
+ class Sphinx::BufferedIO < Net::BufferedIO # :nodoc:
2
+ BUFSIZE = 1024 * 16
3
+
4
+ if RUBY_VERSION < '1.9.1'
5
+ def rbuf_fill
6
+ begin
7
+ @rbuf << @io.read_nonblock(BUFSIZE)
8
+ rescue Errno::EWOULDBLOCK
9
+ retry unless @read_timeout
10
+ if IO.select([@io], nil, nil, @read_timeout)
11
+ retry
12
+ else
13
+ raise Timeout::Error, 'IO timeout'
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def setsockopt(*args)
20
+ @io.setsockopt(*args)
21
+ end
22
+ end
data/lib/sphinx/client.rb CHANGED
@@ -24,13 +24,10 @@
24
24
  # docs = posts.map(&:body)
25
25
  # excerpts = sphinx.BuildExcerpts(docs, 'index', 'test')
26
26
 
27
- require 'socket'
28
-
29
27
  module Sphinx
30
28
  # :stopdoc:
31
29
 
32
30
  class SphinxError < StandardError; end
33
- class SphinxArgumentError < SphinxError; end
34
31
  class SphinxConnectError < SphinxError; end
35
32
  class SphinxResponseError < SphinxError; end
36
33
  class SphinxInternalError < SphinxError; end
@@ -40,7 +37,6 @@ module Sphinx
40
37
  # :startdoc:
41
38
 
42
39
  class Client
43
-
44
40
  # :stopdoc:
45
41
 
46
42
  # Known searchd commands
@@ -91,6 +87,12 @@ module Sphinx
91
87
  SEARCHD_RETRY = 2
92
88
  # general success, warning message and command-specific reply follow
93
89
  SEARCHD_WARNING = 3
90
+
91
+ attr_reader :servers
92
+ attr_reader :timeout
93
+ attr_reader :retries
94
+ attr_reader :reqtimeout
95
+ attr_reader :reqretries
94
96
 
95
97
  # :startdoc:
96
98
 
@@ -191,12 +193,6 @@ module Sphinx
191
193
 
192
194
  # Constructs the <tt>Sphinx::Client</tt> object and sets options to their default values.
193
195
  def initialize
194
- # per-client-object settings
195
- @host = 'localhost' # searchd host (default is "localhost")
196
- @port = 3312 # searchd port (default is 3312)
197
- @path = false
198
- @socket = false
199
-
200
196
  # per-query settings
201
197
  @offset = 0 # how many records to seek from result-set start (default is 0)
202
198
  @limit = 20 # how many records to return from result-set starting at offset (default is 20)
@@ -231,56 +227,204 @@ module Sphinx
231
227
  @reqs = [] # requests storage (for multi-query case)
232
228
  @mbenc = '' # stored mbstring encoding
233
229
  @timeout = 0 # connect timeout
230
+ @retries = 1 # number of connect retries in case of emergency
231
+ @reqtimeout = 0 # request timeout
232
+ @reqretries = 1 # number of request retries in case of emergency
233
+
234
+ # per-client-object settings
235
+ # searchd servers list
236
+ @servers = [Sphinx::Server.new(self, 'localhost', 3312, false)].freeze
237
+ @lastserver = -1
234
238
  end
235
239
 
236
- # Get last error message.
240
+ # Returns last error message, as a string, in human readable format. If there
241
+ # were no errors during the previous API call, empty string is returned.
242
+ #
243
+ # You should call it when any other function (such as +Query+) fails (typically,
244
+ # the failing function returns false). The returned string will contain the
245
+ # error description.
246
+ #
247
+ # The error message is not reset by this call; so you can safely call it
248
+ # several times if needed.
249
+ #
237
250
  def GetLastError
238
251
  @error
239
252
  end
240
253
 
241
- # Get last warning message.
254
+ # Returns last warning message, as a string, in human readable format. If there
255
+ # were no warnings during the previous API call, empty string is returned.
256
+ #
257
+ # You should call it to verify whether your request (such as +Query+) was
258
+ # completed but with warnings. For instance, search query against a distributed
259
+ # index might complete succesfully even if several remote agents timed out.
260
+ # In that case, a warning message would be produced.
261
+ #
262
+ # The warning message is not reset by this call; so you can safely call it
263
+ # several times if needed.
264
+ #
242
265
  def GetLastWarning
243
266
  @warning
244
267
  end
245
268
 
246
- # Get last error flag (to tell network connection errors from
247
- # searchd errors or broken responses)
269
+ # Checks whether the last error was a network error on API side, or a
270
+ # remote error reported by searchd. Returns true if the last connection
271
+ # attempt to searchd failed on API side, false otherwise (if the error
272
+ # was remote, or there were no connection attempts at all).
273
+ #
248
274
  def IsConnectError
249
- @connerror
275
+ @connerror || false
250
276
  end
251
277
 
252
- # Set searchd host name (string) and port (integer).
278
+ # Sets searchd host name and TCP port. All subsequent requests will
279
+ # use the new host and port settings. Default +host+ and +port+ are
280
+ # 'localhost' and 3312, respectively.
281
+ #
282
+ # Also, you can specify an absolute path to Sphinx's UNIX socket as +host+,
283
+ # in this case pass port as +0+ or +nil+.
284
+ #
253
285
  def SetServer(host, port)
254
- assert { host.instance_of? String }
286
+ raise ArgumentError, '"host" argument must be String' unless host.kind_of?(String)
255
287
 
288
+ path = nil
289
+ # Check if UNIX socket should be used
256
290
  if host[0] == ?/
257
- @path = host
258
- return
291
+ path = host
259
292
  elsif host[0, 7] == 'unix://'
260
- @path = host[7..-1]
293
+ path = host[7..-1]
294
+ else
295
+ raise ArgumentError, '"port" argument must be Integer' unless port.respond_to?(:integer?) and port.integer?
261
296
  end
297
+
298
+ host = port = nil unless path.nil?
299
+
300
+ @servers = [Sphinx::Server.new(self, host, port, path)].freeze
301
+ end
302
+
303
+ # Sets the list of searchd servers. Each subsequent request will use next
304
+ # server in list (round-robin). In case of one server failure, request could
305
+ # be retried on another server (see +SetConnectTimeout+ and +SetRequestTimeout+).
306
+ #
307
+ # Method accepts an +Array+ of +Hash+es, each of them should have :host
308
+ # and :port (to connect to searchd through network) or :path (an absolute path
309
+ # to UNIX socket) specified.
310
+ #
311
+ def SetServers(servers)
312
+ raise ArgumentError, '"servers" argument must be Array' unless servers.kind_of?(Array)
313
+ raise ArgumentError, '"servers" argument must be not empty' if servers.empty?
262
314
 
263
- assert { port.instance_of? Fixnum }
315
+ @servers = servers.map do |server|
316
+ raise ArgumentError, '"servers" argument must be Array of Hashes' unless server.kind_of?(Hash)
317
+
318
+ host = server[:path] || server['path'] || server[:host] || server['host']
319
+ port = server[:port] || server['port']
320
+ path = nil
321
+ raise ArgumentError, '"host" argument must be String' unless host.kind_of?(String)
322
+
323
+ # Check if UNIX socket should be used
324
+ if host[0] == ?/
325
+ path = host
326
+ elsif host[0, 7] == 'unix://'
327
+ path = host[7..-1]
328
+ else
329
+ raise ArgumentError, '"port" argument must be Integer' unless port.respond_to?(:integer?) and port.integer?
330
+ end
264
331
 
265
- @host = host
266
- @port = port
332
+ host = port = nil unless path.nil?
333
+
334
+ Sphinx::Server.new(self, host, port, path)
335
+ end.freeze
267
336
  end
268
337
 
269
- def SetConnectTimeout(timeout)
270
- assert { timeout.instance_of? Fixnum }
338
+ # Sets the time allowed to spend connecting to the server before giving up
339
+ # and number of retries to perform.
340
+ #
341
+ # In the event of a failure to connect, an appropriate error code should
342
+ # be returned back to the application in order for application-level error
343
+ # handling to advise the user.
344
+ #
345
+ # When multiple servers configured through +SetServers+ method, and +retries+
346
+ # number is greater than 1, library will try to connect to another server.
347
+ # In case of single server configured, it will try to reconnect +retries+
348
+ # times.
349
+ #
350
+ # Please note, this timeout will only be used for connection establishing, not
351
+ # for regular API requests.
352
+ #
353
+ def SetConnectTimeout(timeout, retries = 1)
354
+ raise ArgumentError, '"timeout" argument must be Integer' unless timeout.respond_to?(:integer?) and timeout.integer?
355
+ raise ArgumentError, '"retries" argument must be Integer' unless retries.respond_to?(:integer?) and retries.integer?
356
+ raise ArgumentError, '"retries" argument must be greater than 0' unless retries > 0
271
357
 
272
358
  @timeout = timeout
359
+ @retries = retries
360
+ end
361
+
362
+ # Sets the time allowed to spend performing request to the server before giving up
363
+ # and number of retries to perform.
364
+ #
365
+ # In the event of a failure to do request, an appropriate error code should
366
+ # be returned back to the application in order for application-level error
367
+ # handling to advise the user.
368
+ #
369
+ # When multiple servers configured through +SetServers+ method, and +retries+
370
+ # number is greater than 1, library will try to do another try with this server
371
+ # (with full reconnect). If connection would fail, behavior depends on
372
+ # +SetConnectTimeout+ settings.
373
+ #
374
+ # Please note, this timeout will only be used for request performing, not
375
+ # for connection establishing.
376
+ #
377
+ def SetRequestTimeout(timeout, retries = 1)
378
+ raise ArgumentError, '"timeout" argument must be Integer' unless timeout.respond_to?(:integer?) and timeout.integer?
379
+ raise ArgumentError, '"retries" argument must be Integer' unless retries.respond_to?(:integer?) and retries.integer?
380
+ raise ArgumentError, '"retries" argument must be greater than 0' unless retries > 0
381
+
382
+ @reqtimeout = timeout
383
+ @reqretries = retries
273
384
  end
274
385
 
275
- # Set offset and count into result set,
276
- # and optionally set max-matches and cutoff limits.
386
+ # Sets offset into server-side result set (+offset+) and amount of matches to
387
+ # return to client starting from that offset (+limit+). Can additionally control
388
+ # maximum server-side result set size for current query (+max_matches+) and the
389
+ # threshold amount of matches to stop searching at (+cutoff+). All parameters
390
+ # must be non-negative integers.
391
+ #
392
+ # First two parameters to +SetLimits+ are identical in behavior to MySQL LIMIT
393
+ # clause. They instruct searchd to return at most +limit+ matches starting from
394
+ # match number +offset+. The default offset and limit settings are +0+ and +20+,
395
+ # that is, to return first +20+ matches.
396
+ #
397
+ # +max_matches+ setting controls how much matches searchd will keep in RAM
398
+ # while searching. All matching documents will be normally processed, ranked,
399
+ # filtered, and sorted even if max_matches is set to +1+. But only best +N+
400
+ # documents are stored in memory at any given moment for performance and RAM
401
+ # usage reasons, and this setting controls that N. Note that there are two
402
+ # places where max_matches limit is enforced. Per-query limit is controlled
403
+ # by this API call, but there also is per-server limit controlled by +max_matches+
404
+ # setting in the config file. To prevent RAM usage abuse, server will not
405
+ # allow to set per-query limit higher than the per-server limit.
406
+ #
407
+ # You can't retrieve more than +max_matches+ matches to the client application.
408
+ # The default limit is set to +1000+. Normally, you must not have to go over
409
+ # this limit. One thousand records is enough to present to the end user.
410
+ # And if you're thinking about pulling the results to application for further
411
+ # sorting or filtering, that would be much more efficient if performed on
412
+ # Sphinx side.
413
+ #
414
+ # +cutoff+ setting is intended for advanced performance control. It tells
415
+ # searchd to forcibly stop search query once $cutoff matches had been found
416
+ # and processed.
417
+ #
277
418
  def SetLimits(offset, limit, max = 0, cutoff = 0)
278
- assert { offset.instance_of? Fixnum }
279
- assert { limit.instance_of? Fixnum }
280
- assert { max.instance_of? Fixnum }
281
- assert { offset >= 0 }
282
- assert { limit > 0 }
283
- assert { max >= 0 }
419
+ raise ArgumentError, '"offset" argument must be Integer' unless offset.respond_to?(:integer?) and offset.integer?
420
+ raise ArgumentError, '"limit" argument must be Integer' unless limit.respond_to?(:integer?) and limit.integer?
421
+ raise ArgumentError, '"max" argument must be Integer' unless max.respond_to?(:integer?) and max.integer?
422
+ raise ArgumentError, '"cutoff" argument must be Integer' unless cutoff.respond_to?(:integer?) and cutoff.integer?
423
+
424
+ raise ArgumentError, '"offset" argument should be greater or equal to zero' unless offset >= 0
425
+ raise ArgumentError, '"limit" argument should be greater to zero' unless limit > 0
426
+ raise ArgumentError, '"max" argument should be greater or equal to zero' unless max >= 0
427
+ raise ArgumentError, '"cutoff" argument should be greater or equal to zero' unless cutoff >= 0
284
428
 
285
429
  @offset = offset
286
430
  @limit = limit
@@ -288,51 +432,92 @@ module Sphinx
288
432
  @cutoff = cutoff if cutoff > 0
289
433
  end
290
434
 
291
- # Set maximum query time, in milliseconds, per-index,
292
- # integer, 0 means "do not limit"
435
+ # Sets maximum search query time, in milliseconds. Parameter must be a
436
+ # non-negative integer. Default valus is +0+ which means "do not limit".
437
+ #
438
+ # Similar to +cutoff+ setting from +SetLimits+, but limits elapsed query
439
+ # time instead of processed matches count. Local search queries will be
440
+ # stopped once that much time has elapsed. Note that if you're performing
441
+ # a search which queries several local indexes, this limit applies to each
442
+ # index separately.
443
+ #
293
444
  def SetMaxQueryTime(max)
294
- assert { max.instance_of? Fixnum }
295
- assert { max >= 0 }
445
+ raise ArgumentError, '"max" argument must be Integer' unless max.respond_to?(:integer?) and max.integer?
446
+ raise ArgumentError, '"max" argument should be greater or equal to zero' unless max >= 0
447
+
296
448
  @maxquerytime = max
297
449
  end
298
450
 
299
- # Set matching mode.
451
+ # Sets full-text query matching mode.
452
+ #
453
+ # Parameter must be a +Fixnum+ constant specifying one of the known modes
454
+ # (+SPH_MATCH_ALL+, +SPH_MATCH_ANY+, etc), +String+ with identifier (<tt>"all"</tt>,
455
+ # <tt>"any"</tt>, etc), or a +Symbol+ (<tt>:all</tt>, <tt>:any</tt>, etc).
456
+ #
457
+ # Corresponding sections in Sphinx reference manual:
458
+ # * {Section 4.1, "Matching modes"}[http://www.sphinxsearch.com/docs/current.html#matching-modes] for details.
459
+ # * {Section 6.3.1, "SetMatchMode"}[http://www.sphinxsearch.com/docs/current.html#api-func-setmatchmode] for details.
460
+ #
300
461
  def SetMatchMode(mode)
301
- assert { mode == SPH_MATCH_ALL \
302
- || mode == SPH_MATCH_ANY \
303
- || mode == SPH_MATCH_PHRASE \
304
- || mode == SPH_MATCH_BOOLEAN \
305
- || mode == SPH_MATCH_EXTENDED \
306
- || mode == SPH_MATCH_FULLSCAN \
307
- || mode == SPH_MATCH_EXTENDED2 }
462
+ case mode
463
+ when String, Symbol
464
+ begin
465
+ mode = self.class.const_get("SPH_MATCH_#{mode.to_s.upcase}")
466
+ rescue NameError
467
+ raise ArgumentError, "\"mode\" argument value \"#{mode}\" is invalid"
468
+ end
469
+ when Fixnum
470
+ raise ArgumentError, "\"mode\" argument value \"#{mode}\" is invalid" unless (SPH_MATCH_ALL..SPH_MATCH_EXTENDED2).include?(mode)
471
+ else
472
+ raise ArgumentError, '"mode" argument must be Fixnum, String, or Symbol'
473
+ end
308
474
 
309
475
  @mode = mode
310
476
  end
311
477
 
312
478
  # Set ranking mode.
479
+ #
480
+ # You can specify ranking mode as String ("proximity_bm25", "bm25", etc),
481
+ # Symbol (:proximity_bm25, :bm25, etc), or
482
+ # Fixnum constant (SPH_RANK_PROXIMITY_BM25, SPH_RANK_BM25, etc).
313
483
  def SetRankingMode(ranker)
314
- assert { ranker == SPH_RANK_PROXIMITY_BM25 \
315
- || ranker == SPH_RANK_BM25 \
316
- || ranker == SPH_RANK_NONE \
317
- || ranker == SPH_RANK_WORDCOUNT \
318
- || ranker == SPH_RANK_PROXIMITY \
319
- || ranker == SPH_RANK_MATCHANY \
320
- || ranker == SPH_RANK_FIELDMASK \
321
- || ranker == SPH_RANK_SPH04 }
484
+ case ranker
485
+ when String, Symbol
486
+ begin
487
+ ranker = self.class.const_get("SPH_RANK_#{ranker.to_s.upcase}")
488
+ rescue NameError
489
+ raise ArgumentError, "\"ranker\" argument value \"#{ranker}\" is invalid"
490
+ end
491
+ when Fixnum
492
+ raise ArgumentError, "\"ranker\" argument value \"#{ranker}\" is invalid" unless (SPH_RANK_PROXIMITY_BM25..SPH_RANK_SPH04).include?(ranker)
493
+ else
494
+ raise ArgumentError, '"ranker" argument must be Fixnum, String, or Symbol'
495
+ end
322
496
 
323
497
  @ranker = ranker
324
498
  end
325
499
 
326
500
  # Set matches sorting mode.
501
+ #
502
+ # You can specify sorting mode as String ("relevance", "attr_desc", etc),
503
+ # Symbol (:relevance, :attr_desc, etc), or
504
+ # Fixnum constant (SPH_SORT_RELEVANCE, SPH_SORT_ATTR_DESC, etc).
327
505
  def SetSortMode(mode, sortby = '')
328
- assert { mode == SPH_SORT_RELEVANCE \
329
- || mode == SPH_SORT_ATTR_DESC \
330
- || mode == SPH_SORT_ATTR_ASC \
331
- || mode == SPH_SORT_TIME_SEGMENTS \
332
- || mode == SPH_SORT_EXTENDED \
333
- || mode == SPH_SORT_EXPR }
334
- assert { sortby.instance_of? String }
335
- assert { mode == SPH_SORT_RELEVANCE || !sortby.empty? }
506
+ case mode
507
+ when String, Symbol
508
+ begin
509
+ mode = self.class.const_get("SPH_SORT_#{mode.to_s.upcase}")
510
+ rescue NameError
511
+ raise ArgumentError, "\"mode\" argument value \"#{mode}\" is invalid"
512
+ end
513
+ when Fixnum
514
+ raise ArgumentError, "\"mode\" argument value \"#{mode}\" is invalid" unless (SPH_SORT_RELEVANCE..SPH_SORT_EXPR).include?(mode)
515
+ else
516
+ raise ArgumentError, '"mode" argument must be Fixnum, String, or Symbol'
517
+ end
518
+
519
+ raise ArgumentError, '"sortby" argument must be String' unless sortby.kind_of?(String)
520
+ raise ArgumentError, '"sortby" should not be empty unless mode is SPH_SORT_RELEVANCE' unless mode == SPH_SORT_RELEVANCE or !sortby.empty?
336
521
 
337
522
  @sort = mode
338
523
  @sortby = sortby
@@ -342,9 +527,9 @@ module Sphinx
342
527
  #
343
528
  # DEPRECATED; use SetFieldWeights() instead.
344
529
  def SetWeights(weights)
345
- assert { weights.instance_of? Array }
530
+ raise ArgumentError, '"weights" argument must be Array' unless weights.kind_of?(Array)
346
531
  weights.each do |weight|
347
- assert { weight.instance_of? Fixnum }
532
+ raise ArgumentError, '"weights" argument must be Array of integers' unless weight.respond_to?(:integer?) and weight.integer?
348
533
  end
349
534
 
350
535
  @weights = weights
@@ -352,15 +537,16 @@ module Sphinx
352
537
 
353
538
  # Bind per-field weights by name.
354
539
  #
355
- # Takes string (field name) to integer name (field weight) hash as an argument.
540
+ # Takes string (field name) to integer (field weight) hash as an argument.
356
541
  # * Takes precedence over SetWeights().
357
542
  # * Unknown names will be silently ignored.
358
543
  # * Unbound fields will be silently given a weight of 1.
359
544
  def SetFieldWeights(weights)
360
- assert { weights.instance_of? Hash }
545
+ raise ArgumentError, '"weights" argument must be Hash' unless weights.kind_of?(Hash)
361
546
  weights.each do |name, weight|
362
- assert { name.instance_of? String }
363
- assert { weight.instance_of? Fixnum }
547
+ unless (name.kind_of?(String) or name.kind_of?(Symbol)) and (weight.respond_to?(:integer?) and weight.integer?)
548
+ raise ArgumentError, '"weights" argument must be Hash map of strings to integers'
549
+ end
364
550
  end
365
551
 
366
552
  @fieldweights = weights
@@ -368,10 +554,11 @@ module Sphinx
368
554
 
369
555
  # Bind per-index weights by name.
370
556
  def SetIndexWeights(weights)
371
- assert { weights.instance_of? Hash }
557
+ raise ArgumentError, '"weights" argument must be Hash' unless weights.kind_of?(Hash)
372
558
  weights.each do |index, weight|
373
- assert { index.instance_of? String }
374
- assert { weight.instance_of? Fixnum }
559
+ unless (index.kind_of?(String) or index.kind_of?(Symbol)) and (weight.respond_to?(:integer?) and weight.integer?)
560
+ raise ArgumentError, '"weights" argument must be Hash map of strings to integers'
561
+ end
375
562
  end
376
563
 
377
564
  @indexweights = weights
@@ -381,9 +568,9 @@ module Sphinx
381
568
  #
382
569
  # Only match records if document ID is beetwen <tt>min_id</tt> and <tt>max_id</tt> (inclusive).
383
570
  def SetIDRange(min, max)
384
- assert { min.instance_of?(Fixnum) or min.instance_of?(Bignum) }
385
- assert { max.instance_of?(Fixnum) or max.instance_of?(Bignum) }
386
- assert { min <= max }
571
+ raise ArgumentError, '"min" argument must be Integer' unless min.respond_to?(:integer?) and min.integer?
572
+ raise ArgumentError, '"max" argument must be Integer' unless max.respond_to?(:integer?) and max.integer?
573
+ raise ArgumentError, '"max" argument greater or equal to "min"' unless min <= max
387
574
 
388
575
  @min_id = min
389
576
  @max_id = max
@@ -394,17 +581,16 @@ module Sphinx
394
581
  # Only match those records where <tt>attribute</tt> column values
395
582
  # are in specified set.
396
583
  def SetFilter(attribute, values, exclude = false)
397
- assert { attribute.instance_of? String }
398
- assert { values.instance_of? Array }
399
- assert { !values.empty? }
584
+ raise ArgumentError, '"attribute" argument must be String or Symbol' unless attribute.kind_of?(String) or attribute.kind_of?(Symbol)
585
+ raise ArgumentError, '"values" argument must be Array' unless values.kind_of?(Array)
586
+ raise ArgumentError, '"values" argument must not be empty' if values.empty?
587
+ raise ArgumentError, '"exclude" argument must be Boolean' unless exclude.kind_of?(TrueClass) or exclude.kind_of?(FalseClass)
400
588
 
401
- if values.instance_of?(Array) && values.size > 0
402
- values.each do |value|
403
- assert { value.instance_of? Fixnum }
404
- end
405
-
406
- @filters << { 'type' => SPH_FILTER_VALUES, 'attr' => attribute, 'exclude' => exclude, 'values' => values }
589
+ values.each do |value|
590
+ raise ArgumentError, '"values" argument must be Array of Integer' unless value.respond_to?(:integer?) and value.integer?
407
591
  end
592
+
593
+ @filters << { 'type' => SPH_FILTER_VALUES, 'attr' => attribute.to_s, 'exclude' => exclude, 'values' => values }
408
594
  end
409
595
 
410
596
  # Set range filter.
@@ -412,12 +598,13 @@ module Sphinx
412
598
  # Only match those records where <tt>attribute</tt> column value
413
599
  # is beetwen <tt>min</tt> and <tt>max</tt> (including <tt>min</tt> and <tt>max</tt>).
414
600
  def SetFilterRange(attribute, min, max, exclude = false)
415
- assert { attribute.instance_of? String }
416
- assert { min.instance_of? Fixnum or min.instance_of? Bignum }
417
- assert { max.instance_of? Fixnum or max.instance_of? Bignum }
418
- assert { min <= max }
601
+ raise ArgumentError, '"attribute" argument must be String or Symbol' unless attribute.kind_of?(String) or attribute.kind_of?(Symbol)
602
+ raise ArgumentError, '"min" argument must be Integer' unless min.respond_to?(:integer?) and min.integer?
603
+ raise ArgumentError, '"max" argument must be Integer' unless max.respond_to?(:integer?) and max.integer?
604
+ raise ArgumentError, '"max" argument greater or equal to "min"' unless min <= max
605
+ raise ArgumentError, '"exclude" argument must be Boolean' unless exclude.kind_of?(TrueClass) or exclude.kind_of?(FalseClass)
419
606
 
420
- @filters << { 'type' => SPH_FILTER_RANGE, 'attr' => attribute, 'exclude' => exclude, 'min' => min, 'max' => max }
607
+ @filters << { 'type' => SPH_FILTER_RANGE, 'attr' => attribute.to_s, 'exclude' => exclude, 'min' => min, 'max' => max }
421
608
  end
422
609
 
423
610
  # Set float range filter.
@@ -425,12 +612,13 @@ module Sphinx
425
612
  # Only match those records where <tt>attribute</tt> column value
426
613
  # is beetwen <tt>min</tt> and <tt>max</tt> (including <tt>min</tt> and <tt>max</tt>).
427
614
  def SetFilterFloatRange(attribute, min, max, exclude = false)
428
- assert { attribute.instance_of? String }
429
- assert { min.instance_of? Float }
430
- assert { max.instance_of? Float }
431
- assert { min <= max }
615
+ raise ArgumentError, '"attribute" argument must be String or Symbol' unless attribute.kind_of?(String) or attribute.kind_of?(Symbol)
616
+ raise ArgumentError, '"min" argument must be Float or Integer' unless min.kind_of?(Float) or (min.respond_to?(:integer?) and min.integer?)
617
+ raise ArgumentError, '"max" argument must be Float or Integer' unless max.kind_of?(Float) or (max.respond_to?(:integer?) and max.integer?)
618
+ raise ArgumentError, '"max" argument greater or equal to "min"' unless min <= max
619
+ raise ArgumentError, '"exclude" argument must be Boolean' unless exclude.kind_of?(TrueClass) or exclude.kind_of?(FalseClass)
432
620
 
433
- @filters << { 'type' => SPH_FILTER_FLOATRANGE, 'attr' => attribute, 'exclude' => exclude, 'min' => min, 'max' => max }
621
+ @filters << { 'type' => SPH_FILTER_FLOATRANGE, 'attr' => attribute.to_s, 'exclude' => exclude, 'min' => min.to_f, 'max' => max.to_f }
434
622
  end
435
623
 
436
624
  # Setup anchor point for geosphere distance calculations.
@@ -444,12 +632,12 @@ module Sphinx
444
632
  # * <tt>lat</tt> -- is anchor point latitude, in radians
445
633
  # * <tt>long</tt> -- is anchor point longitude, in radians
446
634
  def SetGeoAnchor(attrlat, attrlong, lat, long)
447
- assert { attrlat.instance_of? String }
448
- assert { attrlong.instance_of? String }
449
- assert { lat.instance_of? Float }
450
- assert { long.instance_of? Float }
635
+ raise ArgumentError, '"attrlat" argument must be String or Symbol' unless attrlat.kind_of?(String) or attrlat.kind_of?(Symbol)
636
+ raise ArgumentError, '"attrlong" argument must be String or Symbol' unless attrlong.kind_of?(String) or attrlong.kind_of?(Symbol)
637
+ raise ArgumentError, '"lat" argument must be Float or Integer' unless lat.kind_of?(Float) or (lat.respond_to?(:integer?) and lat.integer?)
638
+ raise ArgumentError, '"long" argument must be Float or Integer' unless long.kind_of?(Float) or (long.respond_to?(:integer?) and long.integer?)
451
639
 
452
- @anchor = { 'attrlat' => attrlat, 'attrlong' => attrlong, 'lat' => lat, 'long' => long }
640
+ @anchor = { 'attrlat' => attrlat.to_s, 'attrlong' => attrlong.to_s, 'lat' => lat.to_f, 'long' => long.to_f }
453
641
  end
454
642
 
455
643
  # Set grouping attribute and function.
@@ -489,60 +677,167 @@ module Sphinx
489
677
  # matches published, with day number and per-day match count attached,
490
678
  # and sorted by day number in descending order (ie. recent days first).
491
679
  def SetGroupBy(attribute, func, groupsort = '@group desc')
492
- assert { attribute.instance_of? String }
493
- assert { groupsort.instance_of? String }
494
- assert { func == SPH_GROUPBY_DAY \
495
- || func == SPH_GROUPBY_WEEK \
496
- || func == SPH_GROUPBY_MONTH \
497
- || func == SPH_GROUPBY_YEAR \
498
- || func == SPH_GROUPBY_ATTR \
499
- || func == SPH_GROUPBY_ATTRPAIR }
680
+ raise ArgumentError, '"attribute" argument must be String or Symbol' unless attribute.kind_of?(String) or attribute.kind_of?(Symbol)
681
+ raise ArgumentError, '"groupsort" argument must be String' unless groupsort.kind_of?(String)
682
+
683
+ case func
684
+ when String, Symbol
685
+ begin
686
+ func = self.class.const_get("SPH_GROUPBY_#{func.to_s.upcase}")
687
+ rescue NameError
688
+ raise ArgumentError, "\"func\" argument value \"#{func}\" is invalid"
689
+ end
690
+ when Fixnum
691
+ raise ArgumentError, "\"func\" argument value \"#{func}\" is invalid" unless (SPH_GROUPBY_DAY..SPH_GROUPBY_ATTRPAIR).include?(func)
692
+ else
693
+ raise ArgumentError, '"func" argument must be Fixnum, String, or Symbol'
694
+ end
500
695
 
501
- @groupby = attribute
696
+ @groupby = attribute.to_s
502
697
  @groupfunc = func
503
698
  @groupsort = groupsort
504
699
  end
505
700
 
506
701
  # Set count-distinct attribute for group-by queries.
507
702
  def SetGroupDistinct(attribute)
508
- assert { attribute.instance_of? String }
509
- @groupdistinct = attribute
703
+ raise ArgumentError, '"attribute" argument must be String or Symbol' unless attribute.kind_of?(String) or attribute.kind_of?(Symbol)
704
+
705
+ @groupdistinct = attribute.to_s
510
706
  end
511
707
 
512
- # Set distributed retries count and delay.
708
+ # Sets distributed retry count and delay.
709
+ #
710
+ # On temporary failures searchd will attempt up to +count+ retries per
711
+ # agent. +delay+ is the delay between the retries, in milliseconds. Retries
712
+ # are disabled by default. Note that this call will not make the API itself
713
+ # retry on temporary failure; it only tells searchd to do so. Currently,
714
+ # the list of temporary failures includes all kinds of +connect+
715
+ # failures and maxed out (too busy) remote agents.
716
+ #
513
717
  def SetRetries(count, delay = 0)
514
- assert { count.instance_of? Fixnum }
515
- assert { delay.instance_of? Fixnum }
718
+ raise ArgumentError, '"count" argument must be Integer' unless count.respond_to?(:integer?) and count.integer?
719
+ raise ArgumentError, '"delay" argument must be Integer' unless delay.respond_to?(:integer?) and delay.integer?
516
720
 
517
721
  @retrycount = count
518
722
  @retrydelay = delay
519
723
  end
520
724
 
521
- # Set attribute values override
725
+ # Sets temporary (per-query) per-document attribute value overrides. Only
726
+ # supports scalar attributes. +values+ must be a +Hash+ that maps document
727
+ # IDs to overridden attribute values.
728
+ #
729
+ # Override feature lets you "temporary" update attribute values for some
730
+ # documents within a single query, leaving all other queries unaffected.
731
+ # This might be useful for personalized data. For example, assume you're
732
+ # implementing a personalized search function that wants to boost the posts
733
+ # that the user's friends recommend. Such data is not just dynamic, but
734
+ # also personal; so you can't simply put it in the index because you don't
735
+ # want everyone's searches affected. Overrides, on the other hand, are local
736
+ # to a single query and invisible to everyone else. So you can, say, setup
737
+ # a "friends_weight" value for every document, defaulting to 0, then
738
+ # temporary override it with 1 for documents 123, 456 and 789 (recommended
739
+ # by exactly the friends of current user), and use that value when ranking.
522
740
  #
523
- # There can be only one override per attribute.
524
- # +values+ must be a hash that maps document IDs to attribute values.
525
741
  def SetOverride(attrname, attrtype, values)
526
- assert { attrname.instance_of? String }
527
- assert { [SPH_ATTR_INTEGER, SPH_ATTR_TIMESTAMP, SPH_ATTR_BOOL, SPH_ATTR_FLOAT, SPH_ATTR_BIGINT].include?(attrtype) }
528
- assert { values.instance_of? Hash }
742
+ raise ArgumentError, '"attrname" argument must be String or Symbol' unless attrname.kind_of?(String) or attrname.kind_of?(Symbol)
743
+
744
+ case attrtype
745
+ when String, Symbol
746
+ begin
747
+ attrtype = self.class.const_get("SPH_ATTR_#{attrtype.to_s.upcase}")
748
+ rescue NameError
749
+ raise ArgumentError, "\"attrtype\" argument value \"#{attrtype}\" is invalid"
750
+ end
751
+ when Fixnum
752
+ raise ArgumentError, "\"attrtype\" argument value \"#{attrtype}\" is invalid" unless (SPH_ATTR_INTEGER..SPH_ATTR_BIGINT).include?(attrtype)
753
+ else
754
+ raise ArgumentError, '"attrtype" argument must be Fixnum, String, or Symbol'
755
+ end
529
756
 
530
- @overrides << { 'attr' => attrname, 'type' => attrtype, 'values' => values }
757
+ raise ArgumentError, '"values" argument must be Hash' unless values.kind_of?(Hash)
758
+
759
+ values.each do |id, value|
760
+ raise ArgumentError, '"values" argument must be Hash map of Integer to Integer or Time' unless id.respond_to?(:integer?) and id.integer?
761
+ case attrtype
762
+ when SPH_ATTR_TIMESTAMP
763
+ raise ArgumentError, '"values" argument must be Hash map of Integer to Integer or Time' unless (value.respond_to?(:integer?) and value.integer?) or value.kind_of?(Time)
764
+ when SPH_ATTR_FLOAT
765
+ raise ArgumentError, '"values" argument must be Hash map of Integer to Float or Integer' unless value.kind_of?(Float) or (value.respond_to?(:integer?) and value.integer?)
766
+ else
767
+ # SPH_ATTR_INTEGER, SPH_ATTR_ORDINAL, SPH_ATTR_BOOL, SPH_ATTR_BIGINT
768
+ raise ArgumentError, '"values" argument must be Hash map of Integer to Integer' unless value.respond_to?(:integer?) and value.integer?
769
+ end
770
+ end
771
+
772
+ @overrides << { 'attr' => attrname.to_s, 'type' => attrtype, 'values' => values }
531
773
  end
532
774
 
533
- # Set select-list (attributes or expressions), SQL-like syntax.
775
+ # Sets the select clause, listing specific attributes to fetch, and
776
+ # expressions to compute and fetch. Clause syntax mimics SQL.
777
+ #
778
+ # +SetSelect+ is very similar to the part of a typical SQL query between
779
+ # +SELECT+ and +FROM+. It lets you choose what attributes (columns) to
780
+ # fetch, and also what expressions over the columns to compute and fetch.
781
+ # A certain difference from SQL is that expressions must always be aliased
782
+ # to a correct identifier (consisting of letters and digits) using +AS+
783
+ # keyword. SQL also lets you do that but does not require to. Sphinx enforces
784
+ # aliases so that the computation results can always be returned under a
785
+ #{ }"normal" name in the result set, used in other clauses, etc.
786
+ #
787
+ # Everything else is basically identical to SQL. Star ('*') is supported.
788
+ # Functions are supported. Arbitrary amount of expressions is supported.
789
+ # Computed expressions can be used for sorting, filtering, and grouping,
790
+ # just as the regular attributes.
791
+ #
792
+ # Starting with version 0.9.9-rc2, aggregate functions (<tt>AVG()</tt>,
793
+ # <tt>MIN()</tt>, <tt>MAX()</tt>, <tt>SUM()</tt>) are supported when using
794
+ # <tt>GROUP BY</tt>.
795
+ #
796
+ # Expression sorting (Section 4.5, “SPH_SORT_EXPR mode”) and geodistance
797
+ # functions (+SetGeoAnchor+) are now internally implemented
798
+ # using this computed expressions mechanism, using magic names '<tt>@expr</tt>'
799
+ # and '<tt>@geodist</tt>' respectively.
800
+ #
801
+ # Usage example:
802
+ #
803
+ # sphinx.SetSelect('*, @weight+(user_karma+ln(pageviews))*0.1 AS myweight')
804
+ # sphinx.SetSelect('exp_years, salary_gbp*{$gbp_usd_rate} AS salary_usd, IF(age>40,1,0) AS over40')
805
+ # sphinx.SetSelect('*, AVG(price) AS avgprice')
806
+ #
534
807
  def SetSelect(select)
535
- assert { select.instance_of? String }
808
+ raise ArgumentError, '"select" argument must be String' unless select.kind_of?(String)
809
+
536
810
  @select = select
537
811
  end
538
812
 
539
- # Clear all filters (for multi-queries).
813
+ # Clears all currently set filters.
814
+ #
815
+ # This call is only normally required when using multi-queries. You might want
816
+ # to set different filters for different queries in the batch. To do that,
817
+ # you should call +ResetFilters+ and add new filters using the respective calls.
818
+ #
819
+ # Usage example:
820
+ #
821
+ # sphinx.ResetFilters
822
+ #
540
823
  def ResetFilters
541
824
  @filters = []
542
825
  @anchor = []
543
826
  end
544
827
 
545
- # Clear groupby settings (for multi-queries).
828
+ # Clears all currently group-by settings, and disables group-by.
829
+ #
830
+ # This call is only normally required when using multi-queries. You can
831
+ # change individual group-by settings using +SetGroupBy+ and +SetGroupDistinct+
832
+ # calls, but you can not disable group-by using those calls. +ResetGroupBy+
833
+ # fully resets previous group-by settings and disables group-by mode in the
834
+ # current state, so that subsequent +AddQuery+ calls can perform non-grouping
835
+ # searches.
836
+ #
837
+ # Usage example:
838
+ #
839
+ # sphinx.ResetGroupBy
840
+ #
546
841
  def ResetGroupBy
547
842
  @groupby = ''
548
843
  @groupfunc = SPH_GROUPBY_DAY
@@ -582,7 +877,6 @@ module Sphinx
582
877
  # * <tt>'time'</tt> -- search time
583
878
  # * <tt>'words'</tt> -- hash which maps query terms (stemmed!) to ('docs', 'hits') hash
584
879
  def Query(query, index = '*', comment = '')
585
- assert { @reqs.empty? }
586
880
  @reqs = []
587
881
 
588
882
  self.AddQuery(query, index, comment)
@@ -667,7 +961,7 @@ module Sphinx
667
961
  # per-index weights
668
962
  request.put_int @indexweights.length
669
963
  @indexweights.each do |idx, weight|
670
- request.put_string idx
964
+ request.put_string idx.to_s
671
965
  request.put_int weight
672
966
  end
673
967
 
@@ -677,7 +971,7 @@ module Sphinx
677
971
  # per-field weights
678
972
  request.put_int @fieldweights.length
679
973
  @fieldweights.each do |field, weight|
680
- request.put_string field
974
+ request.put_string field.to_s
681
975
  request.put_int weight
682
976
  end
683
977
 
@@ -690,17 +984,14 @@ module Sphinx
690
984
  request.put_string entry['attr']
691
985
  request.put_int entry['type'], entry['values'].size
692
986
  entry['values'].each do |id, val|
693
- assert { id.instance_of?(Fixnum) || id.instance_of?(Bignum) }
694
- assert { val.instance_of?(Fixnum) || val.instance_of?(Bignum) || val.instance_of?(Float) }
695
-
696
987
  request.put_int64 id
697
988
  case entry['type']
698
989
  when SPH_ATTR_FLOAT
699
- request.put_float val
990
+ request.put_float val.to_f
700
991
  when SPH_ATTR_BIGINT
701
- request.put_int64 val
992
+ request.put_int64 val.to_i
702
993
  else
703
- request.put_int val
994
+ request.put_int val.to_i
704
995
  end
705
996
  end
706
997
  end
@@ -723,6 +1014,7 @@ module Sphinx
723
1014
  #
724
1015
  # * <tt>'error'</tt> -- search error for this query
725
1016
  # * <tt>'words'</tt> -- hash which maps query terms (stemmed!) to ( "docs", "hits" ) hash
1017
+ #
726
1018
  def RunQueries
727
1019
  if @reqs.empty?
728
1020
  @error = 'No queries defined, issue AddQuery() first'
@@ -732,7 +1024,7 @@ module Sphinx
732
1024
  req = @reqs.join('')
733
1025
  nreqs = @reqs.length
734
1026
  @reqs = []
735
- response = PerformRequest(:search, req, nreqs)
1027
+ response = perform_request(:search, req, nreqs)
736
1028
 
737
1029
  # parse response
738
1030
  begin
@@ -868,23 +1160,28 @@ module Sphinx
868
1160
  #
869
1161
  # Returns false on failure.
870
1162
  # Returns an array of string excerpts on success.
1163
+ #
871
1164
  def BuildExcerpts(docs, index, words, opts = {})
872
- assert { docs.instance_of? Array }
873
- assert { index.instance_of? String }
874
- assert { words.instance_of? String }
875
- assert { opts.instance_of? Hash }
1165
+ raise ArgumentError, '"docs" argument must be Array' unless docs.kind_of?(Array)
1166
+ raise ArgumentError, '"index" argument must be String' unless index.kind_of?(String) or index.kind_of?(Symbol)
1167
+ raise ArgumentError, '"words" argument must be String' unless words.kind_of?(String)
1168
+ raise ArgumentError, '"opts" argument must be Hash' unless opts.kind_of?(Hash)
1169
+
1170
+ docs.each do |doc|
1171
+ raise ArgumentError, '"docs" argument must be Array of Strings' unless doc.kind_of?(String)
1172
+ end
876
1173
 
877
1174
  # fixup options
878
- opts['before_match'] ||= '<b>';
879
- opts['after_match'] ||= '</b>';
880
- opts['chunk_separator'] ||= ' ... ';
881
- opts['limit'] ||= 256;
882
- opts['around'] ||= 5;
883
- opts['exact_phrase'] ||= false
884
- opts['single_passage'] ||= false
885
- opts['use_boundaries'] ||= false
886
- opts['weight_order'] ||= false
887
- opts['query_mode'] ||= false
1175
+ opts['before_match'] ||= opts[:before_match] || '<b>';
1176
+ opts['after_match'] ||= opts[:after_match] || '</b>';
1177
+ opts['chunk_separator'] ||= opts[:chunk_separator] || ' ... ';
1178
+ opts['limit'] ||= opts[:limit] || 256;
1179
+ opts['around'] ||= opts[:around] || 5;
1180
+ opts['exact_phrase'] ||= opts[:exact_phrase] || false
1181
+ opts['single_passage'] ||= opts[:single_passage] || false
1182
+ opts['use_boundaries'] ||= opts[:use_boundaries] || false
1183
+ opts['weight_order'] ||= opts[:weight_order] || false
1184
+ opts['query_mode'] ||= opts[:query_mode] || false
888
1185
 
889
1186
  # build request
890
1187
 
@@ -899,7 +1196,7 @@ module Sphinx
899
1196
  request = Request.new
900
1197
  request.put_int 0, flags # mode=0, flags=1 (remove spaces)
901
1198
  # req index
902
- request.put_string index
1199
+ request.put_string index.to_s
903
1200
  # req words
904
1201
  request.put_string words
905
1202
 
@@ -911,13 +1208,9 @@ module Sphinx
911
1208
 
912
1209
  # documents
913
1210
  request.put_int docs.size
914
- docs.each do |doc|
915
- assert { doc.instance_of? String }
916
-
917
- request.put_string doc
918
- end
1211
+ request.put_string(*docs)
919
1212
 
920
- response = PerformRequest(:excerpt, request)
1213
+ response = perform_request(:excerpt, request)
921
1214
 
922
1215
  # parse response
923
1216
  begin
@@ -936,9 +1229,9 @@ module Sphinx
936
1229
  #
937
1230
  # Returns an array of words on success.
938
1231
  def BuildKeywords(query, index, hits)
939
- assert { query.instance_of? String }
940
- assert { index.instance_of? String }
941
- assert { hits.instance_of?(TrueClass) || hits.instance_of?(FalseClass) }
1232
+ raise ArgumentError, '"query" argument must be String' unless query.kind_of?(String)
1233
+ raise ArgumentError, '"index" argument must be String' unless index.kind_of?(String) or index.kind_of?(Symbol)
1234
+ raise ArgumentError, '"hits" argument must be Boolean' unless hits.kind_of?(TrueClass) or hits.kind_of?(FalseClass)
942
1235
 
943
1236
  # build request
944
1237
  request = Request.new
@@ -947,7 +1240,7 @@ module Sphinx
947
1240
  request.put_string index # req index
948
1241
  request.put_int hits ? 1 : 0
949
1242
 
950
- response = PerformRequest(:keywords, request)
1243
+ response = perform_request(:keywords, request)
951
1244
 
952
1245
  # parse response
953
1246
  begin
@@ -983,27 +1276,31 @@ module Sphinx
983
1276
  #
984
1277
  # Usage example:
985
1278
  # sphinx.UpdateAttributes('test1', ['group_id'], { 1 => [456] })
1279
+ # sphinx.UpdateAttributes('test1', ['group_id'], { 1 => [[456, 789]] }, true)
1280
+ #
986
1281
  def UpdateAttributes(index, attrs, values, mva = false)
987
1282
  # verify everything
988
- assert { index.instance_of? String }
989
- assert { mva.instance_of?(TrueClass) || mva.instance_of?(FalseClass) }
1283
+ raise ArgumentError, '"index" argument must be String' unless index.kind_of?(String) or index.kind_of?(Symbol)
1284
+ raise ArgumentError, '"mva" argument must be Boolean' unless mva.kind_of?(TrueClass) or mva.kind_of?(FalseClass)
990
1285
 
991
- assert { attrs.instance_of? Array }
1286
+ raise ArgumentError, '"attrs" argument must be Array' unless attrs.kind_of?(Array)
992
1287
  attrs.each do |attr|
993
- assert { attr.instance_of? String }
1288
+ raise ArgumentError, '"attrs" argument must be Array of Strings' unless attr.kind_of?(String) or attr.kind_of?(Symbol)
994
1289
  end
995
1290
 
996
- assert { values.instance_of? Hash }
1291
+ raise ArgumentError, '"values" argument must be Hash' unless values.kind_of?(Hash)
997
1292
  values.each do |id, entry|
998
- assert { id.instance_of? Fixnum }
999
- assert { entry.instance_of? Array }
1000
- assert { entry.length == attrs.length }
1293
+ raise ArgumentError, '"values" argument must be Hash map of Integer to Array' unless id.respond_to?(:integer?) and id.integer?
1294
+ raise ArgumentError, '"values" argument must be Hash map of Integer to Array' unless entry.kind_of?(Array)
1295
+ raise ArgumentError, "\"values\" argument Hash values Array must have #{attrs.length} elements" unless entry.length == attrs.length
1001
1296
  entry.each do |v|
1002
1297
  if mva
1003
- assert { v.instance_of? Array }
1004
- v.each { |vv| assert { vv.instance_of? Fixnum } }
1298
+ raise ArgumentError, '"values" argument must be Hash map of Integer to Array of Arrays' unless v.kind_of?(Array)
1299
+ v.each do |vv|
1300
+ raise ArgumentError, '"values" argument must be Hash map of Integer to Array of Arrays of Integers' unless vv.respond_to?(:integer?) and vv.integer?
1301
+ end
1005
1302
  else
1006
- assert { v.instance_of? Fixnum }
1303
+ raise ArgumentError, '"values" argument must be Hash map of Integer to Array of Integers' unless v.respond_to?(:integer?) and v.integer?
1007
1304
  end
1008
1305
  end
1009
1306
  end
@@ -1028,7 +1325,7 @@ module Sphinx
1028
1325
  end
1029
1326
  end
1030
1327
 
1031
- response = PerformRequest(:update, request)
1328
+ response = perform_request(:update, request)
1032
1329
 
1033
1330
  # parse response
1034
1331
  begin
@@ -1041,35 +1338,57 @@ module Sphinx
1041
1338
 
1042
1339
  # persistent connections
1043
1340
 
1341
+ # Opens persistent connection to the server.
1342
+ #
1044
1343
  def Open
1045
- unless @socket === false
1046
- @error = 'already connected'
1344
+ if @servers.size > 1
1345
+ @error = 'too many servers. persistent socket allowed only for a single server.'
1047
1346
  return false
1048
1347
  end
1049
1348
 
1349
+ if @servers.first.persistent?
1350
+ @error = 'already connected'
1351
+ return false;
1352
+ end
1353
+
1050
1354
  request = Request.new
1051
1355
  request.put_int(1)
1052
- @socket = PerformRequest(:persist, request, nil, true)
1356
+
1357
+ perform_request(:persist, request, nil) do |server, socket|
1358
+ server.make_persistent!(socket)
1359
+ end
1053
1360
 
1054
1361
  true
1055
1362
  end
1056
1363
 
1364
+ # Closes previously opened persistent connection.
1365
+ #
1057
1366
  def Close
1058
- if @socket === false
1367
+ if @servers.size > 1
1368
+ @error = 'too many servers. persistent socket allowed only for a single server.'
1369
+ return false
1370
+ end
1371
+
1372
+ unless @servers.first.persistent?
1059
1373
  @error = 'not connected'
1060
1374
  return false;
1061
1375
  end
1062
1376
 
1063
- @socket.close
1064
- @socket = false
1065
-
1066
- true
1377
+ @servers.first.close_persistent!
1067
1378
  end
1068
1379
 
1380
+ # Queries searchd status, and returns an array of status variable name
1381
+ # and value pairs.
1382
+ #
1383
+ # Usage example:
1384
+ #
1385
+ # status = sphinx.Status
1386
+ # puts status.map { |key, value| "#{key.rjust(20)}: #{value}" }
1387
+ #
1069
1388
  def Status
1070
1389
  request = Request.new
1071
1390
  request.put_int(1)
1072
- response = PerformRequest(:status, request)
1391
+ response = perform_request(:status, request)
1073
1392
 
1074
1393
  # parse response
1075
1394
  begin
@@ -1092,7 +1411,7 @@ module Sphinx
1092
1411
 
1093
1412
  def FlushAttrs
1094
1413
  request = Request.new
1095
- response = PerformRequest(:flushattrs, request)
1414
+ response = perform_request(:flushattrs, request)
1096
1415
 
1097
1416
  # parse response
1098
1417
  begin
@@ -1103,74 +1422,76 @@ module Sphinx
1103
1422
  end
1104
1423
 
1105
1424
  protected
1106
-
1107
- # Connect to searchd server.
1108
- def Connect
1109
- return @socket unless @socket === false
1110
-
1111
- begin
1112
- if @path
1113
- sock = UNIXSocket.new(@path)
1114
- else
1115
- sock = TCPSocket.new(@host, @port)
1116
- end
1117
- rescue => e
1118
- location = @path || "#{@host}:#{@port}"
1119
- @error = "connection to #{location} failed ("
1120
- if e.kind_of?(SystemCallError)
1121
- @error << "errno=#{e.class::Errno}, "
1122
- end
1123
- @error << "msg=#{e.message})"
1124
- @connerror = true
1125
- raise SphinxConnectError, @error
1126
- end
1425
+
1426
+ # Connect, send query, get response.
1427
+ #
1428
+ # Use this method to communicate with Sphinx server. It ensures connection
1429
+ # will be instantiated properly, all headers will be generated properly, etc.
1430
+ #
1431
+ # Parameters:
1432
+ # * +command+ -- searchd command to perform (<tt>:search</tt>, <tt>:excerpt</tt>,
1433
+ # <tt>:update</tt>, <tt>:keywords</tt>, <tt>:persist</tt>, <tt>:status</tt>,
1434
+ # <tt>:query</tt>, <tt>:flushattrs</tt>. See <tt>SEARCHD_COMMAND_*</tt> for details).
1435
+ # * +request+ -- an instance of <tt>Sphinx::Request</tt> class. Contains request body.
1436
+ # * +additional+ -- additional integer data to be placed between header and body.
1437
+ # * +block+ -- if given, response will not be parsed, plain socket will be
1438
+ # passed instead. this is special mode used for persistent connections,
1439
+ # do not use for other tasks.
1440
+ #
1441
+ def perform_request(command, request, additional = nil, &block)
1442
+ with_server do |server|
1443
+ cmd = command.to_s.upcase
1444
+ command_id = Sphinx::Client.const_get("SEARCHD_COMMAND_#{cmd}")
1445
+ command_ver = Sphinx::Client.const_get("VER_COMMAND_#{cmd}")
1127
1446
 
1128
- # send my version
1129
- # this is a subtle part. we must do it before (!) reading back from searchd.
1130
- # because otherwise under some conditions (reported on FreeBSD for instance)
1131
- # TCP stack could throttle write-write-read pattern because of Nagle.
1132
- sock.send([1].pack('N'), 0)
1133
-
1134
- v = sock.recv(4).unpack('N*').first
1135
- if v < 1
1136
- sock.close
1137
- @error = "expected searchd protocol version 1+, got version '#{v}'"
1138
- raise SphinxConnectError, @error
1447
+ with_socket(server) do |socket|
1448
+ len = request.to_s.length + (additional.nil? ? 0 : 4)
1449
+ header = [command_id, command_ver, len].pack('nnN')
1450
+ header << [additional].pack('N') unless additional.nil?
1451
+
1452
+ socket.write(header + request.to_s)
1453
+
1454
+ if block_given?
1455
+ yield server, socket
1456
+ else
1457
+ parse_response(socket, command_ver)
1458
+ end
1459
+ end
1139
1460
  end
1140
-
1141
- sock
1142
1461
  end
1143
-
1144
- # Get and check response packet from searchd server.
1145
- def GetResponse(sock, client_version)
1462
+
1463
+ # This is internal method which gets and parses response packet from
1464
+ # searchd server.
1465
+ #
1466
+ # There are several exceptions which could be thrown in this method:
1467
+ #
1468
+ # * various network errors -- should be handled by caller (see +with_socket+).
1469
+ # * +SphinxResponseError+ -- incomplete reply from searchd.
1470
+ # * +SphinxInternalError+ -- searchd error.
1471
+ # * +SphinxTemporaryError+ -- temporary searchd error.
1472
+ # * +SphinxUnknownError+ -- unknows searchd error.
1473
+ #
1474
+ # Method returns an instance of <tt>Sphinx::Response</tt> class, which
1475
+ # could be used for context-based parsing of reply from the server.
1476
+ #
1477
+ def parse_response(socket, client_version)
1146
1478
  response = ''
1147
- len = 0
1479
+ status = ver = len = 0
1148
1480
 
1149
- header = sock.recv(8)
1481
+ # Read server reply from server. All exceptions are handled by +with_socket+.
1482
+ header = socket.read(8)
1150
1483
  if header.length == 8
1151
1484
  status, ver, len = header.unpack('n2N')
1152
- left = len.to_i
1153
- while left > 0 do
1154
- begin
1155
- chunk = sock.recv(left)
1156
- if chunk
1157
- response << chunk
1158
- left -= chunk.length
1159
- end
1160
- rescue EOFError
1161
- break
1162
- end
1163
- end
1485
+ response = socket.read(len) if len > 0
1164
1486
  end
1165
- sock.close if @socket === false
1166
1487
 
1167
1488
  # check response
1168
1489
  read = response.length
1169
1490
  if response.empty? or read != len.to_i
1170
- @error = len \
1491
+ error = len > 0 \
1171
1492
  ? "failed to read searchd response (status=#{status}, ver=#{ver}, len=#{len}, read=#{read})" \
1172
1493
  : 'received zero-sized searchd response'
1173
- raise SphinxResponseError, @error
1494
+ raise SphinxResponseError, error
1174
1495
  end
1175
1496
 
1176
1497
  # check status
@@ -1181,18 +1502,18 @@ module Sphinx
1181
1502
  end
1182
1503
 
1183
1504
  if status == SEARCHD_ERROR
1184
- @error = 'searchd error: ' + response[4, response.length - 4]
1185
- raise SphinxInternalError, @error
1505
+ error = 'searchd error: ' + response[4, response.length - 4]
1506
+ raise SphinxInternalError, error
1186
1507
  end
1187
1508
 
1188
1509
  if status == SEARCHD_RETRY
1189
- @error = 'temporary searchd error: ' + response[4, response.length - 4]
1190
- raise SphinxTemporaryError, @error
1510
+ error = 'temporary searchd error: ' + response[4, response.length - 4]
1511
+ raise SphinxTemporaryError, error
1191
1512
  end
1192
1513
 
1193
1514
  unless status == SEARCHD_OK
1194
- @error = "unknown status code: '#{status}'"
1195
- raise SphinxUnknownError, @error
1515
+ error = "unknown status code: '#{status}'"
1516
+ raise SphinxUnknownError, error
1196
1517
  end
1197
1518
 
1198
1519
  # check version
@@ -1201,30 +1522,111 @@ module Sphinx
1201
1522
  "v.#{client_version >> 8}.#{client_version & 0xff}, some options might not work"
1202
1523
  end
1203
1524
 
1204
- return response
1525
+ Response.new(response)
1205
1526
  end
1206
1527
 
1207
- # Connect, send query, get response.
1208
- def PerformRequest(command, request, additional = nil, skip_response = false)
1209
- cmd = command.to_s.upcase
1210
- command_id = Sphinx::Client.const_get('SEARCHD_COMMAND_' + cmd)
1211
- command_ver = Sphinx::Client.const_get('VER_COMMAND_' + cmd)
1212
-
1213
- sock = self.Connect
1214
- len = request.to_s.length + (additional != nil ? 4 : 0)
1215
- header = [command_id, command_ver, len].pack('nnN')
1216
- header << [additional].pack('N') if additional != nil
1217
- sock.send(header + request.to_s, 0)
1218
-
1219
- return sock if skip_response
1220
- response = self.GetResponse(sock, command_ver)
1221
- return Response.new(response)
1528
+ # This is internal method which selects next server (round-robin)
1529
+ # and yields it to the block passed.
1530
+ #
1531
+ # In case of connection error, it will try next server several times
1532
+ # (see +SetConnectionTimeout+ method details). If all servers are down,
1533
+ # it will set +error+ attribute value with the last exception message,
1534
+ # and <tt>connection_timeout?</tt> method will return true. Also,
1535
+ # +SphinxConnectErorr+ exception will be raised.
1536
+ #
1537
+ def with_server
1538
+ attempts = @retries
1539
+ begin
1540
+ # Get the next server
1541
+ @lastserver = (@lastserver + 1) % @servers.size
1542
+ server = @servers[@lastserver]
1543
+ yield server
1544
+ rescue SphinxConnectError => e
1545
+ # Connection error! Do we need to try it again?
1546
+ attempts -= 1
1547
+ retry if attempts > 0
1548
+
1549
+ # Re-raise original exception
1550
+ @error = e.message
1551
+ @connerror = true
1552
+ raise
1553
+ end
1222
1554
  end
1223
1555
 
1224
- # :stopdoc:
1225
- def assert
1226
- raise 'Assertion failed!' unless yield if $DEBUG
1556
+ # This is internal method which retrieves socket for a given server,
1557
+ # initiates Sphinx session, and yields this socket to a block passed.
1558
+ #
1559
+ # In case of any problems with session initiation, +SphinxConnectError+
1560
+ # will be raised, because this is part of connection establishing. See
1561
+ # +with_server+ method details to get more infromation about how this
1562
+ # exception is handled.
1563
+ #
1564
+ # Socket retrieving routine is wrapped in a block with it's own
1565
+ # timeout value (see +SetConnectTimeout+). This is done in
1566
+ # <tt>Server#get_socket</tt> method, so check it for details.
1567
+ #
1568
+ # Request execution is wrapped with block with another timeout
1569
+ # (see +SetRequestTimeout+). This ensures no Sphinx request will
1570
+ # take unreasonable time.
1571
+ #
1572
+ # In case of any Sphinx error (incomplete reply, internal or temporary
1573
+ # error), connection to the server will be re-established, and request
1574
+ # will be retried (see +SetRequestTimeout+). Of course, if connection
1575
+ # could not be established, next server will be selected (see explanation
1576
+ # above).
1577
+ #
1578
+ def with_socket(server)
1579
+ attempts = @reqretries
1580
+ socket = nil
1581
+
1582
+ begin
1583
+ s = server.get_socket do |sock|
1584
+ # Remember socket to close it in case of emergency
1585
+ socket = sock
1586
+
1587
+ # send my version
1588
+ # this is a subtle part. we must do it before (!) reading back from searchd.
1589
+ # because otherwise under some conditions (reported on FreeBSD for instance)
1590
+ # TCP stack could throttle write-write-read pattern because of Nagle.
1591
+ sock.write([1].pack('N'))
1592
+ v = sock.read(4).unpack('N*').first
1593
+
1594
+ # Ouch, invalid protocol!
1595
+ if v < 1
1596
+ raise SphinxConnectError, "expected searchd protocol version 1+, got version '#{v}'"
1597
+ end
1598
+ end
1599
+
1600
+ Sphinx::safe_execute(@reqtimeout) do
1601
+ yield s
1602
+ end
1603
+ rescue SocketError, SystemCallError, IOError, ::Errno::EPIPE => e
1604
+ # Ouch, communication problem, will be treated as a connection problem.
1605
+ raise SphinxConnectError, "failed to read searchd response (msg=#{e.message})"
1606
+ rescue SphinxResponseError, SphinxInternalError, SphinxTemporaryError, SphinxUnknownError, ::Timeout::Error, EOFError => e
1607
+ # EOFError should not occur in ideal world, because we compare response length
1608
+ # with a value passed by Sphinx. But we want to ensure that client will not
1609
+ # fail with unexpected error when Sphinx implementation has bugs, aren't we?
1610
+ if e.kind_of?(EOFError) or e.kind_of?(::Timeout::Error)
1611
+ new_e = SphinxResponseError.new("failed to read searchd response (msg=#{e.message})")
1612
+ new_e.set_backtrace(e.backtrace)
1613
+ e = new_e
1614
+ end
1615
+
1616
+ # Close previously opened socket (in case of it has been really opened)
1617
+ server.free_socket(socket)
1618
+
1619
+ # Request error! Do we need to try it again?
1620
+ attempts -= 1
1621
+ retry if attempts > 0
1622
+
1623
+ # Re-raise original exception
1624
+ @error = e.message
1625
+ raise e
1626
+ ensure
1627
+ # Close previously opened socket on any other error
1628
+ server.free_socket(socket)
1629
+ end
1227
1630
  end
1228
- # :startdoc:
1229
1631
  end
1230
1632
  end