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