Issues (1270)

plugins/search_sphinx/sphinxapi.php (1 issue)

Labels
Severity
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
                }
1049
                break;
1050
1051
            case SPH_FILTER_RANGE:
1052
                $req .= sphPackI64($filter["min"]).sphPackI64($filter["max"]);
1053
                break;
1054
1055
            case SPH_FILTER_FLOATRANGE:
1056
                $req .= $this->_PackFloat($filter["min"]).$this->_PackFloat($filter["max"]);
1057
                break;
1058
1059
            default:
1060
                assert(0 && "internal error: unhandled filter type");
1061
            }
1062
            $req .= pack("N", $filter["exclude"]);
1063
        }
1064
1065
        // group-by clause, max-matches count, group-sort clause, cutoff count
1066
        $req .= pack("NN", $this->_groupfunc, strlen($this->_groupby)).$this->_groupby;
1067
        $req .= pack("N", $this->_maxmatches);
1068
        $req .= pack("N", strlen($this->_groupsort)).$this->_groupsort;
1069
        $req .= pack("NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay);
1070
        $req .= pack("N", strlen($this->_groupdistinct)).$this->_groupdistinct;
1071
1072
        // anchor point
1073
        if (empty($this->_anchor))
1074
        {
1075
            $req .= pack("N", 0);
1076
        } else
1077
        {
1078
            $a = & $this->_anchor;
1079
            $req .= pack("N", 1);
1080
            $req .= pack("N", strlen($a["attrlat"])).$a["attrlat"];
1081
            $req .= pack("N", strlen($a["attrlong"])).$a["attrlong"];
1082
            $req .= $this->_PackFloat($a["lat"]).$this->_PackFloat($a["long"]);
1083
        }
1084
1085
        // per-index weights
1086
        $req .= pack("N", count($this->_indexweights));
1087
        foreach ($this->_indexweights as $idx=>$weight) {
1088
                    $req .= pack("N", strlen($idx)).$idx.pack("N", $weight);
1089
        }
1090
1091
        // max query time
1092
        $req .= pack("N", $this->_maxquerytime);
1093
1094
        // per-field weights
1095
        $req .= pack("N", count($this->_fieldweights));
1096
        foreach ($this->_fieldweights as $field=>$weight) {
1097
                    $req .= pack("N", strlen($field)).$field.pack("N", $weight);
1098
        }
1099
1100
        // comment
1101
        $req .= pack("N", strlen($comment)).$comment;
1102
1103
        // attribute overrides
1104
        $req .= pack("N", count($this->_overrides));
1105
        foreach ($this->_overrides as $key => $entry)
1106
        {
1107
            $req .= pack("N", strlen($entry["attr"])).$entry["attr"];
1108
            $req .= pack("NN", $entry["type"], count($entry["values"]));
1109
            foreach ($entry["values"] as $id=>$val)
1110
            {
1111
                assert(is_numeric($id));
1112
                assert(is_numeric($val));
1113
1114
                $req .= sphPackU64($id);
1115
                switch ($entry["type"])
1116
                {
1117
                case SPH_ATTR_FLOAT:	$req .= $this->_PackFloat($val); break;
1118
                case SPH_ATTR_BIGINT:	$req .= sphPackI64($val); break;
1119
                default:				$req .= pack("N", $val); break;
1120
                }
1121
            }
1122
        }
1123
1124
        // select-list
1125
        $req .= pack("N", strlen($this->_select)).$this->_select;
1126
1127
        // mbstring workaround
1128
        $this->_MBPop();
1129
1130
        // store request to requests array
1131
        $this->_reqs[] = $req;
1132
        return count($this->_reqs) - 1;
1133
    }
1134
1135
    /// connect to searchd, run queries batch, and return an array of result sets
1136
    public function RunQueries()
1137
    {
1138
        if (empty($this->_reqs))
1139
        {
1140
            $this->_error = "no queries defined, issue AddQuery() first";
1141
            return false;
1142
        }
1143
1144
        // mbstring workaround
1145
        $this->_MBPush();
1146
1147
        if (!($fp = $this->_Connect()))
1148
        {
1149
            $this->_MBPop();
1150
            return false;
1151
        }
1152
1153
        // send query, get response
1154
        $nreqs = count($this->_reqs);
1155
        $req = join("", $this->_reqs);
1156
        $len = 8 + strlen($req);
1157
        $req = pack("nnNNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, 0, $nreqs).$req; // add header
1158
1159
        if (!($this->_Send($fp, $req, $len + 8)) ||
1160
             !($response = $this->_GetResponse($fp, VER_COMMAND_SEARCH)))
1161
        {
1162
            $this->_MBPop();
1163
            return false;
1164
        }
1165
1166
        // query sent ok; we can reset reqs now
1167
        $this->_reqs = array();
1168
1169
        // parse and return response
1170
        return $this->_ParseSearchResponse($response, $nreqs);
1171
    }
1172
1173
    /// parse and return search query (or queries) response
1174
    public function _ParseSearchResponse($response, $nreqs)
1175
    {
1176
        $p = 0; // current position
1177
        $max = strlen($response); // max position for checks, to protect against broken responses
1178
1179
        $results = array();
1180
        for ($ires = 0; $ires < $nreqs && $p < $max; $ires++)
1181
        {
1182
            $results[] = array();
1183
            $result = & $results[$ires];
1184
1185
            $result["error"] = "";
1186
            $result["warning"] = "";
1187
1188
            // extract status
1189
            list(,$status) = unpack("N*", substr($response, $p, 4)); $p += 4;
1190
            $result["status"] = $status;
1191
            if ($status != SEARCHD_OK)
1192
            {
1193
                list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1194
                $message = substr($response, $p, $len); $p += $len;
1195
1196
                if ($status == SEARCHD_WARNING)
1197
                {
1198
                    $result["warning"] = $message;
1199
                } else
1200
                {
1201
                    $result["error"] = $message;
1202
                    continue;
1203
                }
1204
            }
1205
1206
            // read schema
1207
            $fields = array();
1208
            $attrs = array();
1209
1210
            list(,$nfields) = unpack("N*", substr($response, $p, 4)); $p += 4;
1211
            while ($nfields-- > 0 && $p < $max)
1212
            {
1213
                list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1214
                $fields[] = substr($response, $p, $len); $p += $len;
1215
            }
1216
            $result["fields"] = $fields;
1217
1218
            list(,$nattrs) = unpack("N*", substr($response, $p, 4)); $p += 4;
1219
            while ($nattrs-- > 0 && $p < $max)
1220
            {
1221
                list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1222
                $attr = substr($response, $p, $len); $p += $len;
1223
                list(,$type) = unpack("N*", substr($response, $p, 4)); $p += 4;
1224
                $attrs[$attr] = $type;
1225
            }
1226
            $result["attrs"] = $attrs;
1227
1228
            // read match count
1229
            list(,$count) = unpack("N*", substr($response, $p, 4)); $p += 4;
1230
            list(,$id64) = unpack("N*", substr($response, $p, 4)); $p += 4;
1231
1232
            // read matches
1233
            $idx = -1;
1234
            while ($count-- > 0 && $p < $max)
1235
            {
1236
                // index into result array
1237
                $idx++;
1238
1239
                // parse document id and weight
1240
                if ($id64)
1241
                {
1242
                    $doc = sphUnpackU64(substr($response, $p, 8)); $p += 8;
1243
                    list(,$weight) = unpack("N*", substr($response, $p, 4)); $p += 4;
1244
                } else
1245
                {
1246
                    list ($doc, $weight) = array_values(unpack("N*N*",
1247
                        substr($response, $p, 8)));
1248
                    $p += 8;
1249
                    $doc = sphFixUint($doc);
1250
                }
1251
                $weight = sprintf("%u", $weight);
1252
1253
                // create match entry
1254
                if ($this->_arrayresult) {
1255
                                    $result["matches"][$idx] = array("id"=>$doc, "weight"=>$weight);
1256
                } else {
1257
                                    $result["matches"][$doc]["weight"] = $weight;
1258
                }
1259
1260
                // parse and create attributes
1261
                $attrvals = array();
1262
                foreach ($attrs as $attr=>$type)
1263
                {
1264
                    // handle 64bit ints
1265
                    if ($type == SPH_ATTR_BIGINT)
1266
                    {
1267
                        $attrvals[$attr] = sphUnpackI64(substr($response, $p, 8)); $p += 8;
1268
                        continue;
1269
                    }
1270
1271
                    // handle floats
1272
                    if ($type == SPH_ATTR_FLOAT)
1273
                    {
1274
                        list(,$uval) = unpack("N*", substr($response, $p, 4)); $p += 4;
1275
                        list(,$fval) = unpack("f*", pack("L", $uval));
1276
                        $attrvals[$attr] = $fval;
1277
                        continue;
1278
                    }
1279
1280
                    // handle everything else as unsigned ints
1281
                    list(,$val) = unpack("N*", substr($response, $p, 4)); $p += 4;
1282
                    if ($type & SPH_ATTR_MULTI)
1283
                    {
1284
                        $attrvals[$attr] = array();
1285
                        $nvalues = $val;
1286
                        while ($nvalues-- > 0 && $p < $max)
1287
                        {
1288
                            list(,$val) = unpack("N*", substr($response, $p, 4)); $p += 4;
1289
                            $attrvals[$attr][] = sphFixUint($val);
1290
                        }
1291
                    } else if ($type == SPH_ATTR_STRING)
1292
                    {
1293
                        $attrvals[$attr] = substr($response, $p, $val);
1294
                        $p += $val;
1295
                    } else
1296
                    {
1297
                        $attrvals[$attr] = sphFixUint($val);
1298
                    }
1299
                }
1300
1301
                if ($this->_arrayresult) {
1302
                                    $result["matches"][$idx]["attrs"] = $attrvals;
1303
                } else {
1304
                                    $result["matches"][$doc]["attrs"] = $attrvals;
1305
                }
1306
            }
1307
1308
            list ($total, $total_found, $msecs, $words) =
1309
                array_values(unpack("N*N*N*N*", substr($response, $p, 16)));
1310
            $result["total"] = sprintf("%u", $total);
1311
            $result["total_found"] = sprintf("%u", $total_found);
1312
            $result["time"] = sprintf("%.3f", $msecs / 1000);
1313
            $p += 16;
1314
1315
            while ($words-- > 0 && $p < $max)
1316
            {
1317
                list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1318
                $word = substr($response, $p, $len); $p += $len;
1319
                list ($docs, $hits) = array_values(unpack("N*N*", substr($response, $p, 8))); $p += 8;
1320
                $result["words"][$word] = array(
1321
                    "docs"=>sprintf("%u", $docs),
1322
                    "hits"=>sprintf("%u", $hits) );
1323
            }
1324
        }
1325
1326
        $this->_MBPop();
1327
        return $results;
1328
    }
1329
1330
    /////////////////////////////////////////////////////////////////////////////
1331
    // excerpts generation
1332
    /////////////////////////////////////////////////////////////////////////////
1333
1334
    /// connect to searchd server, and generate exceprts (snippets)
1335
    /// of given documents for given query. returns false on failure,
1336
    /// an array of snippets on success
1337
    public function BuildExcerpts($docs, $index, $words, $opts = array())
1338
    {
1339
        assert(is_array($docs));
1340
        assert(is_string($index));
1341
        assert(is_string($words));
1342
        assert(is_array($opts));
1343
1344
        $this->_MBPush();
1345
1346
        if (!($fp = $this->_Connect()))
1347
        {
1348
            $this->_MBPop();
1349
            return false;
1350
        }
1351
1352
        /////////////////
1353
        // fixup options
1354
        /////////////////
1355
1356
        if (!isset($opts["before_match"])) {
1357
            $opts["before_match"] = "<b>";
1358
        }
1359
        if (!isset($opts["after_match"])) {
1360
            $opts["after_match"] = "</b>";
1361
        }
1362
        if (!isset($opts["chunk_separator"])) {
1363
            $opts["chunk_separator"] = " ... ";
1364
        }
1365
        if (!isset($opts["limit"])) {
1366
            $opts["limit"] = 256;
1367
        }
1368
        if (!isset($opts["limit_passages"])) {
1369
            $opts["limit_passages"] = 0;
1370
        }
1371
        if (!isset($opts["limit_words"])) {
1372
            $opts["limit_words"] = 0;
1373
        }
1374
        if (!isset($opts["around"])) {
1375
            $opts["around"] = 5;
1376
        }
1377
        if (!isset($opts["exact_phrase"])) {
1378
            $opts["exact_phrase"] = false;
1379
        }
1380
        if (!isset($opts["single_passage"])) {
1381
            $opts["single_passage"] = false;
1382
        }
1383
        if (!isset($opts["use_boundaries"])) {
1384
            $opts["use_boundaries"] = false;
1385
        }
1386
        if (!isset($opts["weight_order"])) {
1387
            $opts["weight_order"] = false;
1388
        }
1389
        if (!isset($opts["query_mode"])) {
1390
            $opts["query_mode"] = false;
1391
        }
1392
        if (!isset($opts["force_all_words"])) {
1393
            $opts["force_all_words"] = false;
1394
        }
1395
        if (!isset($opts["start_passage_id"])) {
1396
            $opts["start_passage_id"] = 1;
1397
        }
1398
        if (!isset($opts["load_files"])) {
1399
            $opts["load_files"] = false;
1400
        }
1401
        if (!isset($opts["html_strip_mode"])) {
1402
            $opts["html_strip_mode"] = "index";
1403
        }
1404
        if (!isset($opts["allow_empty"])) {
1405
            $opts["allow_empty"] = false;
1406
        }
1407
        if (!isset($opts["passage_boundary"])) {
1408
            $opts["passage_boundary"] = "none";
1409
        }
1410
        if (!isset($opts["emit_zones"])) {
1411
            $opts["emit_zones"] = false;
1412
        }
1413
1414
        /////////////////
1415
        // build request
1416
        /////////////////
1417
1418
        // v.1.2 req
1419
        $flags = 1; // remove spaces
1420
        if ($opts["exact_phrase"]) {
1421
            $flags |= 2;
1422
        }
1423
        if ($opts["single_passage"]) {
1424
            $flags |= 4;
1425
        }
1426
        if ($opts["use_boundaries"]) {
1427
            $flags |= 8;
1428
        }
1429
        if ($opts["weight_order"]) {
1430
            $flags |= 16;
1431
        }
1432
        if ($opts["query_mode"]) {
1433
            $flags |= 32;
1434
        }
1435
        if ($opts["force_all_words"]) {
1436
            $flags |= 64;
1437
        }
1438
        if ($opts["load_files"]) {
1439
            $flags |= 128;
1440
        }
1441
        if ($opts["allow_empty"]) {
1442
            $flags |= 256;
1443
        }
1444
        if ($opts["emit_zones"]) {
1445
            $flags |= 512;
1446
        }
1447
        $req = pack("NN", 0, $flags); // mode=0, flags=$flags
1448
        $req .= pack("N", strlen($index)).$index; // req index
1449
        $req .= pack("N", strlen($words)).$words; // req words
1450
1451
        // options
1452
        $req .= pack("N", strlen($opts["before_match"])).$opts["before_match"];
1453
        $req .= pack("N", strlen($opts["after_match"])).$opts["after_match"];
1454
        $req .= pack("N", strlen($opts["chunk_separator"])).$opts["chunk_separator"];
1455
        $req .= pack("NN", (int) $opts["limit"], (int) $opts["around"]);
1456
        $req .= pack("NNN", (int) $opts["limit_passages"], (int) $opts["limit_words"], (int) $opts["start_passage_id"]); // v.1.2
1457
        $req .= pack("N", strlen($opts["html_strip_mode"])).$opts["html_strip_mode"];
1458
        $req .= pack("N", strlen($opts["passage_boundary"])).$opts["passage_boundary"];
1459
1460
        // documents
1461
        $req .= pack("N", count($docs));
1462
        foreach ($docs as $doc)
1463
        {
1464
            assert(is_string($doc));
1465
            $req .= pack("N", strlen($doc)).$doc;
1466
        }
1467
1468
        ////////////////////////////
1469
        // send query, get response
1470
        ////////////////////////////
1471
1472
        $len = strlen($req);
1473
        $req = pack("nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len).$req; // add header
1474
        if (!($this->_Send($fp, $req, $len + 8)) ||
1475
             !($response = $this->_GetResponse($fp, VER_COMMAND_EXCERPT)))
1476
        {
1477
            $this->_MBPop();
1478
            return false;
1479
        }
1480
1481
        //////////////////
1482
        // parse response
1483
        //////////////////
1484
1485
        $pos = 0;
1486
        $res = array();
1487
        $rlen = strlen($response);
1488
        for ($i = 0; $i < count($docs); $i++)
1489
        {
1490
            list(,$len) = unpack("N*", substr($response, $pos, 4));
1491
            $pos += 4;
1492
1493
            if ($pos + $len > $rlen)
1494
            {
1495
                $this->_error = "incomplete reply";
1496
                $this->_MBPop();
1497
                return false;
1498
            }
1499
            $res[] = $len ? substr ($response, $pos, $len) : "";
1500
            $pos += $len;
1501
        }
1502
1503
        $this->_MBPop();
1504
        return $res;
1505
    }
1506
1507
1508
    /////////////////////////////////////////////////////////////////////////////
1509
    // keyword generation
1510
    /////////////////////////////////////////////////////////////////////////////
1511
1512
    /// connect to searchd server, and generate keyword list for a given query
1513
    /// returns false on failure,
1514
    /// an array of words on success
1515
    public function BuildKeywords($query, $index, $hits)
1516
    {
1517
        assert(is_string($query));
1518
        assert(is_string($index));
1519
        assert(is_bool($hits));
1520
1521
        $this->_MBPush();
1522
1523
        if (!($fp = $this->_Connect()))
1524
        {
1525
            $this->_MBPop();
1526
            return false;
1527
        }
1528
1529
        /////////////////
1530
        // build request
1531
        /////////////////
1532
1533
        // v.1.0 req
1534
        $req  = pack("N", strlen($query)).$query; // req query
1535
        $req .= pack("N", strlen($index)).$index; // req index
1536
        $req .= pack("N", (int) $hits);
1537
1538
        ////////////////////////////
1539
        // send query, get response
1540
        ////////////////////////////
1541
1542
        $len = strlen($req);
1543
        $req = pack("nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len).$req; // add header
1544
        if (!($this->_Send($fp, $req, $len + 8)) ||
1545
             !($response = $this->_GetResponse($fp, VER_COMMAND_KEYWORDS)))
1546
        {
1547
            $this->_MBPop();
1548
            return false;
1549
        }
1550
1551
        //////////////////
1552
        // parse response
1553
        //////////////////
1554
1555
        $pos = 0;
1556
        $res = array();
1557
        $rlen = strlen($response);
1558
        list(,$nwords) = unpack("N*", substr($response, $pos, 4));
1559
        $pos += 4;
1560
        for ($i = 0; $i < $nwords; $i++)
1561
        {
1562
            list(,$len) = unpack("N*", substr($response, $pos, 4)); $pos += 4;
1563
            $tokenized = $len ? substr ($response, $pos, $len) : "";
1564
            $pos += $len;
1565
1566
            list(,$len) = unpack("N*", substr($response, $pos, 4)); $pos += 4;
1567
            $normalized = $len ? substr ($response, $pos, $len) : "";
1568
            $pos += $len;
1569
1570
            $res[] = array("tokenized"=>$tokenized, "normalized"=>$normalized);
1571
1572
            if ($hits)
1573
            {
1574
                list($ndocs, $nhits) = array_values(unpack("N*N*", substr($response, $pos, 8)));
1575
                $pos += 8;
1576
                $res [$i]["docs"] = $ndocs;
1577
                $res [$i]["hits"] = $nhits;
1578
            }
1579
1580
            if ($pos > $rlen)
1581
            {
1582
                $this->_error = "incomplete reply";
1583
                $this->_MBPop();
1584
                return false;
1585
            }
1586
        }
1587
1588
        $this->_MBPop();
1589
        return $res;
1590
    }
1591
1592
    public function EscapeString($string)
1593
    {
1594
        $from = array('\\', '(', ')', '|', '-', '!', '@', '~', '"', '&', '/', '^', '$', '=');
1595
        $to   = array('\\\\', '\(', '\)', '\|', '\-', '\!', '\@', '\~', '\"', '\&', '\/', '\^', '\$', '\=');
1596
1597
        return str_replace($from, $to, $string);
1598
    }
1599
1600
    /////////////////////////////////////////////////////////////////////////////
1601
    // attribute updates
1602
    /////////////////////////////////////////////////////////////////////////////
1603
1604
    /// batch update given attributes in given rows in given indexes
1605
    /// returns amount of updated documents (0 or more) on success, or -1 on failure
1606
    public function UpdateAttributes($index, $attrs, $values, $mva = false)
1607
    {
1608
        // verify everything
1609
        assert(is_string($index));
1610
        assert(is_bool($mva));
1611
1612
        assert(is_array($attrs));
1613
        foreach ($attrs as $attr) {
1614
                    assert(is_string($attr));
1615
        }
1616
1617
        assert(is_array($values));
1618
        foreach ($values as $id=>$entry)
1619
        {
1620
            assert(is_numeric($id));
1621
            assert(is_array($entry));
1622
            assert(count($entry) == count($attrs));
1623
            foreach ($entry as $v)
1624
            {
1625
                if ($mva)
1626
                {
1627
                    assert(is_array($v));
1628
                    foreach ($v as $vv) {
1629
                                            assert(is_int($vv));
1630
                    }
1631
                } else {
1632
                                    assert(is_int($v));
1633
                }
1634
            }
1635
        }
1636
1637
        // build request
1638
        $this->_MBPush();
1639
        $req = pack("N", strlen($index)).$index;
1640
1641
        $req .= pack("N", count($attrs));
1642
        foreach ($attrs as $attr)
1643
        {
1644
            $req .= pack("N", strlen($attr)).$attr;
1645
            $req .= pack("N", $mva ? 1 : 0);
1646
        }
1647
1648
        $req .= pack("N", count($values));
1649
        foreach ($values as $id=>$entry)
1650
        {
1651
            $req .= sphPackU64($id);
1652
            foreach ($entry as $v)
1653
            {
1654
                $req .= pack("N", $mva ? count($v) : $v);
1655
                if ($mva) {
1656
                                    foreach ($v as $vv) {
1657
                                                            $req .= pack("N", $vv);
1658
                                    }
1659
                }
1660
            }
1661
        }
1662
1663
        // connect, send query, get response
1664
        if (!($fp = $this->_Connect()))
1665
        {
1666
            $this->_MBPop();
1667
            return -1;
1668
        }
1669
1670
        $len = strlen($req);
1671
        $req = pack("nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len).$req; // add header
1672
        if (!$this->_Send($fp, $req, $len + 8))
1673
        {
1674
            $this->_MBPop();
1675
            return -1;
1676
        }
1677
1678
        if (!($response = $this->_GetResponse($fp, VER_COMMAND_UPDATE)))
1679
        {
1680
            $this->_MBPop();
1681
            return -1;
1682
        }
1683
1684
        // parse response
1685
        list(,$updated) = unpack("N*", substr($response, 0, 4));
1686
        $this->_MBPop();
1687
        return $updated;
1688
    }
1689
1690
    /////////////////////////////////////////////////////////////////////////////
1691
    // persistent connections
1692
    /////////////////////////////////////////////////////////////////////////////
1693
1694
    public function Open()
1695
    {
1696
        if ($this->_socket !== false)
1697
        {
1698
            $this->_error = 'already connected';
1699
            return false;
1700
        }
1701
        if (!$fp = $this->_Connect()) {
1702
                    return false;
1703
        }
1704
1705
        // command, command version = 0, body length = 4, body = 1
1706
        $req = pack("nnNN", SEARCHD_COMMAND_PERSIST, 0, 4, 1);
1707
        if (!$this->_Send($fp, $req, 12)) {
1708
                    return false;
1709
        }
1710
1711
        $this->_socket = $fp;
1712
        return true;
1713
    }
1714
1715
    public function Close()
1716
    {
1717
        if ($this->_socket === false)
1718
        {
1719
            $this->_error = 'not connected';
1720
            return false;
1721
        }
1722
1723
        fclose($this->_socket);
1724
        $this->_socket = false;
1725
1726
        return true;
1727
    }
1728
1729
    //////////////////////////////////////////////////////////////////////////
1730
    // status
1731
    //////////////////////////////////////////////////////////////////////////
1732
1733
    public function Status()
1734
    {
1735
        $this->_MBPush();
1736
        if (!($fp = $this->_Connect()))
1737
        {
1738
            $this->_MBPop();
1739
            return false;
1740
        }
1741
1742
        $req = pack("nnNN", SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, 1); // len=4, body=1
1743
        if (!($this->_Send($fp, $req, 12)) ||
1744
             !($response = $this->_GetResponse($fp, VER_COMMAND_STATUS)))
1745
        {
1746
            $this->_MBPop();
1747
            return false;
1748
        }
1749
1750
        $res = substr($response, 4); // just ignore length, error handling, etc
1751
        $p = 0;
1752
        list ($rows, $cols) = array_values(unpack("N*N*", substr($response, $p, 8))); $p += 8;
1753
1754
        $res = array();
1755
        for ($i = 0; $i < $rows; $i++) {
1756
                    for ($j = 0;
1757
        }
0 ignored issues
show
A parse error occurred: Syntax error, unexpected '}', expecting ';' on line 1757 at column 8
Loading history...
1758
        $j < $cols; $j++)
1759
        {
1760
            list(,$len) = unpack("N*", substr($response, $p, 4)); $p += 4;
1761
            $res[$i][] = substr($response, $p, $len); $p += $len;
1762
        }
1763
1764
        $this->_MBPop();
1765
        return $res;
1766
    }
1767
1768
    //////////////////////////////////////////////////////////////////////////
1769
    // flush
1770
    //////////////////////////////////////////////////////////////////////////
1771
1772
    public function FlushAttributes()
1773
    {
1774
        $this->_MBPush();
1775
        if (!($fp = $this->_Connect()))
1776
        {
1777
            $this->_MBPop();
1778
            return -1;
1779
        }
1780
1781
        $req = pack("nnN", SEARCHD_COMMAND_FLUSHATTRS, VER_COMMAND_FLUSHATTRS, 0); // len=0
1782
        if (!($this->_Send($fp, $req, 8)) ||
1783
             !($response = $this->_GetResponse($fp, VER_COMMAND_FLUSHATTRS)))
1784
        {
1785
            $this->_MBPop();
1786
            return -1;
1787
        }
1788
1789
        $tag = -1;
1790
        if (strlen($response) == 4) {
1791
                    list(,$tag) = unpack("N*", $response);
1792
        } else {
1793
                    $this->_error = "unexpected response length";
1794
        }
1795
1796
        $this->_MBPop();
1797
        return $tag;
1798
    }
1799
}
1800
1801
//
1802
// $Id: sphinxapi.php 2758 2011-04-04 11:10:44Z kevg $
1803
//
1804