Completed
Push — master ( 1873ca...4f9992 )
by Peter
02:40
created
src/Sphinx/Client.php 1 patch
Indentation   +2007 added lines, -2007 removed lines patch added patch discarded remove patch
@@ -1,7 +1,7 @@  discard block
 block discarded – undo
1 1
 <?php
2 2
 /**
3
- * $Id$
4
- */
3
+	 * $Id$
4
+	 */
5 5
 
6 6
 /**
7 7
  * Copyright (c) 2001-2015, Andrew Aksyonoff
@@ -37,2009 +37,2009 @@  discard block
 block discarded – undo
37 37
  */
38 38
 class Client
39 39
 {
40
-    /**
41
-     * Searchd host
42
-     *
43
-     * @var string
44
-     */
45
-    protected $host = 'localhost';
46
-
47
-    /**
48
-     * Searchd port
49
-     *
50
-     * @var int
51
-     */
52
-    protected $port = 9312;
53
-
54
-    /**
55
-     * How many records to seek from result-set start
56
-     *
57
-     * @var int
58
-     */
59
-    protected $offset = 0;
60
-
61
-    /**
62
-     * How many records to return from result-set starting at offset
63
-     *
64
-     * @var int
65
-     */
66
-    protected $limit = 20;
67
-
68
-    /**
69
-     * Query matching mode
70
-     *
71
-     * @var int
72
-     */
73
-    protected $mode = self::MATCH_EXTENDED2;
74
-
75
-    /**
76
-     * Per-field weights (default is 1 for all fields)
77
-     *
78
-     * @var array
79
-     */
80
-    protected $weights = array();
81
-
82
-    /**
83
-     * Match sorting mode
84
-     *
85
-     * @var int
86
-     */
87
-    protected $sort = self::SORT_RELEVANCE;
88
-
89
-    /**
90
-     * Attribute to sort by
91
-     *
92
-     * @var string
93
-     */
94
-    protected $sort_by = '';
95
-
96
-    /**
97
-     * Min ID to match (0 means no limit)
98
-     *
99
-     * @var int
100
-     */
101
-    protected $min_id = 0;
102
-
103
-    /**
104
-     * Max ID to match (0 means no limit)
105
-     *
106
-     * @var int
107
-     */
108
-    protected $max_id = 0;
109
-
110
-    /**
111
-     * Search filters
112
-     *
113
-     * @var array
114
-     */
115
-    protected $filters = array();
116
-
117
-    /**
118
-     * Group-by attribute name
119
-     *
120
-     * @var string
121
-     */
122
-    protected $group_by = '';
123
-
124
-    /**
125
-     * Group-by function (to pre-process group-by attribute value with)
126
-     *
127
-     * @var int
128
-     */
129
-    protected $group_func = self::GROUP_BY_DAY;
130
-
131
-    /**
132
-     * Group-by sorting clause (to sort groups in result set with)
133
-     *
134
-     * @var string
135
-     */
136
-    protected $group_sort = '@group desc';
137
-
138
-    /**
139
-     * Group-by count-distinct attribute
140
-     *
141
-     * @var string
142
-     */
143
-    protected $group_distinct = '';
144
-
145
-    /**
146
-     * Max matches to retrieve
147
-     *
148
-     * @var int
149
-     */
150
-    protected $max_matches = 1000;
151
-
152
-    /**
153
-     * Cutoff to stop searching at
154
-     *
155
-     * @var int
156
-     */
157
-    protected $cutoff = 0;
158
-
159
-    /**
160
-     * Distributed retries count
161
-     *
162
-     * @var int
163
-     */
164
-    protected $retry_count = 0;
165
-
166
-    /**
167
-     * Distributed retries delay
168
-     *
169
-     * @var int
170
-     */
171
-    protected $retry_delay = 0;
172
-
173
-    /**
174
-     * Geographical anchor point
175
-     *
176
-     * @var array
177
-     */
178
-    protected $anchor = array();
179
-
180
-    /**
181
-     * Per-index weights
182
-     *
183
-     * @var array
184
-     */
185
-    protected $index_weights = array();
186
-
187
-    /**
188
-     * Ranking mode
189
-     *
190
-     * @var int
191
-     */
192
-    protected $ranker = self::RANK_PROXIMITY_BM25;
193
-
194
-    /**
195
-     * Ranking mode expression (for self::RANK_EXPR)
196
-     *
197
-     * @var string
198
-     */
199
-    protected $rank_expr = '';
200
-
201
-    /**
202
-     * Max query time, milliseconds (0 means no limit)
203
-     *
204
-     * @var int
205
-     */
206
-    protected $max_query_time = 0;
207
-
208
-    /**
209
-     * Per-field-name weights
210
-     *
211
-     * @var array
212
-     */
213
-    protected $field_weights = array();
214
-
215
-    /**
216
-     * Per-query attribute values overrides
217
-     *
218
-     * @var array
219
-     */
220
-    protected $overrides = array();
221
-
222
-    /**
223
-     * Select-list (attributes or expressions, with optional aliases)
224
-     *
225
-     * @var string
226
-     */
227
-    protected $select = '*';
228
-
229
-    /**
230
-     * Per-query various flags
231
-     *
232
-     * @var int
233
-     */
234
-    protected $query_flags = 0;
235
-
236
-    /**
237
-     * Per-query max_predicted_time
238
-     *
239
-     * @var int
240
-     */
241
-    protected $predicted_time = 0;
242
-
243
-    /**
244
-     * Outer match sort by
245
-     *
246
-     * @var string
247
-     */
248
-    protected $outer_order_by = '';
249
-
250
-    /**
251
-     * Outer offset
252
-     *
253
-     * @var int
254
-     */
255
-    protected $outer_offset = 0;
256
-
257
-    /**
258
-     * Outer limit
259
-     *
260
-     * @var int
261
-     */
262
-    protected $outer_limit = 0;
263
-
264
-    /**
265
-     * @var bool
266
-     */
267
-    protected $has_outer = false;
268
-
269
-    /**
270
-     * Last error message
271
-     *
272
-     * @var string
273
-     */
274
-    protected $error = '';
275
-
276
-    /**
277
-     * Last warning message
278
-     *
279
-     * @var string
280
-     */
281
-    protected $warning = '';
282
-
283
-    /**
284
-     * Connection error vs remote error flag
285
-     *
286
-     * @var bool
287
-     */
288
-    protected $conn_error = false;
289
-
290
-    /**
291
-     * Requests array for multi-query
292
-     *
293
-     * @var array
294
-     */
295
-    protected $reqs = array();
296
-
297
-    /**
298
-     * Stored mbstring encoding
299
-     *
300
-     * @var string
301
-     */
302
-    protected $mbenc = '';
303
-
304
-    /**
305
-     * Whether $result['matches'] should be a hash or an array
306
-     *
307
-     * @var bool
308
-     */
309
-    protected $array_result = false;
310
-
311
-    /**
312
-     * Connect timeout
313
-     *
314
-     * @var int|float
315
-     */
316
-    protected $timeout = 0;
317
-
318
-    /**
319
-     * @var string
320
-     */
321
-    protected $path = '';
322
-
323
-    /**
324
-     * @var resource|bool
325
-     */
326
-    protected $socket = false;
327
-
328
-    // known searchd commands
329
-    const SEARCHD_COMMAND_SEARCH      = 0;
330
-    const SEARCHD_COMMAND_EXCERPT     = 1;
331
-    const SEARCHD_COMMAND_UPDATE      = 2;
332
-    const SEARCHD_COMMAND_KEYWORDS    = 3;
333
-    const SEARCHD_COMMAND_PERSIST     = 4;
334
-    const SEARCHD_COMMAND_STATUS      = 5;
335
-    const SEARCHD_COMMAND_FLUSH_ATTRS = 7;
336
-
337
-    // current client-side command implementation versions
338
-    const VER_COMMAND_SEARCH      = 0x11E;
339
-    const VER_COMMAND_EXCERPT     = 0x104;
340
-    const VER_COMMAND_UPDATE      = 0x103;
341
-    const VER_COMMAND_KEYWORDS    = 0x100;
342
-    const VER_COMMAND_STATUS      = 0x101;
343
-    const VER_COMMAND_QUERY       = 0x100;
344
-    const VER_COMMAND_FLUSH_ATTRS = 0x100;
345
-
346
-    // known searchd status codes
347
-    const SEARCHD_OK      = 0;
348
-    const SEARCHD_ERROR   = 1;
349
-    const SEARCHD_RETRY   = 2;
350
-    const SEARCHD_WARNING = 3;
351
-
352
-    // known match modes
353
-    const MATCH_ALL        = 0;
354
-    const MATCH_ANY        = 1;
355
-    const MATCH_PHRASE     = 2;
356
-    const MATCH_BOOLEAN    = 3;
357
-    const MATCH_EXTENDED   = 4;
358
-    const MATCH_FULL_SCAN  = 5;
359
-    const MATCH_EXTENDED2  = 6; // extended engine V2 (TEMPORARY, WILL BE REMOVED)
360
-
361
-    // known ranking modes (ext2 only)
362
-    const RANK_PROXIMITY_BM25  = 0; // default mode, phrase proximity major factor and BM25 minor one
363
-    const RANK_BM25            = 1; // statistical mode, BM25 ranking only (faster but worse quality)
364
-    const RANK_NONE            = 2; // no ranking, all matches get a weight of 1
365
-    const RANK_WORD_COUNT      = 3; // simple word-count weighting, rank is a weighted sum of per-field keyword
366
-                                    // occurrence counts
367
-    const RANK_PROXIMITY       = 4;
368
-    const RANK_MATCH_ANY       = 5;
369
-    const RANK_FIELD_MASK      = 6;
370
-    const RANK_SPH04           = 7;
371
-    const RANK_EXPR            = 8;
372
-    const RANK_TOTAL           = 9;
373
-
374
-    // known sort modes
375
-    const SORT_RELEVANCE     = 0;
376
-    const SORT_ATTR_DESC     = 1;
377
-    const SORT_ATTR_ASC      = 2;
378
-    const SORT_TIME_SEGMENTS = 3;
379
-    const SORT_EXTENDED      = 4;
380
-    const SORT_EXPR          = 5;
381
-
382
-    // known filter types
383
-    const FILTER_VALUES      = 0;
384
-    const FILTER_RANGE       = 1;
385
-    const FILTER_FLOAT_RANGE = 2;
386
-    const FILTER_STRING      = 3;
387
-
388
-    // known attribute types
389
-    const ATTR_INTEGER   = 1;
390
-    const ATTR_TIMESTAMP = 2;
391
-    const ATTR_ORDINAL   = 3;
392
-    const ATTR_BOOL      = 4;
393
-    const ATTR_FLOAT     = 5;
394
-    const ATTR_BIGINT    = 6;
395
-    const ATTR_STRING    = 7;
396
-    const ATTR_FACTORS   = 1001;
397
-    const ATTR_MULTI     = 0x40000001;
398
-    const ATTR_MULTI64   = 0x40000002;
399
-
400
-    // known grouping functions
401
-    const GROUP_BY_DAY       = 0;
402
-    const GROUP_BY_WEEK      = 1;
403
-    const GROUP_BY_MONTH     = 2;
404
-    const GROUP_BY_YEAR      = 3;
405
-    const GROUP_BY_ATTR      = 4;
406
-    const GROUP_BY_ATTR_PAIR = 5;
407
-
408
-    /////////////////////////////////////////////////////////////////////////////
409
-    // common stuff
410
-    /////////////////////////////////////////////////////////////////////////////
411
-
412
-    public function __construct()
413
-    {
414
-        // default idf=tfidf_normalized
415
-        $this->query_flags = setBit(0, 6, true);
416
-    }
417
-
418
-    public function __destruct()
419
-    {
420
-        if ($this->socket !== false) {
421
-            fclose($this->socket);
422
-        }
423
-    }
424
-
425
-    /**
426
-     * @return string
427
-     */
428
-    public function getLastError()
429
-    {
430
-        return $this->error;
431
-    }
432
-
433
-    /**
434
-     * @return string
435
-     */
436
-    public function getLastWarning()
437
-    {
438
-        return $this->warning;
439
-    }
440
-
441
-    /**
442
-     * Get last error flag (to tell network connection errors from searchd errors or broken responses)
443
-     *
444
-     * @return bool
445
-     */
446
-    public function isConnectError()
447
-    {
448
-        return $this->conn_error;
449
-    }
450
-
451
-    /**
452
-     * Set searchd host name and port
453
-     *
454
-     * @param string $host
455
-     * @param int $port
456
-     */
457
-    public function setServer($host, $port = 0)
458
-    {
459
-        assert(is_string($host));
460
-        if ($host[0] == '/') {
461
-            $this->path = 'unix://' . $host;
462
-            return;
463
-        }
464
-        if (substr($host, 0, 7) == 'unix://') {
465
-            $this->path = $host;
466
-            return;
467
-        }
468
-
469
-        $this->host = $host;
470
-        $port = intval($port);
471
-        assert(0 <= $port && $port < 65536);
472
-        $this->port = $port == 0 ? 9312 : $port;
473
-        $this->path = '';
474
-    }
475
-
476
-    /**
477
-     * Set server connection timeout (0 to remove)
478
-     *
479
-     * @param int|float|string $timeout
480
-     */
481
-    public function setConnectTimeout($timeout)
482
-    {
483
-        assert(is_numeric($timeout));
484
-        $this->timeout = $timeout;
485
-    }
486
-
487
-    /**
488
-     * @param resource $handle
489
-     * @param string $data
490
-     * @param int $length
491
-     *
492
-     * @return bool
493
-     */
494
-    protected function send($handle, $data, $length)
495
-    {
496
-        if (feof($handle) || fwrite($handle, $data, $length) !== $length) {
497
-            $this->error = 'connection unexpectedly closed (timed out?)';
498
-            $this->conn_error = true;
499
-            return false;
500
-        }
501
-        return true;
502
-    }
503
-
504
-    /////////////////////////////////////////////////////////////////////////////
505
-
506
-    /**
507
-     * Enter mbstring workaround mode
508
-     */
509
-    protected function mbPush()
510
-    {
511
-        $this->mbenc = '';
512
-        if (ini_get('mbstring.func_overload') & 2) {
513
-            $this->mbenc = mb_internal_encoding();
514
-            mb_internal_encoding('latin1');
515
-        }
516
-    }
517
-
518
-    /**
519
-     * Leave mbstring workaround mode
520
-     */
521
-    protected function mbPop()
522
-    {
523
-        if ($this->mbenc) {
524
-            mb_internal_encoding($this->mbenc);
525
-        }
526
-    }
527
-
528
-    /**
529
-     * Connect to searchd server
530
-     *
531
-     * @return bool|resource
532
-     */
533
-    protected function connect()
534
-    {
535
-        if (is_resource($this->socket)) {
536
-            // we are in persistent connection mode, so we have a socket
537
-            // however, need to check whether it's still alive
538
-            if (!feof($this->socket)) {
539
-                return $this->socket;
540
-            }
541
-
542
-            // force reopen
543
-            $this->socket = false;
544
-        }
545
-
546
-        $errno = 0;
547
-        $errstr = '';
548
-        $this->conn_error = false;
549
-
550
-        if ($this->path) {
551
-            $host = $this->path;
552
-            $port = 0;
553
-        } else {
554
-            $host = $this->host;
555
-            $port = $this->port;
556
-        }
557
-
558
-        if ($this->timeout <= 0) {
559
-            $fp = @fsockopen($host, $port, $errno, $errstr);
560
-        } else {
561
-            $fp = @fsockopen($host, $port, $errno, $errstr, $this->timeout);
562
-        }
563
-
564
-        if (!is_resource($fp)) {
565
-            if ($this->path) {
566
-                $location = $this->path;
567
-            } else {
568
-                $location = "{$this->host}:{$this->port}";
569
-            }
570
-
571
-            $errstr = trim($errstr);
572
-            $this->error = "connection to $location failed (errno=$errno, msg=$errstr)";
573
-            $this->conn_error = true;
574
-            return false;
575
-        }
576
-
577
-        // send my version
578
-        // this is a subtle part. we must do it before (!) reading back from searchd.
579
-        // because otherwise under some conditions (reported on FreeBSD for instance)
580
-        // TCP stack could throttle write-write-read pattern because of Nagle.
581
-        if (!$this->send($fp, pack('N', 1), 4)) {
582
-            fclose($fp);
583
-            $this->error = 'failed to send client protocol version';
584
-            return false;
585
-        }
586
-
587
-        // check version
588
-        list(, $v) = unpack('N*', fread($fp, 4));
589
-        $v = (int)$v;
590
-        if ($v < 1) {
591
-            fclose($fp);
592
-            $this->error = "expected searchd protocol version 1+, got version '$v'";
593
-            return false;
594
-        }
595
-
596
-        return $fp;
597
-    }
598
-
599
-    /**
600
-     * Get and check response packet from searchd server
601
-     *
602
-     * @param resource $fp
603
-     * @param int $client_ver
604
-     *
605
-     * @return bool|string
606
-     */
607
-    protected function getResponse($fp, $client_ver)
608
-    {
609
-        $ver = 0;
610
-        $len = 0;
611
-        $status = -1;
612
-        $response = '';
613
-
614
-        $header = fread($fp, 8);
615
-        if (strlen($header) == 8) {
616
-            list($status, $ver, $len) = array_values(unpack('n2a/Nb', $header));
617
-            $left = $len;
618
-            while ($left > 0 && !feof($fp)) {
619
-                $chunk = fread($fp, min(8192, $left));
620
-                if ($chunk) {
621
-                    $response .= $chunk;
622
-                    $left -= strlen($chunk);
623
-                }
624
-            }
625
-        }
626
-
627
-        if ($this->socket === false) {
628
-            fclose($fp);
629
-        }
630
-
631
-        // check response
632
-        $read = strlen($response);
633
-        if (!$response || $read != $len) {
634
-            if ($len) {
635
-                $this->error = "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)";
636
-            } else {
637
-                $this->error = 'received zero-sized searchd response';
638
-            }
639
-            return false;
640
-        }
641
-
642
-        switch ($status) {
643
-            case self::SEARCHD_WARNING:
644
-                list(, $wlen) = unpack('N*', substr($response, 0, 4));
645
-                $this->warning = substr($response, 4, $wlen);
646
-                return substr($response, 4 + $wlen);
647
-            case self::SEARCHD_ERROR:
648
-                $this->error = 'searchd error: ' . substr($response, 4);
649
-                return false;
650
-            case self::SEARCHD_RETRY:
651
-                $this->error = 'temporary searchd error: ' . substr($response, 4);
652
-                return false;
653
-            case self::SEARCHD_OK:
654
-                if ($ver < $client_ver) { // check version
655
-                    $this->warning = sprintf(
656
-                        'searchd command v.%d.%d older than client\'s v.%d.%d, some options might not work',
657
-                        $ver >> 8,
658
-                        $ver & 0xff,
659
-                        $client_ver >> 8,
660
-                        $client_ver & 0xff
661
-                    );
662
-                }
663
-
664
-                return $response;
665
-            default:
666
-                $this->error = "unknown status code '$status'";
667
-                return false;
668
-        }
669
-    }
670
-
671
-    /////////////////////////////////////////////////////////////////////////////
672
-    // searching
673
-    /////////////////////////////////////////////////////////////////////////////
674
-
675
-    /**
676
-     * Set offset and count into result set, and optionally set max-matches and cutoff limits
677
-     *
678
-     * @param int $offset
679
-     * @param int $limit
680
-     * @param int $max
681
-     * @param int $cutoff
682
-     */
683
-    public function setLimits($offset, $limit, $max = 0, $cutoff = 0)
684
-    {
685
-        assert(is_int($offset));
686
-        assert(is_int($limit));
687
-        assert($offset >= 0);
688
-        assert($limit > 0);
689
-        assert($max >= 0);
690
-        $this->offset = $offset;
691
-        $this->limit = $limit;
692
-        if ($max > 0) {
693
-            $this->max_matches = $max;
694
-        }
695
-        if ($cutoff > 0) {
696
-            $this->cutoff = $cutoff;
697
-        }
698
-    }
699
-
700
-    /**
701
-     * Set maximum query time, in milliseconds, per-index, 0 means 'do not limit'
702
-     *
703
-     * @param int $max
704
-     */
705
-    public function setMaxQueryTime($max)
706
-    {
707
-        assert(is_int($max));
708
-        assert($max >= 0);
709
-        $this->max_query_time = $max;
710
-    }
711
-
712
-    /**
713
-     * Set matching mode
714
-     *
715
-     * @param int $mode
716
-     */
717
-    public function setMatchMode($mode)
718
-    {
719
-        trigger_error(
720
-            'DEPRECATED: Do not call this method or, even better, use SphinxQL instead of an API',
721
-            E_USER_DEPRECATED
722
-        );
723
-        assert(in_array($mode, array(
724
-            self::MATCH_ALL,
725
-            self::MATCH_ANY,
726
-            self::MATCH_PHRASE,
727
-            self::MATCH_BOOLEAN,
728
-            self::MATCH_EXTENDED,
729
-            self::MATCH_FULL_SCAN,
730
-            self::MATCH_EXTENDED2
731
-        )));
732
-        $this->mode = $mode;
733
-    }
734
-
735
-    /**
736
-     * Set ranking mode
737
-     *
738
-     * @param int $ranker
739
-     * @param string $rank_expr
740
-     */
741
-    public function setRankingMode($ranker, $rank_expr='')
742
-    {
743
-        assert($ranker === 0 || $ranker >= 1 && $ranker < self::RANK_TOTAL);
744
-        assert(is_string($rank_expr));
745
-        $this->ranker = $ranker;
746
-        $this->rank_expr = $rank_expr;
747
-    }
748
-
749
-    /**
750
-     * Set matches sorting mode
751
-     *
752
-     * @param int $mode
753
-     * @param string $sort_by
754
-     */
755
-    public function setSortMode($mode, $sort_by = '')
756
-    {
757
-        assert(in_array($mode, array(
758
-            self::SORT_RELEVANCE,
759
-            self::SORT_ATTR_DESC,
760
-            self::SORT_ATTR_ASC,
761
-            self::SORT_TIME_SEGMENTS,
762
-            self::SORT_EXTENDED,
763
-            self::SORT_EXPR
764
-        )));
765
-        assert(is_string($sort_by));
766
-        assert($mode == self::SORT_RELEVANCE || strlen($sort_by) > 0);
767
-
768
-        $this->sort = $mode;
769
-        $this->sort_by = $sort_by;
770
-    }
771
-
772
-    /**
773
-     * Bind per-field weights by order
774
-     *
775
-     * @deprecated use setFieldWeights() instead
776
-     */
777
-    public function setWeights()
778
-    {
779
-        throw new \RuntimeException('This method is now deprecated; please use setFieldWeights instead');
780
-    }
781
-
782
-    /**
783
-     * Bind per-field weights by name
784
-     *
785
-     * @param array $weights
786
-     */
787
-    public function setFieldWeights(array $weights)
788
-    {
789
-        foreach ($weights as $name => $weight) {
790
-            assert(is_string($name));
791
-            assert(is_int($weight));
792
-        }
793
-        $this->field_weights = $weights;
794
-    }
795
-
796
-    /**
797
-     * Bind per-index weights by name
798
-     *
799
-     * @param array $weights
800
-     */
801
-    public function setIndexWeights(array $weights)
802
-    {
803
-        foreach ($weights as $index => $weight) {
804
-            assert(is_string($index));
805
-            assert(is_int($weight));
806
-        }
807
-        $this->index_weights = $weights;
808
-    }
809
-
810
-    /**
811
-     * Set IDs range to match. Only match records if document ID is beetwen $min and $max (inclusive)
812
-     *
813
-     * @param int $min
814
-     * @param int $max
815
-     */
816
-    public function setIDRange($min, $max)
817
-    {
818
-        assert(is_numeric($min));
819
-        assert(is_numeric($max));
820
-        assert($min <= $max);
821
-
822
-        $this->min_id = $min;
823
-        $this->max_id = $max;
824
-    }
825
-
826
-    /**
827
-     * Set values set filter. Only match records where $attribute value is in given set
828
-     *
829
-     * @param string $attribute
830
-     * @param array $values
831
-     * @param bool $exclude
832
-     */
833
-    public function setFilter($attribute, array $values, $exclude = false)
834
-    {
835
-        assert(is_string($attribute));
836
-        assert(count($values));
837
-
838
-        foreach ($values as $value) {
839
-            assert(is_numeric($value));
840
-        }
841
-
842
-        $this->filters[] = array(
843
-            'type' => self::FILTER_VALUES,
844
-            'attr' => $attribute,
845
-            'exclude' => $exclude,
846
-            'values' => $values
847
-        );
848
-    }
849
-
850
-    /**
851
-     * Set string filter
852
-     * Only match records where $attribute value is equal
853
-     *
854
-     * @param string $attribute
855
-     * @param string $value
856
-     * @param bool $exclude
857
-     */
858
-    public function setFilterString($attribute, $value, $exclude = false)
859
-    {
860
-        assert(is_string($attribute));
861
-        assert(is_string($value));
862
-        $this->filters[] = array(
863
-            'type' => self::FILTER_STRING,
864
-            'attr' => $attribute,
865
-            'exclude' => $exclude,
866
-            'value' => $value
867
-        );
868
-    }    
869
-
870
-    /**
871
-     * Set range filter
872
-     * Only match records if $attribute value is beetwen $min and $max (inclusive)
873
-     *
874
-     * @param string $attribute
875
-     * @param int $min
876
-     * @param int $max
877
-     * @param bool $exclude
878
-     */
879
-    public function setFilterRange($attribute, $min, $max, $exclude = false)
880
-    {
881
-        assert(is_string($attribute));
882
-        assert(is_numeric($min));
883
-        assert(is_numeric($max));
884
-        assert($min <= $max);
885
-
886
-        $this->filters[] = array(
887
-            'type' => self::FILTER_RANGE,
888
-            'attr' => $attribute,
889
-            'exclude' => $exclude,
890
-            'min' => $min,
891
-            'max' => $max
892
-        );
893
-    }
894
-
895
-    /**
896
-     * Set float range filter
897
-     * Only match records if $attribute value is beetwen $min and $max (inclusive)
898
-     *
899
-     * @param string $attribute
900
-     * @param float $min
901
-     * @param float $max
902
-     * @param bool $exclude
903
-     */
904
-    public function setFilterFloatRange($attribute, $min, $max, $exclude = false)
905
-    {
906
-        assert(is_string($attribute));
907
-        assert(is_float($min));
908
-        assert(is_float($max));
909
-        assert($min <= $max);
910
-
911
-        $this->filters[] = array(
912
-            'type' => self::FILTER_FLOAT_RANGE,
913
-            'attr' => $attribute,
914
-            'exclude' => $exclude,
915
-            'min' => $min,
916
-            'max' => $max
917
-        );
918
-    }
919
-
920
-    /**
921
-     * Setup anchor point for geosphere distance calculations
922
-     * Required to use @geodist in filters and sorting
923
-     * Latitude and longitude must be in radians
924
-     *
925
-     * @param string $attr_lat
926
-     * @param string $attr_long
927
-     * @param float $lat
928
-     * @param float $long
929
-     */
930
-    public function setGeoAnchor($attr_lat, $attr_long, $lat, $long)
931
-    {
932
-        assert(is_string($attr_lat));
933
-        assert(is_string($attr_long));
934
-        assert(is_float($lat));
935
-        assert(is_float($long));
936
-
937
-        $this->anchor = array(
938
-            'attrlat' => $attr_lat,
939
-            'attrlong' => $attr_long,
940
-            'lat' => $lat,
941
-            'long' => $long
942
-        );
943
-    }
944
-
945
-    /**
946
-     * Set grouping attribute and function
947
-     *
948
-     * @param string $attribute
949
-     * @param int $func
950
-     * @param string $group_sort
951
-     */
952
-    public function setGroupBy($attribute, $func, $group_sort = '@group desc')
953
-    {
954
-        assert(is_string($attribute));
955
-        assert(is_string($group_sort));
956
-        assert(in_array($func, array(
957
-            self::GROUP_BY_DAY,
958
-            self::GROUP_BY_WEEK,
959
-            self::GROUP_BY_MONTH,
960
-            self::GROUP_BY_YEAR,
961
-            self::GROUP_BY_ATTR,
962
-            self::GROUP_BY_ATTR_PAIR
963
-        )));
964
-
965
-        $this->group_by = $attribute;
966
-        $this->group_func = $func;
967
-        $this->group_sort = $group_sort;
968
-    }
969
-
970
-    /**
971
-     * Set count-distinct attribute for group-by queries
972
-     *
973
-     * @param string $attribute
974
-     */
975
-    public function setGroupDistinct($attribute)
976
-    {
977
-        assert(is_string($attribute));
978
-        $this->group_distinct = $attribute;
979
-    }
980
-
981
-    /**
982
-     * Set distributed retries count and delay
983
-     *
984
-     * @param int $count
985
-     * @param int $delay
986
-     */
987
-    public function setRetries($count, $delay = 0)
988
-    {
989
-        assert(is_int($count) && $count >= 0);
990
-        assert(is_int($delay) && $delay >= 0);
991
-        $this->retry_count = $count;
992
-        $this->retry_delay = $delay;
993
-    }
994
-
995
-    /**
996
-     * Set result set format (hash or array; hash by default)
997
-     * PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs
998
-     *
999
-     * @param bool $array_result
1000
-     */
1001
-    public function setArrayResult($array_result)
1002
-    {
1003
-        assert(is_bool($array_result));
1004
-        $this->array_result = $array_result;
1005
-    }
1006
-
1007
-    /**
1008
-     * Set attribute values override
1009
-     * There can be only one override per attribute
1010
-     * $values must be a hash that maps document IDs to attribute values
1011
-     *
1012
-     * @deprecated Do not call this method. Use SphinxQL REMAP() function instead.
1013
-     *
1014
-     * @param string $attr_name
1015
-     * @param string $attr_type
1016
-     * @param array $values
1017
-     */
1018
-    public function setOverride($attr_name, $attr_type, array $values)
1019
-    {
1020
-        trigger_error(
1021
-            'DEPRECATED: Do not call this method. Use SphinxQL REMAP() function instead.',
1022
-            E_USER_DEPRECATED
1023
-        );
1024
-        assert(is_string($attr_name));
1025
-        assert(in_array($attr_type, array(
1026
-            self::ATTR_INTEGER,
1027
-            self::ATTR_TIMESTAMP,
1028
-            self::ATTR_BOOL,
1029
-            self::ATTR_FLOAT,
1030
-            self::ATTR_BIGINT
1031
-        )));
1032
-
1033
-        $this->overrides[$attr_name] = array(
1034
-            'attr' => $attr_name,
1035
-            'type' => $attr_type,
1036
-            'values' => $values
1037
-        );
1038
-    }
1039
-
1040
-    /**
1041
-     * Set select-list (attributes or expressions), SQL-like syntax
1042
-     *
1043
-     * @param string $select
1044
-     */
1045
-    public function setSelect($select)
1046
-    {
1047
-        assert(is_string($select));
1048
-        $this->select = $select;
1049
-    }
1050
-
1051
-    /**
1052
-     * @param string $flag_name
1053
-     * @param string|int $flag_value
1054
-     */
1055
-    public function setQueryFlag($flag_name, $flag_value)
1056
-    {
1057
-        $known_names = array(
1058
-            'reverse_scan',
1059
-            'sort_method',
1060
-            'max_predicted_time',
1061
-            'boolean_simplify',
1062
-            'idf',
1063
-            'global_idf',
1064
-            'low_priority'
1065
-        );
1066
-        $flags = array (
1067
-            'reverse_scan' => array(0, 1),
1068
-            'sort_method' => array('pq', 'kbuffer'),
1069
-            'max_predicted_time' => array(0),
1070
-            'boolean_simplify' => array(true, false),
1071
-            'idf' => array ('normalized', 'plain', 'tfidf_normalized', 'tfidf_unnormalized'),
1072
-            'global_idf' => array(true, false),
1073
-            'low_priority' => array(true, false)
1074
-        );
1075
-
1076
-        assert(isset($flag_name, $known_names));
1077
-        assert(
1078
-            in_array($flag_value, $flags[$flag_name], true) ||
1079
-            ($flag_name == 'max_predicted_time' && is_int($flag_value) && $flag_value >= 0)
1080
-        );
1081
-
1082
-        switch ($flag_name) {
1083
-            case 'reverse_scan':
1084
-                $this->query_flags = setBit($this->query_flags, 0, $flag_value == 1);
1085
-                break;
1086
-            case 'sort_method':
1087
-                $this->query_flags = setBit($this->query_flags, 1, $flag_value == 'kbuffer');
1088
-                break;
1089
-            case 'max_predicted_time':
1090
-                $this->query_flags = setBit($this->query_flags, 2, $flag_value > 0);
1091
-                $this->predicted_time = (int)$flag_value;
1092
-                break;
1093
-            case 'boolean_simplify':
1094
-                $this->query_flags = setBit($this->query_flags, 3, $flag_value);
1095
-                break;
1096
-            case 'idf':
1097
-                if ($flag_value == 'normalized' || $flag_value == 'plain') {
1098
-                    $this->query_flags = setBit($this->query_flags, 4, $flag_value == 'plain');
1099
-                }
1100
-                if ($flag_value == 'tfidf_normalized' || $flag_value == 'tfidf_unnormalized') {
1101
-                    $this->query_flags = setBit($this->query_flags, 6, $flag_value == 'tfidf_normalized');
1102
-                }
1103
-                break;
1104
-            case 'global_idf':
1105
-                $this->query_flags = setBit($this->query_flags, 5, $flag_value);
1106
-                break;
1107
-            case 'low_priority':
1108
-                $this->query_flags = setBit($this->query_flags, 8, $flag_value);
1109
-                break;
1110
-        }
1111
-    }
1112
-
1113
-    /**
1114
-     * Set outer order by parameters
1115
-     *
1116
-     * @param string $order_by
1117
-     * @param int $offset
1118
-     * @param int $limit
1119
-     */
1120
-    public function setOuterSelect($order_by, $offset, $limit)
1121
-    {
1122
-        assert(is_string($order_by));
1123
-        assert(is_int($offset));
1124
-        assert(is_int($limit));
1125
-        assert($offset >= 0);
1126
-        assert($limit > 0);
1127
-
1128
-        $this->outer_order_by = $order_by;
1129
-        $this->outer_offset = $offset;
1130
-        $this->outer_limit = $limit;
1131
-        $this->has_outer = true;
1132
-    }
1133
-
1134
-
1135
-    //////////////////////////////////////////////////////////////////////////////
1136
-
1137
-    /**
1138
-     * Clear all filters (for multi-queries)
1139
-     */
1140
-    public function resetFilters()
1141
-    {
1142
-        $this->filters = array();
1143
-        $this->anchor = array();
1144
-    }
1145
-
1146
-    /**
1147
-     * Clear groupby settings (for multi-queries)
1148
-     */
1149
-    public function resetGroupBy()
1150
-    {
1151
-        $this->group_by = '';
1152
-        $this->group_func = self::GROUP_BY_DAY;
1153
-        $this->group_sort = '@group desc';
1154
-        $this->group_distinct = '';
1155
-    }
1156
-
1157
-    /**
1158
-     * Clear all attribute value overrides (for multi-queries)
1159
-     */
1160
-    public function resetOverrides()
1161
-    {
1162
-        $this->overrides = array();
1163
-    }
1164
-
1165
-    public function resetQueryFlag()
1166
-    {
1167
-        $this->query_flags = setBit(0, 6, true); // default idf=tfidf_normalized
1168
-        $this->predicted_time = 0;
1169
-    }
1170
-
1171
-    public function resetOuterSelect()
1172
-    {
1173
-        $this->outer_order_by = '';
1174
-        $this->outer_offset = 0;
1175
-        $this->outer_limit = 0;
1176
-        $this->has_outer = false;
1177
-    }
1178
-
1179
-    //////////////////////////////////////////////////////////////////////////////
1180
-
1181
-    /**
1182
-     * Connect to searchd server, run given search query through given indexes, and return the search results
1183
-     *
1184
-     * @param string  $query
1185
-     * @param string $index
1186
-     * @param string $comment
1187
-     *
1188
-     * @return bool
1189
-     */
1190
-    public function query($query, $index = '*', $comment = '')
1191
-    {
1192
-        assert(empty($this->reqs));
1193
-
1194
-        $this->addQuery($query, $index, $comment);
1195
-        $results = $this->runQueries();
1196
-        $this->reqs = array(); // just in case it failed too early
1197
-
1198
-        if (!is_array($results)) {
1199
-            return false; // probably network error; error message should be already filled
1200
-        }
1201
-
1202
-        $this->error = $results[0]['error'];
1203
-        $this->warning = $results[0]['warning'];
1204
-
1205
-        if ($results[0]['status'] == self::SEARCHD_ERROR) {
1206
-            return false;
1207
-        } else {
1208
-            return $results[0];
1209
-        }
1210
-    }
1211
-
1212
-    /**
1213
-     * Helper to pack floats in network byte order
1214
-     *
1215
-     * @param float $float
1216
-     *
1217
-     * @return string
1218
-     */
1219
-    protected function packFloat($float)
1220
-    {
1221
-        $t1 = pack('f', $float); // machine order
1222
-        list(, $t2) = unpack('L*', $t1); // int in machine order
1223
-        return pack('N', $t2);
1224
-    }
1225
-
1226
-    /**
1227
-     * Add query to multi-query batch
1228
-     * Returns index into results array from RunQueries() call
1229
-     *
1230
-     * @param string $query
1231
-     * @param string $index
1232
-     * @param string $comment
1233
-     *
1234
-     * @return int
1235
-     */
1236
-    public function addQuery($query, $index = '*', $comment = '')
1237
-    {
1238
-        // mbstring workaround
1239
-        $this->mbPush();
1240
-
1241
-        // build request
1242
-        $req = pack('NNNNN', $this->query_flags, $this->offset, $this->limit, $this->mode, $this->ranker);
1243
-        if ($this->ranker == self::RANK_EXPR) {
1244
-            $req .= pack('N', strlen($this->rank_expr)) . $this->rank_expr;
1245
-        }
1246
-        $req .= pack('N', $this->sort); // (deprecated) sort mode
1247
-        $req .= pack('N', strlen($this->sort_by)) . $this->sort_by;
1248
-        $req .= pack('N', strlen($query)) . $query; // query itself
1249
-        $req .= pack('N', count($this->weights)); // weights
1250
-        foreach ($this->weights as $weight) {
1251
-            $req .= pack('N', (int)$weight);
1252
-        }
1253
-        $req .= pack('N', strlen($index)) . $index; // indexes
1254
-        $req .= pack('N', 1); // id64 range marker
1255
-        $req .= pack64IntUnsigned($this->min_id) . pack64IntUnsigned($this->max_id); // id64 range
1256
-
1257
-        // filters
1258
-        $req .= pack('N', count($this->filters));
1259
-        foreach ($this->filters as $filter) {
1260
-            $req .= pack('N', strlen($filter['attr'])) . $filter['attr'];
1261
-            $req .= pack('N', $filter['type']);
1262
-            switch ($filter['type']) {
1263
-                case self::FILTER_VALUES:
1264
-                    $req .= pack('N', count($filter['values']));
1265
-                    foreach ($filter['values'] as $value) {
1266
-                        $req .= pack64IntSigned($value);
1267
-                    }
1268
-                    break;
1269
-                case self::FILTER_RANGE:
1270
-                    $req .= pack64IntSigned($filter['min']) . pack64IntSigned($filter['max']);
1271
-                    break;
1272
-                case self::FILTER_FLOAT_RANGE:
1273
-                    $req .= $this->packFloat($filter['min']) . $this->packFloat($filter['max']);
1274
-                    break;
1275
-                case self::FILTER_STRING:
1276
-                    $req .= pack('N', strlen($filter['value'])) . $filter['value'];
1277
-                    break;
1278
-                default:
1279
-                    assert(0 && 'internal error: unhandled filter type');
1280
-            }
1281
-            $req .= pack('N', $filter['exclude']);
1282
-        }
1283
-
1284
-        // group-by clause, max-matches count, group-sort clause, cutoff count
1285
-        $req .= pack('NN', $this->group_func, strlen($this->group_by)) . $this->group_by;
1286
-        $req .= pack('N', $this->max_matches);
1287
-        $req .= pack('N', strlen($this->group_sort)) . $this->group_sort;
1288
-        $req .= pack('NNN', $this->cutoff, $this->retry_count, $this->retry_delay);
1289
-        $req .= pack('N', strlen($this->group_distinct)) . $this->group_distinct;
1290
-
1291
-        // anchor point
1292
-        if (empty($this->anchor)) {
1293
-            $req .= pack('N', 0);
1294
-        } else {
1295
-            $a =& $this->anchor;
1296
-            $req .= pack('N', 1);
1297
-            $req .= pack('N', strlen($a['attrlat'])) . $a['attrlat'];
1298
-            $req .= pack('N', strlen($a['attrlong'])) . $a['attrlong'];
1299
-            $req .= $this->packFloat($a['lat']) . $this->packFloat($a['long']);
1300
-        }
1301
-
1302
-        // per-index weights
1303
-        $req .= pack('N', count($this->index_weights));
1304
-        foreach ($this->index_weights as $idx => $weight) {
1305
-            $req .= pack('N', strlen($idx)) . $idx . pack('N', $weight);
1306
-        }
1307
-
1308
-        // max query time
1309
-        $req .= pack('N', $this->max_query_time);
1310
-
1311
-        // per-field weights
1312
-        $req .= pack('N', count($this->field_weights));
1313
-        foreach ($this->field_weights as $field => $weight) {
1314
-            $req .= pack('N', strlen($field)) . $field . pack('N', $weight);
1315
-        }
1316
-
1317
-        // comment
1318
-        $req .= pack('N', strlen($comment)) . $comment;
1319
-
1320
-        // attribute overrides
1321
-        $req .= pack('N', count($this->overrides));
1322
-        foreach ($this->overrides as $key => $entry) {
1323
-            $req .= pack('N', strlen($entry['attr'])) . $entry['attr'];
1324
-            $req .= pack('NN', $entry['type'], count($entry['values']));
1325
-            foreach ($entry['values'] as $id => $val) {
1326
-                assert(is_numeric($id));
1327
-                assert(is_numeric($val));
1328
-
1329
-                $req .= pack64IntUnsigned($id);
1330
-                switch ($entry['type']) {
1331
-                    case self::ATTR_FLOAT:
1332
-                        $req .= $this->packFloat($val);
1333
-                        break;
1334
-                    case self::ATTR_BIGINT:
1335
-                        $req .= pack64IntSigned($val);
1336
-                        break;
1337
-                    default:
1338
-                        $req .= pack('N', $val);
1339
-                        break;
1340
-                }
1341
-            }
1342
-        }
1343
-
1344
-        // select-list
1345
-        $req .= pack('N', strlen($this->select)) . $this->select;
1346
-
1347
-        // max_predicted_time
1348
-        if ($this->predicted_time > 0) {
1349
-            $req .= pack('N', (int)$this->predicted_time);
1350
-        }
1351
-
1352
-        $req .= pack('N', strlen($this->outer_order_by)) . $this->outer_order_by;
1353
-        $req .= pack('NN', $this->outer_offset, $this->outer_limit);
1354
-        if ($this->has_outer) {
1355
-            $req .= pack('N', 1);
1356
-        } else {
1357
-            $req .= pack('N', 0);
1358
-        }
1359
-
1360
-        // mbstring workaround
1361
-        $this->mbPop();
1362
-
1363
-        // store request to requests array
1364
-        $this->reqs[] = $req;
1365
-        return count($this->reqs) - 1;
1366
-    }
1367
-
1368
-    /**
1369
-     * Connect to searchd, run queries batch, and return an array of result sets
1370
-     *
1371
-     * @return array|bool
1372
-     */
1373
-    public function runQueries()
1374
-    {
1375
-        if (empty($this->reqs)) {
1376
-            $this->error = 'no queries defined, issue AddQuery() first';
1377
-            return false;
1378
-        }
1379
-
1380
-        // mbstring workaround
1381
-        $this->mbPush();
1382
-
1383
-        if (($fp = $this->connect()) === false) {
1384
-            $this->mbPop();
1385
-            return false;
1386
-        }
1387
-
1388
-        // send query, get response
1389
-        $nreqs = count($this->reqs);
1390
-        $req = join('', $this->reqs);
1391
-        $len = 8 + strlen($req);
1392
-        // add header
1393
-        $req = pack('nnNNN', self::SEARCHD_COMMAND_SEARCH, self::VER_COMMAND_SEARCH, $len, 0, $nreqs) . $req;
1394
-
1395
-        if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_SEARCH))) {
1396
-            $this->mbPop();
1397
-            return false;
1398
-        }
1399
-
1400
-        // query sent ok; we can reset reqs now
1401
-        $this->reqs = array();
1402
-
1403
-        // parse and return response
1404
-        return $this->parseSearchResponse($response, $nreqs);
1405
-    }
1406
-
1407
-    /**
1408
-     * Parse and return search query (or queries) response
1409
-     *
1410
-     * @param string $response
1411
-     * @param int $nreqs
1412
-     *
1413
-     * @return array
1414
-     */
1415
-    protected function parseSearchResponse($response, $nreqs)
1416
-    {
1417
-        $p = 0; // current position
1418
-        $max = strlen($response); // max position for checks, to protect against broken responses
1419
-
1420
-        $results = array();
1421
-        for ($ires = 0; $ires < $nreqs && $p < $max; $ires++) {
1422
-            $results[] = array();
1423
-            $result =& $results[$ires];
1424
-
1425
-            $result['error'] = '';
1426
-            $result['warning'] = '';
1427
-
1428
-            // extract status
1429
-            list(, $status) = unpack('N*', substr($response, $p, 4));
1430
-            $p += 4;
1431
-            $result['status'] = $status;
1432
-            if ($status != self::SEARCHD_OK) {
1433
-                list(, $len) = unpack('N*', substr($response, $p, 4));
1434
-                $p += 4;
1435
-                $message = substr($response, $p, $len);
1436
-                $p += $len;
1437
-
1438
-                if ($status == self::SEARCHD_WARNING) {
1439
-                    $result['warning'] = $message;
1440
-                } else {
1441
-                    $result['error'] = $message;
1442
-                    continue;
1443
-                }
1444
-            }
1445
-
1446
-            // read schema
1447
-            $fields = array();
1448
-            $attrs = array();
1449
-
1450
-            list(, $nfields) = unpack('N*', substr($response, $p, 4));
1451
-            $p += 4;
1452
-            while ($nfields --> 0 && $p < $max) {
1453
-                list(, $len) = unpack('N*', substr($response, $p, 4));
1454
-                $p += 4;
1455
-                $fields[] = substr($response, $p, $len);
1456
-                $p += $len;
1457
-            }
1458
-            $result['fields'] = $fields;
1459
-
1460
-            list(, $n_attrs) = unpack('N*', substr($response, $p, 4));
1461
-            $p += 4;
1462
-            while ($n_attrs --> 0 && $p < $max) {
1463
-                list(, $len) = unpack('N*', substr($response, $p, 4));
1464
-                $p += 4;
1465
-                $attr = substr($response, $p, $len);
1466
-                $p += $len;
1467
-                list(, $type) = unpack('N*', substr($response, $p, 4));
1468
-                $p += 4;
1469
-                $attrs[$attr] = $type;
1470
-            }
1471
-            $result['attrs'] = $attrs;
1472
-
1473
-            // read match count
1474
-            list(, $count) = unpack('N*', substr($response, $p, 4));
1475
-            $p += 4;
1476
-            list(, $id64) = unpack('N*', substr($response, $p, 4));
1477
-            $p += 4;
1478
-
1479
-            // read matches
1480
-            $idx = -1;
1481
-            while ($count --> 0 && $p < $max) {
1482
-                // index into result array
1483
-                $idx++;
1484
-
1485
-                // parse document id and weight
1486
-                if ($id64) {
1487
-                    $doc = unpack64IntUnsigned(substr($response, $p, 8));
1488
-                    $p += 8;
1489
-                    list(,$weight) = unpack('N*', substr($response, $p, 4));
1490
-                    $p += 4;
1491
-                } else {
1492
-                    list($doc, $weight) = array_values(unpack('N*N*', substr($response, $p, 8)));
1493
-                    $p += 8;
1494
-                    $doc = fixUInt($doc);
1495
-                }
1496
-                $weight = sprintf('%u', $weight);
1497
-
1498
-                // create match entry
1499
-                if ($this->array_result) {
1500
-                    $result['matches'][$idx] = array('id' => $doc, 'weight' => $weight);
1501
-                } else {
1502
-                    $result['matches'][$doc]['weight'] = $weight;
1503
-                }
1504
-
1505
-                // parse and create attributes
1506
-                $attr_values = array();
1507
-                foreach ($attrs as $attr => $type) {
1508
-                    // handle 64bit int
1509
-                    if ($type == self::ATTR_BIGINT) {
1510
-                        $attr_values[$attr] = unpack64IntSigned(substr($response, $p, 8));
1511
-                        $p += 8;
1512
-                        continue;
1513
-                    }
1514
-
1515
-                    // handle floats
1516
-                    if ($type == self::ATTR_FLOAT) {
1517
-                        list(, $u_value) = unpack('N*', substr($response, $p, 4));
1518
-                        $p += 4;
1519
-                        list(, $f_value) = unpack('f*', pack('L', $u_value));
1520
-                        $attr_values[$attr] = $f_value;
1521
-                        continue;
1522
-                    }
1523
-
1524
-                    // handle everything else as unsigned int
1525
-                    list(, $val) = unpack('N*', substr($response, $p, 4));
1526
-                    $p += 4;
1527
-                    if ($type == self::ATTR_MULTI) {
1528
-                        $attr_values[$attr] = array();
1529
-                        $n_values = $val;
1530
-                        while ($n_values --> 0 && $p < $max) {
1531
-                            list(, $val) = unpack('N*', substr($response, $p, 4));
1532
-                            $p += 4;
1533
-                            $attr_values[$attr][] = fixUInt($val);
1534
-                        }
1535
-                    } elseif ($type == self::ATTR_MULTI64) {
1536
-                        $attr_values[$attr] = array();
1537
-                        $n_values = $val;
1538
-                        while ($n_values > 0 && $p < $max) {
1539
-                            $attr_values[$attr][] = unpack64IntSigned(substr($response, $p, 8));
1540
-                            $p += 8;
1541
-                            $n_values -= 2;
1542
-                        }
1543
-                    } elseif ($type == self::ATTR_STRING) {
1544
-                        $attr_values[$attr] = substr($response, $p, $val);
1545
-                        $p += $val;
1546
-                    } elseif ($type == self::ATTR_FACTORS) {
1547
-                        $attr_values[$attr] = substr($response, $p, $val - 4);
1548
-                        $p += $val-4;
1549
-                    } else {
1550
-                        $attr_values[$attr] = fixUInt($val);
1551
-                    }
1552
-                }
1553
-
1554
-                if ($this->array_result) {
1555
-                    $result['matches'][$idx]['attrs'] = $attr_values;
1556
-                } else {
1557
-                    $result['matches'][$doc]['attrs'] = $attr_values;
1558
-                }
1559
-            }
1560
-
1561
-            list($total, $total_found, $msecs, $words) = array_values(unpack('N*N*N*N*', substr($response, $p, 16)));
1562
-            $result['total'] = sprintf('%u', $total);
1563
-            $result['total_found'] = sprintf('%u', $total_found);
1564
-            $result['time'] = sprintf('%.3f', $msecs / 1000);
1565
-            $p += 16;
1566
-
1567
-            while ($words --> 0 && $p < $max) {
1568
-                list(, $len) = unpack('N*', substr($response, $p, 4));
1569
-                $p += 4;
1570
-                $word = substr($response, $p, $len);
1571
-                $p += $len;
1572
-                list($docs, $hits) = array_values(unpack('N*N*', substr($response, $p, 8)));
1573
-                $p += 8;
1574
-                $result['words'][$word] = array (
1575
-                    'docs' => sprintf('%u', $docs),
1576
-                    'hits' => sprintf('%u', $hits)
1577
-                );
1578
-            }
1579
-        }
1580
-
1581
-        $this->mbPop();
1582
-        return $results;
1583
-    }
1584
-
1585
-    /////////////////////////////////////////////////////////////////////////////
1586
-    // excerpts generation
1587
-    /////////////////////////////////////////////////////////////////////////////
1588
-
1589
-    /**
1590
-     * Connect to searchd server, and generate exceprts (snippets) of given documents for given query.
1591
-     * Returns false on failure, an array of snippets on success
1592
-     *
1593
-     * @param array $docs
1594
-     * @param string $index
1595
-     * @param string $words
1596
-     * @param array $opts
1597
-     *
1598
-     * @return array|bool
1599
-     */
1600
-    public function buildExcerpts(array $docs, $index, $words, array $opts = array())
1601
-    {
1602
-        assert(is_string($index));
1603
-        assert(is_string($words));
1604
-
1605
-        $this->mbPush();
1606
-
1607
-        if (($fp = $this->connect()) === false) {
1608
-            $this->mbPop();
1609
-            return false;
1610
-        }
1611
-
1612
-        /////////////////
1613
-        // fixup options
1614
-        /////////////////
1615
-
1616
-        $opts = array_merge(array(
1617
-            'before_match' => '<b>',
1618
-            'after_match' => '</b>',
1619
-            'chunk_separator' => ' ... ',
1620
-            'limit' => 256,
1621
-            'limit_passages' => 0,
1622
-            'limit_words' => 0,
1623
-            'around' => 5,
1624
-            'exact_phrase' => false,
1625
-            'single_passage' => false,
1626
-            'use_boundaries' => false,
1627
-            'weight_order' => false,
1628
-            'query_mode' => false,
1629
-            'force_all_words' => false,
1630
-            'start_passage_id' => 1,
1631
-            'load_files' => false,
1632
-            'html_strip_mode' => 'index',
1633
-            'allow_empty' => false,
1634
-            'passage_boundary' => 'none',
1635
-            'emit_zones' => false,
1636
-            'load_files_scattered' => false
1637
-        ), $opts);
1638
-
1639
-        /////////////////
1640
-        // build request
1641
-        /////////////////
1642
-
1643
-        // v.1.2 req
1644
-        $flags = 1; // remove spaces
1645
-        if ($opts['exact_phrase']) {
1646
-            $flags |= 2;
1647
-        }
1648
-        if ($opts['single_passage']) {
1649
-            $flags |= 4;
1650
-        }
1651
-        if ($opts['use_boundaries']) {
1652
-            $flags |= 8;
1653
-        }
1654
-        if ($opts['weight_order']) {
1655
-            $flags |= 16;
1656
-        }
1657
-        if ($opts['query_mode']) {
1658
-            $flags |= 32;
1659
-        }
1660
-        if ($opts['force_all_words']) {
1661
-            $flags |= 64;
1662
-        }
1663
-        if ($opts['load_files']) {
1664
-            $flags |= 128;
1665
-        }
1666
-        if ($opts['allow_empty']) {
1667
-            $flags |= 256;
1668
-        }
1669
-        if ($opts['emit_zones']) {
1670
-            $flags |= 512;
1671
-        }
1672
-        if ($opts['load_files_scattered']) {
1673
-            $flags |= 1024;
1674
-        }
1675
-        $req = pack('NN', 0, $flags); // mode=0, flags=$flags
1676
-        $req .= pack('N', strlen($index)) . $index; // req index
1677
-        $req .= pack('N', strlen($words)) . $words; // req words
1678
-
1679
-        // options
1680
-        $req .= pack('N', strlen($opts['before_match'])) . $opts['before_match'];
1681
-        $req .= pack('N', strlen($opts['after_match'])) . $opts['after_match'];
1682
-        $req .= pack('N', strlen($opts['chunk_separator'])) . $opts['chunk_separator'];
1683
-        $req .= pack('NN', (int)$opts['limit'], (int)$opts['around']);
1684
-        // v.1.2
1685
-        $req .= pack('NNN', (int)$opts['limit_passages'], (int)$opts['limit_words'], (int)$opts['start_passage_id']);
1686
-        $req .= pack('N', strlen($opts['html_strip_mode'])) . $opts['html_strip_mode'];
1687
-        $req .= pack('N', strlen($opts['passage_boundary'])) . $opts['passage_boundary'];
1688
-
1689
-        // documents
1690
-        $req .= pack('N', count($docs));
1691
-        foreach ($docs as $doc) {
1692
-            assert(is_string($doc));
1693
-            $req .= pack('N', strlen($doc)) . $doc;
1694
-        }
1695
-
1696
-        ////////////////////////////
1697
-        // send query, get response
1698
-        ////////////////////////////
1699
-
1700
-        $len = strlen($req);
1701
-        $req = pack('nnN', self::SEARCHD_COMMAND_EXCERPT, self::VER_COMMAND_EXCERPT, $len) . $req; // add header
1702
-        if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_EXCERPT))) {
1703
-            $this->mbPop();
1704
-            return false;
1705
-        }
1706
-
1707
-        //////////////////
1708
-        // parse response
1709
-        //////////////////
1710
-
1711
-        $pos = 0;
1712
-        $res = array();
1713
-        $rlen = strlen($response);
1714
-        $count = count($docs);
1715
-        while ($count--) {
1716
-            list(, $len) = unpack('N*', substr($response, $pos, 4));
1717
-            $pos += 4;
1718
-
1719
-            if ($pos + $len > $rlen) {
1720
-                $this->error = 'incomplete reply';
1721
-                $this->mbPop();
1722
-                return false;
1723
-            }
1724
-            $res[] = $len ? substr($response, $pos, $len) : '';
1725
-            $pos += $len;
1726
-        }
1727
-
1728
-        $this->mbPop();
1729
-        return $res;
1730
-    }
1731
-
1732
-
1733
-    /////////////////////////////////////////////////////////////////////////////
1734
-    // keyword generation
1735
-    /////////////////////////////////////////////////////////////////////////////
1736
-
1737
-    /**
1738
-     * Connect to searchd server, and generate keyword list for a given query returns false on failure,
1739
-     * an array of words on success
1740
-     *
1741
-     * @param string $query
1742
-     * @param string $index
1743
-     * @param bool $hits
1744
-     *
1745
-     * @return array|bool
1746
-     */
1747
-    public function buildKeywords($query, $index, $hits)
1748
-    {
1749
-        assert(is_string($query));
1750
-        assert(is_string($index));
1751
-        assert(is_bool($hits));
1752
-
1753
-        $this->mbPush();
1754
-
1755
-        if (($fp = $this->connect()) === false) {
1756
-            $this->mbPop();
1757
-            return false;
1758
-        }
1759
-
1760
-        /////////////////
1761
-        // build request
1762
-        /////////////////
1763
-
1764
-        // v.1.0 req
1765
-        $req  = pack('N', strlen($query)) . $query; // req query
1766
-        $req .= pack('N', strlen($index)) . $index; // req index
1767
-        $req .= pack('N', (int)$hits);
1768
-
1769
-        ////////////////////////////
1770
-        // send query, get response
1771
-        ////////////////////////////
1772
-
1773
-        $len = strlen($req);
1774
-        $req = pack('nnN', self::SEARCHD_COMMAND_KEYWORDS, self::VER_COMMAND_KEYWORDS, $len) . $req; // add header
1775
-        if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_KEYWORDS))) {
1776
-            $this->mbPop();
1777
-            return false;
1778
-        }
1779
-
1780
-        //////////////////
1781
-        // parse response
1782
-        //////////////////
1783
-
1784
-        $pos = 0;
1785
-        $res = array();
1786
-        $rlen = strlen($response);
1787
-        list(, $nwords) = unpack('N*', substr($response, $pos, 4));
1788
-        $pos += 4;
1789
-        for ($i = 0; $i < $nwords; $i++) {
1790
-            list(, $len) = unpack('N*', substr($response, $pos, 4));
1791
-            $pos += 4;
1792
-            $tokenized = $len ? substr($response, $pos, $len) : '';
1793
-            $pos += $len;
1794
-
1795
-            list(, $len) = unpack('N*', substr($response, $pos, 4));
1796
-            $pos += 4;
1797
-            $normalized = $len ? substr($response, $pos, $len) : '';
1798
-            $pos += $len;
1799
-
1800
-            $res[] = array(
1801
-                'tokenized' => $tokenized,
1802
-                'normalized' => $normalized
1803
-            );
1804
-
1805
-            if ($hits) {
1806
-                list($ndocs, $nhits) = array_values(unpack('N*N*', substr($response, $pos, 8)));
1807
-                $pos += 8;
1808
-                $res[$i]['docs'] = $ndocs;
1809
-                $res[$i]['hits'] = $nhits;
1810
-            }
1811
-
1812
-            if ($pos > $rlen) {
1813
-                $this->error = 'incomplete reply';
1814
-                $this->mbPop();
1815
-                return false;
1816
-            }
1817
-        }
1818
-
1819
-        $this->mbPop();
1820
-        return $res;
1821
-    }
1822
-
1823
-    /**
1824
-     * @param string $string
1825
-     *
1826
-     * @return string
1827
-     */
1828
-    public function escapeString($string)
1829
-    {
1830
-        $from = array('\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=', '<');
1831
-        $to   = array('\\\\', '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/', '\^', '\$', '\=', '\<');
1832
-
1833
-        return str_replace($from, $to, $string);
1834
-    }
1835
-
1836
-    /////////////////////////////////////////////////////////////////////////////
1837
-    // attribute updates
1838
-    /////////////////////////////////////////////////////////////////////////////
1839
-
1840
-    /**
1841
-     * Batch update given attributes in given rows in given indexes
1842
-     * Returns amount of updated documents (0 or more) on success, or -1 on failure
1843
-     *
1844
-     * @param string $index
1845
-     * @param array $attrs
1846
-     * @param array $values
1847
-     * @param bool $mva
1848
-     * @param bool $ignore_non_existent
1849
-     *
1850
-     * @return int
1851
-     */
1852
-    public function updateAttributes($index, array $attrs, array $values, $mva = false, $ignore_non_existent = false)
1853
-    {
1854
-        // verify everything
1855
-        assert(is_string($index));
1856
-        assert(is_bool($mva));
1857
-        assert(is_bool($ignore_non_existent));
1858
-
1859
-        foreach ($attrs as $attr) {
1860
-            assert(is_string($attr));
1861
-        }
1862
-
1863
-        foreach ($values as $id => $entry) {
1864
-            assert(is_numeric($id));
1865
-            assert(is_array($entry));
1866
-            assert(count($entry) == count($attrs));
1867
-            foreach ($entry as $v) {
1868
-                if ($mva) {
1869
-                    assert(is_array($v));
1870
-                    foreach ($v as $vv) {
1871
-                        assert(is_int($vv));
1872
-                    }
1873
-                } else {
1874
-                    assert(is_int($v));
1875
-                }
1876
-            }
1877
-        }
1878
-
1879
-        // build request
1880
-        $this->mbPush();
1881
-        $req = pack('N', strlen($index)) . $index;
1882
-
1883
-        $req .= pack('N', count($attrs));
1884
-        $req .= pack('N', $ignore_non_existent ? 1 : 0);
1885
-        foreach ($attrs as $attr) {
1886
-            $req .= pack('N', strlen($attr)) . $attr;
1887
-            $req .= pack('N', $mva ? 1 : 0);
1888
-        }
1889
-
1890
-        $req .= pack('N', count($values));
1891
-        foreach ($values as $id => $entry) {
1892
-            $req .= pack64IntUnsigned($id);
1893
-            foreach ($entry as $v) {
1894
-                $req .= pack('N', $mva ? count($v) : $v);
1895
-                if ($mva) {
1896
-                    foreach ($v as $vv) {
1897
-                        $req .= pack('N', $vv);
1898
-                    }
1899
-                }
1900
-            }
1901
-        }
1902
-
1903
-        // connect, send query, get response
1904
-        if (($fp = $this->connect()) === false) {
1905
-            $this->mbPop();
1906
-            return -1;
1907
-        }
1908
-
1909
-        $len = strlen($req);
1910
-        $req = pack('nnN', self::SEARCHD_COMMAND_UPDATE, self::VER_COMMAND_UPDATE, $len) . $req; // add header
1911
-        if (!$this->send($fp, $req, $len + 8)) {
1912
-            $this->mbPop();
1913
-            return -1;
1914
-        }
1915
-
1916
-        if (!($response = $this->getResponse($fp, self::VER_COMMAND_UPDATE))) {
1917
-            $this->mbPop();
1918
-            return -1;
1919
-        }
1920
-
1921
-        // parse response
1922
-        list(, $updated) = unpack('N*', substr($response, 0, 4));
1923
-        $this->mbPop();
1924
-        return $updated;
1925
-    }
1926
-
1927
-    /////////////////////////////////////////////////////////////////////////////
1928
-    // persistent connections
1929
-    /////////////////////////////////////////////////////////////////////////////
1930
-
1931
-    /**
1932
-     * @return bool
1933
-     */
1934
-    public function open()
1935
-    {
1936
-        if ($this->socket !== false) {
1937
-            $this->error = 'already connected';
1938
-            return false;
1939
-        }
1940
-        if (($fp = $this->connect()) === false)
1941
-            return false;
1942
-
1943
-        // command, command version = 0, body length = 4, body = 1
1944
-        $req = pack('nnNN', self::SEARCHD_COMMAND_PERSIST, 0, 4, 1);
1945
-        if (!$this->send($fp, $req, 12)) {
1946
-            return false;
1947
-        }
1948
-
1949
-        $this->socket = $fp;
1950
-        return true;
1951
-    }
1952
-
1953
-    /**
1954
-     * @return bool
1955
-     */
1956
-    public function close()
1957
-    {
1958
-        if ($this->socket === false) {
1959
-            $this->error = 'not connected';
1960
-            return false;
1961
-        }
1962
-
1963
-        fclose($this->socket);
1964
-        $this->socket = false;
1965
-
1966
-        return true;
1967
-    }
1968
-
1969
-    //////////////////////////////////////////////////////////////////////////
1970
-    // status
1971
-    //////////////////////////////////////////////////////////////////////////
1972
-
1973
-    /**
1974
-     * @param bool $session
1975
-     *
1976
-     * @return array|bool
1977
-     */
1978
-    public function status($session = false)
1979
-    {
1980
-        assert(is_bool($session));
1981
-
1982
-        $this->mbPush();
1983
-        if (($fp = $this->connect()) === false) {
1984
-            $this->mbPop();
1985
-            return false;
1986
-        }
1987
-
1988
-        // len=4, body=1
1989
-        $req = pack('nnNN', self::SEARCHD_COMMAND_STATUS, self::VER_COMMAND_STATUS, 4, $session ? 0 : 1);
1990
-        if (!$this->send($fp, $req, 12) || !($response = $this->getResponse($fp, self::VER_COMMAND_STATUS))) {
1991
-            $this->mbPop();
1992
-            return false;
1993
-        }
1994
-
1995
-        $res = substr($response, 4); // just ignore length, error handling, etc
1996
-        $p = 0;
1997
-        list($rows, $cols) = array_values(unpack('N*N*', substr($response, $p, 8)));
1998
-        $p += 8;
1999
-
2000
-        $res = array();
2001
-        for ($i = 0; $i < $rows; $i++) {
2002
-            for ($j = 0; $j < $cols; $j++) {
2003
-                list(, $len) = unpack('N*', substr($response, $p, 4));
2004
-                $p += 4;
2005
-                $res[$i][] = substr($response, $p, $len);
2006
-                $p += $len;
2007
-            }
2008
-        }
2009
-
2010
-        $this->mbPop();
2011
-        return $res;
2012
-    }
2013
-
2014
-    //////////////////////////////////////////////////////////////////////////
2015
-    // flush
2016
-    //////////////////////////////////////////////////////////////////////////
2017
-
2018
-    /**
2019
-     * @return int
2020
-     */
2021
-    public function flushAttributes()
2022
-    {
2023
-        $this->mbPush();
2024
-        if (($fp = $this->connect()) === false) {
2025
-            $this->mbPop();
2026
-            return -1;
2027
-        }
2028
-
2029
-        $req = pack('nnN', self::SEARCHD_COMMAND_FLUSH_ATTRS, self::VER_COMMAND_FLUSH_ATTRS, 0); // len=0
2030
-        if (!$this->send($fp, $req, 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_FLUSH_ATTRS))) {
2031
-            $this->mbPop();
2032
-            return -1;
2033
-        }
2034
-
2035
-        $tag = -1;
2036
-        if (strlen($response) == 4) {
2037
-            list(, $tag) = unpack('N*', $response);
2038
-        } else {
2039
-            $this->error = 'unexpected response length';
2040
-        }
2041
-
2042
-        $this->mbPop();
2043
-        return $tag;
2044
-    }
40
+	/**
41
+	 * Searchd host
42
+	 *
43
+	 * @var string
44
+	 */
45
+	protected $host = 'localhost';
46
+
47
+	/**
48
+	 * Searchd port
49
+	 *
50
+	 * @var int
51
+	 */
52
+	protected $port = 9312;
53
+
54
+	/**
55
+	 * How many records to seek from result-set start
56
+	 *
57
+	 * @var int
58
+	 */
59
+	protected $offset = 0;
60
+
61
+	/**
62
+	 * How many records to return from result-set starting at offset
63
+	 *
64
+	 * @var int
65
+	 */
66
+	protected $limit = 20;
67
+
68
+	/**
69
+	 * Query matching mode
70
+	 *
71
+	 * @var int
72
+	 */
73
+	protected $mode = self::MATCH_EXTENDED2;
74
+
75
+	/**
76
+	 * Per-field weights (default is 1 for all fields)
77
+	 *
78
+	 * @var array
79
+	 */
80
+	protected $weights = array();
81
+
82
+	/**
83
+	 * Match sorting mode
84
+	 *
85
+	 * @var int
86
+	 */
87
+	protected $sort = self::SORT_RELEVANCE;
88
+
89
+	/**
90
+	 * Attribute to sort by
91
+	 *
92
+	 * @var string
93
+	 */
94
+	protected $sort_by = '';
95
+
96
+	/**
97
+	 * Min ID to match (0 means no limit)
98
+	 *
99
+	 * @var int
100
+	 */
101
+	protected $min_id = 0;
102
+
103
+	/**
104
+	 * Max ID to match (0 means no limit)
105
+	 *
106
+	 * @var int
107
+	 */
108
+	protected $max_id = 0;
109
+
110
+	/**
111
+	 * Search filters
112
+	 *
113
+	 * @var array
114
+	 */
115
+	protected $filters = array();
116
+
117
+	/**
118
+	 * Group-by attribute name
119
+	 *
120
+	 * @var string
121
+	 */
122
+	protected $group_by = '';
123
+
124
+	/**
125
+	 * Group-by function (to pre-process group-by attribute value with)
126
+	 *
127
+	 * @var int
128
+	 */
129
+	protected $group_func = self::GROUP_BY_DAY;
130
+
131
+	/**
132
+	 * Group-by sorting clause (to sort groups in result set with)
133
+	 *
134
+	 * @var string
135
+	 */
136
+	protected $group_sort = '@group desc';
137
+
138
+	/**
139
+	 * Group-by count-distinct attribute
140
+	 *
141
+	 * @var string
142
+	 */
143
+	protected $group_distinct = '';
144
+
145
+	/**
146
+	 * Max matches to retrieve
147
+	 *
148
+	 * @var int
149
+	 */
150
+	protected $max_matches = 1000;
151
+
152
+	/**
153
+	 * Cutoff to stop searching at
154
+	 *
155
+	 * @var int
156
+	 */
157
+	protected $cutoff = 0;
158
+
159
+	/**
160
+	 * Distributed retries count
161
+	 *
162
+	 * @var int
163
+	 */
164
+	protected $retry_count = 0;
165
+
166
+	/**
167
+	 * Distributed retries delay
168
+	 *
169
+	 * @var int
170
+	 */
171
+	protected $retry_delay = 0;
172
+
173
+	/**
174
+	 * Geographical anchor point
175
+	 *
176
+	 * @var array
177
+	 */
178
+	protected $anchor = array();
179
+
180
+	/**
181
+	 * Per-index weights
182
+	 *
183
+	 * @var array
184
+	 */
185
+	protected $index_weights = array();
186
+
187
+	/**
188
+	 * Ranking mode
189
+	 *
190
+	 * @var int
191
+	 */
192
+	protected $ranker = self::RANK_PROXIMITY_BM25;
193
+
194
+	/**
195
+	 * Ranking mode expression (for self::RANK_EXPR)
196
+	 *
197
+	 * @var string
198
+	 */
199
+	protected $rank_expr = '';
200
+
201
+	/**
202
+	 * Max query time, milliseconds (0 means no limit)
203
+	 *
204
+	 * @var int
205
+	 */
206
+	protected $max_query_time = 0;
207
+
208
+	/**
209
+	 * Per-field-name weights
210
+	 *
211
+	 * @var array
212
+	 */
213
+	protected $field_weights = array();
214
+
215
+	/**
216
+	 * Per-query attribute values overrides
217
+	 *
218
+	 * @var array
219
+	 */
220
+	protected $overrides = array();
221
+
222
+	/**
223
+	 * Select-list (attributes or expressions, with optional aliases)
224
+	 *
225
+	 * @var string
226
+	 */
227
+	protected $select = '*';
228
+
229
+	/**
230
+	 * Per-query various flags
231
+	 *
232
+	 * @var int
233
+	 */
234
+	protected $query_flags = 0;
235
+
236
+	/**
237
+	 * Per-query max_predicted_time
238
+	 *
239
+	 * @var int
240
+	 */
241
+	protected $predicted_time = 0;
242
+
243
+	/**
244
+	 * Outer match sort by
245
+	 *
246
+	 * @var string
247
+	 */
248
+	protected $outer_order_by = '';
249
+
250
+	/**
251
+	 * Outer offset
252
+	 *
253
+	 * @var int
254
+	 */
255
+	protected $outer_offset = 0;
256
+
257
+	/**
258
+	 * Outer limit
259
+	 *
260
+	 * @var int
261
+	 */
262
+	protected $outer_limit = 0;
263
+
264
+	/**
265
+	 * @var bool
266
+	 */
267
+	protected $has_outer = false;
268
+
269
+	/**
270
+	 * Last error message
271
+	 *
272
+	 * @var string
273
+	 */
274
+	protected $error = '';
275
+
276
+	/**
277
+	 * Last warning message
278
+	 *
279
+	 * @var string
280
+	 */
281
+	protected $warning = '';
282
+
283
+	/**
284
+	 * Connection error vs remote error flag
285
+	 *
286
+	 * @var bool
287
+	 */
288
+	protected $conn_error = false;
289
+
290
+	/**
291
+	 * Requests array for multi-query
292
+	 *
293
+	 * @var array
294
+	 */
295
+	protected $reqs = array();
296
+
297
+	/**
298
+	 * Stored mbstring encoding
299
+	 *
300
+	 * @var string
301
+	 */
302
+	protected $mbenc = '';
303
+
304
+	/**
305
+	 * Whether $result['matches'] should be a hash or an array
306
+	 *
307
+	 * @var bool
308
+	 */
309
+	protected $array_result = false;
310
+
311
+	/**
312
+	 * Connect timeout
313
+	 *
314
+	 * @var int|float
315
+	 */
316
+	protected $timeout = 0;
317
+
318
+	/**
319
+	 * @var string
320
+	 */
321
+	protected $path = '';
322
+
323
+	/**
324
+	 * @var resource|bool
325
+	 */
326
+	protected $socket = false;
327
+
328
+	// known searchd commands
329
+	const SEARCHD_COMMAND_SEARCH      = 0;
330
+	const SEARCHD_COMMAND_EXCERPT     = 1;
331
+	const SEARCHD_COMMAND_UPDATE      = 2;
332
+	const SEARCHD_COMMAND_KEYWORDS    = 3;
333
+	const SEARCHD_COMMAND_PERSIST     = 4;
334
+	const SEARCHD_COMMAND_STATUS      = 5;
335
+	const SEARCHD_COMMAND_FLUSH_ATTRS = 7;
336
+
337
+	// current client-side command implementation versions
338
+	const VER_COMMAND_SEARCH      = 0x11E;
339
+	const VER_COMMAND_EXCERPT     = 0x104;
340
+	const VER_COMMAND_UPDATE      = 0x103;
341
+	const VER_COMMAND_KEYWORDS    = 0x100;
342
+	const VER_COMMAND_STATUS      = 0x101;
343
+	const VER_COMMAND_QUERY       = 0x100;
344
+	const VER_COMMAND_FLUSH_ATTRS = 0x100;
345
+
346
+	// known searchd status codes
347
+	const SEARCHD_OK      = 0;
348
+	const SEARCHD_ERROR   = 1;
349
+	const SEARCHD_RETRY   = 2;
350
+	const SEARCHD_WARNING = 3;
351
+
352
+	// known match modes
353
+	const MATCH_ALL        = 0;
354
+	const MATCH_ANY        = 1;
355
+	const MATCH_PHRASE     = 2;
356
+	const MATCH_BOOLEAN    = 3;
357
+	const MATCH_EXTENDED   = 4;
358
+	const MATCH_FULL_SCAN  = 5;
359
+	const MATCH_EXTENDED2  = 6; // extended engine V2 (TEMPORARY, WILL BE REMOVED)
360
+
361
+	// known ranking modes (ext2 only)
362
+	const RANK_PROXIMITY_BM25  = 0; // default mode, phrase proximity major factor and BM25 minor one
363
+	const RANK_BM25            = 1; // statistical mode, BM25 ranking only (faster but worse quality)
364
+	const RANK_NONE            = 2; // no ranking, all matches get a weight of 1
365
+	const RANK_WORD_COUNT      = 3; // simple word-count weighting, rank is a weighted sum of per-field keyword
366
+									// occurrence counts
367
+	const RANK_PROXIMITY       = 4;
368
+	const RANK_MATCH_ANY       = 5;
369
+	const RANK_FIELD_MASK      = 6;
370
+	const RANK_SPH04           = 7;
371
+	const RANK_EXPR            = 8;
372
+	const RANK_TOTAL           = 9;
373
+
374
+	// known sort modes
375
+	const SORT_RELEVANCE     = 0;
376
+	const SORT_ATTR_DESC     = 1;
377
+	const SORT_ATTR_ASC      = 2;
378
+	const SORT_TIME_SEGMENTS = 3;
379
+	const SORT_EXTENDED      = 4;
380
+	const SORT_EXPR          = 5;
381
+
382
+	// known filter types
383
+	const FILTER_VALUES      = 0;
384
+	const FILTER_RANGE       = 1;
385
+	const FILTER_FLOAT_RANGE = 2;
386
+	const FILTER_STRING      = 3;
387
+
388
+	// known attribute types
389
+	const ATTR_INTEGER   = 1;
390
+	const ATTR_TIMESTAMP = 2;
391
+	const ATTR_ORDINAL   = 3;
392
+	const ATTR_BOOL      = 4;
393
+	const ATTR_FLOAT     = 5;
394
+	const ATTR_BIGINT    = 6;
395
+	const ATTR_STRING    = 7;
396
+	const ATTR_FACTORS   = 1001;
397
+	const ATTR_MULTI     = 0x40000001;
398
+	const ATTR_MULTI64   = 0x40000002;
399
+
400
+	// known grouping functions
401
+	const GROUP_BY_DAY       = 0;
402
+	const GROUP_BY_WEEK      = 1;
403
+	const GROUP_BY_MONTH     = 2;
404
+	const GROUP_BY_YEAR      = 3;
405
+	const GROUP_BY_ATTR      = 4;
406
+	const GROUP_BY_ATTR_PAIR = 5;
407
+
408
+	/////////////////////////////////////////////////////////////////////////////
409
+	// common stuff
410
+	/////////////////////////////////////////////////////////////////////////////
411
+
412
+	public function __construct()
413
+	{
414
+		// default idf=tfidf_normalized
415
+		$this->query_flags = setBit(0, 6, true);
416
+	}
417
+
418
+	public function __destruct()
419
+	{
420
+		if ($this->socket !== false) {
421
+			fclose($this->socket);
422
+		}
423
+	}
424
+
425
+	/**
426
+	 * @return string
427
+	 */
428
+	public function getLastError()
429
+	{
430
+		return $this->error;
431
+	}
432
+
433
+	/**
434
+	 * @return string
435
+	 */
436
+	public function getLastWarning()
437
+	{
438
+		return $this->warning;
439
+	}
440
+
441
+	/**
442
+	 * Get last error flag (to tell network connection errors from searchd errors or broken responses)
443
+	 *
444
+	 * @return bool
445
+	 */
446
+	public function isConnectError()
447
+	{
448
+		return $this->conn_error;
449
+	}
450
+
451
+	/**
452
+	 * Set searchd host name and port
453
+	 *
454
+	 * @param string $host
455
+	 * @param int $port
456
+	 */
457
+	public function setServer($host, $port = 0)
458
+	{
459
+		assert(is_string($host));
460
+		if ($host[0] == '/') {
461
+			$this->path = 'unix://' . $host;
462
+			return;
463
+		}
464
+		if (substr($host, 0, 7) == 'unix://') {
465
+			$this->path = $host;
466
+			return;
467
+		}
468
+
469
+		$this->host = $host;
470
+		$port = intval($port);
471
+		assert(0 <= $port && $port < 65536);
472
+		$this->port = $port == 0 ? 9312 : $port;
473
+		$this->path = '';
474
+	}
475
+
476
+	/**
477
+	 * Set server connection timeout (0 to remove)
478
+	 *
479
+	 * @param int|float|string $timeout
480
+	 */
481
+	public function setConnectTimeout($timeout)
482
+	{
483
+		assert(is_numeric($timeout));
484
+		$this->timeout = $timeout;
485
+	}
486
+
487
+	/**
488
+	 * @param resource $handle
489
+	 * @param string $data
490
+	 * @param int $length
491
+	 *
492
+	 * @return bool
493
+	 */
494
+	protected function send($handle, $data, $length)
495
+	{
496
+		if (feof($handle) || fwrite($handle, $data, $length) !== $length) {
497
+			$this->error = 'connection unexpectedly closed (timed out?)';
498
+			$this->conn_error = true;
499
+			return false;
500
+		}
501
+		return true;
502
+	}
503
+
504
+	/////////////////////////////////////////////////////////////////////////////
505
+
506
+	/**
507
+	 * Enter mbstring workaround mode
508
+	 */
509
+	protected function mbPush()
510
+	{
511
+		$this->mbenc = '';
512
+		if (ini_get('mbstring.func_overload') & 2) {
513
+			$this->mbenc = mb_internal_encoding();
514
+			mb_internal_encoding('latin1');
515
+		}
516
+	}
517
+
518
+	/**
519
+	 * Leave mbstring workaround mode
520
+	 */
521
+	protected function mbPop()
522
+	{
523
+		if ($this->mbenc) {
524
+			mb_internal_encoding($this->mbenc);
525
+		}
526
+	}
527
+
528
+	/**
529
+	 * Connect to searchd server
530
+	 *
531
+	 * @return bool|resource
532
+	 */
533
+	protected function connect()
534
+	{
535
+		if (is_resource($this->socket)) {
536
+			// we are in persistent connection mode, so we have a socket
537
+			// however, need to check whether it's still alive
538
+			if (!feof($this->socket)) {
539
+				return $this->socket;
540
+			}
541
+
542
+			// force reopen
543
+			$this->socket = false;
544
+		}
545
+
546
+		$errno = 0;
547
+		$errstr = '';
548
+		$this->conn_error = false;
549
+
550
+		if ($this->path) {
551
+			$host = $this->path;
552
+			$port = 0;
553
+		} else {
554
+			$host = $this->host;
555
+			$port = $this->port;
556
+		}
557
+
558
+		if ($this->timeout <= 0) {
559
+			$fp = @fsockopen($host, $port, $errno, $errstr);
560
+		} else {
561
+			$fp = @fsockopen($host, $port, $errno, $errstr, $this->timeout);
562
+		}
563
+
564
+		if (!is_resource($fp)) {
565
+			if ($this->path) {
566
+				$location = $this->path;
567
+			} else {
568
+				$location = "{$this->host}:{$this->port}";
569
+			}
570
+
571
+			$errstr = trim($errstr);
572
+			$this->error = "connection to $location failed (errno=$errno, msg=$errstr)";
573
+			$this->conn_error = true;
574
+			return false;
575
+		}
576
+
577
+		// send my version
578
+		// this is a subtle part. we must do it before (!) reading back from searchd.
579
+		// because otherwise under some conditions (reported on FreeBSD for instance)
580
+		// TCP stack could throttle write-write-read pattern because of Nagle.
581
+		if (!$this->send($fp, pack('N', 1), 4)) {
582
+			fclose($fp);
583
+			$this->error = 'failed to send client protocol version';
584
+			return false;
585
+		}
586
+
587
+		// check version
588
+		list(, $v) = unpack('N*', fread($fp, 4));
589
+		$v = (int)$v;
590
+		if ($v < 1) {
591
+			fclose($fp);
592
+			$this->error = "expected searchd protocol version 1+, got version '$v'";
593
+			return false;
594
+		}
595
+
596
+		return $fp;
597
+	}
598
+
599
+	/**
600
+	 * Get and check response packet from searchd server
601
+	 *
602
+	 * @param resource $fp
603
+	 * @param int $client_ver
604
+	 *
605
+	 * @return bool|string
606
+	 */
607
+	protected function getResponse($fp, $client_ver)
608
+	{
609
+		$ver = 0;
610
+		$len = 0;
611
+		$status = -1;
612
+		$response = '';
613
+
614
+		$header = fread($fp, 8);
615
+		if (strlen($header) == 8) {
616
+			list($status, $ver, $len) = array_values(unpack('n2a/Nb', $header));
617
+			$left = $len;
618
+			while ($left > 0 && !feof($fp)) {
619
+				$chunk = fread($fp, min(8192, $left));
620
+				if ($chunk) {
621
+					$response .= $chunk;
622
+					$left -= strlen($chunk);
623
+				}
624
+			}
625
+		}
626
+
627
+		if ($this->socket === false) {
628
+			fclose($fp);
629
+		}
630
+
631
+		// check response
632
+		$read = strlen($response);
633
+		if (!$response || $read != $len) {
634
+			if ($len) {
635
+				$this->error = "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)";
636
+			} else {
637
+				$this->error = 'received zero-sized searchd response';
638
+			}
639
+			return false;
640
+		}
641
+
642
+		switch ($status) {
643
+			case self::SEARCHD_WARNING:
644
+				list(, $wlen) = unpack('N*', substr($response, 0, 4));
645
+				$this->warning = substr($response, 4, $wlen);
646
+				return substr($response, 4 + $wlen);
647
+			case self::SEARCHD_ERROR:
648
+				$this->error = 'searchd error: ' . substr($response, 4);
649
+				return false;
650
+			case self::SEARCHD_RETRY:
651
+				$this->error = 'temporary searchd error: ' . substr($response, 4);
652
+				return false;
653
+			case self::SEARCHD_OK:
654
+				if ($ver < $client_ver) { // check version
655
+					$this->warning = sprintf(
656
+						'searchd command v.%d.%d older than client\'s v.%d.%d, some options might not work',
657
+						$ver >> 8,
658
+						$ver & 0xff,
659
+						$client_ver >> 8,
660
+						$client_ver & 0xff
661
+					);
662
+				}
663
+
664
+				return $response;
665
+			default:
666
+				$this->error = "unknown status code '$status'";
667
+				return false;
668
+		}
669
+	}
670
+
671
+	/////////////////////////////////////////////////////////////////////////////
672
+	// searching
673
+	/////////////////////////////////////////////////////////////////////////////
674
+
675
+	/**
676
+	 * Set offset and count into result set, and optionally set max-matches and cutoff limits
677
+	 *
678
+	 * @param int $offset
679
+	 * @param int $limit
680
+	 * @param int $max
681
+	 * @param int $cutoff
682
+	 */
683
+	public function setLimits($offset, $limit, $max = 0, $cutoff = 0)
684
+	{
685
+		assert(is_int($offset));
686
+		assert(is_int($limit));
687
+		assert($offset >= 0);
688
+		assert($limit > 0);
689
+		assert($max >= 0);
690
+		$this->offset = $offset;
691
+		$this->limit = $limit;
692
+		if ($max > 0) {
693
+			$this->max_matches = $max;
694
+		}
695
+		if ($cutoff > 0) {
696
+			$this->cutoff = $cutoff;
697
+		}
698
+	}
699
+
700
+	/**
701
+	 * Set maximum query time, in milliseconds, per-index, 0 means 'do not limit'
702
+	 *
703
+	 * @param int $max
704
+	 */
705
+	public function setMaxQueryTime($max)
706
+	{
707
+		assert(is_int($max));
708
+		assert($max >= 0);
709
+		$this->max_query_time = $max;
710
+	}
711
+
712
+	/**
713
+	 * Set matching mode
714
+	 *
715
+	 * @param int $mode
716
+	 */
717
+	public function setMatchMode($mode)
718
+	{
719
+		trigger_error(
720
+			'DEPRECATED: Do not call this method or, even better, use SphinxQL instead of an API',
721
+			E_USER_DEPRECATED
722
+		);
723
+		assert(in_array($mode, array(
724
+			self::MATCH_ALL,
725
+			self::MATCH_ANY,
726
+			self::MATCH_PHRASE,
727
+			self::MATCH_BOOLEAN,
728
+			self::MATCH_EXTENDED,
729
+			self::MATCH_FULL_SCAN,
730
+			self::MATCH_EXTENDED2
731
+		)));
732
+		$this->mode = $mode;
733
+	}
734
+
735
+	/**
736
+	 * Set ranking mode
737
+	 *
738
+	 * @param int $ranker
739
+	 * @param string $rank_expr
740
+	 */
741
+	public function setRankingMode($ranker, $rank_expr='')
742
+	{
743
+		assert($ranker === 0 || $ranker >= 1 && $ranker < self::RANK_TOTAL);
744
+		assert(is_string($rank_expr));
745
+		$this->ranker = $ranker;
746
+		$this->rank_expr = $rank_expr;
747
+	}
748
+
749
+	/**
750
+	 * Set matches sorting mode
751
+	 *
752
+	 * @param int $mode
753
+	 * @param string $sort_by
754
+	 */
755
+	public function setSortMode($mode, $sort_by = '')
756
+	{
757
+		assert(in_array($mode, array(
758
+			self::SORT_RELEVANCE,
759
+			self::SORT_ATTR_DESC,
760
+			self::SORT_ATTR_ASC,
761
+			self::SORT_TIME_SEGMENTS,
762
+			self::SORT_EXTENDED,
763
+			self::SORT_EXPR
764
+		)));
765
+		assert(is_string($sort_by));
766
+		assert($mode == self::SORT_RELEVANCE || strlen($sort_by) > 0);
767
+
768
+		$this->sort = $mode;
769
+		$this->sort_by = $sort_by;
770
+	}
771
+
772
+	/**
773
+	 * Bind per-field weights by order
774
+	 *
775
+	 * @deprecated use setFieldWeights() instead
776
+	 */
777
+	public function setWeights()
778
+	{
779
+		throw new \RuntimeException('This method is now deprecated; please use setFieldWeights instead');
780
+	}
781
+
782
+	/**
783
+	 * Bind per-field weights by name
784
+	 *
785
+	 * @param array $weights
786
+	 */
787
+	public function setFieldWeights(array $weights)
788
+	{
789
+		foreach ($weights as $name => $weight) {
790
+			assert(is_string($name));
791
+			assert(is_int($weight));
792
+		}
793
+		$this->field_weights = $weights;
794
+	}
795
+
796
+	/**
797
+	 * Bind per-index weights by name
798
+	 *
799
+	 * @param array $weights
800
+	 */
801
+	public function setIndexWeights(array $weights)
802
+	{
803
+		foreach ($weights as $index => $weight) {
804
+			assert(is_string($index));
805
+			assert(is_int($weight));
806
+		}
807
+		$this->index_weights = $weights;
808
+	}
809
+
810
+	/**
811
+	 * Set IDs range to match. Only match records if document ID is beetwen $min and $max (inclusive)
812
+	 *
813
+	 * @param int $min
814
+	 * @param int $max
815
+	 */
816
+	public function setIDRange($min, $max)
817
+	{
818
+		assert(is_numeric($min));
819
+		assert(is_numeric($max));
820
+		assert($min <= $max);
821
+
822
+		$this->min_id = $min;
823
+		$this->max_id = $max;
824
+	}
825
+
826
+	/**
827
+	 * Set values set filter. Only match records where $attribute value is in given set
828
+	 *
829
+	 * @param string $attribute
830
+	 * @param array $values
831
+	 * @param bool $exclude
832
+	 */
833
+	public function setFilter($attribute, array $values, $exclude = false)
834
+	{
835
+		assert(is_string($attribute));
836
+		assert(count($values));
837
+
838
+		foreach ($values as $value) {
839
+			assert(is_numeric($value));
840
+		}
841
+
842
+		$this->filters[] = array(
843
+			'type' => self::FILTER_VALUES,
844
+			'attr' => $attribute,
845
+			'exclude' => $exclude,
846
+			'values' => $values
847
+		);
848
+	}
849
+
850
+	/**
851
+	 * Set string filter
852
+	 * Only match records where $attribute value is equal
853
+	 *
854
+	 * @param string $attribute
855
+	 * @param string $value
856
+	 * @param bool $exclude
857
+	 */
858
+	public function setFilterString($attribute, $value, $exclude = false)
859
+	{
860
+		assert(is_string($attribute));
861
+		assert(is_string($value));
862
+		$this->filters[] = array(
863
+			'type' => self::FILTER_STRING,
864
+			'attr' => $attribute,
865
+			'exclude' => $exclude,
866
+			'value' => $value
867
+		);
868
+	}    
869
+
870
+	/**
871
+	 * Set range filter
872
+	 * Only match records if $attribute value is beetwen $min and $max (inclusive)
873
+	 *
874
+	 * @param string $attribute
875
+	 * @param int $min
876
+	 * @param int $max
877
+	 * @param bool $exclude
878
+	 */
879
+	public function setFilterRange($attribute, $min, $max, $exclude = false)
880
+	{
881
+		assert(is_string($attribute));
882
+		assert(is_numeric($min));
883
+		assert(is_numeric($max));
884
+		assert($min <= $max);
885
+
886
+		$this->filters[] = array(
887
+			'type' => self::FILTER_RANGE,
888
+			'attr' => $attribute,
889
+			'exclude' => $exclude,
890
+			'min' => $min,
891
+			'max' => $max
892
+		);
893
+	}
894
+
895
+	/**
896
+	 * Set float range filter
897
+	 * Only match records if $attribute value is beetwen $min and $max (inclusive)
898
+	 *
899
+	 * @param string $attribute
900
+	 * @param float $min
901
+	 * @param float $max
902
+	 * @param bool $exclude
903
+	 */
904
+	public function setFilterFloatRange($attribute, $min, $max, $exclude = false)
905
+	{
906
+		assert(is_string($attribute));
907
+		assert(is_float($min));
908
+		assert(is_float($max));
909
+		assert($min <= $max);
910
+
911
+		$this->filters[] = array(
912
+			'type' => self::FILTER_FLOAT_RANGE,
913
+			'attr' => $attribute,
914
+			'exclude' => $exclude,
915
+			'min' => $min,
916
+			'max' => $max
917
+		);
918
+	}
919
+
920
+	/**
921
+	 * Setup anchor point for geosphere distance calculations
922
+	 * Required to use @geodist in filters and sorting
923
+	 * Latitude and longitude must be in radians
924
+	 *
925
+	 * @param string $attr_lat
926
+	 * @param string $attr_long
927
+	 * @param float $lat
928
+	 * @param float $long
929
+	 */
930
+	public function setGeoAnchor($attr_lat, $attr_long, $lat, $long)
931
+	{
932
+		assert(is_string($attr_lat));
933
+		assert(is_string($attr_long));
934
+		assert(is_float($lat));
935
+		assert(is_float($long));
936
+
937
+		$this->anchor = array(
938
+			'attrlat' => $attr_lat,
939
+			'attrlong' => $attr_long,
940
+			'lat' => $lat,
941
+			'long' => $long
942
+		);
943
+	}
944
+
945
+	/**
946
+	 * Set grouping attribute and function
947
+	 *
948
+	 * @param string $attribute
949
+	 * @param int $func
950
+	 * @param string $group_sort
951
+	 */
952
+	public function setGroupBy($attribute, $func, $group_sort = '@group desc')
953
+	{
954
+		assert(is_string($attribute));
955
+		assert(is_string($group_sort));
956
+		assert(in_array($func, array(
957
+			self::GROUP_BY_DAY,
958
+			self::GROUP_BY_WEEK,
959
+			self::GROUP_BY_MONTH,
960
+			self::GROUP_BY_YEAR,
961
+			self::GROUP_BY_ATTR,
962
+			self::GROUP_BY_ATTR_PAIR
963
+		)));
964
+
965
+		$this->group_by = $attribute;
966
+		$this->group_func = $func;
967
+		$this->group_sort = $group_sort;
968
+	}
969
+
970
+	/**
971
+	 * Set count-distinct attribute for group-by queries
972
+	 *
973
+	 * @param string $attribute
974
+	 */
975
+	public function setGroupDistinct($attribute)
976
+	{
977
+		assert(is_string($attribute));
978
+		$this->group_distinct = $attribute;
979
+	}
980
+
981
+	/**
982
+	 * Set distributed retries count and delay
983
+	 *
984
+	 * @param int $count
985
+	 * @param int $delay
986
+	 */
987
+	public function setRetries($count, $delay = 0)
988
+	{
989
+		assert(is_int($count) && $count >= 0);
990
+		assert(is_int($delay) && $delay >= 0);
991
+		$this->retry_count = $count;
992
+		$this->retry_delay = $delay;
993
+	}
994
+
995
+	/**
996
+	 * Set result set format (hash or array; hash by default)
997
+	 * PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs
998
+	 *
999
+	 * @param bool $array_result
1000
+	 */
1001
+	public function setArrayResult($array_result)
1002
+	{
1003
+		assert(is_bool($array_result));
1004
+		$this->array_result = $array_result;
1005
+	}
1006
+
1007
+	/**
1008
+	 * Set attribute values override
1009
+	 * There can be only one override per attribute
1010
+	 * $values must be a hash that maps document IDs to attribute values
1011
+	 *
1012
+	 * @deprecated Do not call this method. Use SphinxQL REMAP() function instead.
1013
+	 *
1014
+	 * @param string $attr_name
1015
+	 * @param string $attr_type
1016
+	 * @param array $values
1017
+	 */
1018
+	public function setOverride($attr_name, $attr_type, array $values)
1019
+	{
1020
+		trigger_error(
1021
+			'DEPRECATED: Do not call this method. Use SphinxQL REMAP() function instead.',
1022
+			E_USER_DEPRECATED
1023
+		);
1024
+		assert(is_string($attr_name));
1025
+		assert(in_array($attr_type, array(
1026
+			self::ATTR_INTEGER,
1027
+			self::ATTR_TIMESTAMP,
1028
+			self::ATTR_BOOL,
1029
+			self::ATTR_FLOAT,
1030
+			self::ATTR_BIGINT
1031
+		)));
1032
+
1033
+		$this->overrides[$attr_name] = array(
1034
+			'attr' => $attr_name,
1035
+			'type' => $attr_type,
1036
+			'values' => $values
1037
+		);
1038
+	}
1039
+
1040
+	/**
1041
+	 * Set select-list (attributes or expressions), SQL-like syntax
1042
+	 *
1043
+	 * @param string $select
1044
+	 */
1045
+	public function setSelect($select)
1046
+	{
1047
+		assert(is_string($select));
1048
+		$this->select = $select;
1049
+	}
1050
+
1051
+	/**
1052
+	 * @param string $flag_name
1053
+	 * @param string|int $flag_value
1054
+	 */
1055
+	public function setQueryFlag($flag_name, $flag_value)
1056
+	{
1057
+		$known_names = array(
1058
+			'reverse_scan',
1059
+			'sort_method',
1060
+			'max_predicted_time',
1061
+			'boolean_simplify',
1062
+			'idf',
1063
+			'global_idf',
1064
+			'low_priority'
1065
+		);
1066
+		$flags = array (
1067
+			'reverse_scan' => array(0, 1),
1068
+			'sort_method' => array('pq', 'kbuffer'),
1069
+			'max_predicted_time' => array(0),
1070
+			'boolean_simplify' => array(true, false),
1071
+			'idf' => array ('normalized', 'plain', 'tfidf_normalized', 'tfidf_unnormalized'),
1072
+			'global_idf' => array(true, false),
1073
+			'low_priority' => array(true, false)
1074
+		);
1075
+
1076
+		assert(isset($flag_name, $known_names));
1077
+		assert(
1078
+			in_array($flag_value, $flags[$flag_name], true) ||
1079
+			($flag_name == 'max_predicted_time' && is_int($flag_value) && $flag_value >= 0)
1080
+		);
1081
+
1082
+		switch ($flag_name) {
1083
+			case 'reverse_scan':
1084
+				$this->query_flags = setBit($this->query_flags, 0, $flag_value == 1);
1085
+				break;
1086
+			case 'sort_method':
1087
+				$this->query_flags = setBit($this->query_flags, 1, $flag_value == 'kbuffer');
1088
+				break;
1089
+			case 'max_predicted_time':
1090
+				$this->query_flags = setBit($this->query_flags, 2, $flag_value > 0);
1091
+				$this->predicted_time = (int)$flag_value;
1092
+				break;
1093
+			case 'boolean_simplify':
1094
+				$this->query_flags = setBit($this->query_flags, 3, $flag_value);
1095
+				break;
1096
+			case 'idf':
1097
+				if ($flag_value == 'normalized' || $flag_value == 'plain') {
1098
+					$this->query_flags = setBit($this->query_flags, 4, $flag_value == 'plain');
1099
+				}
1100
+				if ($flag_value == 'tfidf_normalized' || $flag_value == 'tfidf_unnormalized') {
1101
+					$this->query_flags = setBit($this->query_flags, 6, $flag_value == 'tfidf_normalized');
1102
+				}
1103
+				break;
1104
+			case 'global_idf':
1105
+				$this->query_flags = setBit($this->query_flags, 5, $flag_value);
1106
+				break;
1107
+			case 'low_priority':
1108
+				$this->query_flags = setBit($this->query_flags, 8, $flag_value);
1109
+				break;
1110
+		}
1111
+	}
1112
+
1113
+	/**
1114
+	 * Set outer order by parameters
1115
+	 *
1116
+	 * @param string $order_by
1117
+	 * @param int $offset
1118
+	 * @param int $limit
1119
+	 */
1120
+	public function setOuterSelect($order_by, $offset, $limit)
1121
+	{
1122
+		assert(is_string($order_by));
1123
+		assert(is_int($offset));
1124
+		assert(is_int($limit));
1125
+		assert($offset >= 0);
1126
+		assert($limit > 0);
1127
+
1128
+		$this->outer_order_by = $order_by;
1129
+		$this->outer_offset = $offset;
1130
+		$this->outer_limit = $limit;
1131
+		$this->has_outer = true;
1132
+	}
1133
+
1134
+
1135
+	//////////////////////////////////////////////////////////////////////////////
1136
+
1137
+	/**
1138
+	 * Clear all filters (for multi-queries)
1139
+	 */
1140
+	public function resetFilters()
1141
+	{
1142
+		$this->filters = array();
1143
+		$this->anchor = array();
1144
+	}
1145
+
1146
+	/**
1147
+	 * Clear groupby settings (for multi-queries)
1148
+	 */
1149
+	public function resetGroupBy()
1150
+	{
1151
+		$this->group_by = '';
1152
+		$this->group_func = self::GROUP_BY_DAY;
1153
+		$this->group_sort = '@group desc';
1154
+		$this->group_distinct = '';
1155
+	}
1156
+
1157
+	/**
1158
+	 * Clear all attribute value overrides (for multi-queries)
1159
+	 */
1160
+	public function resetOverrides()
1161
+	{
1162
+		$this->overrides = array();
1163
+	}
1164
+
1165
+	public function resetQueryFlag()
1166
+	{
1167
+		$this->query_flags = setBit(0, 6, true); // default idf=tfidf_normalized
1168
+		$this->predicted_time = 0;
1169
+	}
1170
+
1171
+	public function resetOuterSelect()
1172
+	{
1173
+		$this->outer_order_by = '';
1174
+		$this->outer_offset = 0;
1175
+		$this->outer_limit = 0;
1176
+		$this->has_outer = false;
1177
+	}
1178
+
1179
+	//////////////////////////////////////////////////////////////////////////////
1180
+
1181
+	/**
1182
+	 * Connect to searchd server, run given search query through given indexes, and return the search results
1183
+	 *
1184
+	 * @param string  $query
1185
+	 * @param string $index
1186
+	 * @param string $comment
1187
+	 *
1188
+	 * @return bool
1189
+	 */
1190
+	public function query($query, $index = '*', $comment = '')
1191
+	{
1192
+		assert(empty($this->reqs));
1193
+
1194
+		$this->addQuery($query, $index, $comment);
1195
+		$results = $this->runQueries();
1196
+		$this->reqs = array(); // just in case it failed too early
1197
+
1198
+		if (!is_array($results)) {
1199
+			return false; // probably network error; error message should be already filled
1200
+		}
1201
+
1202
+		$this->error = $results[0]['error'];
1203
+		$this->warning = $results[0]['warning'];
1204
+
1205
+		if ($results[0]['status'] == self::SEARCHD_ERROR) {
1206
+			return false;
1207
+		} else {
1208
+			return $results[0];
1209
+		}
1210
+	}
1211
+
1212
+	/**
1213
+	 * Helper to pack floats in network byte order
1214
+	 *
1215
+	 * @param float $float
1216
+	 *
1217
+	 * @return string
1218
+	 */
1219
+	protected function packFloat($float)
1220
+	{
1221
+		$t1 = pack('f', $float); // machine order
1222
+		list(, $t2) = unpack('L*', $t1); // int in machine order
1223
+		return pack('N', $t2);
1224
+	}
1225
+
1226
+	/**
1227
+	 * Add query to multi-query batch
1228
+	 * Returns index into results array from RunQueries() call
1229
+	 *
1230
+	 * @param string $query
1231
+	 * @param string $index
1232
+	 * @param string $comment
1233
+	 *
1234
+	 * @return int
1235
+	 */
1236
+	public function addQuery($query, $index = '*', $comment = '')
1237
+	{
1238
+		// mbstring workaround
1239
+		$this->mbPush();
1240
+
1241
+		// build request
1242
+		$req = pack('NNNNN', $this->query_flags, $this->offset, $this->limit, $this->mode, $this->ranker);
1243
+		if ($this->ranker == self::RANK_EXPR) {
1244
+			$req .= pack('N', strlen($this->rank_expr)) . $this->rank_expr;
1245
+		}
1246
+		$req .= pack('N', $this->sort); // (deprecated) sort mode
1247
+		$req .= pack('N', strlen($this->sort_by)) . $this->sort_by;
1248
+		$req .= pack('N', strlen($query)) . $query; // query itself
1249
+		$req .= pack('N', count($this->weights)); // weights
1250
+		foreach ($this->weights as $weight) {
1251
+			$req .= pack('N', (int)$weight);
1252
+		}
1253
+		$req .= pack('N', strlen($index)) . $index; // indexes
1254
+		$req .= pack('N', 1); // id64 range marker
1255
+		$req .= pack64IntUnsigned($this->min_id) . pack64IntUnsigned($this->max_id); // id64 range
1256
+
1257
+		// filters
1258
+		$req .= pack('N', count($this->filters));
1259
+		foreach ($this->filters as $filter) {
1260
+			$req .= pack('N', strlen($filter['attr'])) . $filter['attr'];
1261
+			$req .= pack('N', $filter['type']);
1262
+			switch ($filter['type']) {
1263
+				case self::FILTER_VALUES:
1264
+					$req .= pack('N', count($filter['values']));
1265
+					foreach ($filter['values'] as $value) {
1266
+						$req .= pack64IntSigned($value);
1267
+					}
1268
+					break;
1269
+				case self::FILTER_RANGE:
1270
+					$req .= pack64IntSigned($filter['min']) . pack64IntSigned($filter['max']);
1271
+					break;
1272
+				case self::FILTER_FLOAT_RANGE:
1273
+					$req .= $this->packFloat($filter['min']) . $this->packFloat($filter['max']);
1274
+					break;
1275
+				case self::FILTER_STRING:
1276
+					$req .= pack('N', strlen($filter['value'])) . $filter['value'];
1277
+					break;
1278
+				default:
1279
+					assert(0 && 'internal error: unhandled filter type');
1280
+			}
1281
+			$req .= pack('N', $filter['exclude']);
1282
+		}
1283
+
1284
+		// group-by clause, max-matches count, group-sort clause, cutoff count
1285
+		$req .= pack('NN', $this->group_func, strlen($this->group_by)) . $this->group_by;
1286
+		$req .= pack('N', $this->max_matches);
1287
+		$req .= pack('N', strlen($this->group_sort)) . $this->group_sort;
1288
+		$req .= pack('NNN', $this->cutoff, $this->retry_count, $this->retry_delay);
1289
+		$req .= pack('N', strlen($this->group_distinct)) . $this->group_distinct;
1290
+
1291
+		// anchor point
1292
+		if (empty($this->anchor)) {
1293
+			$req .= pack('N', 0);
1294
+		} else {
1295
+			$a =& $this->anchor;
1296
+			$req .= pack('N', 1);
1297
+			$req .= pack('N', strlen($a['attrlat'])) . $a['attrlat'];
1298
+			$req .= pack('N', strlen($a['attrlong'])) . $a['attrlong'];
1299
+			$req .= $this->packFloat($a['lat']) . $this->packFloat($a['long']);
1300
+		}
1301
+
1302
+		// per-index weights
1303
+		$req .= pack('N', count($this->index_weights));
1304
+		foreach ($this->index_weights as $idx => $weight) {
1305
+			$req .= pack('N', strlen($idx)) . $idx . pack('N', $weight);
1306
+		}
1307
+
1308
+		// max query time
1309
+		$req .= pack('N', $this->max_query_time);
1310
+
1311
+		// per-field weights
1312
+		$req .= pack('N', count($this->field_weights));
1313
+		foreach ($this->field_weights as $field => $weight) {
1314
+			$req .= pack('N', strlen($field)) . $field . pack('N', $weight);
1315
+		}
1316
+
1317
+		// comment
1318
+		$req .= pack('N', strlen($comment)) . $comment;
1319
+
1320
+		// attribute overrides
1321
+		$req .= pack('N', count($this->overrides));
1322
+		foreach ($this->overrides as $key => $entry) {
1323
+			$req .= pack('N', strlen($entry['attr'])) . $entry['attr'];
1324
+			$req .= pack('NN', $entry['type'], count($entry['values']));
1325
+			foreach ($entry['values'] as $id => $val) {
1326
+				assert(is_numeric($id));
1327
+				assert(is_numeric($val));
1328
+
1329
+				$req .= pack64IntUnsigned($id);
1330
+				switch ($entry['type']) {
1331
+					case self::ATTR_FLOAT:
1332
+						$req .= $this->packFloat($val);
1333
+						break;
1334
+					case self::ATTR_BIGINT:
1335
+						$req .= pack64IntSigned($val);
1336
+						break;
1337
+					default:
1338
+						$req .= pack('N', $val);
1339
+						break;
1340
+				}
1341
+			}
1342
+		}
1343
+
1344
+		// select-list
1345
+		$req .= pack('N', strlen($this->select)) . $this->select;
1346
+
1347
+		// max_predicted_time
1348
+		if ($this->predicted_time > 0) {
1349
+			$req .= pack('N', (int)$this->predicted_time);
1350
+		}
1351
+
1352
+		$req .= pack('N', strlen($this->outer_order_by)) . $this->outer_order_by;
1353
+		$req .= pack('NN', $this->outer_offset, $this->outer_limit);
1354
+		if ($this->has_outer) {
1355
+			$req .= pack('N', 1);
1356
+		} else {
1357
+			$req .= pack('N', 0);
1358
+		}
1359
+
1360
+		// mbstring workaround
1361
+		$this->mbPop();
1362
+
1363
+		// store request to requests array
1364
+		$this->reqs[] = $req;
1365
+		return count($this->reqs) - 1;
1366
+	}
1367
+
1368
+	/**
1369
+	 * Connect to searchd, run queries batch, and return an array of result sets
1370
+	 *
1371
+	 * @return array|bool
1372
+	 */
1373
+	public function runQueries()
1374
+	{
1375
+		if (empty($this->reqs)) {
1376
+			$this->error = 'no queries defined, issue AddQuery() first';
1377
+			return false;
1378
+		}
1379
+
1380
+		// mbstring workaround
1381
+		$this->mbPush();
1382
+
1383
+		if (($fp = $this->connect()) === false) {
1384
+			$this->mbPop();
1385
+			return false;
1386
+		}
1387
+
1388
+		// send query, get response
1389
+		$nreqs = count($this->reqs);
1390
+		$req = join('', $this->reqs);
1391
+		$len = 8 + strlen($req);
1392
+		// add header
1393
+		$req = pack('nnNNN', self::SEARCHD_COMMAND_SEARCH, self::VER_COMMAND_SEARCH, $len, 0, $nreqs) . $req;
1394
+
1395
+		if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_SEARCH))) {
1396
+			$this->mbPop();
1397
+			return false;
1398
+		}
1399
+
1400
+		// query sent ok; we can reset reqs now
1401
+		$this->reqs = array();
1402
+
1403
+		// parse and return response
1404
+		return $this->parseSearchResponse($response, $nreqs);
1405
+	}
1406
+
1407
+	/**
1408
+	 * Parse and return search query (or queries) response
1409
+	 *
1410
+	 * @param string $response
1411
+	 * @param int $nreqs
1412
+	 *
1413
+	 * @return array
1414
+	 */
1415
+	protected function parseSearchResponse($response, $nreqs)
1416
+	{
1417
+		$p = 0; // current position
1418
+		$max = strlen($response); // max position for checks, to protect against broken responses
1419
+
1420
+		$results = array();
1421
+		for ($ires = 0; $ires < $nreqs && $p < $max; $ires++) {
1422
+			$results[] = array();
1423
+			$result =& $results[$ires];
1424
+
1425
+			$result['error'] = '';
1426
+			$result['warning'] = '';
1427
+
1428
+			// extract status
1429
+			list(, $status) = unpack('N*', substr($response, $p, 4));
1430
+			$p += 4;
1431
+			$result['status'] = $status;
1432
+			if ($status != self::SEARCHD_OK) {
1433
+				list(, $len) = unpack('N*', substr($response, $p, 4));
1434
+				$p += 4;
1435
+				$message = substr($response, $p, $len);
1436
+				$p += $len;
1437
+
1438
+				if ($status == self::SEARCHD_WARNING) {
1439
+					$result['warning'] = $message;
1440
+				} else {
1441
+					$result['error'] = $message;
1442
+					continue;
1443
+				}
1444
+			}
1445
+
1446
+			// read schema
1447
+			$fields = array();
1448
+			$attrs = array();
1449
+
1450
+			list(, $nfields) = unpack('N*', substr($response, $p, 4));
1451
+			$p += 4;
1452
+			while ($nfields --> 0 && $p < $max) {
1453
+				list(, $len) = unpack('N*', substr($response, $p, 4));
1454
+				$p += 4;
1455
+				$fields[] = substr($response, $p, $len);
1456
+				$p += $len;
1457
+			}
1458
+			$result['fields'] = $fields;
1459
+
1460
+			list(, $n_attrs) = unpack('N*', substr($response, $p, 4));
1461
+			$p += 4;
1462
+			while ($n_attrs --> 0 && $p < $max) {
1463
+				list(, $len) = unpack('N*', substr($response, $p, 4));
1464
+				$p += 4;
1465
+				$attr = substr($response, $p, $len);
1466
+				$p += $len;
1467
+				list(, $type) = unpack('N*', substr($response, $p, 4));
1468
+				$p += 4;
1469
+				$attrs[$attr] = $type;
1470
+			}
1471
+			$result['attrs'] = $attrs;
1472
+
1473
+			// read match count
1474
+			list(, $count) = unpack('N*', substr($response, $p, 4));
1475
+			$p += 4;
1476
+			list(, $id64) = unpack('N*', substr($response, $p, 4));
1477
+			$p += 4;
1478
+
1479
+			// read matches
1480
+			$idx = -1;
1481
+			while ($count --> 0 && $p < $max) {
1482
+				// index into result array
1483
+				$idx++;
1484
+
1485
+				// parse document id and weight
1486
+				if ($id64) {
1487
+					$doc = unpack64IntUnsigned(substr($response, $p, 8));
1488
+					$p += 8;
1489
+					list(,$weight) = unpack('N*', substr($response, $p, 4));
1490
+					$p += 4;
1491
+				} else {
1492
+					list($doc, $weight) = array_values(unpack('N*N*', substr($response, $p, 8)));
1493
+					$p += 8;
1494
+					$doc = fixUInt($doc);
1495
+				}
1496
+				$weight = sprintf('%u', $weight);
1497
+
1498
+				// create match entry
1499
+				if ($this->array_result) {
1500
+					$result['matches'][$idx] = array('id' => $doc, 'weight' => $weight);
1501
+				} else {
1502
+					$result['matches'][$doc]['weight'] = $weight;
1503
+				}
1504
+
1505
+				// parse and create attributes
1506
+				$attr_values = array();
1507
+				foreach ($attrs as $attr => $type) {
1508
+					// handle 64bit int
1509
+					if ($type == self::ATTR_BIGINT) {
1510
+						$attr_values[$attr] = unpack64IntSigned(substr($response, $p, 8));
1511
+						$p += 8;
1512
+						continue;
1513
+					}
1514
+
1515
+					// handle floats
1516
+					if ($type == self::ATTR_FLOAT) {
1517
+						list(, $u_value) = unpack('N*', substr($response, $p, 4));
1518
+						$p += 4;
1519
+						list(, $f_value) = unpack('f*', pack('L', $u_value));
1520
+						$attr_values[$attr] = $f_value;
1521
+						continue;
1522
+					}
1523
+
1524
+					// handle everything else as unsigned int
1525
+					list(, $val) = unpack('N*', substr($response, $p, 4));
1526
+					$p += 4;
1527
+					if ($type == self::ATTR_MULTI) {
1528
+						$attr_values[$attr] = array();
1529
+						$n_values = $val;
1530
+						while ($n_values --> 0 && $p < $max) {
1531
+							list(, $val) = unpack('N*', substr($response, $p, 4));
1532
+							$p += 4;
1533
+							$attr_values[$attr][] = fixUInt($val);
1534
+						}
1535
+					} elseif ($type == self::ATTR_MULTI64) {
1536
+						$attr_values[$attr] = array();
1537
+						$n_values = $val;
1538
+						while ($n_values > 0 && $p < $max) {
1539
+							$attr_values[$attr][] = unpack64IntSigned(substr($response, $p, 8));
1540
+							$p += 8;
1541
+							$n_values -= 2;
1542
+						}
1543
+					} elseif ($type == self::ATTR_STRING) {
1544
+						$attr_values[$attr] = substr($response, $p, $val);
1545
+						$p += $val;
1546
+					} elseif ($type == self::ATTR_FACTORS) {
1547
+						$attr_values[$attr] = substr($response, $p, $val - 4);
1548
+						$p += $val-4;
1549
+					} else {
1550
+						$attr_values[$attr] = fixUInt($val);
1551
+					}
1552
+				}
1553
+
1554
+				if ($this->array_result) {
1555
+					$result['matches'][$idx]['attrs'] = $attr_values;
1556
+				} else {
1557
+					$result['matches'][$doc]['attrs'] = $attr_values;
1558
+				}
1559
+			}
1560
+
1561
+			list($total, $total_found, $msecs, $words) = array_values(unpack('N*N*N*N*', substr($response, $p, 16)));
1562
+			$result['total'] = sprintf('%u', $total);
1563
+			$result['total_found'] = sprintf('%u', $total_found);
1564
+			$result['time'] = sprintf('%.3f', $msecs / 1000);
1565
+			$p += 16;
1566
+
1567
+			while ($words --> 0 && $p < $max) {
1568
+				list(, $len) = unpack('N*', substr($response, $p, 4));
1569
+				$p += 4;
1570
+				$word = substr($response, $p, $len);
1571
+				$p += $len;
1572
+				list($docs, $hits) = array_values(unpack('N*N*', substr($response, $p, 8)));
1573
+				$p += 8;
1574
+				$result['words'][$word] = array (
1575
+					'docs' => sprintf('%u', $docs),
1576
+					'hits' => sprintf('%u', $hits)
1577
+				);
1578
+			}
1579
+		}
1580
+
1581
+		$this->mbPop();
1582
+		return $results;
1583
+	}
1584
+
1585
+	/////////////////////////////////////////////////////////////////////////////
1586
+	// excerpts generation
1587
+	/////////////////////////////////////////////////////////////////////////////
1588
+
1589
+	/**
1590
+	 * Connect to searchd server, and generate exceprts (snippets) of given documents for given query.
1591
+	 * Returns false on failure, an array of snippets on success
1592
+	 *
1593
+	 * @param array $docs
1594
+	 * @param string $index
1595
+	 * @param string $words
1596
+	 * @param array $opts
1597
+	 *
1598
+	 * @return array|bool
1599
+	 */
1600
+	public function buildExcerpts(array $docs, $index, $words, array $opts = array())
1601
+	{
1602
+		assert(is_string($index));
1603
+		assert(is_string($words));
1604
+
1605
+		$this->mbPush();
1606
+
1607
+		if (($fp = $this->connect()) === false) {
1608
+			$this->mbPop();
1609
+			return false;
1610
+		}
1611
+
1612
+		/////////////////
1613
+		// fixup options
1614
+		/////////////////
1615
+
1616
+		$opts = array_merge(array(
1617
+			'before_match' => '<b>',
1618
+			'after_match' => '</b>',
1619
+			'chunk_separator' => ' ... ',
1620
+			'limit' => 256,
1621
+			'limit_passages' => 0,
1622
+			'limit_words' => 0,
1623
+			'around' => 5,
1624
+			'exact_phrase' => false,
1625
+			'single_passage' => false,
1626
+			'use_boundaries' => false,
1627
+			'weight_order' => false,
1628
+			'query_mode' => false,
1629
+			'force_all_words' => false,
1630
+			'start_passage_id' => 1,
1631
+			'load_files' => false,
1632
+			'html_strip_mode' => 'index',
1633
+			'allow_empty' => false,
1634
+			'passage_boundary' => 'none',
1635
+			'emit_zones' => false,
1636
+			'load_files_scattered' => false
1637
+		), $opts);
1638
+
1639
+		/////////////////
1640
+		// build request
1641
+		/////////////////
1642
+
1643
+		// v.1.2 req
1644
+		$flags = 1; // remove spaces
1645
+		if ($opts['exact_phrase']) {
1646
+			$flags |= 2;
1647
+		}
1648
+		if ($opts['single_passage']) {
1649
+			$flags |= 4;
1650
+		}
1651
+		if ($opts['use_boundaries']) {
1652
+			$flags |= 8;
1653
+		}
1654
+		if ($opts['weight_order']) {
1655
+			$flags |= 16;
1656
+		}
1657
+		if ($opts['query_mode']) {
1658
+			$flags |= 32;
1659
+		}
1660
+		if ($opts['force_all_words']) {
1661
+			$flags |= 64;
1662
+		}
1663
+		if ($opts['load_files']) {
1664
+			$flags |= 128;
1665
+		}
1666
+		if ($opts['allow_empty']) {
1667
+			$flags |= 256;
1668
+		}
1669
+		if ($opts['emit_zones']) {
1670
+			$flags |= 512;
1671
+		}
1672
+		if ($opts['load_files_scattered']) {
1673
+			$flags |= 1024;
1674
+		}
1675
+		$req = pack('NN', 0, $flags); // mode=0, flags=$flags
1676
+		$req .= pack('N', strlen($index)) . $index; // req index
1677
+		$req .= pack('N', strlen($words)) . $words; // req words
1678
+
1679
+		// options
1680
+		$req .= pack('N', strlen($opts['before_match'])) . $opts['before_match'];
1681
+		$req .= pack('N', strlen($opts['after_match'])) . $opts['after_match'];
1682
+		$req .= pack('N', strlen($opts['chunk_separator'])) . $opts['chunk_separator'];
1683
+		$req .= pack('NN', (int)$opts['limit'], (int)$opts['around']);
1684
+		// v.1.2
1685
+		$req .= pack('NNN', (int)$opts['limit_passages'], (int)$opts['limit_words'], (int)$opts['start_passage_id']);
1686
+		$req .= pack('N', strlen($opts['html_strip_mode'])) . $opts['html_strip_mode'];
1687
+		$req .= pack('N', strlen($opts['passage_boundary'])) . $opts['passage_boundary'];
1688
+
1689
+		// documents
1690
+		$req .= pack('N', count($docs));
1691
+		foreach ($docs as $doc) {
1692
+			assert(is_string($doc));
1693
+			$req .= pack('N', strlen($doc)) . $doc;
1694
+		}
1695
+
1696
+		////////////////////////////
1697
+		// send query, get response
1698
+		////////////////////////////
1699
+
1700
+		$len = strlen($req);
1701
+		$req = pack('nnN', self::SEARCHD_COMMAND_EXCERPT, self::VER_COMMAND_EXCERPT, $len) . $req; // add header
1702
+		if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_EXCERPT))) {
1703
+			$this->mbPop();
1704
+			return false;
1705
+		}
1706
+
1707
+		//////////////////
1708
+		// parse response
1709
+		//////////////////
1710
+
1711
+		$pos = 0;
1712
+		$res = array();
1713
+		$rlen = strlen($response);
1714
+		$count = count($docs);
1715
+		while ($count--) {
1716
+			list(, $len) = unpack('N*', substr($response, $pos, 4));
1717
+			$pos += 4;
1718
+
1719
+			if ($pos + $len > $rlen) {
1720
+				$this->error = 'incomplete reply';
1721
+				$this->mbPop();
1722
+				return false;
1723
+			}
1724
+			$res[] = $len ? substr($response, $pos, $len) : '';
1725
+			$pos += $len;
1726
+		}
1727
+
1728
+		$this->mbPop();
1729
+		return $res;
1730
+	}
1731
+
1732
+
1733
+	/////////////////////////////////////////////////////////////////////////////
1734
+	// keyword generation
1735
+	/////////////////////////////////////////////////////////////////////////////
1736
+
1737
+	/**
1738
+	 * Connect to searchd server, and generate keyword list for a given query returns false on failure,
1739
+	 * an array of words on success
1740
+	 *
1741
+	 * @param string $query
1742
+	 * @param string $index
1743
+	 * @param bool $hits
1744
+	 *
1745
+	 * @return array|bool
1746
+	 */
1747
+	public function buildKeywords($query, $index, $hits)
1748
+	{
1749
+		assert(is_string($query));
1750
+		assert(is_string($index));
1751
+		assert(is_bool($hits));
1752
+
1753
+		$this->mbPush();
1754
+
1755
+		if (($fp = $this->connect()) === false) {
1756
+			$this->mbPop();
1757
+			return false;
1758
+		}
1759
+
1760
+		/////////////////
1761
+		// build request
1762
+		/////////////////
1763
+
1764
+		// v.1.0 req
1765
+		$req  = pack('N', strlen($query)) . $query; // req query
1766
+		$req .= pack('N', strlen($index)) . $index; // req index
1767
+		$req .= pack('N', (int)$hits);
1768
+
1769
+		////////////////////////////
1770
+		// send query, get response
1771
+		////////////////////////////
1772
+
1773
+		$len = strlen($req);
1774
+		$req = pack('nnN', self::SEARCHD_COMMAND_KEYWORDS, self::VER_COMMAND_KEYWORDS, $len) . $req; // add header
1775
+		if (!$this->send($fp, $req, $len + 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_KEYWORDS))) {
1776
+			$this->mbPop();
1777
+			return false;
1778
+		}
1779
+
1780
+		//////////////////
1781
+		// parse response
1782
+		//////////////////
1783
+
1784
+		$pos = 0;
1785
+		$res = array();
1786
+		$rlen = strlen($response);
1787
+		list(, $nwords) = unpack('N*', substr($response, $pos, 4));
1788
+		$pos += 4;
1789
+		for ($i = 0; $i < $nwords; $i++) {
1790
+			list(, $len) = unpack('N*', substr($response, $pos, 4));
1791
+			$pos += 4;
1792
+			$tokenized = $len ? substr($response, $pos, $len) : '';
1793
+			$pos += $len;
1794
+
1795
+			list(, $len) = unpack('N*', substr($response, $pos, 4));
1796
+			$pos += 4;
1797
+			$normalized = $len ? substr($response, $pos, $len) : '';
1798
+			$pos += $len;
1799
+
1800
+			$res[] = array(
1801
+				'tokenized' => $tokenized,
1802
+				'normalized' => $normalized
1803
+			);
1804
+
1805
+			if ($hits) {
1806
+				list($ndocs, $nhits) = array_values(unpack('N*N*', substr($response, $pos, 8)));
1807
+				$pos += 8;
1808
+				$res[$i]['docs'] = $ndocs;
1809
+				$res[$i]['hits'] = $nhits;
1810
+			}
1811
+
1812
+			if ($pos > $rlen) {
1813
+				$this->error = 'incomplete reply';
1814
+				$this->mbPop();
1815
+				return false;
1816
+			}
1817
+		}
1818
+
1819
+		$this->mbPop();
1820
+		return $res;
1821
+	}
1822
+
1823
+	/**
1824
+	 * @param string $string
1825
+	 *
1826
+	 * @return string
1827
+	 */
1828
+	public function escapeString($string)
1829
+	{
1830
+		$from = array('\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=', '<');
1831
+		$to   = array('\\\\', '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/', '\^', '\$', '\=', '\<');
1832
+
1833
+		return str_replace($from, $to, $string);
1834
+	}
1835
+
1836
+	/////////////////////////////////////////////////////////////////////////////
1837
+	// attribute updates
1838
+	/////////////////////////////////////////////////////////////////////////////
1839
+
1840
+	/**
1841
+	 * Batch update given attributes in given rows in given indexes
1842
+	 * Returns amount of updated documents (0 or more) on success, or -1 on failure
1843
+	 *
1844
+	 * @param string $index
1845
+	 * @param array $attrs
1846
+	 * @param array $values
1847
+	 * @param bool $mva
1848
+	 * @param bool $ignore_non_existent
1849
+	 *
1850
+	 * @return int
1851
+	 */
1852
+	public function updateAttributes($index, array $attrs, array $values, $mva = false, $ignore_non_existent = false)
1853
+	{
1854
+		// verify everything
1855
+		assert(is_string($index));
1856
+		assert(is_bool($mva));
1857
+		assert(is_bool($ignore_non_existent));
1858
+
1859
+		foreach ($attrs as $attr) {
1860
+			assert(is_string($attr));
1861
+		}
1862
+
1863
+		foreach ($values as $id => $entry) {
1864
+			assert(is_numeric($id));
1865
+			assert(is_array($entry));
1866
+			assert(count($entry) == count($attrs));
1867
+			foreach ($entry as $v) {
1868
+				if ($mva) {
1869
+					assert(is_array($v));
1870
+					foreach ($v as $vv) {
1871
+						assert(is_int($vv));
1872
+					}
1873
+				} else {
1874
+					assert(is_int($v));
1875
+				}
1876
+			}
1877
+		}
1878
+
1879
+		// build request
1880
+		$this->mbPush();
1881
+		$req = pack('N', strlen($index)) . $index;
1882
+
1883
+		$req .= pack('N', count($attrs));
1884
+		$req .= pack('N', $ignore_non_existent ? 1 : 0);
1885
+		foreach ($attrs as $attr) {
1886
+			$req .= pack('N', strlen($attr)) . $attr;
1887
+			$req .= pack('N', $mva ? 1 : 0);
1888
+		}
1889
+
1890
+		$req .= pack('N', count($values));
1891
+		foreach ($values as $id => $entry) {
1892
+			$req .= pack64IntUnsigned($id);
1893
+			foreach ($entry as $v) {
1894
+				$req .= pack('N', $mva ? count($v) : $v);
1895
+				if ($mva) {
1896
+					foreach ($v as $vv) {
1897
+						$req .= pack('N', $vv);
1898
+					}
1899
+				}
1900
+			}
1901
+		}
1902
+
1903
+		// connect, send query, get response
1904
+		if (($fp = $this->connect()) === false) {
1905
+			$this->mbPop();
1906
+			return -1;
1907
+		}
1908
+
1909
+		$len = strlen($req);
1910
+		$req = pack('nnN', self::SEARCHD_COMMAND_UPDATE, self::VER_COMMAND_UPDATE, $len) . $req; // add header
1911
+		if (!$this->send($fp, $req, $len + 8)) {
1912
+			$this->mbPop();
1913
+			return -1;
1914
+		}
1915
+
1916
+		if (!($response = $this->getResponse($fp, self::VER_COMMAND_UPDATE))) {
1917
+			$this->mbPop();
1918
+			return -1;
1919
+		}
1920
+
1921
+		// parse response
1922
+		list(, $updated) = unpack('N*', substr($response, 0, 4));
1923
+		$this->mbPop();
1924
+		return $updated;
1925
+	}
1926
+
1927
+	/////////////////////////////////////////////////////////////////////////////
1928
+	// persistent connections
1929
+	/////////////////////////////////////////////////////////////////////////////
1930
+
1931
+	/**
1932
+	 * @return bool
1933
+	 */
1934
+	public function open()
1935
+	{
1936
+		if ($this->socket !== false) {
1937
+			$this->error = 'already connected';
1938
+			return false;
1939
+		}
1940
+		if (($fp = $this->connect()) === false)
1941
+			return false;
1942
+
1943
+		// command, command version = 0, body length = 4, body = 1
1944
+		$req = pack('nnNN', self::SEARCHD_COMMAND_PERSIST, 0, 4, 1);
1945
+		if (!$this->send($fp, $req, 12)) {
1946
+			return false;
1947
+		}
1948
+
1949
+		$this->socket = $fp;
1950
+		return true;
1951
+	}
1952
+
1953
+	/**
1954
+	 * @return bool
1955
+	 */
1956
+	public function close()
1957
+	{
1958
+		if ($this->socket === false) {
1959
+			$this->error = 'not connected';
1960
+			return false;
1961
+		}
1962
+
1963
+		fclose($this->socket);
1964
+		$this->socket = false;
1965
+
1966
+		return true;
1967
+	}
1968
+
1969
+	//////////////////////////////////////////////////////////////////////////
1970
+	// status
1971
+	//////////////////////////////////////////////////////////////////////////
1972
+
1973
+	/**
1974
+	 * @param bool $session
1975
+	 *
1976
+	 * @return array|bool
1977
+	 */
1978
+	public function status($session = false)
1979
+	{
1980
+		assert(is_bool($session));
1981
+
1982
+		$this->mbPush();
1983
+		if (($fp = $this->connect()) === false) {
1984
+			$this->mbPop();
1985
+			return false;
1986
+		}
1987
+
1988
+		// len=4, body=1
1989
+		$req = pack('nnNN', self::SEARCHD_COMMAND_STATUS, self::VER_COMMAND_STATUS, 4, $session ? 0 : 1);
1990
+		if (!$this->send($fp, $req, 12) || !($response = $this->getResponse($fp, self::VER_COMMAND_STATUS))) {
1991
+			$this->mbPop();
1992
+			return false;
1993
+		}
1994
+
1995
+		$res = substr($response, 4); // just ignore length, error handling, etc
1996
+		$p = 0;
1997
+		list($rows, $cols) = array_values(unpack('N*N*', substr($response, $p, 8)));
1998
+		$p += 8;
1999
+
2000
+		$res = array();
2001
+		for ($i = 0; $i < $rows; $i++) {
2002
+			for ($j = 0; $j < $cols; $j++) {
2003
+				list(, $len) = unpack('N*', substr($response, $p, 4));
2004
+				$p += 4;
2005
+				$res[$i][] = substr($response, $p, $len);
2006
+				$p += $len;
2007
+			}
2008
+		}
2009
+
2010
+		$this->mbPop();
2011
+		return $res;
2012
+	}
2013
+
2014
+	//////////////////////////////////////////////////////////////////////////
2015
+	// flush
2016
+	//////////////////////////////////////////////////////////////////////////
2017
+
2018
+	/**
2019
+	 * @return int
2020
+	 */
2021
+	public function flushAttributes()
2022
+	{
2023
+		$this->mbPush();
2024
+		if (($fp = $this->connect()) === false) {
2025
+			$this->mbPop();
2026
+			return -1;
2027
+		}
2028
+
2029
+		$req = pack('nnN', self::SEARCHD_COMMAND_FLUSH_ATTRS, self::VER_COMMAND_FLUSH_ATTRS, 0); // len=0
2030
+		if (!$this->send($fp, $req, 8) || !($response = $this->getResponse($fp, self::VER_COMMAND_FLUSH_ATTRS))) {
2031
+			$this->mbPop();
2032
+			return -1;
2033
+		}
2034
+
2035
+		$tag = -1;
2036
+		if (strlen($response) == 4) {
2037
+			list(, $tag) = unpack('N*', $response);
2038
+		} else {
2039
+			$this->error = 'unexpected response length';
2040
+		}
2041
+
2042
+		$this->mbPop();
2043
+		return $tag;
2044
+	}
2045 2045
 }
Please login to merge, or discard this patch.