File Coverage

blib/lib/Sphinx/Search.pm
Criterion Covered Total %
statement 301 1138 26.4
branch 23 410 5.6
condition 2 219 0.9
subroutine 87 144 60.4
pod 46 46 100.0
total 459 1957 23.4


line stmt bran cond sub pod time code
1             package Sphinx::Search;
2              
3 6     6   288305 use warnings;
  6         41  
  6         175  
4 6     6   25 use strict;
  6         8  
  6         128  
5              
6 6     6   21 use base 'Exporter';
  6         10  
  6         630  
7              
8 6     6   35 use Carp;
  6         17  
  6         310  
9 6     6   1820 use Socket;
  6         11675  
  6         2059  
10 6     6   38 use Config;
  6         22  
  6         296  
11 6     6   5507 use Math::BigInt;
  6         127412  
  6         24  
12 6     6   111076 use IO::Socket::INET;
  6         75124  
  6         42  
13 6     6   3434 use IO::Socket::UNIX;
  6         11  
  6         41  
14 6     6   5171 use Encode qw/encode_utf8 decode_utf8/;
  6         49683  
  6         375  
15 6     6   2627 use List::MoreUtils qw/any/;
  6         58671  
  6         30  
16              
17             my $is_native64 = $Config{longsize} == 8 || defined $Config{use64bitint} || defined $Config{use64bitall};
18            
19              
20             =head1 NAME
21              
22             Sphinx::Search - Sphinx search engine API Perl client
23              
24             =head1 VERSION
25              
26             Please note that you *MUST* install a version which is compatible with your version of Sphinx.
27              
28             Use version 0.31 for Sphinx-2.2.8-release or later (or use DBI instead)
29              
30             Use version 0.28 for Sphinx-2.0.8-release or later
31              
32             Use version 0.27.2 for Sphinx-2.0.3-release (svn-r3043)
33              
34             Use version 0.26.1 for Sphinx-2.0.1-beta (svn-r2792)
35              
36             Use version 0.25_03 for Sphinx svn-r2575
37              
38             Use version 0.24.1 for Sphinx-1.10-beta (svn-r2420)
39              
40             Use version 0.23_02 for Sphinx svn-r2269 (experimental)
41              
42             Use version 0.22 for Sphinx 0.9.9-rc2 and later (Please read the Compatibility Note under L regarding encoding changes)
43              
44             Use version 0.15 for Sphinx 0.9.9-svn-r1674
45              
46             Use version 0.12 for Sphinx 0.9.8
47              
48             Use version 0.11 for Sphinx 0.9.8-rc1
49              
50             Use version 0.10 for Sphinx 0.9.8-svn-r1112
51              
52             Use version 0.09 for Sphinx 0.9.8-svn-r985
53              
54             Use version 0.08 for Sphinx 0.9.8-svn-r871
55              
56             Use version 0.06 for Sphinx 0.9.8-svn-r820
57              
58             Use version 0.05 for Sphinx 0.9.8-cvs-20070907
59              
60             Use version 0.02 for Sphinx 0.9.8-cvs-20070818
61              
62             =cut
63              
64             our $VERSION = '0.31';
65              
66             =head1 SYNOPSIS
67              
68             use Sphinx::Search;
69              
70             $sph = Sphinx::Search->new();
71              
72             # Standard API query
73             $results = $sph->SetSortMode(SPH_SORT_RELEVANCE)
74             ->Query("search terms");
75              
76             # SphinxQL query
77             $results = $sph->SphinxQL("SELECT * FROM myindex WHERE MATCH('search terms')");
78              
79             =head1 DESCRIPTION
80              
81             This is the Perl API client for the Sphinx open-source SQL full-text indexing
82             search engine, L.
83              
84             Since 0.9.9, Sphinx supports a native MySQL-protocol client, i.e. DBI with DBD::mysql. That is, you can configure the server to have a mysql41 listening port and then simply do
85              
86             my $dbh = DBI->connect('dbi:mysql:host=127.0.0.1;port=9306;mysql_enable_utf8=1') or die "Failed to connect via DBI";
87             my $sth = $dbh->prepare_cached("SELECT * FROM myindex WHERE MATCH('search terms')");
88             $sth->execute();
89             while (my $row = $sth->fetchrow_arrayref) {
90             ... # Collect results
91             }
92              
93             The DBI client turns out to be significantly (about 5x) faster than this pure-Perl API. You should probably be using that instead.
94              
95             This module also supports SphinxQL queries, with the small advantage that you can use either the standard API or the SphinxQL API over the one port (i.e. the searchd server does not need to be configured with a mysql41 listening port).
96              
97             Given that the DBI client has several advantages over this API, future updates of this module are unlikely.
98              
99            
100             =cut
101              
102             # Constants to export.
103             our @EXPORT = qw(
104             SPH_MATCH_ALL SPH_MATCH_ANY SPH_MATCH_PHRASE SPH_MATCH_BOOLEAN SPH_MATCH_EXTENDED
105             SPH_MATCH_FULLSCAN SPH_MATCH_EXTENDED2
106             SPH_RANK_PROXIMITY_BM25 SPH_RANK_BM25 SPH_RANK_NONE SPH_RANK_WORDCOUNT
107             SPH_RANK_PROXIMITY SPH_RANK_MATCHANY SPH_RANK_FIELDMASK SPH_RANK_SPH04 SPH_RANK_EXPR
108             SPH_RANK_TOTAL
109             SPH_SORT_RELEVANCE SPH_SORT_ATTR_DESC SPH_SORT_ATTR_ASC SPH_SORT_TIME_SEGMENTS
110             SPH_SORT_EXTENDED SPH_SORT_EXPR
111             SPH_GROUPBY_DAY SPH_GROUPBY_WEEK SPH_GROUPBY_MONTH SPH_GROUPBY_YEAR SPH_GROUPBY_ATTR
112             SPH_GROUPBY_ATTRPAIR
113             SPH_ATTR_INTEGER SPH_ATTR_TIMESTAMP SPH_ATTR_ORDINAL SPH_ATTR_BOOL
114             SPH_ATTR_FLOAT SPH_ATTR_BIGINT SPH_ATTR_STRING SPH_ATTR_MULTI SPH_ATTR_MULTI64
115             SPH_QF_REVERSE_SCAN SPH_QF_SORT_METHOD SPH_QF_MAX_PREDICTED_TIME
116             SPH_QF_BOOLEAN_SIMPLIFY SPH_QF_IDF SPH_QF_GLOBAL_IDF
117             );
118              
119             # known searchd commands
120 6     6   5286 use constant SEARCHD_COMMAND_SEARCH => 0;
  6         21  
  6         361  
121 6     6   29 use constant SEARCHD_COMMAND_EXCERPT => 1;
  6         23  
  6         233  
122 6     6   27 use constant SEARCHD_COMMAND_UPDATE => 2;
  6         9  
  6         238  
123 6     6   30 use constant SEARCHD_COMMAND_KEYWORDS => 3;
  6         15  
  6         281  
124 6     6   34 use constant SEARCHD_COMMAND_PERSIST => 4;
  6         12  
  6         255  
125 6     6   34 use constant SEARCHD_COMMAND_STATUS => 5;
  6         8  
  6         218  
126 6     6   28 use constant SEARCHD_COMMAND_FLUSHATTRS => 7;
  6         12  
  6         227  
127 6     6   30 use constant SEARCHD_COMMAND_SPHINXQL => 8;
  6         8  
  6         254  
128              
129             # current client-side command implementation versions
130 6     6   39 use constant VER_COMMAND_SEARCH => 0x11E;
  6         9  
  6         215  
131 6     6   26 use constant VER_COMMAND_EXCERPT => 0x104;
  6         14  
  6         274  
132 6     6   29 use constant VER_COMMAND_UPDATE => 0x103;
  6         13  
  6         256  
133 6     6   30 use constant VER_COMMAND_KEYWORDS => 0x100;
  6         10  
  6         221  
134 6     6   27 use constant VER_COMMAND_STATUS => 0x101;
  6         8  
  6         224  
135 6     6   26 use constant VER_COMMAND_FLUSHATTRS => 0x100;
  6         18  
  6         204  
136 6     6   28 use constant VER_COMMAND_SPHINXQL => 0x100;
  6         7  
  6         212  
137              
138             # known searchd status codes
139 6     6   28 use constant SEARCHD_OK => 0;
  6         8  
  6         217  
140 6     6   28 use constant SEARCHD_ERROR => 1;
  6         9  
  6         242  
141 6     6   28 use constant SEARCHD_RETRY => 2;
  6         11  
  6         249  
142 6     6   29 use constant SEARCHD_WARNING => 3;
  6         19  
  6         206  
143              
144             # known match modes
145 6     6   25 use constant SPH_MATCH_ALL => 0;
  6         13  
  6         240  
146 6     6   29 use constant SPH_MATCH_ANY => 1;
  6         14  
  6         267  
147 6     6   27 use constant SPH_MATCH_PHRASE => 2;
  6         9  
  6         218  
148 6     6   28 use constant SPH_MATCH_BOOLEAN => 3;
  6         9  
  6         214  
149 6     6   27 use constant SPH_MATCH_EXTENDED => 4;
  6         14  
  6         212  
150 6     6   26 use constant SPH_MATCH_FULLSCAN => 5;
  6         13  
  6         228  
151 6     6   27 use constant SPH_MATCH_EXTENDED2 => 6; # extended engine V2 (TEMPORARY, WILL BE REMOVED
  6         10  
  6         222  
152              
153             # known ranking modes (ext2 only)
154 6     6   25 use constant SPH_RANK_PROXIMITY_BM25 => 0; # default mode, phrase proximity major factor and BM25 minor one
  6         9  
  6         210  
155 6     6   26 use constant SPH_RANK_BM25 => 1; # statistical mode, BM25 ranking only (faster but worse quality)
  6         7  
  6         204  
156 6     6   32 use constant SPH_RANK_NONE => 2; # no ranking, all matches get a weight of 1
  6         10  
  6         251  
157 6     6   34 use constant SPH_RANK_WORDCOUNT => 3; # simple word-count weighting, rank is a weighted sum of per-field keyword occurence counts
  6         11  
  6         207  
158 6     6   26 use constant SPH_RANK_PROXIMITY => 4;
  6         10  
  6         240  
159 6     6   28 use constant SPH_RANK_MATCHANY => 5;
  6         10  
  6         243  
160 6     6   36 use constant SPH_RANK_FIELDMASK => 6;
  6         8  
  6         231  
161 6     6   34 use constant SPH_RANK_SPH04 => 7;
  6         7  
  6         286  
162 6     6   29 use constant SPH_RANK_EXPR => 8;
  6         9  
  6         210  
163 6     6   24 use constant SPH_RANK_TOTAL => 9;
  6         8  
  6         240  
164              
165             # known sort modes
166 6     6   27 use constant SPH_SORT_RELEVANCE => 0;
  6         15  
  6         230  
167 6     6   29 use constant SPH_SORT_ATTR_DESC => 1;
  6         9  
  6         219  
168 6     6   26 use constant SPH_SORT_ATTR_ASC => 2;
  6         8  
  6         211  
169 6     6   28 use constant SPH_SORT_TIME_SEGMENTS => 3;
  6         7  
  6         210  
170 6     6   28 use constant SPH_SORT_EXTENDED => 4;
  6         30  
  6         248  
171 6     6   27 use constant SPH_SORT_EXPR => 5;
  6         17  
  6         230  
172              
173             # known filter types
174 6     6   27 use constant SPH_FILTER_VALUES => 0;
  6         9  
  6         215  
175 6     6   41 use constant SPH_FILTER_RANGE => 1;
  6         12  
  6         248  
176 6     6   42 use constant SPH_FILTER_FLOATRANGE => 2;
  6         10  
  6         223  
177 6     6   27 use constant SPH_FILTER_STRING => 3;
  6         7  
  6         243  
178              
179             # known attribute types
180 6     6   29 use constant SPH_ATTR_INTEGER => 1;
  6         8  
  6         256  
181 6     6   29 use constant SPH_ATTR_TIMESTAMP => 2;
  6         9  
  6         243  
182 6     6   29 use constant SPH_ATTR_ORDINAL => 3;
  6         7  
  6         250  
183 6     6   28 use constant SPH_ATTR_BOOL => 4;
  6         10  
  6         249  
184 6     6   28 use constant SPH_ATTR_FLOAT => 5;
  6         8  
  6         221  
185 6     6   30 use constant SPH_ATTR_BIGINT => 6;
  6         7  
  6         224  
186 6     6   31 use constant SPH_ATTR_STRING => 7;
  6         75  
  6         229  
187 6     6   30 use constant SPH_ATTR_FACTORS => 1001;
  6         66  
  6         249  
188 6     6   31 use constant SPH_ATTR_MULTI => 0x40000001;
  6         9  
  6         215  
189 6     6   27 use constant SPH_ATTR_MULTI64 => 0x40000002;
  6         10  
  6         243  
190              
191             # known grouping functions
192 6     6   28 use constant SPH_GROUPBY_DAY => 0;
  6         10  
  6         257  
193 6     6   29 use constant SPH_GROUPBY_WEEK => 1;
  6         9  
  6         230  
194 6     6   26 use constant SPH_GROUPBY_MONTH => 2;
  6         8  
  6         209  
195 6     6   27 use constant SPH_GROUPBY_YEAR => 3;
  6         10  
  6         210  
196 6     6   34 use constant SPH_GROUPBY_ATTR => 4;
  6         8  
  6         226  
197 6     6   40 use constant SPH_GROUPBY_ATTRPAIR => 5;
  6         9  
  6         356  
198              
199             use constant {
200 6         797 SPH_QF_REVERSE_SCAN => 'reverse_scan',
201             SPH_QF_SORT_METHOD => 'sort_method',
202             SPH_QF_MAX_PREDICTED_TIME => 'max_predicted_time',
203             SPH_QF_BOOLEAN_SIMPLIFY => 'boolean_simplify',
204             SPH_QF_IDF => 'idf',
205             SPH_QF_GLOBAL_IDF => 'global_idf',
206 6     6   47 };
  6         11  
207              
208             my %query_flags = ( SPH_QF_REVERSE_SCAN() => [ 0, 1 ],
209             SPH_QF_SORT_METHOD() => [ qw/pq kbuffer/ ],
210             SPH_QF_MAX_PREDICTED_TIME() => [ 0 ],
211             SPH_QF_BOOLEAN_SIMPLIFY() => [1, 0],
212             SPH_QF_IDF() => [ qw/normalized plain tfidf_normalized tfidf_unnormalized/ ],
213             SPH_QF_GLOBAL_IDF() => [1, 0],
214             );
215              
216 6     6   42 use constant MYSQL_COL_STRING => 254;
  6         9  
  6         49852  
217              
218             # Floating point number matching expression
219             my $num_re = qr/^-?\d*\.?\d*(?:[eE][+-]?\d+)?$/;
220              
221             # portably pack numeric to 64 signed bits, network order
222             sub _sphPackI64 {
223 22     22   2412 my $self = shift;
224 22         21 my $v = shift;
225              
226             # x64 route
227 22 50       35 my $i = $is_native64 ? int($v) : Math::BigInt->new("$v");
228 22         54 return pack ( "NN", $i>>32, $i & 4294967295 );
229             }
230              
231             # portably pack numeric to 64 unsigned bits, network order
232             sub _sphPackU64 {
233 9     9   2228 my $self = shift;
234 9         12 my $v = shift;
235              
236 9 50       21 my $i = $is_native64 ? int($v) : Math::BigInt->new("$v");
237 9         35 return pack ( "NN", $i>>32, $i & 4294967295 );
238             }
239              
240             sub _sphPackI64array {
241 0     0   0 my $self = shift;
242 0   0     0 my $values = shift || [];
243              
244 0         0 my $s = pack("N", scalar @$values);
245 0         0 $s .= $self->_sphPackI64($_) for @$values;
246 0         0 return $s;
247             }
248              
249             # portably unpack 64 unsigned bits, network order to numeric
250             sub _sphUnpackU64
251             {
252 9     9   13 my $self = shift;
253 9         11 my $v = shift;
254              
255 9         20 my ($h,$l) = unpack ( "N*N*", $v );
256              
257             # x64 route
258 9 50       60 return ($h<<32) + $l if $is_native64;
259              
260             # x32 route, BigInt
261 0         0 $h = Math::BigInt->new($h);
262 0         0 $h->blsft(32)->badd($l);
263            
264 0         0 return $h->bstr;
265             }
266              
267             # portably unpack 64 signed bits, network order to numeric
268             sub _sphUnpackI64
269             {
270 11     11   23 my $self = shift;
271 11         12 my $v = shift;
272              
273 11         24 my ($h,$l) = unpack ( "N*N*", $v );
274              
275 11 100       20 my $neg = ($h & 0x80000000) ? 1 : 0;
276              
277             # x64 route
278 11 50       21 if ( $is_native64 ) {
279 11 100       35 return -(~(($h<<32) + $l) + 1) if $neg;
280 5         21 return ($h<<32) + $l;
281             }
282              
283             # x32 route, BigInt
284 0 0       0 if ($neg) {
285 0         0 $h = ~$h;
286 0         0 $l = ~$l;
287             }
288              
289 0         0 my $x = Math::BigInt->new($h);
290 0         0 $x->blsft(32)->badd($l);
291 0 0       0 $x->binc()->bneg() if $neg;
292              
293 0         0 return $x->bstr;
294             }
295              
296             sub _sphSetBit {
297 3     3   9 my ($self, $flag, $bit, $on) = @_;
298              
299 3 50       10 if ($on) {
300 3         7 $flag |= (1 << $bit);
301             }
302             else {
303 0         0 $flag &= ~(1 << $bit);
304             }
305 3         20 return $flag;
306             }
307              
308              
309              
310              
311              
312             =head1 CONSTRUCTOR
313              
314             =head2 new
315              
316             $sph = Sphinx::Search->new;
317             $sph = Sphinx::Search->new(\%options);
318              
319             Create a new Sphinx::Search instance.
320              
321             OPTIONS
322              
323             =over 4
324              
325             =item log
326              
327             Specify an optional logger instance. This can be any class that provides error,
328             warn, info, and debug methods (e.g. see L). Logging is disabled
329             if no logger instance is provided.
330              
331             =item debug
332              
333             Debug flag. If set (and a logger instance is specified), debugging messages
334             will be generated.
335              
336             =back
337              
338             =cut
339              
340             # create a new client object and fill defaults
341             sub new {
342 3     3 1 1128 my ($class, $options) = @_;
343 3         61 my $self = {
344             # per=client-object settings
345             _host => 'localhost',
346             _port => 9312,
347             _path => undef,
348             _socket => undef,
349              
350             _persistent => undef,
351             _connectretries => 1,
352              
353             # per-query settings
354             _offset => 0,
355             _limit => 20,
356             _mode => SPH_MATCH_EXTENDED2,
357             _weights => [],
358             _sort => SPH_SORT_RELEVANCE,
359             _sortby => "",
360             _min_id => 0,
361             _max_id => 0,
362             _filters => [],
363             _groupby => "",
364             _groupdistinct => "",
365             _groupfunc => SPH_GROUPBY_DAY,
366             _groupsort => '@group desc',
367             _maxmatches => 1000,
368             _cutoff => 0,
369             _retrycount => 0,
370             _retrydelay => 0,
371             _anchor => undef,
372             _indexweights => undef,
373             _ranker => SPH_RANK_PROXIMITY_BM25,
374             _rankexpr => "",
375             _maxquerytime => 0,
376             _fieldweights => {},
377             _overrides => {},
378             _select => q{*},
379              
380             # per-reply fields (for single-query case)
381             _error => '',
382             _warning => '',
383             _connerror => '',
384            
385             # request storage (for multi-query case)
386             _reqs => [],
387             _timeout => 0,
388              
389             _string_encoder => \&encode_utf8,
390             _string_decoder => \&decode_utf8,
391             };
392 3   33     19 bless $self, ref($class) || $class;
393              
394 3         11 $self->ResetQueryFlag;
395 3         9 $self->ResetOuterSelect;
396              
397             # These options are supported in the constructor, but not recommended
398             # since there is no validation. Use the Set* methods instead.
399 3         9 my %legal_opts = map { $_ => 1 } qw/host port offset limit mode weights sort sortby groupby groupbyfunc maxmatches cutoff retrycount retrydelay log debug string_encoder string_decoder/;
  54         90  
400 3         13 for my $opt (keys %$options) {
401 1 50       4 $self->{'_' . $opt} = $options->{$opt} if $legal_opts{$opt};
402             }
403             # Disable debug unless we have something to log to
404 3 50       12 $self->{_debug} = 0 unless $self->{_log};
405              
406 3         14 return $self;
407             }
408              
409              
410             =head1 METHODS
411              
412             =cut
413              
414             sub _Error {
415 2     2   4 my ($self, $msg) = @_;
416              
417 2         3 $self->{_error} = $msg;
418 2 50 33     7 $self->{_log}->error($msg) if $self->{_log} && defined $msg;
419 2         2 return;
420             }
421              
422             sub _Throw {
423 0     0   0 my ($self, $msg) = @_;
424              
425 0         0 die $msg;
426             }
427              
428             =head2 GetLastError
429              
430             $error = $sph->GetLastError;
431              
432             Get last error message (string)
433              
434             =cut
435              
436             sub GetLastError {
437 0     0 1 0 my $self = shift;
438 0         0 return $self->{_error};
439             }
440              
441             sub _Warning {
442 0     0   0 my ($self, $msg) = @_;
443              
444 0         0 $self->{_warning} = $msg;
445 0 0 0     0 $self->{_log}->warn($msg) if $self->{_log} && defined $msg;
446 0         0 return;
447             }
448              
449             =head2 GetLastWarning
450              
451             $warning = $sph->GetLastWarning;
452              
453             Get last warning message (string)
454              
455             =cut
456              
457             sub GetLastWarning {
458 0     0 1 0 my $self = shift;
459 0         0 return $self->{_warning};
460             }
461              
462              
463             =head2 IsConnectError
464              
465             Check connection error flag (to differentiate between network connection errors
466             and bad responses). Returns true value on connection error.
467              
468             =cut
469              
470             sub IsConnectError {
471 0     0 1 0 return shift->{_connerror};
472             }
473              
474             =head2 SetEncoders
475              
476             $sph->SetEncoders(\&encode_function, \&decode_function)
477              
478             COMPATIBILITY NOTE: SetEncoders() was introduced in version 0.17.
479             Prior to that, all strings were considered to be sequences of bytes
480             which may have led to issues with multi-byte characters. If you were
481             previously encoding/decoding strings external to Sphinx::Search, you
482             will need to disable encoding/decoding by setting Sphinx::Search to
483             use raw values as explained below (or modify your code and let
484             Sphinx::Search do the recoding).
485              
486             Set the string encoder/decoder functions for transferring strings
487             between perl and Sphinx. The encoder should take the perl internal
488             representation and convert to the bytestream that searchd expects, and
489             the decoder should take the bytestream returned by searchd and convert to
490             perl format.
491              
492             The searchd format will depend on the 'charset_type' index setting in
493             the Sphinx configuration file.
494              
495             The coders default to encode_utf8 and decode_utf8 respectively, which
496             are compatible with the 'utf8' charset_type.
497              
498             If either the encoder or decoder functions are left undefined in the
499             call to SetEncoders, they return to their default values.
500              
501             If you wish to send raw values (no encoding/decoding), supply a
502             function that simply returns its argument, e.g.
503              
504             $sph->SetEncoders( sub { shift }, sub { shift });
505              
506             Returns $sph.
507              
508             =cut
509              
510             sub SetEncoders {
511 0     0 1 0 my $self = shift;
512 0         0 my $encoder = shift;
513 0         0 my $decoder = shift;
514              
515 0 0       0 $self->{_string_encoder} = $encoder ? $encoder : \&encode_utf8;
516 0 0       0 $self->{_string_decoder} = $decoder ? $decoder : \&decode_utf8;
517            
518 0         0 return $self;
519             }
520              
521             =head2 SetServer
522              
523             $sph->SetServer($host, $port);
524             $sph->SetServer($path, $port);
525              
526             In the first form, sets the host (string) and port (integer) details for the
527             searchd server using a network (INET) socket (default is localhost:9312).
528              
529             In the second form, where $path is a local filesystem path (optionally prefixed
530             by 'unix://'), sets the client to access the searchd server via a local (UNIX
531             domain) socket at the specified path.
532              
533             Returns $sph.
534              
535             =cut
536              
537             sub SetServer {
538 0     0 1 0 my $self = shift;
539 0         0 my $host = shift;
540 0         0 my $port = shift;
541              
542 0 0       0 croak("host is not defined") unless defined($host);
543 0 0       0 if (substr($host, 0, 1) eq '/') {
    0          
544 0         0 $self->{_path} = $host;
545 0         0 return;
546             }
547             elsif (substr($host, 0, 7) eq 'unix://') {
548 0         0 $self->{_path} = substr($host, 7);
549 0         0 return;
550             }
551            
552 0   0     0 $port ||= 0;
553 0 0       0 croak("port is not an number") unless $port =~ m/^\d+/o;
554 0         0 $port = int($port);
555 0 0 0     0 croak("port $port out of range 0 to 65536") if $port <0 || $port >= 65536;
556              
557 0         0 $self->{_host} = $host;
558 0 0       0 $self->{_port} = $port == 0 ? 9312 : $port;
559 0         0 $self->{_path} = undef;
560              
561 0         0 return $self;
562             }
563              
564             =head2 SetConnectTimeout
565              
566             $sph->SetConnectTimeout($timeout)
567              
568             Set server connection timeout (in seconds).
569              
570             Returns $sph.
571              
572             =cut
573              
574             sub SetConnectTimeout {
575 1     1 1 264 my $self = shift;
576 1         1 my $timeout = shift;
577              
578 1 50       8 croak("timeout is not numeric") unless ($timeout =~ m/$num_re/);
579 1         2 $self->{_timeout} = $timeout;
580 1         2 return $self;
581             }
582              
583             =head2 SetConnectRetries
584              
585             $sph->SetConnectRetries($retries)
586              
587             Set server connection retries (in case of connection fail).
588              
589             Returns $sph.
590              
591             =cut
592              
593             sub SetConnectRetries {
594 0     0 1 0 my $self = shift;
595 0         0 my $retries = shift;
596 0 0       0 croak("connect retries is not numeric") unless ($retries =~ m/$num_re/);
597 0         0 $self->{_connectretries} = $retries;
598 0         0 return $self;
599             }
600              
601              
602             sub _Send {
603 0     0   0 my $self = shift;
604 0         0 my $fp = shift;
605 0         0 my $data = shift;
606              
607 0 0       0 $self->{_log}->debug("Writing to socket") if $self->{_debug};
608 0 0       0 unless ( send($fp,$data,0)){
609 0         0 $self->_Error("connection unexpectedly closed (timed out?): $!");
610 0         0 $self->{_connerror} = 1;
611 0 0       0 if ($self->{_socket}) {
612 0         0 close($self->{_socket});
613 0         0 undef $self->{_socket};
614             }
615 0         0 return 0;
616             }
617 0         0 return 1;
618             }
619              
620             # connect to searchd server
621              
622             sub _Connect {
623 1     1   3 my $self = shift;
624            
625 1         2 $self->_Error(); #reset old errors in new connection
626            
627 1 50       2 if ($self->{_socket}) {
628             # persistent connection, check it
629 0 0       0 return $self->{_socket} if $self->{_socket}->connected;
630             # force reopen
631 0         0 undef $self->{_socket};
632             }
633              
634 1         2 my $debug = $self->{_debug};
635 1 50       8 my $str_dest = $self->{_path} ? 'unix://' . $self->{_path} : "$self->{_host}:$self->{_port}";
636 1 50       2 $self->{_log}->debug("Connecting to $str_dest") if $debug;
637              
638             # connect socket
639 1         2 $self->{_connerror} = q{};
640              
641 1         1 my $fp;
642 1         2 my %params = (); # ( Blocking => 0 );
643 1 50       3 $params{Timeout} = $self->{_timeout} if $self->{_timeout};
644 1 50       2 if ($self->{_path}) {
645             $fp = IO::Socket::UNIX->new( Peer => $self->{_path},
646 0         0 %params,
647             );
648             }
649             else {
650             $fp = IO::Socket::INET->new( PeerPort => $self->{_port},
651             PeerAddr => $self->{_host},
652 1         8 Proto => 'tcp',
653             %params,
654             );
655             }
656 1 50       480 if (! $fp) {
657 1         5 $self->_Error("Failed to open connection to $str_dest: $!");
658 1         2 $self->{_connerror} = 1;
659 1         5 return 0;
660             }
661 0         0 binmode($fp, ':bytes');
662              
663             # check version
664 0         0 my $buf = '';
665 0 0       0 $fp->read($buf, 4) or do {
666 0         0 $self->_Error("Failed on initial read from $str_dest: $!");
667 0         0 $self->{_connerror} = 1;
668 0         0 return 0;
669             };
670 0         0 my $v = unpack("N*", $buf);
671 0         0 $v = int($v);
672 0 0       0 $self->{_log}->debug("Got version $v from searchd") if $debug;
673 0 0       0 if ($v < 1) {
674 0         0 close($fp);
675 0         0 $self->_Error("expected searchd protocol version 1+, got version '$v'");
676 0         0 return 0;
677             }
678              
679 0 0       0 $self->{_log}->debug("Sending version") if $debug;
680              
681             # All ok, send my version
682 0 0       0 unless ($self->_Send($fp, pack("N", 1))) {
683 0         0 $self->{_connerror} = 1;
684 0         0 $self->_Error("error on sending version");
685 0         0 return 0;
686             }
687 0 0       0 $self->{_log}->debug("Connection complete") if $debug;
688            
689 0 0       0 if ($self->{_persistent}) {
690 0         0 my $req = pack("nnNN", SEARCHD_COMMAND_PERSIST, 0, 4, 1);
691 0 0       0 unless ($self->_Send($fp, $req)) {
692 0         0 $self->{_connerror} = 1;
693 0         0 $self->_Error("error on setting persistent connection");
694 0         0 return 0;
695             }
696 0         0 $self->{_socket} = $fp;
697             }
698              
699 0         0 return $fp;
700              
701             }
702              
703             #-------------------------------------------------------------
704              
705             # get and check response packet from searchd server
706             sub _GetResponse {
707 0     0   0 my $self = shift;
708 0         0 my $fp = shift;
709 0         0 my $client_ver = shift;
710              
711 0         0 my $header;
712 0         0 my $resp = $fp->read($header, 8, 0);
713              
714 0 0 0     0 if (!defined($resp) || $resp==0) {
715 0         0 close $self->{_socket};
716 0         0 undef $self->{_socket};
717 0         0 $self->_Error("read failed: $!");
718 0         0 return 0;
719             }
720              
721 0         0 my ($status, $ver, $len ) = unpack("n2N", $header);
722 0 0       0 if ( ! defined($len) ) {
723 0         0 $self->_Error("read failed: $!");
724 0         0 return 0;
725             }
726              
727 0         0 my $response = q{};
728 0         0 my $lasterror = q{};
729 0         0 my $lentotal = 0;
730 0         0 while (my $rlen = $fp->read(my $chunk, $len)) {
731 0 0       0 if ($rlen < 0) {
732 0         0 $lasterror = $!;
733 0         0 last;
734             }
735 0         0 $response .= $chunk;
736 0         0 $lentotal += $rlen;
737 0 0       0 last if $lentotal >= $len;
738             }
739 0 0       0 close($fp) unless $self->{_socket};
740              
741             # check response
742 0 0       0 if ( length($response) != $len ) {
743 0 0       0 $self->_Error( $len
744             ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=". length($response) . ", last error=$lasterror)"
745             : "received zero-sized searchd response");
746 0         0 return 0;
747             }
748              
749             # check status
750 0 0       0 if ( $status==SEARCHD_WARNING ) {
751 0         0 my ($wlen) = unpack ( "N*", substr ( $response, 0, 4 ) );
752 0         0 $self->_Warning(substr ( $response, 4, $wlen ));
753 0         0 return substr ( $response, 4+$wlen );
754             }
755 0 0       0 if ( $status==SEARCHD_ERROR ) {
756 0         0 $self->_Error("searchd error: " . substr ( $response, 4 ));
757 0         0 return 0;
758             }
759 0 0       0 if ( $status==SEARCHD_RETRY ) {
760 0         0 $self->_Error("temporary searchd error: " . substr ( $response, 4 ));
761 0         0 return 0;
762             }
763 0 0       0 if ( $status!=SEARCHD_OK ) {
764 0         0 $self->_Error("unknown status code '$status'");
765 0         0 return 0;
766             }
767              
768             # check version
769 0 0       0 if ( $ver<$client_ver ) {
770 0         0 $self->_Warning(sprintf ( "searchd command v.%d.%d older than client's v.%d.%d, some options might not work",
771             $ver>>8, $ver&0xff, $client_ver>>8, $client_ver&0xff ));
772             }
773 0         0 return $response;
774             }
775              
776             #-----------------------------------------------
777             # connect to searchd, send request and get data
778              
779             sub _ProcessRequest {
780 0     0   0 my ($self, $req, $response_command_version) = @_;
781 0 0       0 return unless $req;
782 0         0 my $tries = $self->{_connectretries} + 1;
783 0         0 while( $tries-- ) {
784 0         0 my $fp = $self->_Connect;
785 0 0       0 if (! $fp) {
786 0 0       0 next if $self->IsConnectError;
787 0         0 last;
788             }
789 0 0       0 $self->_Send($fp, $req) or next;
790 0         0 my $response = $self->_GetResponse ($fp, $response_command_version);
791 0 0       0 return $response if $response;
792             }
793 0 0       0 $self->_Error($self->GetLastError . "... ConnectRetries exceed...") if $self->IsConnectError;
794 0         0 return 0;
795             }
796              
797              
798             =head2 SetLimits
799              
800             $sph->SetLimits($offset, $limit);
801             $sph->SetLimits($offset, $limit, $max);
802              
803             Set match offset/limits, and optionally the max number of matches to return.
804              
805             Returns $sph.
806              
807             =cut
808              
809             sub SetLimits {
810 0     0 1 0 my $self = shift;
811 0         0 my $offset = shift;
812 0         0 my $limit = shift;
813 0   0     0 my $max = shift || 0;
814 0 0 0     0 croak("offset should be an integer >= 0") unless ($offset =~ /^\d+$/ && $offset >= 0) ;
815 0 0 0     0 croak("limit should be an integer >= 0") unless ($limit =~ /^\d+$/ && $limit >= 0);
816 0         0 $self->{_offset} = $offset;
817 0         0 $self->{_limit} = $limit;
818 0 0       0 if($max > 0) {
819 0         0 $self->{_maxmatches} = $max;
820             }
821 0         0 return $self;
822             }
823              
824             =head2 SetMaxQueryTime
825              
826             $sph->SetMaxQueryTime($millisec);
827              
828             Set maximum query time, in milliseconds, per index.
829              
830             The value may not be negative; 0 means "do not limit".
831              
832             Returns $sph.
833              
834             =cut
835              
836             sub SetMaxQueryTime {
837 0     0 1 0 my $self = shift;
838 0         0 my $max = shift;
839              
840 0 0 0     0 croak("max value should be an integer >= 0") unless ($max =~ /^\d+$/ && $max >= 0) ;
841 0         0 $self->{_maxquerytime} = $max;
842 0         0 return $self;
843             }
844              
845              
846             =head2 SetMatchMode
847              
848             ** DEPRECATED **
849              
850             $sph->SetMatchMode($mode);
851              
852             Set match mode, which may be one of:
853              
854             =over 4
855              
856             =item * SPH_MATCH_ALL
857              
858             Match all words
859              
860             =item * SPH_MATCH_ANY
861              
862             Match any words
863              
864             =item * SPH_MATCH_PHRASE
865              
866             Exact phrase match
867              
868             =item * SPH_MATCH_BOOLEAN
869              
870             Boolean match, using AND (&), OR (|), NOT (!,-) and parenthetic grouping.
871              
872             =item * SPH_MATCH_EXTENDED
873              
874             Extended match, which includes the Boolean syntax plus field, phrase and
875             proximity operators.
876              
877             =back
878              
879             Returns $sph.
880              
881             =cut
882              
883             sub SetMatchMode {
884 0     0 1 0 my $self = shift;
885 0         0 my $mode = shift;
886 0         0 warn "SetMatchMode is DEPRECATED. Do not call this method - use extended query syntax instead.";
887              
888 0 0       0 croak("Match mode not defined") unless defined($mode);
889 0 0 0     0 croak("Unknown matchmode: $mode") unless ( $mode==SPH_MATCH_ALL
      0        
      0        
      0        
      0        
      0        
890             || $mode==SPH_MATCH_ANY
891             || $mode==SPH_MATCH_PHRASE
892             || $mode==SPH_MATCH_BOOLEAN
893             || $mode==SPH_MATCH_EXTENDED
894             || $mode==SPH_MATCH_FULLSCAN
895             || $mode==SPH_MATCH_EXTENDED2 );
896 0         0 $self->{_mode} = $mode;
897 0         0 return $self;
898             }
899              
900              
901             =head2 SetRankingMode
902              
903             $sph->SetRankingMode(SPH_RANK_BM25, $rank_exp);
904              
905             Set ranking mode, which may be one of:
906              
907             =over 4
908              
909             =item * SPH_RANK_PROXIMITY_BM25
910              
911             Default mode, phrase proximity major factor and BM25 minor one
912              
913             =item * SPH_RANK_BM25
914              
915             Statistical mode, BM25 ranking only (faster but worse quality)
916              
917             =item * SPH_RANK_NONE
918              
919             No ranking, all matches get a weight of 1
920              
921             =item * SPH_RANK_WORDCOUNT
922              
923             Simple word-count weighting, rank is a weighted sum of per-field keyword
924             occurence counts
925              
926             =item * SPH_RANK_MATCHANY
927              
928             Returns rank as it was computed in SPH_MATCH_ANY mode earlier, and is internally used to emulate SPH_MATCH_ANY queries.
929              
930             =item * SPH_RANK_FIELDMASK
931              
932             Returns a 32-bit mask with N-th bit corresponding to N-th fulltext field, numbering from 0. The bit will only be set when the respective field has any keyword occurences satisfiying the query.
933              
934             =item * SPH_RANK_SPH04
935              
936             SPH_RANK_SPH04 is generally based on the default SPH_RANK_PROXIMITY_BM25 ranker, but additionally boosts the matches when they occur in the very beginning or the very end of a text field.
937              
938             =item * SPH_RANK_EXPR
939              
940             Allows the ranking formula to be specified at run time. It exposes a number of internal text factors and lets you define how the final weight should be computed from those factors. $rank_exp should be set to the ranking expression string, e.g. to emulate SPH_RANK_PROXIMITY_BM25, use "sum(lcs*user_weight)*1000+bm25".
941              
942             =back
943              
944             Returns $sph.
945              
946             =cut
947              
948             sub SetRankingMode {
949 0     0 1 0 my $self = shift;
950 0         0 my $ranker = shift;
951 0         0 my $rankexp = shift;
952              
953 0 0 0     0 croak("Unknown ranking mode: $ranker") unless ( $ranker == 0
      0        
954             || ( $ranker >= 1
955             && $ranker < SPH_RANK_TOTAL ));
956              
957 0         0 $self->{_ranker} = $ranker;
958 0   0     0 $self->{_rankexpr} = $rankexp || "";
959              
960 0         0 return $self;
961             }
962              
963              
964             =head2 SetSortMode
965              
966             $sph->SetSortMode(SPH_SORT_RELEVANCE);
967             $sph->SetSortMode($mode, $sortby);
968              
969             Set sort mode, which may be any of:
970              
971             =over 4
972              
973             =item SPH_SORT_RELEVANCE - sort by relevance
974              
975             =item SPH_SORT_ATTR_DESC, SPH_SORT_ATTR_ASC
976              
977             Sort by attribute descending/ascending. $sortby specifies the sorting attribute.
978              
979             =item SPH_SORT_TIME_SEGMENTS
980              
981             Sort by time segments (last hour/day/week/month) in descending order, and then
982             by relevance in descending order. $sortby specifies the time attribute.
983              
984             =item SPH_SORT_EXTENDED
985              
986             Sort by SQL-like syntax. $sortby is the sorting specification.
987              
988             =item SPH_SORT_EXPR
989              
990              
991             =back
992              
993             Returns $sph.
994              
995             =cut
996              
997             sub SetSortMode {
998 0     0 1 0 my $self = shift;
999 0         0 my $mode = shift;
1000 0   0     0 my $sortby = shift || "";
1001 0 0       0 croak("Sort mode not defined") unless defined($mode);
1002 0 0 0     0 croak("Unknown sort mode: $mode") unless ( $mode == SPH_SORT_RELEVANCE
      0        
      0        
      0        
      0        
1003             || $mode == SPH_SORT_ATTR_DESC
1004             || $mode == SPH_SORT_ATTR_ASC
1005             || $mode == SPH_SORT_TIME_SEGMENTS
1006             || $mode == SPH_SORT_EXTENDED
1007             || $mode == SPH_SORT_EXPR
1008             );
1009 0 0 0     0 croak("Sortby must be defined") unless ($mode==SPH_SORT_RELEVANCE || length($sortby));
1010 0         0 $self->{_sort} = $mode;
1011 0         0 $self->{_sortby} = $sortby;
1012 0         0 return $self;
1013             }
1014              
1015             =head2 SetWeights
1016            
1017             ** DEPRECATED **
1018              
1019             $sph->SetWeights([ 1, 2, 3, 4]);
1020              
1021             This method is deprecated. Use L instead.
1022              
1023             Set per-field (integer) weights. The ordering of the weights correspond to the
1024             ordering of fields as indexed.
1025              
1026             Returns $sph.
1027              
1028             =cut
1029              
1030             sub SetWeights {
1031 0     0 1 0 my $self = shift;
1032 0         0 my $weights = shift;
1033 0         0 warn "SetWeights is DEPRECATED, Do not call this method; use SetFieldWeights instead";
1034              
1035 0 0       0 croak("Weights is not an array reference") unless (ref($weights) eq 'ARRAY');
1036 0         0 foreach my $weight (@$weights) {
1037 0 0       0 croak("Weight: $weight is not an integer") unless ($weight =~ /^\d+$/);
1038             }
1039 0         0 $self->{_weights} = $weights;
1040 0         0 return $self;
1041             }
1042              
1043             =head2 SetFieldWeights
1044            
1045             $sph->SetFieldWeights(\%weights);
1046              
1047             Set per-field (integer) weights by field name. The weights hash provides field
1048             name to weight mappings.
1049              
1050             Takes precedence over L.
1051              
1052             Unknown names will be silently ignored. Missing fields will be given a weight of 1.
1053              
1054             Returns $sph.
1055              
1056             =cut
1057              
1058             sub SetFieldWeights {
1059 0     0 1 0 my $self = shift;
1060 0         0 my $weights = shift;
1061 0 0       0 croak("Weights is not a hash reference") unless (ref($weights) eq 'HASH');
1062 0         0 foreach my $field (keys %$weights) {
1063 0 0       0 croak("Weight: $weights->{$field} is not an integer >= 0") unless ($weights->{$field} =~ /^\d+$/);
1064             }
1065 0         0 $self->{_fieldweights} = $weights;
1066 0         0 return $self;
1067             }
1068              
1069             =head2 SetIndexWeights
1070            
1071             $sph->SetIndexWeights(\%weights);
1072              
1073             Set per-index (integer) weights. The weights hash is a mapping of index name to integer weight.
1074              
1075             Returns $sph.
1076              
1077             =cut
1078              
1079             sub SetIndexWeights {
1080 0     0 1 0 my $self = shift;
1081 0         0 my $weights = shift;
1082 0 0       0 croak("Weights is not a hash reference") unless (ref($weights) eq 'HASH');
1083 0         0 foreach (keys %$weights) {
1084 0 0       0 croak("IndexWeight $_: $weights->{$_} is not an integer") unless ($weights->{$_} =~ /^\d+$/);
1085             }
1086 0         0 $self->{_indexweights} = $weights;
1087 0         0 return $self;
1088             }
1089              
1090              
1091              
1092             =head2 SetIDRange
1093              
1094             $sph->SetIDRange($min, $max);
1095              
1096             Set IDs range only match those records where document ID
1097             is between $min and $max (including $min and $max)
1098              
1099             Returns $sph.
1100              
1101             =cut
1102              
1103             sub SetIDRange {
1104 0     0 1 0 my $self = shift;
1105 0         0 my $min = shift;
1106 0         0 my $max = shift;
1107 0 0       0 croak("min_id is not numeric") unless ($min =~ m/$num_re/);
1108 0 0       0 croak("max_id is not numeric") unless ($max =~ m/$num_re/);
1109 0 0       0 croak("min_id is larger than or equal to max_id") unless ($min < $max);
1110 0         0 $self->{_min_id} = $min;
1111 0         0 $self->{_max_id} = $max;
1112 0         0 return $self;
1113             }
1114              
1115             =head2 SetFilter
1116              
1117             $sph->SetFilter($attr, \@values);
1118             $sph->SetFilter($attr, \@values, $exclude);
1119              
1120             Sets the results to be filtered on the given attribute. Only results which have
1121             attributes matching the given values will be returned. (Attribute values must be integers).
1122              
1123             This may be called multiple times with different attributes to select on
1124             multiple attributes.
1125              
1126             If 'exclude' is set, excludes results that match the filter.
1127              
1128             Returns $sph.
1129              
1130             =cut
1131              
1132             sub SetFilter {
1133 1     1 1 10 my ($self, $attribute, $values, $exclude) = @_;
1134              
1135 1 50       3 croak("attribute is not defined") unless (defined $attribute);
1136 1 50       4 croak("values is not an array reference") unless (ref($values) eq 'ARRAY');
1137 1 50       4 croak("values reference is empty") unless (scalar(@$values));
1138              
1139 1 50       2 push(@{$self->{_filters}}, {
  1         7  
1140             type => SPH_FILTER_VALUES,
1141             attr => $attribute,
1142             values => $values,
1143             exclude => $exclude ? 1 : 0,
1144             });
1145              
1146 1         3 return $self;
1147             }
1148              
1149             =head2 SetFilterString
1150              
1151             $sph->SetFilterString($attr, $value)
1152             $sph->SetFilterString($attr, $value, $exclude)
1153              
1154             Adds new string value filter. Only those documents where $attr column value matches the string value from $value will be matched (or rejected, if $exclude is true).
1155              
1156             =cut
1157              
1158             sub SetFilterString {
1159 0     0 1 0 my ($self, $attribute, $value, $exclude) = @_;
1160              
1161 0 0       0 croak("attribute is not defined") unless (defined $attribute);
1162 0 0 0     0 croak("value is not a string") unless ($value && ! ref($value));
1163              
1164 0 0       0 push(@{$self->{_filters}}, {
  0         0  
1165             type => SPH_FILTER_STRING,
1166             attr => $attribute,
1167             value => $value,
1168             exclude => $exclude ? 1 : 0,
1169             });
1170              
1171 0         0 return $self;
1172             }
1173              
1174             =head2 SetFilterRange
1175              
1176             $sph->SetFilterRange($attr, $min, $max);
1177             $sph->SetFilterRange($attr, $min, $max, $exclude);
1178              
1179             Sets the results to be filtered on a range of values for the given
1180             attribute. Only those records where $attr column value is between $min and $max
1181             (including $min and $max) will be returned.
1182              
1183             If 'exclude' is set, excludes results that fall within the given range.
1184              
1185             Returns $sph.
1186              
1187             =cut
1188              
1189             sub SetFilterRange {
1190 0     0 1 0 my ($self, $attribute, $min, $max, $exclude) = @_;
1191 0 0       0 croak("attribute is not defined") unless (defined $attribute);
1192 0 0       0 croak("min: $min is not an integer") unless ($min =~ m/$num_re/);
1193 0 0       0 croak("max: $max is not an integer") unless ($max =~ m/$num_re/);
1194 0 0       0 croak("min value should be <= max") unless ($min <= $max);
1195              
1196 0 0       0 push(@{$self->{_filters}}, {
  0         0  
1197             type => SPH_FILTER_RANGE,
1198             attr => $attribute,
1199             min => $min,
1200             max => $max,
1201             exclude => $exclude ? 1 : 0,
1202             });
1203              
1204 0         0 return $self;
1205             }
1206              
1207             =head2 SetFilterFloatRange
1208              
1209             $sph->SetFilterFloatRange($attr, $min, $max, $exclude);
1210              
1211             Same as L, but allows floating point values.
1212              
1213             Returns $sph.
1214              
1215             =cut
1216              
1217             sub SetFilterFloatRange {
1218 0     0 1 0 my ($self, $attribute, $min, $max, $exclude) = @_;
1219 0 0       0 croak("attribute is not defined") unless (defined $attribute);
1220 0 0       0 croak("min: $min is not numeric") unless ($min =~ m/$num_re/);
1221 0 0       0 croak("max: $max is not numeric") unless ($max =~ m/$num_re/);
1222 0 0       0 croak("min value should be <= max") unless ($min <= $max);
1223              
1224 0 0       0 push(@{$self->{_filters}}, {
  0         0  
1225             type => SPH_FILTER_FLOATRANGE,
1226             attr => $attribute,
1227             min => $min,
1228             max => $max,
1229             exclude => $exclude ? 1 : 0,
1230             });
1231              
1232 0         0 return $self;
1233              
1234             }
1235              
1236             =head2 SetGeoAnchor
1237              
1238             $sph->SetGeoAnchor($attrlat, $attrlong, $lat, $long);
1239              
1240             Setup anchor point for using geosphere distance calculations in filters and sorting.
1241             Distance will be computed with respect to this point
1242              
1243             =over 4
1244              
1245             =item $attrlat is the name of latitude attribute
1246              
1247             =item $attrlong is the name of longitude attribute
1248              
1249             =item $lat is anchor point latitude, in radians
1250              
1251             =item $long is anchor point longitude, in radians
1252              
1253             =back
1254              
1255             Returns $sph.
1256              
1257             =cut
1258              
1259             sub SetGeoAnchor {
1260 0     0 1 0 my ($self, $attrlat, $attrlong, $lat, $long) = @_;
1261              
1262 0 0       0 croak("attrlat is not defined") unless defined $attrlat;
1263 0 0       0 croak("attrlong is not defined") unless defined $attrlong;
1264 0 0       0 croak("lat: $lat is not numeric") unless ($lat =~ m/$num_re/);
1265 0 0       0 croak("long: $long is not numeric") unless ($long =~ m/$num_re/);
1266              
1267             $self->{_anchor} = {
1268 0         0 attrlat => $attrlat,
1269             attrlong => $attrlong,
1270             lat => $lat,
1271             long => $long,
1272             };
1273 0         0 return $self;
1274             }
1275              
1276             =head2 SetGroupBy
1277              
1278             $sph->SetGroupBy($attr, $func);
1279             $sph->SetGroupBy($attr, $func, $groupsort);
1280              
1281             Sets attribute and function of results grouping.
1282              
1283             In grouping mode, all matches are assigned to different groups based on grouping
1284             function value. Each group keeps track of the total match count, and the best
1285             match (in this group) according to current sorting function. The final result
1286             set contains one best match per group, with grouping function value and matches
1287             count attached.
1288              
1289             $attr is any valid attribute. Use L to disable grouping.
1290              
1291             $func is one of:
1292              
1293             =over 4
1294              
1295             =item * SPH_GROUPBY_DAY
1296              
1297             Group by day (assumes timestamp type attribute of form YYYYMMDD)
1298              
1299             =item * SPH_GROUPBY_WEEK
1300              
1301             Group by week (assumes timestamp type attribute of form YYYYNNN)
1302              
1303             =item * SPH_GROUPBY_MONTH
1304              
1305             Group by month (assumes timestamp type attribute of form YYYYMM)
1306              
1307             =item * SPH_GROUPBY_YEAR
1308              
1309             Group by year (assumes timestamp type attribute of form YYYY)
1310              
1311             =item * SPH_GROUPBY_ATTR
1312              
1313             Group by attribute value
1314              
1315             =item * SPH_GROUPBY_ATTRPAIR
1316              
1317             Group by two attributes, being the given attribute and the attribute that
1318             immediately follows it in the sequence of indexed attributes. The specified
1319             attribute may therefore not be the last of the indexed attributes.
1320              
1321             =back
1322              
1323             Groups in the set of results can be sorted by any SQL-like sorting clause,
1324             including both document attributes and the following special internal Sphinx
1325             attributes:
1326              
1327             =over 4
1328              
1329             =item @id - document ID;
1330              
1331             =item @weight, @rank, @relevance - match weight;
1332              
1333             =item @group - group by function value;
1334              
1335             =item @count - number of matches in group.
1336              
1337             =back
1338              
1339             The default mode is to sort by groupby value in descending order,
1340             ie. by "@group desc".
1341              
1342             In the results set, "total_found" contains the total amount of matching groups
1343             over the whole index.
1344              
1345             WARNING: grouping is done in fixed memory and thus its results
1346             are only approximate; so there might be more groups reported
1347             in total_found than actually present. @count might also
1348             be underestimated.
1349              
1350             For example, if sorting by relevance and grouping by a "published"
1351             attribute with SPH_GROUPBY_DAY function, then the result set will
1352             contain only the most relevant match for each day when there were any
1353             matches published, with day number and per-day match count attached,
1354             and sorted by day number in descending order (ie. recent days first).
1355              
1356             =cut
1357              
1358             sub SetGroupBy {
1359 0     0 1 0 my $self = shift;
1360 0         0 my $attribute = shift;
1361 0         0 my $func = shift;
1362 0   0     0 my $groupsort = shift || '@group desc';
1363 0 0       0 croak("attribute is not defined") unless (defined $attribute);
1364 0 0 0     0 croak("Unknown grouping function: $func") unless ($func==SPH_GROUPBY_DAY
      0        
      0        
      0        
      0        
1365             || $func==SPH_GROUPBY_WEEK
1366             || $func==SPH_GROUPBY_MONTH
1367             || $func==SPH_GROUPBY_YEAR
1368             || $func==SPH_GROUPBY_ATTR
1369             || $func==SPH_GROUPBY_ATTRPAIR
1370             );
1371              
1372 0         0 $self->{_groupby} = $attribute;
1373 0         0 $self->{_groupfunc} = $func;
1374 0         0 $self->{_groupsort} = $groupsort;
1375 0         0 return $self;
1376             }
1377              
1378             =head2 SetGroupDistinct
1379              
1380             $sph->SetGroupDistinct($attr);
1381              
1382             Set count-distinct attribute for group-by queries
1383              
1384             =cut
1385              
1386             sub SetGroupDistinct {
1387 0     0 1 0 my $self = shift;
1388 0         0 my $attribute = shift;
1389 0 0       0 croak("attribute is not defined") unless (defined $attribute);
1390 0         0 $self->{_groupdistinct} = $attribute;
1391 0         0 return $self;
1392             }
1393              
1394             =head2 SetRetries
1395              
1396             $sph->SetRetries($count, $delay);
1397              
1398             Set distributed retries count and delay
1399              
1400             =cut
1401              
1402             sub SetRetries {
1403 0     0 1 0 my $self = shift;
1404 0         0 my $count = shift;
1405 0   0     0 my $delay = shift || 0;
1406              
1407 0 0 0     0 croak("count: $count is not an integer >= 0") unless ($count =~ /^\d+$/o && $count >= 0);
1408 0 0 0     0 croak("delay: $delay is not an integer >= 0") unless ($delay =~ /^\d+$/o && $delay >= 0);
1409 0         0 $self->{_retrycount} = $count;
1410 0         0 $self->{_retrydelay} = $delay;
1411 0         0 return $self;
1412             }
1413              
1414             =head2 SetOverride
1415              
1416             ** DEPRECATED **
1417              
1418             $sph->SetOverride($attrname, $attrtype, $values);
1419              
1420             Set attribute values override. There can be only one override per attribute.
1421             $values must be a hash that maps document IDs to attribute values
1422              
1423             =cut
1424              
1425             sub SetOverride {
1426 0     0 1 0 my $self = shift;
1427 0         0 my $attrname = shift;
1428 0         0 my $attrtype = shift;
1429 0         0 my $values = shift;
1430              
1431 0         0 die "SetOverride is DEPRECATED. Do not call this method.";
1432              
1433 0 0       0 croak("attribute name is not defined") unless defined $attrname;
1434 0 0 0     0 croak("Uknown attribute type: $attrtype") unless ($attrtype == SPH_ATTR_INTEGER
      0        
      0        
      0        
1435             || $attrtype == SPH_ATTR_TIMESTAMP
1436             || $attrtype == SPH_ATTR_BOOL
1437             || $attrtype == SPH_ATTR_FLOAT
1438             || $attrtype == SPH_ATTR_BIGINT);
1439 0         0 $self->{_overrides}->{$attrname} = { attr => $attrname,
1440             type => $attrtype,
1441             values => $values,
1442             };
1443            
1444 0         0 return $self;
1445             }
1446              
1447              
1448             =head2 SetSelect
1449              
1450             $sph->SetSelect($select)
1451              
1452             Set select list (attributes or expressions). SQL-like syntax.
1453              
1454             =cut
1455              
1456             sub SetSelect {
1457 0     0 1 0 my $self = shift;
1458 0         0 $self->{_select} = shift;
1459 0         0 return $self;
1460             }
1461              
1462             =head2 SetQueryFlag
1463              
1464             $sph->SetQueryFlag($flag_name, $flag_value);
1465              
1466              
1467             =cut
1468              
1469             sub SetQueryFlag {
1470 0     0 1 0 my ($self, $flag_name, $flag_value) = @_;
1471              
1472 0 0       0 croak("Unknown flag $flag_name") unless exists $query_flags{$flag_name};
1473             croak("Unknown or illegal flag value ($flag_value) for '$flag_name'") unless
1474 0 0 0 0   0 (any { $_ eq $flag_value } @{$query_flags{$flag_name}})
  0   0     0  
  0         0  
1475             || ($flag_name eq 'max_predicted_time' && $flag_value =~ m/^\d+$/);
1476              
1477 0 0       0 if ($flag_name eq SPH_QF_REVERSE_SCAN) {
    0          
    0          
    0          
    0          
    0          
1478 0         0 $self->{_query_flags} = $self->_sphSetBit( $self->{_query_flags}, 0, $flag_value == 1);
1479             }
1480             elsif ($flag_name eq SPH_QF_SORT_METHOD) {
1481 0         0 $self->{_query_flags} = $self->_sphSetBit( $self->{_query_flags}, 1, $flag_value == "kbuffer");
1482             }
1483             elsif ($flag_name eq SPH_QF_MAX_PREDICTED_TIME) {
1484 0         0 $self->{_query_flags} = $self->_sphSetBit( $self->{_query_flags}, 2, $flag_value > 0);
1485 0         0 $self->{_predictedtime} = $flag_value;
1486             }
1487             elsif ($flag_name eq SPH_QF_BOOLEAN_SIMPLIFY) {
1488 0         0 $self->{_query_flags} = $self->_sphSetBit( $self->{_query_flags}, 3, $flag_value);
1489             }
1490             elsif ($flag_name eq SPH_QF_IDF) {
1491 0 0 0     0 if ($flag_value eq 'normalized' || $flag_value eq 'plain') {
1492 0         0 $self->{_query_flags} = $self->_sphSetBit( $self->{_query_flags}, 4, $flag_value eq 'normalized');
1493             }
1494             else { # must be tfidf_normalized or tfidf_unnormalized
1495 0         0 $self->{_query_flags} = $self->_sphSetBit( $self->{_query_flags}, 6, $flag_value eq 'tfidf_normalized');
1496             }
1497             }
1498             elsif ($flag_name eq SPH_QF_GLOBAL_IDF) {
1499 0         0 $self->{_query_flags} = $self->_sphSetBit( $self->{_query_flags}, 5, $flag_value);
1500             }
1501              
1502 0         0 return $self;
1503             }
1504              
1505             =head2 SetOuterSelect
1506              
1507             $sph->SetOuterSelect($orderby, $offset, $limit)
1508              
1509             =cut
1510              
1511             sub SetOuterSelect {
1512 0     0 1 0 my ($self, $orderby, $offset, $limit) = @_;
1513              
1514 0 0 0     0 croak("orderby must be a string") unless $orderby && ! ref($orderby);
1515 0 0 0     0 croak("offset and limit must be integers > 0") unless $offset =~ m/^\d+$/ && $limit =~ m/^\d+$/;
1516              
1517 0         0 $self->{_outerorderby} = $orderby;
1518 0         0 $self->{_outeroffsetlimit} = $offset;
1519 0         0 $self->{_outerlimit} = $limit;
1520 0         0 $self->{_hasouter} = 1;
1521              
1522 0         0 return $self;
1523             }
1524              
1525             =head2 ResetFilters
1526              
1527             $sph->ResetFilters;
1528              
1529             Clear all filters.
1530              
1531             =cut
1532              
1533             sub ResetFilters {
1534 0     0 1 0 my $self = shift;
1535              
1536 0         0 $self->{_filters} = [];
1537 0         0 $self->{_anchor} = undef;
1538              
1539 0         0 return $self;
1540             }
1541              
1542             =head2 ResetGroupBy
1543              
1544             $sph->ResetGroupBy;
1545              
1546             Clear all group-by settings (for multi-queries)
1547              
1548             =cut
1549              
1550             sub ResetGroupBy {
1551 0     0 1 0 my $self = shift;
1552              
1553 0         0 $self->{_groupby} = "";
1554 0         0 $self->{_groupfunc} = SPH_GROUPBY_DAY;
1555 0         0 $self->{_groupsort} = '@group desc';
1556 0         0 $self->{_groupdistinct} = "";
1557              
1558 0         0 return $self;
1559             }
1560              
1561             =head2 ResetOverrides
1562              
1563             Clear all attribute value overrides (for multi-queries)
1564              
1565             =cut
1566              
1567             sub ResetOverrides {
1568 0     0 1 0 my $self = shift;
1569              
1570 0         0 $self->{_select} = undef;
1571 0         0 return $self;
1572             }
1573              
1574             =head2 ResetQueryFlag
1575              
1576             Clear all query flags.
1577              
1578             =cut
1579              
1580             sub ResetQueryFlag {
1581 3     3 1 5 my $self = shift;
1582              
1583 3         10 $self->{_query_flags} = $self->_sphSetBit(0, 6, 1);
1584 3         8 $self->{_predictedtime} = 0;
1585 3         5 return $self;
1586             }
1587              
1588             =head2 ResetOuterSelect
1589              
1590             Clear all outer select settings.
1591              
1592             =cut
1593              
1594             sub ResetOuterSelect {
1595 3     3 1 5 my $self = shift;
1596              
1597 3         7 $self->{_outerorderby} = '';
1598 3         5 $self->{_outeroffset} = 0;
1599 3         5 $self->{_outerlimit} = 0;
1600 3         6 $self->{_hasouter} = 0;
1601              
1602 3         6 return $self;
1603             }
1604              
1605              
1606             =head2 Query
1607              
1608             $results = $sph->Query($query, $index);
1609              
1610             Connect to searchd server and run given search query.
1611              
1612             =over 4
1613              
1614             =item query is query string
1615              
1616             =item index is index name to query, default is "*" which means to query all indexes. Use a space or comma separated list to search multiple indexes.
1617              
1618             =back
1619              
1620             Returns undef on failure
1621              
1622             Returns hash which has the following keys on success:
1623              
1624             =over 4
1625              
1626             =item matches
1627            
1628             Array containing hashes with found documents ( "doc", "weight", "group", "stamp" )
1629            
1630             =item total
1631              
1632             Total amount of matches retrieved (upto SPH_MAX_MATCHES, see sphinx.h)
1633              
1634             =item total_found
1635            
1636             Total amount of matching documents in index
1637            
1638             =item time
1639            
1640             Search time
1641              
1642             =item words
1643            
1644             Hash which maps query terms (stemmed!) to ( "docs", "hits" ) hash
1645              
1646             =back
1647              
1648             Returns the results array on success, undef on error.
1649              
1650             =cut
1651              
1652             sub Query {
1653 0     0 1   my $self = shift;
1654 0           my $query = shift;
1655 0   0       my $index = shift || '*';
1656 0   0       my $comment = shift || '';
1657              
1658 0 0         croak("_reqs is not empty") unless @{$self->{_reqs}} == 0;
  0            
1659              
1660 0           $self->AddQuery($query, $index, $comment);
1661 0 0         my $results = $self->RunQueries or return;
1662 0 0         $self->_Error($results->[0]->{error}) if $results->[0]->{error};
1663 0 0         $self->_Warning($results->[0]->{warning}) if $results->[0]->{warning};
1664 0 0 0       return if $results->[0]->{status} && $results->[0]->{status} == SEARCHD_ERROR;
1665              
1666 0           return $results->[0];
1667             }
1668              
1669             # helper to pack floats in network byte order
1670             sub _PackFloat {
1671 0     0     my $f = shift;
1672 0           my $t1 = pack ( "f", $f ); # machine order
1673 0           my $t2 = unpack ( "L*", $t1 ); # int in machine order
1674 0           return pack ( "N", $t2 );
1675             }
1676              
1677              
1678             =head2 AddQuery
1679              
1680             $sph->AddQuery($query, $index);
1681              
1682             Add a query to a batch request.
1683              
1684             Batch queries enable searchd to perform internal optimizations,
1685             if possible; and reduce network connection overheads in all cases.
1686              
1687             For instance, running exactly the same query with different
1688             groupby settings will enable searched to perform expensive
1689             full-text search and ranking operation only once, but compute
1690             multiple groupby results from its output.
1691              
1692             Parameters are exactly the same as in Query() call.
1693              
1694             Returns corresponding index to the results array returned by RunQueries() call.
1695              
1696             =cut
1697              
1698             sub AddQuery {
1699 0     0 1   my $self = shift;
1700 0           my $query = shift;
1701 0   0       my $index = shift || '*';
1702 0   0       my $comment = shift || '';
1703              
1704             ##################
1705             # build request
1706             ##################
1707              
1708 0           my $req;
1709 0           $req = pack ( "NNNNN", $self->{_query_flags}, $self->{_offset}, $self->{_limit}, $self->{_mode}, $self->{_ranker}); # mode and limits
1710              
1711 0 0         if ($self->{_ranker} == SPH_RANK_EXPR) {
1712 0           $req .= pack ( "N/a*", $self->{_rankexpr});
1713             }
1714 0           $req .= pack ( "N", $self->{_sort} ); # (deprecated) sort mode
1715 0           $req .= pack ( "N/a*", $self->{_sortby});
1716 0           $req .= pack ( "N/a*", $self->{_string_encoder}->($query) ); # query itself
1717 0           $req .= pack ( "N*", scalar(@{$self->{_weights}}), @{$self->{_weights}});
  0            
  0            
1718 0           $req .= pack ( "N/a*", $index); # indexes
1719             $req .= pack ( "N", 1)
1720             . $self->_sphPackU64($self->{_min_id})
1721 0           . $self->_sphPackU64($self->{_max_id}); # id64 range
1722              
1723             # filters
1724 0           $req .= pack ( "N", scalar @{$self->{_filters}} );
  0            
1725 0           foreach my $filter (@{$self->{_filters}}) {
  0            
1726 0           $req .= pack ( "N/a*", $filter->{attr});
1727 0           $req .= pack ( "N", $filter->{type});
1728              
1729 0           my $t = $filter->{type};
1730 0 0         if ($t == SPH_FILTER_VALUES) {
    0          
    0          
    0          
1731 0           $req .= $self->_sphPackI64array($filter->{values});
1732             }
1733             elsif ($t == SPH_FILTER_RANGE) {
1734 0           $req .= $self->_sphPackI64($filter->{min}) . $self->_sphPackI64($filter->{max});
1735             }
1736             elsif ($t == SPH_FILTER_FLOATRANGE) {
1737 0           $req .= _PackFloat ( $filter->{"min"} ) . _PackFloat ( $filter->{"max"} );
1738             }
1739             elsif ($t == SPH_FILTER_STRING) {
1740 0           $req .= pack ( "N/a*", $filter->{value});
1741             }
1742             else {
1743 0           croak("Unhandled filter type $t");
1744             }
1745 0           $req .= pack ( "N", $filter->{exclude});
1746             }
1747              
1748             # group-by clause, max-matches count, group-sort clause, cutoff count
1749 0           $req .= pack ( "NN/a*", $self->{_groupfunc}, $self->{_groupby} );
1750 0           $req .= pack ( "N", $self->{_maxmatches} );
1751 0           $req .= pack ( "N/a*", $self->{_groupsort});
1752 0           $req .= pack ( "NNN", $self->{_cutoff}, $self->{_retrycount}, $self->{_retrydelay} );
1753 0           $req .= pack ( "N/a*", $self->{_groupdistinct});
1754              
1755 0 0         if (!defined $self->{_anchor}) {
1756 0           $req .= pack ( "N", 0);
1757             }
1758             else {
1759 0           my $a = $self->{_anchor};
1760 0           $req .= pack ( "N", 1);
1761 0           $req .= pack ( "N/a*", $a->{attrlat});
1762 0           $req .= pack ( "N/a*", $a->{attrlong});
1763 0           $req .= _PackFloat($a->{lat}) . _PackFloat($a->{long});
1764             }
1765              
1766             # per-index weights
1767 0           $req .= pack( "N", scalar keys %{$self->{_indexweights}});
  0            
1768 0           $req .= pack ( "N/a*N", $_, $self->{_indexweights}->{$_} ) for keys %{$self->{_indexweights}};
  0            
1769              
1770             # max query time
1771 0           $req .= pack ( "N", $self->{_maxquerytime} );
1772              
1773             # per-field weights
1774 0           $req .= pack ( "N", scalar keys %{$self->{_fieldweights}} );
  0            
1775 0           $req .= pack ( "N/a*N", $_, $self->{_fieldweights}->{$_}) for keys %{$self->{_fieldweights}};
  0            
1776             # comment
1777 0           $req .= pack ( "N/a*", $comment);
1778              
1779             # attribute overrides
1780 0           $req .= pack ( "N", scalar keys %{$self->{_overrides}} );
  0            
1781 0           for my $entry (values %{$self->{_overrides}}) {
  0            
1782             $req .= pack ("N/a*", $entry->{attr})
1783 0           . pack ("NN", $entry->{type}, scalar keys %{$entry->{values}});
  0            
1784 0           for my $id (keys %{$entry->{values}}) {
  0            
1785 0 0         croak "Attribute value key is not numeric" unless $id =~ m/$num_re/;
1786 0           my $v = $entry->{values}->{$id};
1787 0 0         croak "Attribute value key is not numeric" unless $v =~ m/$num_re/;
1788 0           $req .= $self->_sphPackU64($id);
1789 0 0         if ($entry->{type} == SPH_ATTR_FLOAT) {
    0          
1790 0           $req .= $self->_packfloat($v);
1791             }
1792             elsif ($entry->{type} == SPH_ATTR_BIGINT) {
1793 0           $req .= $self->_sphPackI64($v);
1794             }
1795             else {
1796 0           $req .= pack("N", $v);
1797             }
1798             }
1799             }
1800            
1801             # select list
1802 0   0       $req .= pack("N/a*", $self->{_select} || '');
1803              
1804             # max_predicted_time
1805 0 0         if ($self->{_predictedtime} > 0) {
1806 0           $req .= pack ( "N", $self->{_predictedtime} );
1807             }
1808              
1809 0           $req .= pack ( "N/a*", $self->{_outerorderby});
1810 0           $req .= pack ( "NN", $self->{_outeroffset}, $self->{_outerlimit} );
1811 0 0         $req .= pack ("N", $self->{_hasouter} ? 1 : 0 );
1812              
1813 0           push(@{$self->{_reqs}}, $req);
  0            
1814              
1815 0           return scalar $#{$self->{_reqs}};
  0            
1816             }
1817              
1818             =head2 RunQueries
1819              
1820             $sph->RunQueries
1821              
1822             Run batch of queries, as added by AddQuery.
1823              
1824             Returns undef on network IO failure.
1825              
1826             Returns an array of result sets on success.
1827              
1828             Each result set in the returned array is a hash which contains
1829             the same keys as the hash returned by L, plus:
1830              
1831             =over 4
1832              
1833             =item * error
1834              
1835             Errors, if any, for this query.
1836              
1837             =item * warning
1838            
1839             Any warnings associated with the query.
1840              
1841             =back
1842              
1843             =cut
1844              
1845             sub RunQueries {
1846 0     0 1   my $self = shift;
1847              
1848 0 0         unless (@{$self->{_reqs}}) {
  0            
1849 0           $self->_Error("no queries defined, issue AddQuery() first");
1850 0           return;
1851             }
1852              
1853             ##################
1854             # send query, get response
1855             ##################
1856 0           my $nreqs = @{$self->{_reqs}};
  0            
1857 0           my $req = pack("NNa*", 0, $nreqs, join("", @{$self->{_reqs}}));
  0            
1858 0           $req = pack ( "nnN/a*", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $req); # add header
1859 0           my $response = $self->_ProcessRequest($req, VER_COMMAND_SEARCH);
1860 0           $self->{_reqs} = [];
1861 0 0         return unless $response;
1862              
1863             ##################
1864             # parse response
1865             ##################
1866              
1867 0           my $p = 0;
1868 0           my $max = length($response); # Protection from broken response
1869              
1870 0           my @results;
1871 0           for (my $ires = 0; $ires < $nreqs; $ires++) {
1872 0           my $result = {}; # Empty hash ref
1873 0           push(@results, $result);
1874 0           $result->{matches} = []; # Empty array ref
1875 0           $result->{error} = "";
1876 0           $result->{warning} = "";
1877              
1878             # extract status
1879 0           my $status = unpack("N", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1880 0 0         if ($status != SEARCHD_OK) {
1881 0           my $len = unpack("N", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1882 0           my $message = substr ( $response, $p, $len ); $p += $len;
  0            
1883 0 0         if ($status == SEARCHD_WARNING) {
1884 0           $result->{warning} = $message;
1885             }
1886             else {
1887 0           $result->{error} = $message;
1888 0           next;
1889             }
1890             }
1891              
1892             # read schema
1893 0           my @fields;
1894 0           my (%attrs, @attr_list);
1895              
1896 0           my $nfields = unpack ( "N", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1897 0   0       while ( $nfields-->0 && $p<$max ) {
1898 0           my $len = unpack ( "N", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1899 0           push(@fields, substr ( $response, $p, $len )); $p += $len;
  0            
1900             }
1901 0           $result->{"fields"} = \@fields;
1902              
1903 0           my $nattrs = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1904 0   0       while ( $nattrs-->0 && $p<$max ) {
1905 0           my $len = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1906 0           my $attr = substr ( $response, $p, $len ); $p += $len;
  0            
1907 0           my $type = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1908 0           $attrs{$attr} = $type;
1909 0           push(@attr_list, $attr);
1910             }
1911 0           $result->{"attrs"} = \%attrs;
1912              
1913             # read match count
1914 0           my $count = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1915 0           my $id64 = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1916              
1917             # read matches
1918 0   0       while ( $count-->0 && $p<$max ) {
1919 0           my $data = {};
1920 0 0         if ($id64) {
1921 0           $data->{doc} = $self->_sphUnpackU64(substr($response, $p, 8)); $p += 8;
  0            
1922 0           $data->{weight} = unpack("N*", substr($response, $p, 4)); $p += 4;
  0            
1923             }
1924             else {
1925 0           ( $data->{doc}, $data->{weight} ) = unpack("N*N*", substr($response,$p,8));
1926 0           $p += 8;
1927             }
1928 0           foreach my $attr (@attr_list) {
1929 0 0         if ($attrs{$attr} == SPH_ATTR_BIGINT) {
1930 0           $data->{$attr} = $self->_sphUnpackI64(substr($response, $p, 8)); $p += 8;
  0            
1931 0           next;
1932             }
1933 0 0         if ($attrs{$attr} == SPH_ATTR_FLOAT) {
1934 0           my $uval = unpack( "N*", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1935 0           $data->{$attr} = [ unpack("f*", pack("L", $uval)) ];
1936 0           next;
1937             }
1938 0           my $val = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1939 0 0         if ($attrs{$attr} == SPH_ATTR_MULTI) {
    0          
    0          
    0          
1940 0           my $nvalues = $val;
1941 0           $data->{$attr} = [];
1942 0   0       while ($nvalues-- > 0 && $p < $max) {
1943 0           $val = unpack( "N*", substr ( $response, $p, 4 ) ); $p += 4;
  0            
1944 0           push(@{$data->{$attr}}, $val);
  0            
1945             }
1946             }
1947             elsif ($attrs{$attr} == SPH_ATTR_MULTI64) {
1948 0           my $nvalues = $val;
1949 0           $data->{$attr} = [];
1950 0   0       while ($nvalues > 0 && $p < $max) {
1951 0           $val = unpack( "q*", substr ( $response, $p, 8 ) ); $p += 8;
  0            
1952 0           push(@{$data->{$attr}}, $val);
  0            
1953 0           $nvalues -= 2;
1954             }
1955             }
1956             elsif ($attrs{$attr} == SPH_ATTR_STRING) {
1957 0           $data->{$attr} = $self->{_string_decoder}->(substr ($response, $p, $val));
1958 0           $p += $val;
1959             }
1960             elsif ($attrs{$attr} == SPH_ATTR_FACTORS) {
1961 0           $data->{$attr} = $self->{_string_decoder}->(substr ($response, $p, $val - 4));
1962 0           $p += $val - 4;
1963             }
1964             else {
1965 0           $data->{$attr} = $val;
1966             }
1967             }
1968 0           push(@{$result->{matches}}, $data);
  0            
1969             }
1970 0           my $words;
1971 0           ($result->{total}, $result->{total_found}, $result->{time}, $words) = unpack("N*N*N*N*", substr($response, $p, 16));
1972 0           $result->{time} = sprintf ( "%.3f", $result->{"time"}/1000 );
1973 0           $p += 16;
1974              
1975 0   0       while ( $words-->0 && $p < $max) {
1976 0           my $len = unpack ( "N*", substr ( $response, $p, 4 ) );
1977 0           $p += 4;
1978 0           my $word = $self->{_string_decoder}->( substr ( $response, $p, $len ) );
1979 0           $p += $len;
1980 0           my ($docs, $hits) = unpack ("N*N*", substr($response, $p, 8));
1981 0           $p += 8;
1982 0           $result->{words}{$word} = {
1983             "docs" => $docs,
1984             "hits" => $hits
1985             };
1986             }
1987             }
1988              
1989 0           return \@results;
1990             }
1991              
1992             =head2 SphinxQL
1993              
1994             my $results = $sph->SphinxQL($sphinxql_query);
1995              
1996             This is an alternative implementation of the SphinxQL API to the DBI option. Frankly, it was an experiment, and the DBI driver proved to have much better performance. Whilst this may be useful to some, in general if you are considering using this method then you should probably look at connecting directly via DBI instead.
1997              
1998             Results are return in a hash containing an array of 'columns' and 'rows' and possibly a warning count. If a server-side error occurs, the hash contains the 'error' field. If a communication error occurs, the return value will be undefined. In either error case, GetLastError will return the error.
1999              
2000             =cut
2001              
2002             sub SphinxQL {
2003 0     0 1   my ($self, $ql) = @_;
2004              
2005 0           my $req = pack ("N/a*", $self->{_string_encoder}->($ql));
2006 0           $req = pack ( "nnN/a*", SEARCHD_COMMAND_SPHINXQL, VER_COMMAND_SPHINXQL, $req); # add header
2007 0           my $response = $self->_ProcessRequest($req, VER_COMMAND_SPHINXQL);
2008              
2009 0 0         return if ! $response;
2010              
2011 0           my ($result) = $self->_mysql_unpack_table($response, 0, length($response));
2012              
2013 0           return $result;
2014             }
2015              
2016             sub _mysql_unpack_header {
2017 0     0     my ($self, $response, $p) = @_;
2018              
2019 0           my $max = length($response);
2020 0 0         return (undef, undef, $p, 1, undef, undef) if $p == $max;
2021 0 0         $self->_Throw("Decode pointer ($p) beyond end of buffer ($max)") if $p > $max;
2022              
2023 0           my $header = unpack ( "L<", substr( $response, $p, 4 ) ); $p += 4; # packet ID << 24 | packet length
  0            
2024 0           my $packet_id = $header >> 24;
2025 0           my $packet_len = $header & 0x00FFFFFF;
2026 0 0         return ($packet_id, $packet_len, $p, 1, undef, undef) if $packet_len == 0;
2027              
2028             # Peek for EOF or error
2029 0           my ($is_eof, $warns, $more_results);
2030 0           my $peek_byte = unpack("C", substr($response, $p, 1));
2031 0 0 0       if ($packet_len == 5 && $peek_byte == 0xfe) { # EOF
    0          
2032 0           $is_eof = 1;
2033 0           $p += 1; # 0xFE
2034 0           my $warns = unpack ( "L<", substr( $response, $p, 4 ) ); $p += 4;
  0            
2035 0           my $more_results = (($warns >> 16) & 8) != 0; # SPH_MYSQL_FLAG_MORE_RESULTS = 8
2036 0           $warns &= 0xFFFF;
2037             }
2038             elsif ($peek_byte == 0xff) {
2039 0           $p += 1; # 0xFF
2040 0           my $error_code = unpack ( "S<", substr( $response, $p, 2 ) ); $p += 2;
  0            
2041 0           my $error_code2 = unpack("a*", substr($response, $p, 6)); $p += 6;
  0            
2042 0           my $len = $packet_len - 9;
2043 0           my $err_msg = $self->{_string_decoder}->(unpack("a*", substr($response, $p, $len))); $p += $len;
  0            
2044 0           $self->_Throw("Error Code $error_code: $error_code2 $err_msg");
2045             }
2046 0           return ($packet_id, $packet_len, $p, $is_eof, $warns, $more_results);
2047             }
2048              
2049             sub _mysql_unpack_table {
2050 0     0     my ($self, $response, $p, $max) = @_;
2051              
2052 0           my ($columns, $warns, $more_results, $rows);
2053              
2054 0           eval {
2055 0           ($columns, $warns, $more_results, $p) = $self->_mysql_unpack_table_header($response, $p);
2056 0           ($rows, $p, $warns, $more_results) = $self->_mysql_unpack_table_rows($response, $p, $max, $columns);
2057             };
2058 0 0         if (my $e = $@) {
2059 0           $self->_Error($e);
2060 0           return ({ error => $e }, $p);
2061             }
2062              
2063 0           my %result = (columns => $columns,
2064             rows => $rows,
2065             warnings => $warns,
2066             );
2067 0           return (\%result, $p);
2068             }
2069              
2070             sub _mysql_unpack_table_header {
2071 0     0     my ($self, $response, $p) = @_;
2072              
2073 0           my @columns;
2074              
2075             # table header begin
2076 0           my ($packet_id, $packet_len, $is_eof, $warns, $more_results);
2077 0           ($packet_id, $packet_len, $p, $is_eof, $warns, $more_results) = $self->_mysql_unpack_header($response, $p);
2078              
2079 0 0         if (! $is_eof) {
2080 0           my $ncols;
2081 0           ($ncols, $p) = $self->_mysql_unpack_varint($response, $p);
2082              
2083             # column info
2084 0           for my $i (0 .. $ncols - 1) {
2085 0           ($columns[$i], $p) = $self->_mysql_unpack_field_packet($response, $p);
2086             }
2087              
2088             # table header end
2089 0           ($packet_id, $packet_len, $p, $is_eof, $warns, $more_results) = $self->_mysql_unpack_header($response, $p);
2090             }
2091 0           return (\@columns, $warns, $more_results, $p);
2092             }
2093              
2094             sub _mysql_unpack_table_rows {
2095 0     0     my ($self, $response, $p, $max, $columns) = @_;
2096              
2097 0           my @rows;
2098 0           my ($warns, $more_results);
2099 0           while ($p < $max) {
2100 0           my $row;
2101 0           ($row, $p, $warns, $more_results) = $self->_mysql_unpack_table_row($response, $p, $columns);
2102 0 0         if ($row) {
2103 0           push(@rows, $row);
2104             }
2105             else {
2106 0           last;
2107             }
2108             }
2109 0           return (\@rows, $p, $warns, $more_results);
2110             }
2111              
2112             sub _mysql_unpack_table_row {
2113 0     0     my ($self, $response, $p, $columns) = @_;
2114              
2115 0           my ($packet_id, $packet_len, $is_eof, $warns, $more_results);
2116 0           ($packet_id, $packet_len, $p, $is_eof, $warns, $more_results) = $self->_mysql_unpack_header($response, $p);
2117 0 0         if ($is_eof) {
2118 0           return(undef, $p, $warns, $more_results);
2119             }
2120              
2121 0           my @row;
2122 0           for my $col (@$columns) {
2123 0           my $val;
2124 0 0         if ($col->{column_type} == MYSQL_COL_STRING) {
2125 0           ($val, $p) = $self->_mysql_unpack_string($response, $p, 1);
2126             }
2127             else {
2128 0           ($val, $p) = $self->_mysql_unpack_string($response, $p);
2129             }
2130 0           push(@row, $val);
2131             }
2132 0           return (\@row, $p);
2133             }
2134              
2135              
2136             sub _mysql_unpack_varint {
2137 0     0     my ($self, $response, $p) = @_;
2138              
2139 0           my $prefix = unpack("C", substr($response, $p, 1)); $p += 1;
  0            
2140 0 0         if ($prefix < 251) {
2141 0           return ($prefix, $p);
2142             }
2143 0 0         if ($prefix == 0xFC) {
2144 0           $prefix = unpack("S<", substr($response, $p, 2)); $p += 2;
  0            
2145 0           return ($prefix, $p);
2146             }
2147 0 0         if ($prefix == 0xFC) {
2148 0           $prefix = unpack("S<", substr($response, $p, 2)); $p += 2;
  0            
2149 0           $prefix += unpack("C", substr($response, $p, 1)) << 16; $p += 1;
  0            
2150 0           return ($prefix, $p);
2151             }
2152 0 0         if ($prefix == 0xFE) {
2153 0           $prefix = unpack("L<", substr($response, $p, 4)); $p += 4;
  0            
2154             }
2155 0           $p += 4; # discard 4 null bytes
2156 0           return ($prefix, $p);
2157             }
2158              
2159             sub _mysql_unpack_string {
2160 0     0     my ($self, $response, $p, $decode) = @_;
2161              
2162 0           my $len;
2163 0           ($len, $p) = $self->_mysql_unpack_varint($response, $p);
2164 0           my $s = substr($response, $p, $len); $p += $len;
  0            
2165 0 0         $s = $self->{_string_decoder}->($s) if $decode;
2166              
2167 0           return ($s, $p);
2168             }
2169              
2170             sub _mysql_unpack_field_packet {
2171 0     0     my ($self, $response, $p) = @_;
2172              
2173 0           my ($packet_id, $packet_len, $is_eof, $warns, $more_results);
2174 0           ($packet_id, $packet_len, $p, $is_eof, $warns, $more_results) = $self->_mysql_unpack_header($response, $p);
2175              
2176 0           my %field;
2177 0           ($field{catalog}, $p) = $self->_mysql_unpack_string($response, $p);
2178 0           ($field{db}, $p) = $self->_mysql_unpack_string($response, $p);
2179 0           ($field{table}, $p) = $self->_mysql_unpack_string($response, $p);
2180 0           ($field{org_table}, $p) = $self->_mysql_unpack_string($response, $p);
2181 0           ($field{name}, $p) = $self->_mysql_unpack_string($response, $p);
2182 0           ($field{org_name}, $p) = $self->_mysql_unpack_string($response, $p);
2183              
2184 0           $p += 3; # filler=12, charset_nr=0x21 (utf8)
2185 0           $field{column_length} = unpack ( "L<", substr( $response, $p, 4 ) ); $p += 4;
  0            
2186 0           $field{column_type} = unpack("C", substr($response, $p, 1)); $p += 1;
  0            
2187 0           $p += 5; # flags, decimals, filler
2188              
2189 0           return (\%field, $p);
2190             }
2191              
2192             =head2 BuildExcerpts
2193              
2194             $excerpts = $sph->BuildExcerpts($docs, $index, $words, $opts)
2195              
2196             Generate document excerpts for the specified documents.
2197              
2198             =over 4
2199              
2200             =item docs
2201              
2202             An array reference of strings which represent the document
2203             contents
2204              
2205             =item index
2206              
2207             A string specifiying the index whose settings will be used
2208             for stemming, lexing and case folding
2209              
2210             =item words
2211              
2212             A string which contains the words to highlight
2213              
2214             =item opts
2215              
2216             A hash which contains additional optional highlighting parameters:
2217              
2218             =over 4
2219              
2220             =item before_match - a string to insert before a set of matching words, default is ""
2221              
2222             =item after_match - a string to insert after a set of matching words, default is ""
2223              
2224             =item chunk_separator - a string to insert between excerpts chunks, default is " ... "
2225              
2226             =item limit - max excerpt size in symbols (codepoints), default is 256
2227              
2228             =item limit_passages - Limits the maximum number of passages that can be included into the snippet. Integer, default is 0 (no limit).
2229              
2230             =item limit_words - Limits the maximum number of keywords that can be included into the snippet. Integer, default is 0 (no limit).
2231              
2232             =item around - how many words to highlight around each match, default is 5
2233              
2234             =item exact_phrase - whether to highlight exact phrase matches only, default is false
2235              
2236             =item single_passage - whether to extract single best passage only, default is false
2237              
2238             =item use_boundaries
2239              
2240             =item weight_order - Whether to sort the extracted passages in order of relevance (decreasing weight), or in order of appearance in the document (increasing position). Boolean, default is false.
2241              
2242             =item query_mode - Whether to handle $words as a query in extended syntax, or as a bag of words (default behavior). For instance, in query mode ("one two" | "three four") will only highlight and include those occurrences "one two" or "three four" when the two words from each pair are adjacent to each other. In default mode, any single occurrence of "one", "two", "three", or "four" would be highlighted. Boolean, default is false.
2243              
2244             =item force_all_words - Ignores the snippet length limit until it includes all the keywords. Boolean, default is false.
2245              
2246             =item start_passage_id - Specifies the starting value of %PASSAGE_ID% macro (that gets detected and expanded in before_match, after_match strings). Integer, default is 1.
2247              
2248             =item load_files - Whether to handle $docs as data to extract snippets from (default behavior), or to treat it as file names, and load data from specified files on the server side. Boolean, default is false.
2249              
2250             =item html_strip_mode - HTML stripping mode setting. Defaults to "index", which means that index settings will be used. The other values are "none" and "strip", that forcibly skip or apply stripping irregardless of index settings; and "retain", that retains HTML markup and protects it from highlighting. The "retain" mode can only be used when highlighting full documents and thus requires that no snippet size limits are set. String, allowed values are "none", "strip", "index", and "retain".
2251              
2252             =item allow_empty - Allows empty string to be returned as highlighting result when a snippet could not be generated (no keywords match, or no passages fit the limit). By default, the beginning of original text would be returned instead of an empty string. Boolean, default is false.
2253              
2254             =item passage_boundary
2255              
2256             =item emit_zones
2257              
2258             =item load_files_scattered
2259              
2260             =back
2261              
2262             =back
2263              
2264             Returns undef on failure.
2265              
2266             Returns an array ref of string excerpts on success.
2267              
2268             =cut
2269              
2270             sub BuildExcerpts {
2271 0     0 1   my ($self, $docs, $index, $words, $opts) = @_;
2272 0   0       $opts ||= {};
2273 0 0 0       croak("BuildExcepts() called with incorrect parameters")
      0        
      0        
2274             unless (ref($docs) eq 'ARRAY'
2275             && defined($index)
2276             && defined($words)
2277             && ref($opts) eq 'HASH');
2278              
2279             ##################
2280             # fixup options
2281             ##################
2282 0   0       $opts->{"before_match"} ||= "";
2283 0   0       $opts->{"after_match"} ||= "";
2284 0   0       $opts->{"chunk_separator"} ||= " ... ";
2285 0   0       $opts->{"limit"} ||= 256;
2286 0   0       $opts->{"limit_passages"} ||= 0;
2287 0   0       $opts->{"limit_words"} ||= 0;
2288 0   0       $opts->{"around"} ||= 5;
2289 0   0       $opts->{"exact_phrase"} ||= 0;
2290 0   0       $opts->{"single_passage"} ||= 0;
2291 0   0       $opts->{"use_boundaries"} ||= 0;
2292 0   0       $opts->{"weight_order"} ||= 0;
2293 0   0       $opts->{"query_mode"} ||= 0;
2294 0   0       $opts->{"force_all_words"} ||= 0;
2295 0   0       $opts->{"start_passage_id"} ||= 1;
2296 0   0       $opts->{"load_files"} ||= 0;
2297 0   0       $opts->{"html_strip_mode"} ||= "index";
2298 0   0       $opts->{"allow_empty"} ||= 0;
2299 0   0       $opts->{"passage_boundary"} ||= "none";
2300 0   0       $opts->{"emit_zones"} ||= 0;
2301 0   0       $opts->{"load_files_scattered"} ||= 0;
2302              
2303             ##################
2304             # build request
2305             ##################
2306              
2307             # v.1.2 req
2308 0           my $req;
2309 0           my $flags = 1; # remove spaces
2310 0 0         $flags |= 2 if ( $opts->{"exact_phrase"} );
2311 0 0         $flags |= 4 if ( $opts->{"single_passage"} );
2312 0 0         $flags |= 8 if ( $opts->{"use_boundaries"} );
2313 0 0         $flags |= 16 if ( $opts->{"weight_order"} );
2314 0 0         $flags |= 32 if ( $opts->{"query_mode"} );
2315 0 0         $flags |= 64 if ( $opts->{"force_all_words"} );
2316 0 0         $flags |= 128 if ( $opts->{"load_files"} );
2317 0 0         $flags |= 256 if ( $opts->{"allow_empty"} );
2318 0 0         $flags |= 512 if ( $opts->{"emit_zones"} );
2319 0 0         $flags |= 1024 if ( $opts->{"load_files_scattered"} );
2320 0           $req = pack ( "NN", 0, $flags ); # mode=0, flags=$flags
2321              
2322 0           $req .= pack ( "N/a*", $index ); # req index
2323 0           $req .= pack ( "N/a*", $self->{_string_encoder}->($words)); # req words
2324              
2325             # options
2326 0           $req .= pack ( "N/a*", $opts->{"before_match"});
2327 0           $req .= pack ( "N/a*", $opts->{"after_match"});
2328 0           $req .= pack ( "N/a*", $opts->{"chunk_separator"});
2329 0           $req .= pack ( "NN", int($opts->{"limit"}), int($opts->{"around"}) );
2330             $req .= pack ( "NNN", int($opts->{"limit_passages"}),
2331             int($opts->{"limit_words"}),
2332 0           int($opts->{"start_passage_id"}) ); # v1.2
2333 0           $req .= pack ( "N/a*", $opts->{"html_strip_mode"});
2334 0           $req .= pack ( "N/a*", $opts->{"passage_boundary"});
2335              
2336             # documents
2337 0           $req .= pack ( "N", scalar(@$docs) );
2338 0           foreach my $doc (@$docs) {
2339 0 0         croak('BuildExcerpts: Found empty document in $docs') unless ($doc);
2340 0           $req .= pack("N/a*", $self->{_string_encoder}->($doc));
2341             }
2342              
2343             ##########################
2344             # send query, get response
2345             ##########################
2346              
2347 0           $req = pack ( "nnN/a*", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $req); # add header
2348 0           my $response = $self->_ProcessRequest($req, VER_COMMAND_EXCERPT);
2349 0 0         return unless $response;
2350 0           my ($pos, $i) = 0;
2351 0           my $res = []; # Empty hash ref
2352 0           my $rlen = length($response);
2353 0           for ( $i=0; $i< scalar(@$docs); $i++ ) {
2354 0           my $len = unpack ( "N*", substr ( $response, $pos, 4 ) );
2355 0           $pos += 4;
2356              
2357 0 0         if ( $pos+$len > $rlen ) {
2358 0           $self->_Error("incomplete reply");
2359 0           return;
2360             }
2361 0           push(@$res, $self->{_string_decoder}->( substr ( $response, $pos, $len ) ));
2362 0           $pos += $len;
2363             }
2364 0           return $res;
2365             }
2366              
2367              
2368             =head2 BuildKeywords
2369              
2370             $results = $sph->BuildKeywords($query, $index, $hits)
2371              
2372             Generate keyword list for a given query
2373             Returns undef on failure,
2374             Returns an array of hashes, where each hash describes a word in the query with the following keys:
2375              
2376             =over 4
2377              
2378             =item * tokenized
2379              
2380             Tokenised term from query
2381              
2382             =item * normalized
2383              
2384             Normalised term from query
2385              
2386             =item * docs
2387              
2388             Number of docs in which word was found (if $hits is true)
2389              
2390             =item * hits
2391              
2392             Number of occurrences of word (if $hits is true)
2393              
2394             =back
2395              
2396             =cut
2397              
2398             sub BuildKeywords {
2399 0     0 1   my ( $self, $query, $index, $hits ) = @_;
2400              
2401              
2402             # v.1.0 req
2403 0           my $req = pack("N/a*", $self->{_string_encoder}->($query) );
2404 0           $req .= pack("N/a*", $index);
2405 0           $req .= pack("N", $self->{_string_encoder}->($hits) );
2406              
2407             ##################
2408             # send query, get response
2409             ##################
2410              
2411 0           $req = pack ( "nnN/a*", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $req);
2412 0           my $response = $self->_ProcessRequest($req, VER_COMMAND_KEYWORDS);
2413 0 0         return unless $response;
2414              
2415             ##################
2416             # parse response
2417             ##################
2418              
2419 0           my $p = 0;
2420 0           my @res;
2421 0           my $rlen = length($response);
2422              
2423 0           my $nwords = unpack("N", substr ( $response, $p, 4 ) ); $p += 4;
  0            
2424              
2425 0           for (my $i=0; $i < $nwords; $i++ ) {
2426 0           my $len = unpack("N", substr ( $response, $p, 4 ) ); $p += 4;
  0            
2427              
2428 0 0         my $tokenized = $len ? $self->{_string_decoder}->( substr ( $response, $p, $len ) ) : ""; $p += $len;
  0            
2429 0           $len = unpack("N", substr ( $response, $p, 4 ) ); $p += 4;
  0            
2430              
2431 0 0         my $normalized = $len ? $self->{_string_decoder}->( substr ( $response, $p, $len ) ) : ""; $p += $len;
  0            
2432 0           my %data = ( tokenized => $tokenized, normalized => $normalized );
2433            
2434 0 0         if ($hits) {
2435 0           ( $data{docs}, $data{hits} ) = unpack("N*N*", substr($response,$p,8));
2436 0           $p += 8;
2437            
2438             }
2439 0           push(@res, \%data);
2440             }
2441 0 0         if ( $p > $rlen ) {
2442 0           $self->_Error("incomplete reply");
2443 0           return;
2444             }
2445              
2446 0           return \@res;
2447             }
2448              
2449             =head2 EscapeString
2450              
2451             $escaped = $sph->EscapeString('abcde!@#$%')
2452              
2453             Inserts backslash before all non-word characters in the given string.
2454              
2455             =cut
2456              
2457             sub EscapeString {
2458 0     0 1   my $self = shift;
2459 0           return quotemeta(shift);
2460             }
2461              
2462              
2463             =head2 UpdateAttributes
2464              
2465             $sph->UpdateAttributes($index, \@attrs, \%values);
2466             $sph->UpdateAttributes($index, \@attrs, \%values, $mva);
2467             $sph->UpdateAttributes($index, \@attrs, \%values, $mva, $ignorenonexistent);
2468              
2469             Update specified attributes on specified documents
2470              
2471             =over 4
2472              
2473             =item index
2474              
2475             Name of the index to be updated
2476              
2477             =item attrs
2478              
2479             Array of attribute name strings
2480              
2481             =item values
2482              
2483             A hash with key as document id, value as an array of new attribute values
2484              
2485             =item mva
2486              
2487             If set, indicates that there is update of MVA attributes
2488              
2489             =item ignorenonexistent
2490              
2491             If set, the update will silently ignore any warnings about trying to update a column which is not exists in current index schema.
2492              
2493             =back
2494              
2495             Returns number of actually updated documents (0 or more) on success
2496              
2497             Returns undef on failure
2498              
2499             Usage example:
2500              
2501             $sph->UpdateAttributes("test1", [ qw/group_id/ ], { 1 => [ 456] }) );
2502              
2503             =cut
2504              
2505             sub UpdateAttributes {
2506 0     0 1   my ($self, $index, $attrs, $values, $mva, $ignorenonexistent ) = @_;
2507              
2508 0 0         croak("index is not defined") unless (defined $index);
2509 0 0         croak("attrs must be an array") unless ref($attrs) eq "ARRAY";
2510 0           for my $attr (@$attrs) {
2511 0 0         croak("attribute is not defined") unless (defined $attr);
2512             }
2513 0 0         croak("values must be a hashref") unless ref($values) eq "HASH";
2514              
2515 0           for my $id (keys %$values) {
2516 0           my $entry = $values->{$id};
2517 0 0         croak("value id $id is not numeric") unless ($id =~ /$num_re/);
2518 0 0         croak("value entry must be an array") unless ref($entry) eq "ARRAY";
2519 0 0         croak("size of values must match size of attrs") unless @$entry == @$attrs;
2520 0           for my $v (@$entry) {
2521 0 0         if ($mva) {
2522 0 0         croak("multi-valued entry $v is not an array") unless ref($v) eq 'ARRAY';
2523 0           for my $vv (@$v) {
2524 0 0         croak("array entry value $vv is not an integer") unless ($vv =~ /^(\d+)$/o);
2525             }
2526             }
2527             else {
2528 0 0         croak("entry value $v is not an integer") unless ($v =~ /^(\d+)$/o);
2529             }
2530             }
2531             }
2532              
2533             ## build request
2534 0           my $req = pack ( "N/a*", $index);
2535              
2536 0           $req .= pack ( "N", scalar @$attrs );
2537 0 0         $req .= pack ( "N", $ignorenonexistent ? 1 : 0 );
2538 0           for my $attr (@$attrs) {
2539 0 0         $req .= pack ( "N/a*", $attr)
2540             . pack("N", $mva ? 1 : 0);
2541             }
2542 0           $req .= pack ( "N", scalar keys %$values );
2543 0           foreach my $id (keys %$values) {
2544 0           my $entry = $values->{$id};
2545 0           $req .= $self->_sphPackU64($id);
2546 0 0         if ($mva) {
2547 0           for my $v ( @$entry ) {
2548 0           $req .= pack ( "N", @$v );
2549 0           for my $vv (@$v) {
2550 0           $req .= pack ("N", $vv);
2551             }
2552             }
2553             }
2554             else {
2555 0           for my $v ( @$entry ) {
2556 0           $req .= pack ( "N", $v );
2557             }
2558             }
2559             }
2560              
2561             ## connect, send query, get response
2562              
2563 0           $req = pack ( "nnN/a*", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $req); ## add header
2564 0           my $response = $self->_ProcessRequest($req, VER_COMMAND_UPDATE);
2565              
2566 0 0         return unless $response;
2567              
2568             ## parse response
2569 0           my ($updated) = unpack ( "N*", substr ( $response, 0, 4 ) );
2570 0           return $updated;
2571             }
2572              
2573             =head2 Open
2574              
2575             $sph->Open()
2576              
2577             Opens a persistent connection for subsequent queries.
2578              
2579             To reduce the network connection overhead of making Sphinx queries, you can call
2580             $sph->Open(), then run any number of queries, and call $sph->Close() when
2581             finished.
2582              
2583             Returns 1 on success, 0 on failure.
2584              
2585             =cut
2586              
2587             sub Open {
2588 0     0 1   my $self = shift;
2589 0           $self->{_persistent} = 1;
2590 0 0         if ($self->{_socket}) {
2591 0           $self->_Error("already connected");
2592 0           return 0;
2593             }
2594 0 0         my $fp = $self->_Connect() or return 0;
2595 0           return 1;
2596             }
2597              
2598             =head2 Close
2599              
2600             $sph->Close()
2601              
2602             Closes a persistent connection.
2603              
2604             Returns 1 on success, 0 on failure.
2605              
2606             =cut
2607              
2608             sub Close {
2609 0     0 1   my $self = shift;
2610 0           $self->{_persistent} = 0;
2611              
2612 0 0         if (! $self->{_socket}) {
2613 0           $self->_Error("not connected");
2614 0           return 0;
2615             }
2616            
2617 0           close($self->{_socket});
2618 0           $self->{_socket} = undef;
2619              
2620 0           return 1;
2621             }
2622              
2623             =head2 Status
2624              
2625             $status = $sph->Status()
2626             $status = $sph->Status($session)
2627              
2628             Queries searchd status, and returns a hash of status variable name and value pairs.
2629              
2630             Returns undef on failure.
2631              
2632             =cut
2633              
2634             sub Status {
2635            
2636 0     0 1   my ($self, $session) = @_;
2637              
2638 0 0         my $req = pack("nnNN", SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, $session ? 0 : 1 ); # len=4, body=1
2639              
2640 0           my $response = $self->_ProcessRequest($req, VER_COMMAND_STATUS);
2641 0 0         return unless $response;
2642              
2643 0           my $p = 0;
2644 0           my ($rows, $cols) = unpack("N*N*", substr ( $response, $p, 8 ) ); $p += 8;
  0            
2645              
2646 0 0 0       return {} unless $rows && $cols;
2647 0           my %res;
2648 0           for (1 .. $rows ) {
2649 0           my @entry;
2650 0           for ( 1 .. $cols) {
2651 0           my $len = unpack("N*", substr ( $response, $p, 4 ) ); $p += 4;
  0            
2652 0 0         push(@entry, $len ? substr ( $response, $p, $len ) : ""); $p += $len;
  0            
2653             }
2654 0 0         if ($cols <= 2) {
2655 0           $res{$entry[0]} = $entry[1];
2656             }
2657             else {
2658 0           my $name = shift @entry;
2659 0           $res{$name} = \@entry;
2660             }
2661             }
2662 0           return \%res;
2663             }
2664            
2665             =head2 FlushAttributes
2666              
2667             =cut
2668              
2669             sub FlushAttributes {
2670 0     0 1   my $self = shift;
2671            
2672 0           my $req = pack("nnN", SEARCHD_COMMAND_FLUSHATTRS, VER_COMMAND_FLUSHATTRS, 0 ); # len=0
2673 0           my $response = $self->_ProcessRequest($req, VER_COMMAND_FLUSHATTRS);
2674 0 0         return unless $response;
2675            
2676 0           my $tag = -1;
2677 0 0         if (length($response) == 4) {
2678 0           $tag = unpack ( "N*", substr ( $response, 0, 4 ) );
2679             }
2680             else {
2681 0           $self->_Error("unexpected response length");
2682             }
2683 0           return $tag;
2684             }
2685              
2686             =head1 SEE ALSO
2687              
2688             L
2689              
2690             =head1 NOTES
2691              
2692             There is (or was) a bundled Sphinx.pm in the contrib area of the Sphinx source
2693             distribution, which was used as the starting point of Sphinx::Search.
2694             Maintenance of that version appears to have lapsed at sphinx-0.9.7, so many of
2695             the newer API calls are not available there. Sphinx::Search is mostly
2696             compatible with the old Sphinx.pm except:
2697              
2698             =over 4
2699              
2700             =item On failure, Sphinx::Search returns undef rather than 0 or -1.
2701              
2702             =item Sphinx::Search 'Set' functions are cascadable, e.g. you can do
2703             Sphinx::Search->new
2704             ->SetMatchMode(SPH_MATCH_ALL)
2705             ->SetSortMode(SPH_SORT_RELEVANCE)
2706             ->Query("search terms")
2707              
2708             =back
2709              
2710             Sphinx::Search also provides documentation and unit tests, which were the main
2711             motivations for branching from the earlier work.
2712              
2713             =head1 AUTHOR
2714              
2715             Jon Schutz
2716              
2717             L
2718              
2719             =head1 BUGS
2720              
2721             Please report any bugs or feature requests to
2722             C, or through the web interface at
2723             L.
2724             I will be notified, and then you'll automatically be notified of progress on
2725             your bug as I make changes.
2726              
2727             =head1 SUPPORT
2728              
2729             You can find documentation for this module with the perldoc command.
2730              
2731             perldoc Sphinx::Search
2732              
2733             You can also look for information at:
2734              
2735             =over 4
2736              
2737             =item * AnnoCPAN: Annotated CPAN documentation
2738              
2739             L
2740              
2741             =item * CPAN Ratings
2742              
2743             L
2744              
2745             =item * RT: CPAN's request tracker
2746              
2747             L
2748              
2749             =item * Search CPAN
2750              
2751             L
2752              
2753             =back
2754              
2755             =head1 ACKNOWLEDGEMENTS
2756              
2757             This module is based on Sphinx.pm (not deployed to CPAN) for Sphinx version
2758             0.9.7-rc1, by Len Kranendonk, which was in turn based on the Sphinx PHP API.
2759              
2760             Thanks to Alexey Kholodkov for contributing a significant patch for handling persistent connections.
2761              
2762             =head1 COPYRIGHT & LICENSE
2763              
2764             Copyright 2015 Jon Schutz, all rights reserved.
2765              
2766             This program is free software; you can redistribute it and/or modify it
2767             under the terms of the GNU General Public License.
2768              
2769             =cut
2770              
2771              
2772             1;