1<?php 2/** 3 * Modernized Sphinx Search API Client 4 * 5 * Updated for PHP 8.x compatibility 6 * @license GPL 2 7 */ 8 9/// known searchd commands 10const SEARCHD_COMMAND_SEARCH = 0; 11const SEARCHD_COMMAND_EXCERPT = 1; 12const SEARCHD_COMMAND_UPDATE = 2; 13const SEARCHD_COMMAND_KEYWORDS = 3; 14const SEARCHD_COMMAND_PERSIST = 4; 15const SEARCHD_COMMAND_STATUS = 5; 16const SEARCHD_COMMAND_QUERY = 6; 17 18/// current client-side command implementation versions 19const VER_COMMAND_SEARCH = 0x116; 20const VER_COMMAND_EXCERPT = 0x100; 21const VER_COMMAND_UPDATE = 0x102; 22const VER_COMMAND_KEYWORDS = 0x100; 23const VER_COMMAND_STATUS = 0x100; 24const VER_COMMAND_QUERY = 0x100; 25 26/// known searchd status codes 27const SEARCHD_OK = 0; 28const SEARCHD_ERROR = 1; 29const SEARCHD_RETRY = 2; 30const SEARCHD_WARNING = 3; 31 32/// known match modes 33const SPH_MATCH_ALL = 0; 34const SPH_MATCH_ANY = 1; 35const SPH_MATCH_PHRASE = 2; 36const SPH_MATCH_BOOLEAN = 3; 37const SPH_MATCH_EXTENDED = 4; 38const SPH_MATCH_FULLSCAN = 5; 39const SPH_MATCH_EXTENDED2 = 6; 40 41/// known ranking modes 42const SPH_RANK_PROXIMITY_BM25 = 0; 43const SPH_RANK_BM25 = 1; 44const SPH_RANK_NONE = 2; 45const SPH_RANK_WORDCOUNT = 3; 46const SPH_RANK_PROXIMITY = 4; 47const SPH_RANK_MATCHANY = 5; 48const SPH_RANK_FIELDMASK = 6; 49 50/// known sort modes 51const SPH_SORT_RELEVANCE = 0; 52const SPH_SORT_ATTR_DESC = 1; 53const SPH_SORT_ATTR_ASC = 2; 54const SPH_SORT_TIME_SEGMENTS = 3; 55const SPH_SORT_EXTENDED = 4; 56const SPH_SORT_EXPR = 5; 57 58/// known filter types 59const SPH_FILTER_VALUES = 0; 60const SPH_FILTER_RANGE = 1; 61const SPH_FILTER_FLOATRANGE = 2; 62 63/// known attribute types 64const SPH_ATTR_INTEGER = 1; 65const SPH_ATTR_TIMESTAMP = 2; 66const SPH_ATTR_ORDINAL = 3; 67const SPH_ATTR_BOOL = 4; 68const SPH_ATTR_FLOAT = 5; 69const SPH_ATTR_BIGINT = 6; 70const SPH_ATTR_MULTI = 0x40000000; 71 72/// known grouping functions 73const SPH_GROUPBY_DAY = 0; 74const SPH_GROUPBY_WEEK = 1; 75const SPH_GROUPBY_MONTH = 2; 76const SPH_GROUPBY_YEAR = 3; 77const SPH_GROUPBY_ATTR = 4; 78const SPH_GROUPBY_ATTRPAIR = 5; 79 80/** 81 * 64-bit integer packing helpers 82 */ 83function sphPackI64($v): string { 84 $v = (string)$v; 85 if (PHP_INT_SIZE >= 8) { 86 $v = (int)$v; 87 return pack("NN", $v >> 32, $v & 0xFFFFFFFF); 88 } 89 if (function_exists("bcmul")) { 90 if (bccomp($v, "0") == -1) $v = bcadd("18446744073709551616", $v); 91 $h = bcdiv($v, "4294967296", 0); 92 $l = bcmod($v, "4294967296"); 93 return pack("NN", (float)$h, (float)$l); 94 } 95 return pack("NN", 0, (int)$v); // Fallback for simple ints 96} 97 98function sphPackU64($v): string { 99 return sphPackI64($v); 100} 101 102function sphUnpackU64($v) { 103 $data = unpack("Nhi/Nlo", $v); 104 $hi = $data['hi']; 105 $lo = $data['lo']; 106 107 if (PHP_INT_SIZE >= 8) { 108 if ($hi < 0) $hi += (1 << 32); 109 if ($lo < 0) $lo += (1 << 32); 110 return ($hi << 32) + $lo; 111 } 112 113 $hi = sprintf("%u", $hi); 114 $lo = sprintf("%u", $lo); 115 if (function_exists("bcmul")) { 116 return bcadd($lo, bcmul($hi, "4294967296")); 117 } 118 return $lo; 119} 120 121function sphUnpackI64($v) { 122 return sphUnpackU64($v); 123} 124 125function sphFixUint($value) { 126 if (PHP_INT_SIZE >= 8) { 127 if ($value < 0) $value += (1 << 32); 128 return $value; 129 } 130 return sprintf("%u", $value); 131} 132 133class SphinxClient { 134 public $_host = "localhost"; 135 public $_port = 9312; 136 public $_offset = 0; 137 public $_limit = 20; 138 public $_mode = SPH_MATCH_ALL; 139 public $_weights = []; 140 public $_sort = SPH_SORT_RELEVANCE; 141 public $_sortby = ""; 142 public $_min_id = 0; 143 public $_max_id = 0; 144 public $_filters = []; 145 public $_groupby = ""; 146 public $_groupfunc = SPH_GROUPBY_DAY; 147 public $_groupsort = "@group desc"; 148 public $_groupdistinct = ""; 149 public $_maxmatches = 1000; 150 public $_cutoff = 0; 151 public $_retrycount = 0; 152 public $_retrydelay = 0; 153 public $_anchor = []; 154 public $_indexweights = []; 155 public $_ranker = SPH_RANK_PROXIMITY_BM25; 156 public $_maxquerytime = 0; 157 public $_fieldweights = []; 158 public $_overrides = []; 159 public $_select = "*"; 160 161 public $_error = ""; 162 public $_warning = ""; 163 public $_connerror = false; 164 165 public $_reqs = []; 166 public $_mbenc = ""; 167 public $_arrayresult = false; 168 public $_timeout = 0; 169 170 protected $_socket = false; 171 protected $_path = false; 172 173 public function __construct() { 174 // Initialized via property defaults 175 } 176 177 public function __destruct() { 178 if ($this->_socket !== false) { 179 fclose($this->_socket); 180 } 181 } 182 183 public function GetLastError(): string { 184 return $this->_error; 185 } 186 187 public function GetLastWarning(): string { 188 return $this->_warning; 189 } 190 191 public function IsConnectError(): bool { 192 return $this->_connerror; 193 } 194 195 public function SetServer(string $host, int $port = 0): void { 196 if ($host !== '' && $host[0] === '/') { 197 $this->_path = 'unix://' . $host; 198 return; 199 } 200 if (str_starts_with($host, "unix://")) { 201 $this->_path = $host; 202 return; 203 } 204 205 $this->_host = $host; 206 $this->_port = $port; 207 $this->_path = ''; 208 } 209 210 public function SetConnectTimeout($timeout): void { 211 $this->_timeout = (float)$timeout; 212 } 213 214 protected function _Send($handle, $data, $length): bool { 215 if (feof($handle) || fwrite($handle, $data, $length) !== $length) { 216 $this->_error = 'connection unexpectedly closed (timed out?)'; 217 $this->_connerror = true; 218 return false; 219 } 220 return true; 221 } 222 223 protected function _MBPush(): void { 224 $this->_mbenc = ""; 225 // mbstring.func_overload is removed in PHP 8.0, but we check for legacy 226 if (function_exists('mb_internal_encoding') && (ini_get("mbstring.func_overload") & 2)) { 227 $this->_mbenc = mb_internal_encoding(); 228 mb_internal_encoding("latin1"); 229 } 230 } 231 232 protected function _MBPop(): void { 233 if ($this->_mbenc !== "" && function_exists('mb_internal_encoding')) { 234 mb_internal_encoding($this->_mbenc); 235 } 236 } 237 238 protected function _Connect() { 239 if ($this->_socket !== false) { 240 if (!feof($this->_socket)) return $this->_socket; 241 $this->_socket = false; 242 } 243 244 $errno = 0; 245 $errstr = ""; 246 $this->_connerror = false; 247 248 $host = $this->_path ?: $this->_host; 249 $port = $this->_path ? 0 : $this->_port; 250 251 $fp = @fsockopen($host, $port, $errno, $errstr, $this->_timeout > 0 ? $this->_timeout : 5); 252 253 if (!$fp) { 254 $location = $this->_path ?: "{$this->_host}:{$this->_port}"; 255 $this->_error = "connection to $location failed (errno=$errno, msg=" . trim($errstr) . ")"; 256 $this->_connerror = true; 257 return false; 258 } 259 260 if (!$this->_Send($fp, pack("N", 1), 4)) { 261 fclose($fp); 262 $this->_error = "failed to send client protocol version"; 263 return false; 264 } 265 266 $vData = fread($fp, 4); 267 if (strlen($vData) < 4) { 268 fclose($fp); 269 $this->_error = "failed to read searchd version"; 270 return false; 271 } 272 $v = unpack("N", $vData)[1]; 273 if ($v < 1) { 274 fclose($fp); 275 $this->_error = "expected searchd protocol version 1+, got version '$v'"; 276 return false; 277 } 278 279 return $fp; 280 } 281 282 protected function _GetResponse($fp, $client_ver) { 283 $response = ""; 284 $header = fread($fp, 8); 285 if (strlen($header) == 8) { 286 $data = unpack("nstatus/nver/Nlen", $header); 287 $status = $data['status']; 288 $ver = $data['ver']; 289 $len = $data['len']; 290 $left = $len; 291 while ($left > 0 && !feof($fp)) { 292 $chunk = fread($fp, min($left, 8192)); 293 if ($chunk) { 294 $response .= $chunk; 295 $left -= strlen($chunk); 296 } 297 } 298 } else { 299 $status = SEARCHD_ERROR; $ver = 0; $len = 0; 300 } 301 302 if ($this->_socket === false) fclose($fp); 303 304 $read = strlen($response); 305 if (!$response && $len > 0) { 306 $this->_error = "received zero-sized searchd response"; 307 return false; 308 } 309 310 if ($status == SEARCHD_WARNING) { 311 $wlen = unpack("N", substr($response, 0, 4))[1]; 312 $this->_warning = substr($response, 4, $wlen); 313 return substr($response, 4 + $wlen); 314 } 315 if ($status == SEARCHD_ERROR || $status == SEARCHD_RETRY) { 316 $this->_error = "searchd error: " . substr($response, 4); 317 return false; 318 } 319 if ($status != SEARCHD_OK) { 320 $this->_error = "unknown status code '$status'"; 321 return false; 322 } 323 324 if ($ver < $client_ver) { 325 $this->_warning = sprintf("searchd command v.%d.%d older than client, some options might not work", $ver >> 8, $ver & 0xff); 326 } 327 328 return $response; 329 } 330 331 public function SetLimits(int $offset, int $limit, int $max = 0, int $cutoff = 0): void { 332 $this->_offset = $offset; 333 $this->_limit = $limit; 334 if ($max > 0) $this->_maxmatches = $max; 335 if ($cutoff > 0) $this->_cutoff = $cutoff; 336 } 337 338 public function SetMaxQueryTime(int $max): void { 339 $this->_maxquerytime = max(0, $max); 340 } 341 342 public function SetMatchMode(int $mode): void { 343 $this->_mode = $mode; 344 } 345 346 public function SetRankingMode(int $ranker): void { 347 $this->_ranker = $ranker; 348 } 349 350 public function SetSortMode(int $mode, string $sortby = ""): void { 351 $this->_sort = $mode; 352 $this->_sortby = $sortby; 353 } 354 355 public function SetFieldWeights(array $weights): void { 356 $this->_fieldweights = $weights; 357 } 358 359 public function SetIndexWeights(array $weights): void { 360 $this->_indexweights = $weights; 361 } 362 363 public function SetIDRange($min, $max): void { 364 $this->_min_id = $min; 365 $this->_max_id = $max; 366 } 367 368 public function SetFilter(string $attribute, array $values, bool $exclude = false): void { 369 if (!empty($values)) { 370 $this->_filters[] = ["type" => SPH_FILTER_VALUES, "attr" => $attribute, "exclude" => $exclude, "values" => $values]; 371 } 372 } 373 374 public function SetFilterRange(string $attribute, $min, $max, bool $exclude = false): void { 375 $this->_filters[] = ["type" => SPH_FILTER_RANGE, "attr" => $attribute, "exclude" => $exclude, "min" => $min, "max" => $max]; 376 } 377 378 public function SetFilterFloatRange(string $attribute, float $min, float $max, bool $exclude = false): void { 379 $this->_filters[] = ["type" => SPH_FILTER_FLOATRANGE, "attr" => $attribute, "exclude" => $exclude, "min" => $min, "max" => $max]; 380 } 381 382 public function SetGeoAnchor(string $attrlat, string $attrlong, float $lat, float $long): void { 383 $this->_anchor = ["attrlat" => $attrlat, "attrlong" => $attrlong, "lat" => $lat, "long" => $long]; 384 } 385 386 public function SetGroupBy(string $attribute, int $func, string $groupsort = "@group desc"): void { 387 $this->_groupby = $attribute; 388 $this->_groupfunc = $func; 389 $this->_groupsort = $groupsort; 390 } 391 392 public function SetGroupDistinct(string $attribute): void { 393 $this->_groupdistinct = $attribute; 394 } 395 396 public function SetRetries(int $count, int $delay = 0): void { 397 $this->_retrycount = $count; 398 $this->_retrydelay = $delay; 399 } 400 401 public function SetArrayResult(bool $arrayresult): void { 402 $this->_arrayresult = $arrayresult; 403 } 404 405 public function SetOverride(string $attrname, int $attrtype, array $values): void { 406 $this->_overrides[$attrname] = ["attr" => $attrname, "type" => $attrtype, "values" => $values]; 407 } 408 409 public function SetSelect(string $select): void { 410 $this->_select = $select; 411 } 412 413 public function ResetFilters(): void { 414 $this->_filters = []; 415 $this->_anchor = []; 416 } 417 418 public function ResetGroupBy(): void { 419 $this->_groupby = ""; 420 $this->_groupfunc = SPH_GROUPBY_DAY; 421 $this->_groupsort = "@group desc"; 422 $this->_groupdistinct = ""; 423 } 424 425 public function ResetOverrides(): void { 426 $this->_overrides = []; 427 } 428 429 public function Query(string $query, string $index = "*", string $comment = "") { 430 $this->_reqs = []; 431 $this->AddQuery($query, $index, $comment); 432 $results = $this->RunQueries(); 433 $this->_reqs = []; 434 435 if (!is_array($results) || !isset($results[0])) return false; 436 437 $this->_error = $results[0]["error"]; 438 $this->_warning = $results[0]["warning"]; 439 return ($results[0]["status"] == SEARCHD_ERROR) ? false : $results[0]; 440 } 441 442 protected function _PackFloat($f): string { 443 return pack("N", unpack("L", pack("f", (float)$f))[1]); 444 } 445 446 public function AddQuery(string $query, string $index = "*", string $comment = ""): int { 447 $this->_MBPush(); 448 449 $req = pack("NNNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker, $this->_sort); 450 $req .= pack("N", strlen($this->_sortby)) . $this->_sortby; 451 $req .= pack("N", strlen($query)) . $query; 452 $req .= pack("N", count($this->_weights)); 453 foreach ($this->_weights as $weight) $req .= pack("N", (int)$weight); 454 $req .= pack("N", strlen($index)) . $index; 455 $req .= pack("N", 1); 456 $req .= sphPackU64($this->_min_id) . sphPackU64($this->_max_id); 457 458 $req .= pack("N", count($this->_filters)); 459 foreach ($this->_filters as $filter) { 460 $req .= pack("N", strlen($filter["attr"])) . $filter["attr"]; 461 $req .= pack("N", $filter["type"]); 462 switch ($filter["type"]) { 463 case SPH_FILTER_VALUES: 464 $req .= pack("N", count($filter["values"])); 465 foreach ($filter["values"] as $value) $req .= sphPackI64($value); 466 break; 467 case SPH_FILTER_RANGE: 468 $req .= sphPackI64($filter["min"]) . sphPackI64($filter["max"]); 469 break; 470 case SPH_FILTER_FLOATRANGE: 471 $req .= $this->_PackFloat($filter["min"]) . $this->_PackFloat($filter["max"]); 472 break; 473 } 474 $req .= pack("N", (int)$filter["exclude"]); 475 } 476 477 $req .= pack("NN", $this->_groupfunc, strlen($this->_groupby)) . $this->_groupby; 478 $req .= pack("N", $this->_maxmatches); 479 $req .= pack("N", strlen($this->_groupsort)) . $this->_groupsort; 480 $req .= pack("NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay); 481 $req .= pack("N", strlen($this->_groupdistinct)) . $this->_groupdistinct; 482 483 if (empty($this->_anchor)) { 484 $req .= pack("N", 0); 485 } else { 486 $req .= pack("N", 1); 487 $req .= pack("N", strlen($this->_anchor["attrlat"])) . $this->_anchor["attrlat"]; 488 $req .= pack("N", strlen($this->_anchor["attrlong"])) . $this->_anchor["attrlong"]; 489 $req .= $this->_PackFloat($this->_anchor["lat"]) . $this->_PackFloat($this->_anchor["long"]); 490 } 491 492 $req .= pack("N", count($this->_indexweights)); 493 foreach ($this->_indexweights as $idx => $weight) $req .= pack("N", strlen($idx)) . $idx . pack("N", (int)$weight); 494 495 $req .= pack("N", $this->_maxquerytime); 496 497 $req .= pack("N", count($this->_fieldweights)); 498 foreach ($this->_fieldweights as $field => $weight) $req .= pack("N", strlen($field)) . $field . pack("N", (int)$weight); 499 500 $req .= pack("N", strlen($comment)) . $comment; 501 502 $req .= pack("N", count($this->_overrides)); 503 foreach ($this->_overrides as $entry) { 504 $req .= pack("N", strlen($entry["attr"])) . $entry["attr"]; 505 $req .= pack("NN", $entry["type"], count($entry["values"])); 506 foreach ($entry["values"] as $id => $val) { 507 $req .= sphPackU64($id); 508 switch ($entry["type"]) { 509 case SPH_ATTR_FLOAT: $req .= $this->_PackFloat($val); break; 510 case SPH_ATTR_BIGINT: $req .= sphPackI64($val); break; 511 default: $req .= pack("N", (int)$val); break; 512 } 513 } 514 } 515 516 $req .= pack("N", strlen($this->_select)) . $this->_select; 517 518 $this->_MBPop(); 519 $this->_reqs[] = $req; 520 return count($this->_reqs) - 1; 521 } 522 523 public function RunQueries() { 524 if (empty($this->_reqs)) { 525 $this->_error = "no queries defined, issue AddQuery() first"; 526 return false; 527 } 528 529 $this->_MBPush(); 530 if (!($fp = $this->_Connect())) { 531 $this->_MBPop(); 532 return false; 533 } 534 535 $nreqs = count($this->_reqs); 536 $req = implode("", $this->_reqs); 537 $len = 4 + strlen($req); 538 $req = pack("nnNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, $nreqs) . $req; 539 540 if (!$this->_Send($fp, $req, $len + 8) || !($response = $this->_GetResponse($fp, VER_COMMAND_SEARCH))) { 541 $this->_MBPop(); 542 return false; 543 } 544 545 $this->_reqs = []; 546 $res = $this->_ParseSearchResponse($response, $nreqs); 547 $this->_MBPop(); 548 return $res; 549 } 550 551 protected function _ParseSearchResponse($response, $nreqs): array { 552 $p = 0; 553 $max = strlen($response); 554 $results = []; 555 556 for ($ires = 0; $ires < $nreqs && $p < $max; $ires++) { 557 $result = ["error" => "", "warning" => ""]; 558 $status = unpack("N", substr($response, $p, 4))[1]; $p += 4; 559 $result["status"] = $status; 560 561 if ($status != SEARCHD_OK) { 562 $len = unpack("N", substr($response, $p, 4))[1]; $p += 4; 563 $message = substr($response, $p, $len); $p += $len; 564 if ($status == SEARCHD_WARNING) $result["warning"] = $message; 565 else { $result["error"] = $message; $results[] = $result; continue; } 566 } 567 568 $fields = []; 569 $nfields = unpack("N", substr($response, $p, 4))[1]; $p += 4; 570 while ($nfields-- > 0 && $p < $max) { 571 $len = unpack("N", substr($response, $p, 4))[1]; $p += 4; 572 $fields[] = substr($response, $p, $len); $p += $len; 573 } 574 $result["fields"] = $fields; 575 576 $attrs = []; 577 $nattrs = unpack("N", substr($response, $p, 4))[1]; $p += 4; 578 while ($nattrs-- > 0 && $p < $max) { 579 $len = unpack("N", substr($response, $p, 4))[1]; $p += 4; 580 $attr = substr($response, $p, $len); $p += $len; 581 $type = unpack("N", substr($response, $p, 4))[1]; $p += 4; 582 $attrs[$attr] = $type; 583 } 584 $result["attrs"] = $attrs; 585 586 $count = unpack("N", substr($response, $p, 4))[1]; $p += 4; 587 $id64 = unpack("N", substr($response, $p, 4))[1]; $p += 4; 588 589 $result["matches"] = []; 590 while ($count-- > 0 && $p < $max) { 591 if ($id64) { 592 $doc = sphUnpackU64(substr($response, $p, 8)); $p += 8; 593 $weight = unpack("N", substr($response, $p, 4))[1]; $p += 4; 594 } else { 595 $matchData = unpack("Ndoc/Nweight", substr($response, $p, 8)); $p += 8; 596 $doc = sphFixUint($matchData['doc']); 597 $weight = $matchData['weight']; 598 } 599 600 $attrvals = []; 601 foreach ($attrs as $attr => $type) { 602 if ($type == SPH_ATTR_BIGINT) { 603 $attrvals[$attr] = sphUnpackI64(substr($response, $p, 8)); $p += 8; 604 } elseif ($type == SPH_ATTR_FLOAT) { 605 $uval = unpack("N", substr($response, $p, 4))[1]; $p += 4; 606 $attrvals[$attr] = unpack("f", pack("L", $uval))[1]; 607 } else { 608 $val = unpack("N", substr($response, $p, 4))[1]; $p += 4; 609 if ($type & SPH_ATTR_MULTI) { 610 $vals = []; 611 $nvals = $val; 612 while ($nvals-- > 0 && $p < $max) { 613 $vals[] = sphFixUint(unpack("N", substr($response, $p, 4))[1]); 614 $p += 4; 615 } 616 $attrvals[$attr] = $vals; 617 } else { 618 $attrvals[$attr] = sphFixUint($val); 619 } 620 } 621 } 622 623 if ($this->_arrayresult) $result["matches"][] = ["id" => $doc, "weight" => $weight, "attrs" => $attrvals]; 624 else $result["matches"][$doc] = ["weight" => $weight, "attrs" => $attrvals]; 625 } 626 627 $stats = unpack("Ntotal/Nfound/Nmsecs/Nwords", substr($response, $p, 16)); $p += 16; 628 $result["total"] = sprintf("%u", $stats['total']); 629 $result["total_found"] = sprintf("%u", $stats['found']); 630 $result["time"] = sprintf("%.3f", $stats['msecs'] / 1000); 631 632 $nwords = $stats['words']; 633 while ($nwords-- > 0 && $p < $max) { 634 $len = unpack("N", substr($response, $p, 4))[1]; $p += 4; 635 $word = substr($response, $p, $len); $p += $len; 636 $wstats = unpack("Ndocs/Nhits", substr($response, $p, 8)); $p += 8; 637 $result["words"][$word] = ["docs" => sprintf("%u", $wstats['docs']), "hits" => sprintf("%u", $wstats['hits'])]; 638 } 639 $results[] = $result; 640 } 641 return $results; 642 } 643 644 public function BuildExcerpts(array $docs, string $index, string $words, array $opts = []) { 645 $this->_MBPush(); 646 if (!($fp = $this->_Connect())) { $this->_MBPop(); return false; } 647 648 $opts += [ 649 "before_match" => "<b>", "after_match" => "</b>", "chunk_separator" => " ... ", 650 "limit" => 256, "around" => 5, "exact_phrase" => false, "single_passage" => false, 651 "use_boundaries" => false, "weight_order" => false 652 ]; 653 654 $flags = 1; 655 if ($opts["exact_phrase"]) $flags |= 2; 656 if ($opts["single_passage"]) $flags |= 4; 657 if ($opts["use_boundaries"]) $flags |= 8; 658 if ($opts["weight_order"]) $flags |= 16; 659 660 $req = pack("NN", 0, $flags); 661 $req .= pack("N", strlen($index)) . $index; 662 $req .= pack("N", strlen($words)) . $words; 663 $req .= pack("N", strlen($opts["before_match"])) . $opts["before_match"]; 664 $req .= pack("N", strlen($opts["after_match"])) . $opts["after_match"]; 665 $req .= pack("N", strlen($opts["chunk_separator"])) . $opts["chunk_separator"]; 666 $req .= pack("N", (int)$opts["limit"]); 667 $req .= pack("N", (int)$opts["around"]); 668 $req .= pack("N", count($docs)); 669 foreach ($docs as $doc) $req .= pack("N", strlen($doc)) . $doc; 670 671 $len = strlen($req); 672 $req = pack("nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len) . $req; 673 if (!$this->_Send($fp, $req, $len + 8) || !($response = $this->_GetResponse($fp, VER_COMMAND_EXCERPT))) { 674 $this->_MBPop(); return false; 675 } 676 677 $pos = 0; $res = []; 678 foreach ($docs as $unused) { 679 $len = unpack("N", substr($response, $pos, 4))[1]; $pos += 4; 680 $res[] = substr($response, $pos, $len); $pos += $len; 681 } 682 $this->_MBPop(); 683 return $res; 684 } 685 686 public function BuildKeywords(string $query, string $index, bool $hits) { 687 $this->_MBPush(); 688 if (!($fp = $this->_Connect())) { $this->_MBPop(); return false; } 689 690 $req = pack("N", strlen($query)) . $query; 691 $req .= pack("N", strlen($index)) . $index; 692 $req .= pack("N", (int)$hits); 693 694 $len = strlen($req); 695 $req = pack("nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len) . $req; 696 if (!$this->_Send($fp, $req, $len + 8) || !($response = $this->_GetResponse($fp, VER_COMMAND_KEYWORDS))) { 697 $this->_MBPop(); return false; 698 } 699 700 $pos = 0; $res = []; 701 $nwords = unpack("N", substr($response, $pos, 4))[1]; $pos += 4; 702 for ($i = 0; $i < $nwords; $i++) { 703 $len = unpack("N", substr($response, $pos, 4))[1]; $pos += 4; 704 $tokenized = substr($response, $pos, $len); $pos += $len; 705 $len = unpack("N", substr($response, $pos, 4))[1]; $pos += 4; 706 $normalized = substr($response, $pos, $len); $pos += $len; 707 $entry = ["tokenized" => $tokenized, "normalized" => $normalized]; 708 if ($hits) { 709 $wstats = unpack("Ndocs/Nhits", substr($response, $pos, 8)); $pos += 8; 710 $entry["docs"] = $wstats['docs']; $entry["hits"] = $wstats['hits']; 711 } 712 $res[] = $entry; 713 } 714 $this->_MBPop(); 715 return $res; 716 } 717 718 public function EscapeString(string $string): string { 719 $chars = ['\\', '(', ')', '|', '-', '!', '@', '~', '"', '&', '/', '^', '$', '=']; 720 $replacements = array_map(fn($c) => '\\' . $c, $chars); 721 return str_replace($chars, $replacements, $string); 722 } 723 724 public function UpdateAttributes(string $index, array $attrs, array $values, bool $mva = false): int { 725 $req = pack("N", strlen($index)) . $index; 726 $req .= pack("N", count($attrs)); 727 foreach ($attrs as $attr) { 728 $req .= pack("N", strlen($attr)) . $attr; 729 $req .= pack("N", (int)$mva); 730 } 731 $req .= pack("N", count($values)); 732 foreach ($values as $id => $entry) { 733 $req .= sphPackU64($id); 734 foreach ($entry as $v) { 735 $req .= pack("N", $mva ? count($v) : (int)$v); 736 if ($mva) foreach ($v as $vv) $req .= pack("N", (int)$vv); 737 } 738 } 739 740 if (!($fp = $this->_Connect())) return -1; 741 $len = strlen($req); 742 $req = pack("nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len) . $req; 743 if (!$this->_Send($fp, $req, $len + 8) || !($response = $this->_GetResponse($fp, VER_COMMAND_UPDATE))) return -1; 744 745 return unpack("N", substr($response, 0, 4))[1]; 746 } 747 748 public function Status() { 749 $this->_MBPush(); 750 if (!($fp = $this->_Connect())) { $this->_MBPop(); return false; } 751 $req = pack("nnNN", SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, 1); 752 if (!$this->_Send($fp, $req, 12) || !($response = $this->_GetResponse($fp, VER_COMMAND_STATUS))) { 753 $this->_MBPop(); return false; 754 } 755 756 $p = 4; 757 $stats = unpack("Nrows/Ncols", substr($response, $p, 8)); $p += 8; 758 $rows = $stats['rows']; $cols = $stats['cols']; 759 $res = []; 760 for ($i = 0; $i < $rows; $i++) { 761 for ($j = 0; $j < $cols; $j++) { 762 $len = unpack("N", substr($response, $p, 4))[1]; $p += 4; 763 $res[$i][] = substr($response, $p, $len); $p += $len; 764 } 765 } 766 $this->_MBPop(); 767 return $res; 768 } 769} 770