yf_sphinxsearch classSphinx search querying.
| File | plugins/search/classes/yf_sphinxsearch.class.php |
|---|---|
| Use | _class('sphinxsearch') |
A site sets them in its config with $PROJECT_CONF['sphinxsearch']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$CACHE_TTL |
300 |
|
$CACHE_WRITESbool |
true |
false: cache only reads (SELECT, SHOW, CALL, DESCRIBE); true caches writes too, so a repeated INSERT / DELETE within CACHE_TTL is skipped. Default flips in 4.0 |
$CONNECT_TIMEOUT |
2 |
In seconds |
$DEF_PORT |
'9306' |
|
$EMPTY_RESULTS_LOG_PATH |
'' |
|
$HOST |
'127.0.0.1:9306' |
Host:port like this: 127.0.0.1:9306 |
$QUERY_RETRY_ERROR_CODES |
[2003, 2006, 2013, 2020] |
Lis of mysql client library errors, returned from sphinxsearch server, when we can retry query 2003 - Can't connect to MySQL server on 2006 - MySQL server has gone away 2013 - Lost connection to MySQL server during query 2020 - Got packet bigger than 'max_allowed_packet' bytes. |
$QUERY_RETRY_SLEEP |
1000000 |
In milli-seconds (1 second == 1000000) |
$RECONNECT_SLEEP |
1000000 |
In milli-seconds (1 second == 1000000) |
$RECONNECT_TRIES |
2 |
Just counter, set to 0 to disable reconnect tries |
$_connects_tried |
null |
public function escape_string($string)
MATCH-syntax escaping only: inside a '...' SQL string the result still needs literal escaping (_es() / mysqli_real_escape_string()), because the literal parser drops the backslash of \( etc.; the extended-query operators < [ ] { } ? * % are not escaped.
public function escape_string($string)
{
$from = ['\\', '(', ')', '|', '-', '!', '@', '~', '"', '&', '/', '^', '$', '=', "'"];
$to = ['\\\\', '\(', '\)', '\|', '\-', '\!', '\@', '\~', '\"', '\&', '\/', '\^', '\$', '\=', "\\'"];
return str_replace($from, $to, $string);
}
public function execute($sql)
One SphinxQL statement without the cache, the empty-results log, globals or warnings (the transport of the search drivers): ['rows' => first result set, 'sets' => later result sets (FACET), 'affected' => rows written, 'error' => '', 'errno' => 0]. Nothing is escaped: trusted SQL only, never request data. A read (_is_read()) that lost the link (2006 / 2013) reconnects and runs once more; a write never runs twice.
Returns array{rows: array, sets: array, affected: int, error: string, errno: int}
public function execute($sql)
{
$sql = (string) $sql;
$out = ['rows' => [], 'sets' => [], 'affected' => 0, 'error' => '', 'errno' => 0];
if (trim($sql) === '') {
$out['error'] = 'empty query';
return $out;
}
$time = microtime(true);
if ( ! isset($this->sphinx_connection)) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
$out['error'] = $this->_sphinx_error() ?: 'no connection to sphinx at ' . $this->HOST;
$out['errno'] = $this->_sphinx_errno();
return $out;
}
$q = $this->_sphinx_query($sql);
if ($q === false && in_array($this->_sphinx_errno(), [2006, 2013], true) && $this->_is_read($sql)) {
$this->sphinx_connection = null;
$this->_connects_tried = 0;
if ($this->_connect()) {
$q = $this->_sphinx_query($sql);
}
}
if ($q instanceof mysqli_result) {
$out['rows'] = mysqli_fetch_all($q, MYSQLI_ASSOC);
mysqli_free_result($q);
} elseif ($q === true) {
$out['affected'] = max(0, (int) mysqli_affected_rows($this->sphinx_connection));
}
$out['sets'] = $this->_sphinx_extra_sets;
$out['error'] = $this->_sphinx_error();
$out['errno'] = $this->_sphinx_errno();
if (DEBUG_MODE) {
$this->_set_query_debug([
'query' => $sql,
'time' => $time,
'trace' => main()->trace_string(),
'meta' => [],
'error' => $out['error'],
'warnings' => [],
'describe' => [],
'results' => $out['rows'],
'cached' => 0,
'time_wo_cache' => microtime(true) - $time,
]);
}
return $out;
}
public function query($sql, $need_meta = false)
Sphinx QL query wrapper.
public function query($sql, $need_meta = false)
{
if (empty($sql)) {
return false;
}
$trace = null;
$time = null;
if (DEBUG_MODE) {
$trace = main()->trace_string();
$time = microtime(true);
}
$data = null;
$CACHE_NAME = 'SPHINX_' . md5($sql);
$use_cache = $this->_is_cacheable($sql);
$cached = $use_cache ? cache_get($CACHE_NAME) : null;
if ($cached) {
list($data, $meta, $warnings, $query_error, $describe, $time_wo_cache) = $cached;
$facets = $cached[6] ?? [];
if (DEBUG_MODE) {
$this->_set_query_debug([
'query' => $sql,
'time' => $time,
'trace' => $trace,
'meta' => $meta,
'warnings' => $warnings,
'describe' => $describe,
'error' => $query_error,
'results' => $data,
'cached' => 1,
'time_wo_cache' => $time_wo_cache,
]);
}
$GLOBALS['_SPHINX_META'] = $meta;
$GLOBALS['_SPHINX_FACETS'] = $facets;
return $data;
}
$results = [];
$facets = [];
$query_error = '';
$q_error_num = '';
if ( ! isset($this->sphinx_connection)) {
$this->_connect();
}
if ($this->sphinx_connection) {
$q = $this->_sphinx_query($sql);
if ( ! $q) {
$query_error = $this->_sphinx_error();
$q_error_num = $this->_sphinx_errno();
// Try to execute query again in case of these errors returned; a write may have run before the link dropped
if (in_array($q_error_num, $this->QUERY_RETRY_ERROR_CODES) && $this->_is_read($sql)) {
usleep($this->QUERY_RETRY_SLEEP);
$q = $this->_sphinx_query($sql);
if ( ! $q) {
$query_error = $this->_sphinx_error();
$q_error_num = $this->_sphinx_errno();
}
}
if ($query_error) {
trigger_error('Sphinx error: ' . $query_error . '; for query: ' . $sql, E_USER_WARNING);
conf('http_headers::X-Details', conf('http_headers::X-Details') . ';SE=(' . $q_error_num . ') ' . $query_error . ';');
}
}
// true for INSERT / REPLACE / UPDATE / DELETE: no rows to fetch
if ($q instanceof mysqli_result) {
while ($a = mysqli_fetch_assoc($q)) {
$results[] = $a;
}
$facets = $this->_sphinx_extra_sets;
if (count((array) $results) == 0) {
$this->_save_empty_results_log($sql);
}
}
$GLOBALS['_SPHINX_FACETS'] = $facets;
$meta = [];
$warnings = [];
$describe = [];
if ($need_meta || DEBUG_MODE) {
$meta = $this->_get_latest_meta();
$warnings = $this->_get_latest_warnings();
$describe = $this->_get_latest_describe($sql);
}
$time_wo_cache = 0;
if (DEBUG_MODE) {
$this->_set_query_debug([
'query' => $sql,
'time' => $time,
'trace' => $trace,
'meta' => $meta,
'error' => $query_error,
'warnings' => $warnings,
'describe' => $describe,
'results' => $results,
]);
$time_wo_cache = microtime(true) - $time;
}
}
if ($use_cache && empty($query_error) && $this->sphinx_connection) {
cache_set($CACHE_NAME, [$results, $meta, $warnings, $query_error, $describe, $time_wo_cache, $facets], $this->CACHE_TTL);
}
return $results;
}
_ hooks and helpers; not routable as page actionspublic function __call($name, $args)
Catch missing method call.
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args);
}
public function _connect()
public function _connect()
{
if (isset($this->sphinx_connection)) {
return $this->sphinx_connection;
}
if ($this->_connects_tried > $this->RECONNECT_TRIES) {
return false;
}
$time = null;
if (DEBUG_MODE) {
$time = microtime(true);
}
$this->sphinx_connection = $this->_sphinx_connect();
$this->_connects_tried++;
// Try to reconnect
if ( ! $this->sphinx_connection && $this->RECONNECT_TRIES) {
for ($i = 0; $i < $this->RECONNECT_TRIES; $i++) {
usleep($this->RECONNECT_SLEEP);
$this->sphinx_connection = $this->_sphinx_connect();
$this->_connects_tried++;
}
}
$query_error = null;
if ( ! $this->sphinx_connection) {
$query_error = $this->_sphinx_error();
$q_error_num = $this->_sphinx_errno();
conf('http_headers::X-Details', conf('http_headers::X-Details') . ';SE=(' . $q_error_num . ') ' . $query_error . ';');
trigger_error('No connection to sphinx', E_USER_WARNING);
}
if (DEBUG_MODE) {
$this->_set_query_debug([
'query' => 'sphinx connect',
'time' => $time,
'error' => $query_error,
]);
}
return $this->sphinx_connection;
}
public function _get_host()
public function _get_host()
{
return $this->HOST;
}
public function _get_latest_describe($sql = '')
public function _get_latest_describe($sql = '')
{
if ( ! $this->sphinx_connection) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
return false;
}
$describe = [];
if (preg_match('/SELECT[\s\t]+.+[\s\t]+FROM[\s\t]+([a-z0-9\_]+)[\s\t]+WHERE[\s\t]+/ims', $sql, $m)) {
$describe_sql = 'DESCRIBE ' . $m[1];
$q = $this->_sphinx_query('DESCRIBE ' . $m[1]);
if ( ! is_bool($q)) {
while ($a = mysqli_fetch_row($q)) {
$describe[$a[0]] = $a[1];
}
}
}
return $describe;
}
public function _get_latest_meta()
Get latest query internal details.
public function _get_latest_meta()
{
if ( ! $this->sphinx_connection) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
return false;
}
$q = $this->_sphinx_query('SHOW META');
$meta = null;
if ( ! is_bool($q)) {
while ($a = mysqli_fetch_row($q)) {
$meta[$a[0]] = $a[1];
}
}
$GLOBALS['_SPHINX_META'] = $meta;
return $meta;
}
public function _get_latest_warnings()
Get latest query warnings.
public function _get_latest_warnings()
{
if ( ! $this->sphinx_connection) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
return false;
}
$q = $this->_sphinx_query('SHOW WARNINGS');
$warnings = null;
if ( ! is_bool($q)) {
while ($a = mysqli_fetch_row($q)) {
$warnings[$a[0]] = $a[1];
}
}
$GLOBALS['_SPHINX_WARNINGS'] = $warnings;
return $warnings;
}
public function _get_server_status()
public function _get_server_status()
{
if ( ! $this->sphinx_connection) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
return false;
}
$status = [];
$q = $this->_sphinx_query('SHOW STATUS');
if ( ! is_bool($q)) {
while ($a = mysqli_fetch_row($q)) {
$status[$a[0]] = $a[1];
}
}
return $status;
}
public function _get_server_version()
public function _get_server_version()
{
if ( ! $this->sphinx_connection) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
return false;
}
return $this->sphinx_connection ? mysqli_get_server_info($this->sphinx_connection) : '';
}
public function _init()
public function _init()
{
// SPHINX_HOST can contain port or not, both variants supported
if (defined('SPHINX_HOST')) {
$this->HOST = SPHINX_HOST;
if (false === strpos($this->HOST, ':')) {
$this->HOST .= ':' . (defined('SPHINX_PORT') ? SPHINX_PORT : $this->DEF_PORT);
}
}
if ( ! $this->EMPTY_RESULTS_LOG_PATH && common()->SPHINX_EMPTY_LOG_PATH) {
$this->EMPTY_RESULTS_LOG_PATH = common()->SPHINX_EMPTY_LOG_PATH;
}
}
public function _is_cacheable($sql)
$sql stringReturns bool
public function _is_cacheable($sql)
{
if ($this->CACHE_WRITES) {
return true;
}
return $this->_is_read($sql);
}
public function _is_read($sql)
$sql stringReturns bool
public function _is_read($sql)
{
return (bool) preg_match('/^\s*(SELECT|SHOW|CALL|DESCRIBE)\b/i', (string) $sql);
}
public function _save_empty_results_log($sql)
public function _save_empty_results_log($sql)
{
if ( ! $this->EMPTY_RESULTS_LOG_PATH) {
return false;
}
$out = implode('#|#', [
date('YmdH'),
conf('CUR_DOMAIN_SHORT'),
_es($_SERVER['HTTP_REFERER'] ?? ''),
_es(($_SERVER['HTTP_HOST'] ?? '') . ($_SERVER['REQUEST_URI'] ?? '')),
_es($_SERVER['HTTP_USER_AGENT'] ?? ''),
_es($sql),
]) . PHP_EOL;
return file_put_contents($this->EMPTY_RESULTS_LOG_PATH, $out, FILE_APPEND);
}
public function _set_query_debug($a)
public function _set_query_debug($a)
{
debug('sphinxsearch[]', [
'host' => $this->HOST,
'query' => $a['query'],
'results' => $a['results'] ?? null,
'count' => is_array($a['results'] ?? null) ? count($a['results']) : '',
'meta' => $a['meta'] ?? [],
'error' => $a['error'] ?? null,
'warnings' => $a['warnings'] ?? [],
'describe' => $a['describe'] ?? [],
'trace' => ($a['trace'] ?? null) ?: main()->trace_string(),
'cached' => (int) ($a['cached'] ?? 0),
'time' => round(microtime(true) - $a['time'], 5),
'time_wo_cache' => ($a['time_wo_cache'] ?? 0) ? round($a['time_wo_cache'], 5) : '',
]);
}