Passed
Push — master ( 402455...10ca4a )
by Cody
03:10 queued 20s
created

SphinxClient::FlushAttributes()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
c 0
b 0
f 0
nc 4
nop 0
dl 0
loc 25
rs 9.4222
1
<?php
2
3
//
4
// $Id: sphinxapi.php 2758 2011-04-04 11:10:44Z kevg $
5
//
6
7
//
8
// Copyright (c) 2001-2011, Andrew Aksyonoff
9
// Copyright (c) 2008-2011, Sphinx Technologies Inc
10
// All rights reserved
11
//
12
// This program is free software; you can redistribute it and/or modify
13
// it under the terms of the GNU General Public License. You should have
14
// received a copy of the GPL license along with this program; if you
15
// did not, you can find it at http://www.gnu.org/
16
//
17
18
/////////////////////////////////////////////////////////////////////////////
19
// PHP version of Sphinx searchd client (PHP API)
20
/////////////////////////////////////////////////////////////////////////////
21
22
/// known searchd commands
23
define("SEARCHD_COMMAND_SEARCH", 0);
24
define("SEARCHD_COMMAND_EXCERPT", 1);
25
define("SEARCHD_COMMAND_UPDATE", 2);
26
define("SEARCHD_COMMAND_KEYWORDS", 3);
27
define("SEARCHD_COMMAND_PERSIST", 4);
28
define("SEARCHD_COMMAND_STATUS", 5);
29
define("SEARCHD_COMMAND_FLUSHATTRS", 7);
30
31
/// current client-side command implementation versions
32
define("VER_COMMAND_SEARCH", 0x118);
33
define("VER_COMMAND_EXCERPT", 0x103);
34
define("VER_COMMAND_UPDATE", 0x102);
35
define("VER_COMMAND_KEYWORDS", 0x100);
36
define("VER_COMMAND_STATUS", 0x100);
37
define("VER_COMMAND_QUERY", 0x100);
38
define("VER_COMMAND_FLUSHATTRS", 0x100);
39
40
/// known searchd status codes
41
define("SEARCHD_OK", 0);
42
define("SEARCHD_ERROR", 1);
43
define("SEARCHD_RETRY", 2);
44
define("SEARCHD_WARNING", 3);
45
46
/// known match modes
47
define("SPH_MATCH_ALL", 0);
48
define("SPH_MATCH_ANY", 1);
49
define("SPH_MATCH_PHRASE", 2);
50
define("SPH_MATCH_BOOLEAN", 3);
51
define("SPH_MATCH_EXTENDED", 4);
52
define("SPH_MATCH_FULLSCAN", 5);
53
define("SPH_MATCH_EXTENDED2", 6); // extended engine V2 (TEMPORARY, WILL BE REMOVED)
54
55
/// known ranking modes (ext2 only)
56
define("SPH_RANK_PROXIMITY_BM25", 0); ///< default mode, phrase proximity major factor and BM25 minor one
57
define("SPH_RANK_BM25", 1); ///< statistical mode, BM25 ranking only (faster but worse quality)
58
define("SPH_RANK_NONE", 2); ///< no ranking, all matches get a weight of 1
59
define("SPH_RANK_WORDCOUNT", 3); ///< simple word-count weighting, rank is a weighted sum of per-field keyword occurence counts
60
define("SPH_RANK_PROXIMITY", 4);
61
define("SPH_RANK_MATCHANY", 5);
62
define("SPH_RANK_FIELDMASK", 6);
63
define("SPH_RANK_SPH04", 7);
64
define("SPH_RANK_TOTAL", 8);
65
66
/// known sort modes
67
define("SPH_SORT_RELEVANCE", 0);
68
define("SPH_SORT_ATTR_DESC", 1);
69
define("SPH_SORT_ATTR_ASC", 2);
70
define("SPH_SORT_TIME_SEGMENTS", 3);
71
define("SPH_SORT_EXTENDED", 4);
72
define("SPH_SORT_EXPR", 5);
73
74
/// known filter types
75
define("SPH_FILTER_VALUES", 0);
76
define("SPH_FILTER_RANGE", 1);
77
define("SPH_FILTER_FLOATRANGE", 2);
78
79
/// known attribute types
80
define("SPH_ATTR_INTEGER", 1);
81
define("SPH_ATTR_TIMESTAMP", 2);
82
define("SPH_ATTR_ORDINAL", 3);
83
define("SPH_ATTR_BOOL", 4);
84
define("SPH_ATTR_FLOAT", 5);
85
define("SPH_ATTR_BIGINT", 6);
86
define("SPH_ATTR_STRING", 7);
87
define("SPH_ATTR_MULTI", 0x40000000);
88
89
/// known grouping functions
90
define("SPH_GROUPBY_DAY", 0);
91
define("SPH_GROUPBY_WEEK", 1);
92
define("SPH_GROUPBY_MONTH", 2);
93
define("SPH_GROUPBY_YEAR", 3);
94
define("SPH_GROUPBY_ATTR", 4);
95
define("SPH_GROUPBY_ATTRPAIR", 5);
96
97
// important properties of PHP's integers:
98
//  - always signed (one bit short of PHP_INT_SIZE)
99
//  - conversion from string to int is saturated
100
//  - float is double
101
//  - div converts arguments to floats
102
//  - mod converts arguments to ints
103
104
// the packing code below works as follows:
105
//  - when we got an int, just pack it
106
//    if performance is a problem, this is the branch users should aim for
107
//
108
//  - otherwise, we got a number in string form
109
//    this might be due to different reasons, but we assume that this is
110
//    because it didn't fit into PHP int
111
//
112
//  - factor the string into high and low ints for packing
113
//    - if we have bcmath, then it is used
114
//    - if we don't, we have to do it manually (this is the fun part)
115
//
116
//    - x64 branch does factoring using ints
117
//    - x32 (ab)uses floats, since we can't fit unsigned 32-bit number into an int
118
//
119
// unpacking routines are pretty much the same.
120
//  - return ints if we can
121
//  - otherwise format number into a string
122
123
/// pack 64-bit signed
124
function sphPackI64($v)
125
{
126
    assert(is_numeric($v));
127
128
    // x64
129
    if (PHP_INT_SIZE >= 8)
130
    {
131
        $v = (int) $v;
132
        return pack("NN", $v >> 32, $v & 0xFFFFFFFF);
133
    }
134
135
    // x32, int
136
    if (is_int($v)) {
137
            return pack("NN", $v < 0 ? -1 : 0, $v);
138
    }
139
140
    // x32, bcmath
141
    if (function_exists("bcmul"))
142
    {
143
        if (bccomp($v, 0) == -1) {
144
                    $v = bcadd("18446744073709551616", $v);
145
        }
146
        $h = bcdiv($v, "4294967296", 0);
147
        $l = bcmod($v, "4294967296");
148
        return pack("NN", (float) $h, (float) $l); // conversion to float is intentional; int would lose 31st bit
149
    }
150
151
    // x32, no-bcmath
152
    $p = max(0, strlen($v) - 13);
153
    $lo = abs((float) substr($v, $p));
154
    $hi = abs((float) substr($v, 0, $p));
155
156
    $m = $lo + $hi * 1316134912.0; // (10 ^ 13) % (1 << 32) = 1316134912
157
    $q = floor($m / 4294967296.0);
158
    $l = $m - ($q * 4294967296.0);
159
    $h = $hi * 2328.0 + $q; // (10 ^ 13) / (1 << 32) = 2328
160
161
    if ($v < 0)
162
    {
163
        if ($l == 0) {
164
                    $h = 4294967296.0 - $h;
165
        } else
166
        {
167
            $h = 4294967295.0 - $h;
168
            $l = 4294967296.0 - $l;
169
        }
170
    }
171
    return pack("NN", $h, $l);
172
}
173
174
/// pack 64-bit unsigned
175
function sphPackU64($v)
176
{
177
    assert(is_numeric($v));
178
179
    // x64
180
    if (PHP_INT_SIZE >= 8)
181
    {
182
        assert($v >= 0);
183
184
        // x64, int
185
        if (is_int($v)) {
186
                    return pack("NN", $v >> 32, $v & 0xFFFFFFFF);
187
        }
188
189
        // x64, bcmath
190
        if (function_exists("bcmul"))
191
        {
192
            $h = bcdiv($v, 4294967296, 0);
193
            $l = bcmod($v, 4294967296);
194
            return pack("NN", $h, $l);
195
        }
196
197
        // x64, no-bcmath
198
        $p = max(0, strlen($v) - 13);
199
        $lo = (int) substr($v, $p);
200
        $hi = (int) substr($v, 0, $p);
201
202
        $m = $lo + $hi * 1316134912;
203
        $l = $m % 4294967296;
204
        $h = $hi * 2328 + (int) ($m / 4294967296);
205
206
        return pack("NN", $h, $l);
207
    }
208
209
    // x32, int
210
    if (is_int($v)) {
211
            return pack("NN", 0, $v);
212
    }
213
214
    // x32, bcmath
215
    if (function_exists("bcmul"))
216
    {
217
        $h = bcdiv($v, "4294967296", 0);
218
        $l = bcmod($v, "4294967296");
219
        return pack("NN", (float) $h, (float) $l); // conversion to float is intentional; int would lose 31st bit
220
    }
221
222
    // x32, no-bcmath
223
    $p = max(0, strlen($v) - 13);
224
    $lo = (float) substr($v, $p);
225
    $hi = (float) substr($v, 0, $p);
226
227
    $m = $lo + $hi * 1316134912.0;
228
    $q = floor($m / 4294967296.0);
229
    $l = $m - ($q * 4294967296.0);
230
    $h = $hi * 2328.0 + $q;
231
232
    return pack("NN", $h, $l);
233
}
234
235
// unpack 64-bit unsigned
236
function sphUnpackU64($v)
237
{
238
    list ($hi, $lo) = array_values(unpack("N*N*", $v));
239
240
    if (PHP_INT_SIZE >= 8)
241
    {
242
        if ($hi < 0) {
243
            $hi += (1 << 32);
244
        }
245
        // because php 5.2.2 to 5.2.5 is totally fucked up again
246
        if ($lo < 0) {
247
            $lo += (1 << 32);
248
        }
249
250
        // x64, int
251
        if ($hi <= 2147483647) {
252
                    return ($hi << 32) + $lo;
253
        }
254
255
        // x64, bcmath
256
        if (function_exists("bcmul")) {
257
                    return bcadd($lo, bcmul($hi, "4294967296"));
258
        }
259
260
        // x64, no-bcmath
261
        $C = 100000;
262
        $h = ((int) ($hi / $C) << 32) + (int) ($lo / $C);
263
        $l = (($hi % $C) << 32) + ($lo % $C);
264
        if ($l > $C)
265
        {
266
            $h += (int) ($l / $C);
267
            $l  = $l % $C;
268
        }
269
270
        if ($h == 0) {
271
                    return $l;
272
        }
273
        return sprintf("%d%05d", $h, $l);
274
    }
275
276
    // x32, int
277
    if ($hi == 0)
278
    {
279
        if ($lo > 0) {
280
                    return $lo;
281
        }
282
        return sprintf("%u", $lo);
283
    }
284
285
    $hi = sprintf("%u", $hi);
286
    $lo = sprintf("%u", $lo);
287
288
    // x32, bcmath
289
    if (function_exists("bcmul")) {
290
            return bcadd($lo, bcmul($hi, "4294967296"));
291
    }
292
293
    // x32, no-bcmath
294
    $hi = (float) $hi;
295
    $lo = (float) $lo;
296
297
    $q = floor($hi / 10000000.0);
298
    $r = $hi - $q * 10000000.0;
299
    $m = $lo + $r * 4967296.0;
300
    $mq = floor($m / 10000000.0);
301
    $l = $m - $mq * 10000000.0;
302
    $h = $q * 4294967296.0 + $r * 429.0 + $mq;
303
304
    $h = sprintf("%.0f", $h);
305
    $l = sprintf("%07.0f", $l);
306
    if ($h == "0") {
307
            return sprintf("%.0f", (float) $l);
308
    }
309
    return $h.$l;
310
}
311
312
// unpack 64-bit signed
313
function sphUnpackI64($v)
314
{
315
    list ($hi, $lo) = array_values(unpack("N*N*", $v));
316
317
    // x64
318
    if (PHP_INT_SIZE >= 8)
319
    {
320
        if ($hi < 0) {
321
            $hi += (1 << 32);
322
        }
323
        // because php 5.2.2 to 5.2.5 is totally fucked up again
324
        if ($lo < 0) {
325
            $lo += (1 << 32);
326
        }
327
328
        return ($hi << 32) + $lo;
329
    }
330
331
    // x32, int
332
    if ($hi == 0)
333
    {
334
        if ($lo > 0) {
335
                    return $lo;
336
        }
337
        return sprintf("%u", $lo);
338
    }
339
    // x32, int
340
    elseif ($hi == -1)
341
    {
342
        if ($lo < 0) {
343
                    return $lo;
344
        }
345
        return sprintf("%.0f", $lo - 4294967296.0);
346
    }
347
348
    $neg = "";
349
    $c = 0;
350
    if ($hi < 0)
351
    {
352
        $hi = ~$hi;
353
        $lo = ~$lo;
354
        $c = 1;
355
        $neg = "-";
356
    }
357
358
    $hi = sprintf("%u", $hi);
359
    $lo = sprintf("%u", $lo);
360
361
    // x32, bcmath
362
    if (function_exists("bcmul")) {
363
            return $neg.bcadd(bcadd($lo, bcmul($hi, "4294967296")), $c);
364
    }
365
366
    // x32, no-bcmath
367
    $hi = (float) $hi;
368
    $lo = (float) $lo;
369
370
    $q = floor($hi / 10000000.0);
371
    $r = $hi - $q * 10000000.0;
372
    $m = $lo + $r * 4967296.0;
373
    $mq = floor($m / 10000000.0);
374
    $l = $m - $mq * 10000000.0 + $c;
375
    $h = $q * 4294967296.0 + $r * 429.0 + $mq;
376
    if ($l == 10000000)
377
    {
378
        $l = 0;
379
        $h += 1;
380
    }
381
382
    $h = sprintf("%.0f", $h);
383
    $l = sprintf("%07.0f", $l);
384
    if ($h == "0") {
385
            return $neg.sprintf("%.0f", (float) $l);
386
    }
387
    return $neg.$h.$l;
388
}
389
390
391
function sphFixUint($value)
392
{
393
    if (PHP_INT_SIZE >= 8)
394
    {
395
        // x64 route, workaround broken unpack() in 5.2.2+
396
        if ($value < 0) {
397
            $value += (1 << 32);
398
        }
399
        return $value;
400
    } else
401
    {
402
        // x32 route, workaround php signed/unsigned braindamage
403
        return sprintf("%u", $value);
404
    }
405
}
406
407
408
/// sphinx searchd client class
409
class SphinxClient
410
{
411
    var $_host; ///< searchd host (default is "localhost")
412
    var $_port; ///< searchd port (default is 9312)
413
    var $_offset; ///< how many records to seek from result-set start (default is 0)
414
    var $_limit; ///< how many records to return from result-set starting at offset (default is 20)
415
    var $_mode; ///< query matching mode (default is SPH_MATCH_ALL)
416
    var $_weights; ///< per-field weights (default is 1 for all fields)
417
    var $_sort; ///< match sorting mode (default is SPH_SORT_RELEVANCE)
418
    var $_sortby; ///< attribute to sort by (defualt is "")
419
    var $_min_id; ///< min ID to match (default is 0, which means no limit)
420
    var $_max_id; ///< max ID to match (default is 0, which means no limit)
421
    var $_filters; ///< search filters
422
    var $_groupby; ///< group-by attribute name
423
    var $_groupfunc; ///< group-by function (to pre-process group-by attribute value with)
424
    var $_groupsort; ///< group-by sorting clause (to sort groups in result set with)
425
    var $_groupdistinct; ///< group-by count-distinct attribute
426
    var $_maxmatches; ///< max matches to retrieve
427
    var $_cutoff; ///< cutoff to stop searching at (default is 0)
428
    var $_retrycount; ///< distributed retries count
429
    var $_retrydelay; ///< distributed retries delay
430
    var $_anchor; ///< geographical anchor point
431
    var $_indexweights; ///< per-index weights
432
    var $_ranker; ///< ranking mode (default is SPH_RANK_PROXIMITY_BM25)
433
    var $_maxquerytime; ///< max query time, milliseconds (default is 0, do not limit)
434
    var $_fieldweights; ///< per-field-name weights
435
    var $_overrides; ///< per-query attribute values overrides
436
    var $_select; ///< select-list (attributes or expressions, with optional aliases)
437
438
    var $_error; ///< last error message
439
    var $_warning; ///< last warning message
440
    var $_connerror; ///< connection error vs remote error flag
441
442
    var $_reqs; ///< requests array for multi-query
443
    var $_mbenc; ///< stored mbstring encoding
444
    var $_arrayresult; ///< whether $result["matches"] should be a hash or an array
445
    var $_timeout; ///< connect timeout
446
447
    /////////////////////////////////////////////////////////////////////////////
448
    // common stuff
449
    /////////////////////////////////////////////////////////////////////////////
450
451
    /// create a new client object and fill defaults
452
    public function __construct()
453
    {
454
        // per-client-object settings
455
        $this->_host		= "localhost";
456
        $this->_port		= 9312;
457
        $this->_path		= false;
458
        $this->_socket		= false;
459
460
        // per-query settings
461
        $this->_offset		= 0;
462
        $this->_limit = 20;
463
        $this->_mode		= SPH_MATCH_ALL;
464
        $this->_weights = array();
465
        $this->_sort		= SPH_SORT_RELEVANCE;
466
        $this->_sortby		= "";
467
        $this->_min_id		= 0;
468
        $this->_max_id		= 0;
469
        $this->_filters		= array();
470
        $this->_groupby		= "";
471
        $this->_groupfunc	= SPH_GROUPBY_DAY;
472
        $this->_groupsort	= "@group desc";
473
        $this->_groupdistinct = "";
474
        $this->_maxmatches	= 1000;
475
        $this->_cutoff		= 0;
476
        $this->_retrycount	= 0;
477
        $this->_retrydelay	= 0;
478
        $this->_anchor		= array();
479
        $this->_indexweights = array();
480
        $this->_ranker		= SPH_RANK_PROXIMITY_BM25;
481
        $this->_maxquerytime = 0;
482
        $this->_fieldweights = array();
483
        $this->_overrides = array();
484
        $this->_select = "*";
485
486
        $this->_error = ""; // per-reply fields (for single-query case)
487
        $this->_warning = "";
488
        $this->_connerror = false;
489
490
        $this->_reqs = array(); // requests storage (for multi-query case)
491
        $this->_mbenc = "";
492
        $this->_arrayresult = false;
493
        $this->_timeout = 0;
494
    }
495
496
    public function __destruct()
497
    {
498
        if ($this->_socket !== false) {
499
                    fclose($this->_socket);
500
        }
501
    }
502
503
    /// get last error message (string)
504
    public function GetLastError()
505
    {
506
        return $this->_error;
507
    }
508
509
    /// get last warning message (string)
510
    public function GetLastWarning()
511
    {
512
        return $this->_warning;
513
    }
514
515
    /// get last error flag (to tell network connection errors from searchd errors or broken responses)
516
    public function IsConnectError()
517
    {
518
        return $this->_connerror;
519
    }
520
521
    /// set searchd host name (string) and port (integer)
522
    public function SetServer($host, $port = 0)
523
    {
524
        assert(is_string($host));
525
        if ($host[0] == '/')
526
        {
527
            $this->_path = 'unix://'.$host;
528
            return;
529
        }
530
        if (substr($host, 0, 7) == "unix://")
531
        {
532
            $this->_path = $host;
533
            return;
534
        }
535
536
        assert(is_int($port));
537
        $this->_host = $host;
538
        $this->_port = $port;
539
        $this->_path = '';
540
541
    }
542
543
    /// set server connection timeout (0 to remove)
544
    public function SetConnectTimeout($timeout)
545
    {
546
        assert(is_numeric($timeout));
547
        $this->_timeout = $timeout;
548
    }
549
550
551
    public function _Send($handle, $data, $length)
552
    {
553
        if (feof($handle) || fwrite($handle, $data, $length) !== $length)
554
        {
555
            $this->_error = 'connection unexpectedly closed (timed out?)';
556
            $this->_connerror = true;
557
            return false;
558
        }
559
        return true;
560
    }
561
562
    /////////////////////////////////////////////////////////////////////////////
563
564
    /// enter mbstring workaround mode
565
    public function _MBPush()
566
    {
567
        $this->_mbenc = "";
568
        if (ini_get("mbstring.func_overload") & 2)
569
        {
570
            $this->_mbenc = mb_internal_encoding();
571
            mb_internal_encoding("latin1");
572
        }
573
    }
574
575
    /// leave mbstring workaround mode
576
    public function _MBPop()
577
    {
578
        if ($this->_mbenc) {
579
                    mb_internal_encoding($this->_mbenc);
580
        }
581
    }
582
583
    /// connect to searchd server
584
    public function _Connect()
585
    {
586
        if ($this->_socket !== false)
587
        {
588
            // we are in persistent connection mode, so we have a socket
589
            // however, need to check whether it's still alive
590
            if (!@feof($this->_socket)) {
591
                            return $this->_socket;
592
            }
593
594
            // force reopen
595
            $this->_socket = false;
596
        }
597
598
        $errno = 0;
599
        $errstr = "";
600
        $this->_connerror = false;
601
602
        if ($this->_path)
603
        {
604
            $host = $this->_path;
605
            $port = 0;
606
        } else
607
        {
608
            $host = $this->_host;
609
            $port = $this->_port;
610
        }
611
612
        if ($this->_timeout <= 0) {
613
                    $fp = @fsockopen($host, $port, $errno, $errstr);
614
        } else {
615
                    $fp = @fsockopen($host, $port, $errno, $errstr, $this->_timeout);
616
        }
617
618
        if (!$fp)
619
        {
620
            if ($this->_path) {
621
                            $location = $this->_path;
622
            } else {
623
                            $location = "{$this->_host}:{$this->_port}";
624
            }
625
626
            $errstr = trim($errstr);
627
            $this->_error = "connection to $location failed (errno=$errno, msg=$errstr)";
628
            $this->_connerror = true;
629
            return false;
630
        }
631
632
        // send my version
633
        // this is a subtle part. we must do it before (!) reading back from searchd.
634
        // because otherwise under some conditions (reported on FreeBSD for instance)
635
        // TCP stack could throttle write-write-read pattern because of Nagle.
636
        if (!$this->_Send($fp, pack("N", 1), 4))
637
        {
638
            fclose($fp);
639
            $this->_error = "failed to send client protocol version";
640
            return false;
641
        }
642
643
        // check version
644
        list(,$v) = unpack("N*", fread($fp, 4));
645
        $v = (int) $v;
646
        if ($v < 1)
647
        {
648
            fclose($fp);
649
            $this->_error = "expected searchd protocol version 1+, got version '$v'";
650
            return false;
651
        }
652
653
        return $fp;
654
    }
655
656
    /// get and check response packet from searchd server
657
    public function _GetResponse($fp, $client_ver)
658
    {
659
        $response = "";
660
        $len = 0;
661
662
        $header = fread($fp, 8);
663
        if (strlen($header) == 8)
664
        {
665
            list ($status, $ver, $len) = array_values(unpack("n2a/Nb", $header));
666
            $left = $len;
667
            while ($left > 0 && !feof($fp))
668
            {
669
                $chunk = fread($fp, min(8192, $left));
670
                if ($chunk)
671
                {
672
                    $response .= $chunk;
673
                    $left -= strlen($chunk);
674
                }
675
            }
676
        }
677
        if ($this->_socket === false) {
678
                    fclose($fp);
679
        }
680
681
        // check response
682
        $read = strlen($response);
683
        if (!$response || $read != $len)
684
        {
685
            $this->_error = $len
686
                ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)"
687
                : "received zero-sized searchd response";
688
            return false;
689
        }
690
691
        // check status
692
        if ($status == SEARCHD_WARNING)
693
        {
694
            list(,$wlen) = unpack("N*", substr($response, 0, 4));
695
            $this->_warning = substr($response, 4, $wlen);
696
            return substr($response, 4 + $wlen);
697
        }
698
        if ($status == SEARCHD_ERROR)
699
        {
700
            $this->_error = "searchd error: ".substr($response, 4);
701
            return false;
702
        }
703
        if ($status == SEARCHD_RETRY)
704
        {
705
            $this->_error = "temporary searchd error: ".substr($response, 4);
706
            return false;
707
        }
708
        if ($status != SEARCHD_OK)
709
        {
710
            $this->_error = "unknown status code '$status'";
711
            return false;
712
        }
713
714
        // check version
715
        if ($ver < $client_ver)
716
        {
717
            $this->_warning = sprintf("searchd command v.%d.%d older than client's v.%d.%d, some options might not work",
718
                $ver >> 8, $ver & 0xff, $client_ver >> 8, $client_ver & 0xff);
719
        }
720
721
        return $response;
722
    }
723
724
    /////////////////////////////////////////////////////////////////////////////
725
    // searching
726
    /////////////////////////////////////////////////////////////////////////////
727
728
    /// set offset and count into result set,
729
    /// and optionally set max-matches and cutoff limits
730
    public function SetLimits($offset, $limit, $max = 0, $cutoff = 0)
731
    {
732
        assert(is_int($offset));
733
        assert(is_int($limit));
734
        assert($offset >= 0);
735
        assert($limit > 0);
736
        assert($max >= 0);
737
        $this->_offset = $offset;
738
        $this->_limit = $limit;
739
        if ($max > 0) {
740
                    $this->_maxmatches = $max;
741
        }
742
        if ($cutoff > 0) {
743
                    $this->_cutoff = $cutoff;
744
        }
745
    }
746
747
    /// set maximum query time, in milliseconds, per-index
748
    /// integer, 0 means "do not limit"
749
    public function SetMaxQueryTime($max)
750
    {
751
        assert(is_int($max));
752
        assert($max >= 0);
753
        $this->_maxquerytime = $max;
754
    }
755
756
    /// set matching mode
757
    public function SetMatchMode($mode)
758
    {
759
        assert($mode == SPH_MATCH_ALL
760
            || $mode == SPH_MATCH_ANY
761
            || $mode == SPH_MATCH_PHRASE
762
            || $mode == SPH_MATCH_BOOLEAN
763
            || $mode == SPH_MATCH_EXTENDED
764
            || $mode == SPH_MATCH_FULLSCAN
765
            || $mode == SPH_MATCH_EXTENDED2);
766
        $this->_mode = $mode;
767
    }
768
769
    /// set ranking mode
770
    public function SetRankingMode($ranker)
771
    {
772
        assert($ranker >= 0 && $ranker < SPH_RANK_TOTAL);
773
        $this->_ranker = $ranker;
774
    }
775
776
    /// set matches sorting mode
777
    public function SetSortMode($mode, $sortby = "")
778
    {
779
        assert(
780
            $mode == SPH_SORT_RELEVANCE ||
781
            $mode == SPH_SORT_ATTR_DESC ||
782
            $mode == SPH_SORT_ATTR_ASC ||
783
            $mode == SPH_SORT_TIME_SEGMENTS ||
784
            $mode == SPH_SORT_EXTENDED ||
785
            $mode == SPH_SORT_EXPR );
786
        assert(is_string($sortby));
787
        assert($mode == SPH_SORT_RELEVANCE || strlen($sortby) > 0);
788
789
        $this->_sort = $mode;
790
        $this->_sortby = $sortby;
791
    }
792
793
    /// bind per-field weights by order
794
    /// DEPRECATED; use SetFieldWeights() instead
795
    public function SetWeights($weights)
796
    {
797
        assert(is_array($weights));
798
        foreach ($weights as $weight) {
799
                    assert(is_int($weight));
800
        }
801
802
        $this->_weights = $weights;
803
    }
804
805
    /// bind per-field weights by name
806
    public function SetFieldWeights($weights)
807
    {
808
        assert(is_array($weights));
809
        foreach ($weights as $name=>$weight)
810
        {
811
            assert(is_string($name));
812
            assert(is_int($weight));
813
        }
814
        $this->_fieldweights = $weights;
815
    }
816
817
    /// bind per-index weights by name
818
    public function SetIndexWeights($weights)
819
    {
820
        assert(is_array($weights));
821
        foreach ($weights as $index=>$weight)
822
        {
823
            assert(is_string($index));
824
            assert(is_int($weight));
825
        }
826
        $this->_indexweights = $weights;
827
    }
828
829
    /// set IDs range to match
830
    /// only match records if document ID is beetwen $min and $max (inclusive)
831
    public function SetIDRange($min, $max)
832
    {
833
        assert(is_numeric($min));
834
        assert(is_numeric($max));
835
        assert($min <= $max);
836
        $this->_min_id = $min;
837
        $this->_max_id = $max;
838
    }
839
840
    /// set values set filter
841
    /// only match records where $attribute value is in given set
842
    public function SetFilter($attribute, $values, $exclude = false)
843
    {
844
        assert(is_string($attribute));
845
        assert(is_array($values));
846
        assert(count($values));
847
848
        if (is_array($values) && count($values))
849
        {
850
            foreach ($values as $value) {
851
                            assert(is_numeric($value));
852
            }
853
854
            $this->_filters[] = array("type"=>SPH_FILTER_VALUES, "attr"=>$attribute, "exclude"=>$exclude, "values"=>$values);
855
        }
856
    }
857
858
    /// set range filter
859
    /// only match records if $attribute value is beetwen $min and $max (inclusive)
860
    public function SetFilterRange($attribute, $min, $max, $exclude = false)
861
    {
862
        assert(is_string($attribute));
863
        assert(is_numeric($min));
864
        assert(is_numeric($max));
865
        assert($min <= $max);
866
867
        $this->_filters[] = array("type"=>SPH_FILTER_RANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max);
868
    }
869
870
    /// set float range filter
871
    /// only match records if $attribute value is beetwen $min and $max (inclusive)
872
    public function SetFilterFloatRange($attribute, $min, $max, $exclude = false)
873
    {
874
        assert(is_string($attribute));
875
        assert(is_float($min));
876
        assert(is_float($max));
877
        assert($min <= $max);
878
879
        $this->_filters[] = array("type"=>SPH_FILTER_FLOATRANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max);
880
    }
881
882
    /// setup anchor point for geosphere distance calculations
883
    /// required to use @geodist in filters and sorting
884
    /// latitude and longitude must be in radians
885
    public function SetGeoAnchor($attrlat, $attrlong, $lat, $long)
886
    {
887
        assert(is_string($attrlat));
888
        assert(is_string($attrlong));
889
        assert(is_float($lat));
890
        assert(is_float($long));
891
892
        $this->_anchor = array("attrlat"=>$attrlat, "attrlong"=>$attrlong, "lat"=>$lat, "long"=>$long);
893
    }
894
895
    /// set grouping attribute and function
896
    public function SetGroupBy($attribute, $func, $groupsort = "@group desc")
897
    {
898
        assert(is_string($attribute));
899
        assert(is_string($groupsort));
900
        assert($func == SPH_GROUPBY_DAY
901
            || $func == SPH_GROUPBY_WEEK
902
            || $func == SPH_GROUPBY_MONTH
903
            || $func == SPH_GROUPBY_YEAR
904
            || $func == SPH_GROUPBY_ATTR
905
            || $func == SPH_GROUPBY_ATTRPAIR);
906
907
        $this->_groupby = $attribute;
908
        $this->_groupfunc = $func;
909
        $this->_groupsort = $groupsort;
910
    }
911
912
    /// set count-distinct attribute for group-by queries
913
    public function SetGroupDistinct($attribute)
914
    {
915
        assert(is_string($attribute));
916
        $this->_groupdistinct = $attribute;
917
    }
918
919
    /// set distributed retries count and delay
920
    public function SetRetries($count, $delay = 0)
921
    {
922
        assert(is_int($count) && $count >= 0);
923
        assert(is_int($delay) && $delay >= 0);
924
        $this->_retrycount = $count;
925
        $this->_retrydelay = $delay;
926
    }
927
928
    /// set result set format (hash or array; hash by default)
929
    /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs
930
    public function SetArrayResult($arrayresult)
931
    {
932
        assert(is_bool($arrayresult));
933
        $this->_arrayresult = $arrayresult;
934
    }
935
936
    /// set attribute values override
937
    /// there can be only one override per attribute
938
    /// $values must be a hash that maps document IDs to attribute values
939
    public function SetOverride($attrname, $attrtype, $values)
940
    {
941
        assert(is_string($attrname));
942
        assert(in_array($attrtype, array(SPH_ATTR_INTEGER, SPH_ATTR_TIMESTAMP, SPH_ATTR_BOOL, SPH_ATTR_FLOAT, SPH_ATTR_BIGINT)));
943
        assert(is_array($values));
944
945
        $this->_overrides[$attrname] = array("attr"=>$attrname, "type"=>$attrtype, "values"=>$values);
946
    }
947
948
    /// set select-list (attributes or expressions), SQL-like syntax
949
    public function SetSelect($select)
950
    {
951
        assert(is_string($select));
952
        $this->_select = $select;
953
    }
954
955
    //////////////////////////////////////////////////////////////////////////////
956
957
    /// clear all filters (for multi-queries)
958
    public function ResetFilters()
959
    {
960
        $this->_filters = array();
961
        $this->_anchor = array();
962
    }
963
964
    /// clear groupby settings (for multi-queries)
965
    public function ResetGroupBy()
966
    {
967
        $this->_groupby = "";
968
        $this->_groupfunc	= SPH_GROUPBY_DAY;
969
        $this->_groupsort	= "@group desc";
970
        $this->_groupdistinct = "";
971
    }
972
973
    /// clear all attribute value overrides (for multi-queries)
974
    public function ResetOverrides()
975
    {
976
        $this->_overrides = array();
977
    }
978
979
    //////////////////////////////////////////////////////////////////////////////
980
981
    /// connect to searchd server, run given search query through given indexes,
982
    /// and return the search results
983
    public function Query($query, $index = "*", $comment = "")
984
    {
985
        assert(empty($this->_reqs));
986
987
        $this->AddQuery($query, $index, $comment);
988
        $results = $this->RunQueries();
989
        $this->_reqs = array(); // just in case it failed too early
990
991
        if (!is_array($results)) {
992
                    return false;
993
        }
994
        // probably network error; error message should be already filled
995
996
        $this->_error = $results[0]["error"];
997
        $this->_warning = $results[0]["warning"];
998
        if ($results[0]["status"] == SEARCHD_ERROR) {
999
                    return false;
1000
        } else {
1001
                    return $results[0];
1002
        }
1003
    }
1004
1005
    /// helper to pack floats in network byte order
1006
    public function _PackFloat($f)
1007
    {
1008
        $t1 = pack("f", $f); // machine order
1009
        list(,$t2) = unpack("L*", $t1); // int in machine order
1010
        return pack("N", $t2);
1011
    }
1012
1013
    /**
1014
     * @SuppressWarnings(unused)
1015
     */
1016
1017
    /// add query to multi-query batch
1018
    /// returns index into results array from RunQueries() call
1019
    public function AddQuery($query, $index = "*", $comment = "")
1020
    {
1021
        // mbstring workaround
1022
        $this->_MBPush();
1023
1024
        // build request
1025
        $req = pack("NNNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker, $this->_sort); // mode and limits
1026
        $req .= pack("N", strlen($this->_sortby)).$this->_sortby;
1027
        $req .= pack("N", strlen($query)).$query; // query itself
1028
        $req .= pack("N", count($this->_weights)); // weights
1029
        foreach ($this->_weights as $weight) {
1030
                    $req .= pack("N", (int) $weight);
1031
        }
1032
        $req .= pack("N", strlen($index)).$index; // indexes
1033
        $req .= pack("N", 1); // id64 range marker
1034
        $req .= sphPackU64($this->_min_id).sphPackU64($this->_max_id); // id64 range
1035
1036
        // filters
1037
        $req .= pack("N", count($this->_filters));
1038
        foreach ($this->_filters as $filter)
1039
        {
1040
            $req .= pack("N", strlen($filter["attr"])).$filter["attr"];
1041
            $req .= pack("N", $filter["type"]);
1042
            switch ($filter["type"])
1043
            {
1044
            case SPH_FILTER_VALUES:
1045
                $req .= pack("N", count($filter["values"]));
1046
                foreach ($filter["values"] as $value)
1047
                    $req .= sphPackI64($value);
1048
                break;
1049
1050
            case SPH_FILTER_RANGE:
1051
                $req .= sphPackI64($filter["min"]).sphPackI64($filter["max"]);
1052
                break;
1053
1054
            case SPH_FILTER_FLOATRANGE:
1055
                $req .= $this->_PackFloat($filter["min"]).$this->_PackFloat($filter["max"]);
1056
                break;
1057
1058
            default:
1059
                assert(0 && "internal error: unhandled filter type");
1060
            }
1061
            $req .= pack("N", $filter["exclude"]);
1062
        }
1063
1064
        // group-by clause, max-matches count, group-sort clause, cutoff count
1065
        $req .= pack("NN", $this->_groupfunc, strlen($this->_groupby)).$this->_groupby;
1066
        $req .= pack("N", $this->_maxmatches);
1067
        $req .= pack("N", strlen($this->_groupsort)).$this->_groupsort;
1068
        $req .= pack("NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay);
1069
        $req .= pack("N", strlen($this->_groupdistinct)).$this->_groupdistinct;
1070
1071
        // anchor point
1072
        if (empty($this->_anchor))
1073
        {
1074
            $req .= pack("N", 0);
1075
        } else
1076
        {
1077
            $a = & $this->_anchor;
1078
            $req .= pack("N", 1);
1079
            $req .= pack("N", strlen($a["attrlat"])).$a["attrlat"];
1080
            $req .= pack("N", strlen($a["attrlong"])).$a["attrlong"];
1081
            $req .= $this->_PackFloat($a["lat"]).$this->_PackFloat($a["long"]);
1082
        }
1083
1084
        // per-index weights
1085
        $req .= pack("N", count($this->_indexweights));
1086
        foreach ($this->_indexweights as $idx=>$weight) {
1087
                    $req .= pack("N", strlen($idx)).$idx.pack("N", $weight);
1088
        }
1089
1090
        // max query time
1091
        $req .= pack("N", $this->_maxquerytime);
1092
1093
        // per-field weights
1094
        $req .= pack("N", count($this->_fieldweights));
1095
        foreach ($this->_fieldweights as $field=>$weight) {
1096
                    $req .= pack("N", strlen($field)).$field.pack("N", $weight);
1097
        }
1098
1099
        // comment
1100
        $req .= pack("N", strlen($comment)).$comment;
1101
1102
        // attribute overrides
1103
        $req .= pack("N", count($this->_overrides));
1104
        foreach ($this->_overrides as $key => $entry)
1105
        {
1106
            $req .= pack("N", strlen($entry["attr"])).$entry["attr"];
1107
            $req .= pack("NN", $entry["type"], count($entry["values"]));
1108
            foreach ($entry["values"] as $id=>$val)
1109
            {
1110
                assert(is_numeric($id));
1111
                assert(is_numeric($val));
1112
1113
                $req .= sphPackU64($id);
1114
                switch ($entry["type"])
1115
                {
1116
                case SPH_ATTR_FLOAT:	$req .= $this->_PackFloat($val); break;
1117
                case SPH_ATTR_BIGINT:	$req .= sphPackI64($val); break;
1118
                default:				$req .= pack("N", $val); break;
1119
                }
1120
            }
1121
        }
1122
1123
        // select-list
1124
        $req .= pack("N", strlen($this->_select)).$this->_select;
1125
1126
        // mbstring workaround
1127
        $this->_MBPop();
1128
1129
        // store request to requests array
1130
        $this->_reqs[] = $req;
1131
        return count($this->_reqs) - 1;
1132
    }
1133
1134
    /// connect to searchd, run queries batch, and return an array of result sets
1135
    public function RunQueries()
1136
    {
1137
        if (empty($this->_reqs))
1138
        {
1139
            $this->_error = "no queries defined, issue AddQuery() first";
1140
            return false;
1141
        }
1142
1143
        // mbstring workaround
1144
        $this->_MBPush();
1145
1146
        if (!($fp = $this->_Connect()))
1147
        {
1148
            $this->_MBPop();
1149
            return false;
1150
        }
1151
1152
        // send query, get response
1153
        $nreqs = count($this->_reqs);
1154
        $req = join("", $this->_reqs);
1155
        $len = 8 + strlen($req);
1156
        $req = pack("nnNNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, 0, $nreqs).$req; // add header
1157
1158
        if (!($this->_Send($fp, $req, $len + 8)) ||
1159
             !($response = $this->_GetResponse($fp, VER_COMMAND_SEARCH)))
1160
        {
1161
            $this->_MBPop();
1162
            return false;
1163
        }
1164
1165
        // query sent ok; we can reset reqs now
1166
        $this->_reqs = array();
1167
1168
        // parse and return response
1169
        return $this->_ParseSearchResponse($response, $nreqs);
1170
    }
1171
1172
    /// parse and return search query (or queries) response
1173
    public function _ParseSearchResponse($response, $nreqs)
1174
    {
1175
        $p = 0; // current position
1176
        $max = strlen($response); // max position for checks, to protect against broken responses
1177
1178
        $results = array();
1179
        for ($ires = 0; $ires < $nreqs && $p < $max; $ires++)
1180
        {
1181
            $results[] = array();
1182
            $result = & $results[$ires];
1183
1184
            $result["error"] = "";
1185
            $result["warning"] = "";
1186
1187
            // extract status
1188
            list(,$status) = unpack("N*", substr($response, $p, 4)); $p += 4;
1189
            $result["status"] = $status;
1190
            if ($status != SEARCHD_OK)
1191
            {
1192
                list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1193
                $message = substr($response, $p, $len); $p += $len;
1194
1195
                if ($status == SEARCHD_WARNING)
1196
                {
1197
                    $result["warning"] = $message;
1198
                } else
1199
                {
1200
                    $result["error"] = $message;
1201
                    continue;
1202
                }
1203
            }
1204
1205
            // read schema
1206
            $fields = array();
1207
            $attrs = array();
1208
1209
            list(,$nfields) = unpack("N*", substr($response, $p, 4)); $p += 4;
1210
            while ($nfields-- > 0 && $p < $max)
1211
            {
1212
                list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1213
                $fields[] = substr($response, $p, $len); $p += $len;
1214
            }
1215
            $result["fields"] = $fields;
1216
1217
            list(,$nattrs) = unpack("N*", substr($response, $p, 4)); $p += 4;
1218
            while ($nattrs-- > 0 && $p < $max)
1219
            {
1220
                list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1221
                $attr = substr($response, $p, $len); $p += $len;
1222
                list(,$type) = unpack("N*", substr($response, $p, 4)); $p += 4;
1223
                $attrs[$attr] = $type;
1224
            }
1225
            $result["attrs"] = $attrs;
1226
1227
            // read match count
1228
            list(,$count) = unpack("N*", substr($response, $p, 4)); $p += 4;
1229
            list(,$id64) = unpack("N*", substr($response, $p, 4)); $p += 4;
1230
1231
            // read matches
1232
            $idx = -1;
1233
            while ($count-- > 0 && $p < $max)
1234
            {
1235
                // index into result array
1236
                $idx++;
1237
1238
                // parse document id and weight
1239
                if ($id64)
1240
                {
1241
                    $doc = sphUnpackU64(substr($response, $p, 8)); $p += 8;
1242
                    list(,$weight) = unpack("N*", substr($response, $p, 4)); $p += 4;
1243
                } else
1244
                {
1245
                    list ($doc, $weight) = array_values(unpack("N*N*",
1246
                        substr($response, $p, 8)));
1247
                    $p += 8;
1248
                    $doc = sphFixUint($doc);
1249
                }
1250
                $weight = sprintf("%u", $weight);
1251
1252
                // create match entry
1253
                if ($this->_arrayresult) {
1254
                                    $result["matches"][$idx] = array("id"=>$doc, "weight"=>$weight);
1255
                } else {
1256
                                    $result["matches"][$doc]["weight"] = $weight;
1257
                }
1258
1259
                // parse and create attributes
1260
                $attrvals = array();
1261
                foreach ($attrs as $attr=>$type)
1262
                {
1263
                    // handle 64bit ints
1264
                    if ($type == SPH_ATTR_BIGINT)
1265
                    {
1266
                        $attrvals[$attr] = sphUnpackI64(substr($response, $p, 8)); $p += 8;
1267
                        continue;
1268
                    }
1269
1270
                    // handle floats
1271
                    if ($type == SPH_ATTR_FLOAT)
1272
                    {
1273
                        list(,$uval) = unpack("N*", substr($response, $p, 4)); $p += 4;
1274
                        list(,$fval) = unpack("f*", pack("L", $uval));
1275
                        $attrvals[$attr] = $fval;
1276
                        continue;
1277
                    }
1278
1279
                    // handle everything else as unsigned ints
1280
                    list(,$val) = unpack("N*", substr($response, $p, 4)); $p += 4;
1281
                    if ($type & SPH_ATTR_MULTI)
1282
                    {
1283
                        $attrvals[$attr] = array();
1284
                        $nvalues = $val;
1285
                        while ($nvalues-- > 0 && $p < $max)
1286
                        {
1287
                            list(,$val) = unpack("N*", substr($response, $p, 4)); $p += 4;
1288
                            $attrvals[$attr][] = sphFixUint($val);
1289
                        }
1290
                    } else if ($type == SPH_ATTR_STRING)
1291
                    {
1292
                        $attrvals[$attr] = substr($response, $p, $val);
1293
                        $p += $val;
1294
                    } else
1295
                    {
1296
                        $attrvals[$attr] = sphFixUint($val);
1297
                    }
1298
                }
1299
1300
                if ($this->_arrayresult) {
1301
                                    $result["matches"][$idx]["attrs"] = $attrvals;
1302
                } else {
1303
                                    $result["matches"][$doc]["attrs"] = $attrvals;
1304
                }
1305
            }
1306
1307
            list ($total, $total_found, $msecs, $words) =
1308
                array_values(unpack("N*N*N*N*", substr($response, $p, 16)));
1309
            $result["total"] = sprintf("%u", $total);
1310
            $result["total_found"] = sprintf("%u", $total_found);
1311
            $result["time"] = sprintf("%.3f", $msecs / 1000);
1312
            $p += 16;
1313
1314
            while ($words-- > 0 && $p < $max)
1315
            {
1316
                list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1317
                $word = substr($response, $p, $len); $p += $len;
1318
                list ($docs, $hits) = array_values(unpack("N*N*", substr($response, $p, 8))); $p += 8;
1319
                $result["words"][$word] = array(
1320
                    "docs"=>sprintf("%u", $docs),
1321
                    "hits"=>sprintf("%u", $hits) );
1322
            }
1323
        }
1324
1325
        $this->_MBPop();
1326
        return $results;
1327
    }
1328
1329
    /////////////////////////////////////////////////////////////////////////////
1330
    // excerpts generation
1331
    /////////////////////////////////////////////////////////////////////////////
1332
1333
    /// connect to searchd server, and generate exceprts (snippets)
1334
    /// of given documents for given query. returns false on failure,
1335
    /// an array of snippets on success
1336
    public function BuildExcerpts($docs, $index, $words, $opts = array())
1337
    {
1338
        assert(is_array($docs));
1339
        assert(is_string($index));
1340
        assert(is_string($words));
1341
        assert(is_array($opts));
1342
1343
        $this->_MBPush();
1344
1345
        if (!($fp = $this->_Connect()))
1346
        {
1347
            $this->_MBPop();
1348
            return false;
1349
        }
1350
1351
        /////////////////
1352
        // fixup options
1353
        /////////////////
1354
1355
        if (!isset($opts["before_match"])) {
1356
            $opts["before_match"] = "<b>";
1357
        }
1358
        if (!isset($opts["after_match"])) {
1359
            $opts["after_match"] = "</b>";
1360
        }
1361
        if (!isset($opts["chunk_separator"])) {
1362
            $opts["chunk_separator"] = " ... ";
1363
        }
1364
        if (!isset($opts["limit"])) {
1365
            $opts["limit"] = 256;
1366
        }
1367
        if (!isset($opts["limit_passages"])) {
1368
            $opts["limit_passages"] = 0;
1369
        }
1370
        if (!isset($opts["limit_words"])) {
1371
            $opts["limit_words"] = 0;
1372
        }
1373
        if (!isset($opts["around"])) {
1374
            $opts["around"] = 5;
1375
        }
1376
        if (!isset($opts["exact_phrase"])) {
1377
            $opts["exact_phrase"] = false;
1378
        }
1379
        if (!isset($opts["single_passage"])) {
1380
            $opts["single_passage"] = false;
1381
        }
1382
        if (!isset($opts["use_boundaries"])) {
1383
            $opts["use_boundaries"] = false;
1384
        }
1385
        if (!isset($opts["weight_order"])) {
1386
            $opts["weight_order"] = false;
1387
        }
1388
        if (!isset($opts["query_mode"])) {
1389
            $opts["query_mode"] = false;
1390
        }
1391
        if (!isset($opts["force_all_words"])) {
1392
            $opts["force_all_words"] = false;
1393
        }
1394
        if (!isset($opts["start_passage_id"])) {
1395
            $opts["start_passage_id"] = 1;
1396
        }
1397
        if (!isset($opts["load_files"])) {
1398
            $opts["load_files"] = false;
1399
        }
1400
        if (!isset($opts["html_strip_mode"])) {
1401
            $opts["html_strip_mode"] = "index";
1402
        }
1403
        if (!isset($opts["allow_empty"])) {
1404
            $opts["allow_empty"] = false;
1405
        }
1406
        if (!isset($opts["passage_boundary"])) {
1407
            $opts["passage_boundary"] = "none";
1408
        }
1409
        if (!isset($opts["emit_zones"])) {
1410
            $opts["emit_zones"] = false;
1411
        }
1412
1413
        /////////////////
1414
        // build request
1415
        /////////////////
1416
1417
        // v.1.2 req
1418
        $flags = 1; // remove spaces
1419
        if ($opts["exact_phrase"]) {
1420
            $flags |= 2;
1421
        }
1422
        if ($opts["single_passage"]) {
1423
            $flags |= 4;
1424
        }
1425
        if ($opts["use_boundaries"]) {
1426
            $flags |= 8;
1427
        }
1428
        if ($opts["weight_order"]) {
1429
            $flags |= 16;
1430
        }
1431
        if ($opts["query_mode"]) {
1432
            $flags |= 32;
1433
        }
1434
        if ($opts["force_all_words"]) {
1435
            $flags |= 64;
1436
        }
1437
        if ($opts["load_files"]) {
1438
            $flags |= 128;
1439
        }
1440
        if ($opts["allow_empty"]) {
1441
            $flags |= 256;
1442
        }
1443
        if ($opts["emit_zones"]) {
1444
            $flags |= 512;
1445
        }
1446
        $req = pack("NN", 0, $flags); // mode=0, flags=$flags
1447
        $req .= pack("N", strlen($index)).$index; // req index
1448
        $req .= pack("N", strlen($words)).$words; // req words
1449
1450
        // options
1451
        $req .= pack("N", strlen($opts["before_match"])).$opts["before_match"];
1452
        $req .= pack("N", strlen($opts["after_match"])).$opts["after_match"];
1453
        $req .= pack("N", strlen($opts["chunk_separator"])).$opts["chunk_separator"];
1454
        $req .= pack("NN", (int) $opts["limit"], (int) $opts["around"]);
1455
        $req .= pack("NNN", (int) $opts["limit_passages"], (int) $opts["limit_words"], (int) $opts["start_passage_id"]); // v.1.2
1456
        $req .= pack("N", strlen($opts["html_strip_mode"])).$opts["html_strip_mode"];
1457
        $req .= pack("N", strlen($opts["passage_boundary"])).$opts["passage_boundary"];
1458
1459
        // documents
1460
        $req .= pack("N", count($docs));
1461
        foreach ($docs as $doc)
1462
        {
1463
            assert(is_string($doc));
1464
            $req .= pack("N", strlen($doc)).$doc;
1465
        }
1466
1467
        ////////////////////////////
1468
        // send query, get response
1469
        ////////////////////////////
1470
1471
        $len = strlen($req);
1472
        $req = pack("nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len).$req; // add header
1473
        if (!($this->_Send($fp, $req, $len + 8)) ||
1474
             !($response = $this->_GetResponse($fp, VER_COMMAND_EXCERPT)))
1475
        {
1476
            $this->_MBPop();
1477
            return false;
1478
        }
1479
1480
        //////////////////
1481
        // parse response
1482
        //////////////////
1483
1484
        $pos = 0;
1485
        $res = array();
1486
        $rlen = strlen($response);
1487
        for ($i = 0; $i < count($docs); $i++)
1488
        {
1489
            list(,$len) = unpack("N*", substr($response, $pos, 4));
1490
            $pos += 4;
1491
1492
            if ($pos + $len > $rlen)
1493
            {
1494
                $this->_error = "incomplete reply";
1495
                $this->_MBPop();
1496
                return false;
1497
            }
1498
            $res[] = $len ? substr ($response, $pos, $len) : "";
1499
            $pos += $len;
1500
        }
1501
1502
        $this->_MBPop();
1503
        return $res;
1504
    }
1505
1506
1507
    /////////////////////////////////////////////////////////////////////////////
1508
    // keyword generation
1509
    /////////////////////////////////////////////////////////////////////////////
1510
1511
    /// connect to searchd server, and generate keyword list for a given query
1512
    /// returns false on failure,
1513
    /// an array of words on success
1514
    public function BuildKeywords($query, $index, $hits)
1515
    {
1516
        assert(is_string($query));
1517
        assert(is_string($index));
1518
        assert(is_bool($hits));
1519
1520
        $this->_MBPush();
1521
1522
        if (!($fp = $this->_Connect()))
1523
        {
1524
            $this->_MBPop();
1525
            return false;
1526
        }
1527
1528
        /////////////////
1529
        // build request
1530
        /////////////////
1531
1532
        // v.1.0 req
1533
        $req  = pack("N", strlen($query)).$query; // req query
1534
        $req .= pack("N", strlen($index)).$index; // req index
1535
        $req .= pack("N", (int) $hits);
1536
1537
        ////////////////////////////
1538
        // send query, get response
1539
        ////////////////////////////
1540
1541
        $len = strlen($req);
1542
        $req = pack("nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len).$req; // add header
1543
        if (!($this->_Send($fp, $req, $len + 8)) ||
1544
             !($response = $this->_GetResponse($fp, VER_COMMAND_KEYWORDS)))
1545
        {
1546
            $this->_MBPop();
1547
            return false;
1548
        }
1549
1550
        //////////////////
1551
        // parse response
1552
        //////////////////
1553
1554
        $pos = 0;
1555
        $res = array();
1556
        $rlen = strlen($response);
1557
        list(,$nwords) = unpack("N*", substr($response, $pos, 4));
1558
        $pos += 4;
1559
        for ($i = 0; $i < $nwords; $i++)
1560
        {
1561
            list(,$len) = unpack("N*", substr($response, $pos, 4)); $pos += 4;
1562
            $tokenized = $len ? substr ($response, $pos, $len) : "";
1563
            $pos += $len;
1564
1565
            list(,$len) = unpack("N*", substr($response, $pos, 4)); $pos += 4;
1566
            $normalized = $len ? substr ($response, $pos, $len) : "";
1567
            $pos += $len;
1568
1569
            $res[] = array("tokenized"=>$tokenized, "normalized"=>$normalized);
1570
1571
            if ($hits)
1572
            {
1573
                list($ndocs, $nhits) = array_values(unpack("N*N*", substr($response, $pos, 8)));
1574
                $pos += 8;
1575
                $res [$i]["docs"] = $ndocs;
1576
                $res [$i]["hits"] = $nhits;
1577
            }
1578
1579
            if ($pos > $rlen)
1580
            {
1581
                $this->_error = "incomplete reply";
1582
                $this->_MBPop();
1583
                return false;
1584
            }
1585
        }
1586
1587
        $this->_MBPop();
1588
        return $res;
1589
    }
1590
1591
    public function EscapeString($string)
1592
    {
1593
        $from = array('\\', '(', ')', '|', '-', '!', '@', '~', '"', '&', '/', '^', '$', '=');
1594
        $to   = array('\\\\', '\(', '\)', '\|', '\-', '\!', '\@', '\~', '\"', '\&', '\/', '\^', '\$', '\=');
1595
1596
        return str_replace($from, $to, $string);
1597
    }
1598
1599
    /////////////////////////////////////////////////////////////////////////////
1600
    // attribute updates
1601
    /////////////////////////////////////////////////////////////////////////////
1602
1603
    /// batch update given attributes in given rows in given indexes
1604
    /// returns amount of updated documents (0 or more) on success, or -1 on failure
1605
    public function UpdateAttributes($index, $attrs, $values, $mva = false)
1606
    {
1607
        // verify everything
1608
        assert(is_string($index));
1609
        assert(is_bool($mva));
1610
1611
        assert(is_array($attrs));
1612
        foreach ($attrs as $attr) {
1613
                    assert(is_string($attr));
1614
        }
1615
1616
        assert(is_array($values));
1617
        foreach ($values as $id=>$entry)
1618
        {
1619
            assert(is_numeric($id));
1620
            assert(is_array($entry));
1621
            assert(count($entry) == count($attrs));
1622
            foreach ($entry as $v)
1623
            {
1624
                if ($mva)
1625
                {
1626
                    assert(is_array($v));
1627
                    foreach ($v as $vv) {
1628
                                            assert(is_int($vv));
1629
                    }
1630
                } else {
1631
                                    assert(is_int($v));
1632
                }
1633
            }
1634
        }
1635
1636
        // build request
1637
        $this->_MBPush();
1638
        $req = pack("N", strlen($index)).$index;
1639
1640
        $req .= pack("N", count($attrs));
1641
        foreach ($attrs as $attr)
1642
        {
1643
            $req .= pack("N", strlen($attr)).$attr;
1644
            $req .= pack("N", $mva ? 1 : 0);
1645
        }
1646
1647
        $req .= pack("N", count($values));
1648
        foreach ($values as $id=>$entry)
1649
        {
1650
            $req .= sphPackU64($id);
1651
            foreach ($entry as $v)
1652
            {
1653
                $req .= pack("N", $mva ? count($v) : $v);
1654
                if ($mva) {
1655
                                    foreach ($v as $vv)
1656
                        $req .= pack("N", $vv);
1657
                }
1658
            }
1659
        }
1660
1661
        // connect, send query, get response
1662
        if (!($fp = $this->_Connect()))
1663
        {
1664
            $this->_MBPop();
1665
            return -1;
1666
        }
1667
1668
        $len = strlen($req);
1669
        $req = pack("nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len).$req; // add header
1670
        if (!$this->_Send($fp, $req, $len + 8))
1671
        {
1672
            $this->_MBPop();
1673
            return -1;
1674
        }
1675
1676
        if (!($response = $this->_GetResponse($fp, VER_COMMAND_UPDATE)))
1677
        {
1678
            $this->_MBPop();
1679
            return -1;
1680
        }
1681
1682
        // parse response
1683
        list(,$updated) = unpack("N*", substr($response, 0, 4));
1684
        $this->_MBPop();
1685
        return $updated;
1686
    }
1687
1688
    /////////////////////////////////////////////////////////////////////////////
1689
    // persistent connections
1690
    /////////////////////////////////////////////////////////////////////////////
1691
1692
    public function Open()
1693
    {
1694
        if ($this->_socket !== false)
1695
        {
1696
            $this->_error = 'already connected';
1697
            return false;
1698
        }
1699
        if (!$fp = $this->_Connect()) {
1700
                    return false;
1701
        }
1702
1703
        // command, command version = 0, body length = 4, body = 1
1704
        $req = pack("nnNN", SEARCHD_COMMAND_PERSIST, 0, 4, 1);
1705
        if (!$this->_Send($fp, $req, 12)) {
1706
                    return false;
1707
        }
1708
1709
        $this->_socket = $fp;
1710
        return true;
1711
    }
1712
1713
    public function Close()
1714
    {
1715
        if ($this->_socket === false)
1716
        {
1717
            $this->_error = 'not connected';
1718
            return false;
1719
        }
1720
1721
        fclose($this->_socket);
1722
        $this->_socket = false;
1723
1724
        return true;
1725
    }
1726
1727
    //////////////////////////////////////////////////////////////////////////
1728
    // status
1729
    //////////////////////////////////////////////////////////////////////////
1730
1731
    public function Status()
1732
    {
1733
        $this->_MBPush();
1734
        if (!($fp = $this->_Connect()))
1735
        {
1736
            $this->_MBPop();
1737
            return false;
1738
        }
1739
1740
        $req = pack("nnNN", SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, 1); // len=4, body=1
1741
        if (!($this->_Send($fp, $req, 12)) ||
1742
             !($response = $this->_GetResponse($fp, VER_COMMAND_STATUS)))
1743
        {
1744
            $this->_MBPop();
1745
            return false;
1746
        }
1747
1748
        $res = substr($response, 4); // just ignore length, error handling, etc
1749
        $p = 0;
1750
        list ($rows, $cols) = array_values(unpack("N*N*", substr($response, $p, 8))); $p += 8;
1751
1752
        $res = array();
1753
        for ($i = 0; $i < $rows; $i++) {
1754
                    for ($j = 0;
1755
        }
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected '}', expecting ';' on line 1755 at column 8
Loading history...
1756
        $j < $cols; $j++)
1757
        {
1758
            list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1759
            $res[$i][] = substr($response, $p, $len); $p += $len;
1760
        }
1761
1762
        $this->_MBPop();
1763
        return $res;
1764
    }
1765
1766
    //////////////////////////////////////////////////////////////////////////
1767
    // flush
1768
    //////////////////////////////////////////////////////////////////////////
1769
1770
    public function FlushAttributes()
1771
    {
1772
        $this->_MBPush();
1773
        if (!($fp = $this->_Connect()))
1774
        {
1775
            $this->_MBPop();
1776
            return -1;
1777
        }
1778
1779
        $req = pack("nnN", SEARCHD_COMMAND_FLUSHATTRS, VER_COMMAND_FLUSHATTRS, 0); // len=0
1780
        if (!($this->_Send($fp, $req, 8)) ||
1781
             !($response = $this->_GetResponse($fp, VER_COMMAND_FLUSHATTRS)))
1782
        {
1783
            $this->_MBPop();
1784
            return -1;
1785
        }
1786
1787
        $tag = -1;
1788
        if (strlen($response) == 4) {
1789
                    list(,$tag) = unpack("N*", $response);
1790
        } else {
1791
                    $this->_error = "unexpected response length";
1792
        }
1793
1794
        $this->_MBPop();
1795
        return $tag;
1796
    }
1797
}
1798
1799
//
1800
// $Id: sphinxapi.php 2758 2011-04-04 11:10:44Z kevg $
1801
//
1802