plugins/search/classes/yf_sphinxsearch.class.php
<?php
/**
* Sphinx search querying.
*/
class yf_sphinxsearch
{
/** In seconds */
private $CONNECT_TIMEOUT = 2;
/** Just counter, set to 0 to disable reconnect tries */
private $RECONNECT_TRIES = 2;
/** In milli-seconds (1 second == 1000000) */
private $RECONNECT_SLEEP = 1000000;
/** In milli-seconds (1 second == 1000000) */
private $QUERY_RETRY_SLEEP = 1000000;
/**
* 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.
*/
private $QUERY_RETRY_ERROR_CODES = [2003, 2006, 2013, 2020];
/** [Resource] or null */
private $sphinx_connection = null;
/** Host:port like this: 127.0.0.1:9306 */
private $HOST = '127.0.0.1:9306';
/***/
private $DEF_PORT = '9306';
/***/
private $EMPTY_RESULTS_LOG_PATH = '';
/***/
private $CACHE_TTL = 300;
/**
* Catch missing method call.
* @param mixed $name
* @param mixed $args
*/
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args);
}
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_HOST') ? 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;
}
}
/**
* Sphinx QL query wrapper.
* @param mixed $sql
* @param mixed $need_meta
*/
public function query($sql, $need_meta = false)
{
if (empty($sql)) {
return false;
}
if (DEBUG_MODE) {
$trace = main()->trace_string();
$time = microtime(true);
}
$data = null;
$CACHE_NAME = 'SPHINX_' . md5($sql);
$cached = cache_get($CACHE_NAME);
if ($cached) {
list($data, $meta, $warnings, $query_error, $describe, $time_wo_cache) = $cached;
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;
return $data;
}
$results = [];
$query_error = '';
$q_error_num = '';
if ( ! isset($this->sphinx_connection)) {
$this->_connect();
}
if ($this->sphinx_connection) {
$q = mysql_query($sql, $this->sphinx_connection);
if ( ! $q) {
$query_error = mysql_error($this->sphinx_connection);
$q_error_num = mysql_errno($this->sphinx_connection);
// Try to execute query again in case of these errors returned:
if (in_array($q_error_num, $this->QUERY_RETRY_ERROR_CODES)) {
usleep($this->QUERY_RETRY_SLEEP);
$q = mysql_query($sql, $this->sphinx_connection);
if ( ! $q) {
$query_error = mysql_error($this->sphinx_connection);
$q_error_num = mysql_errno($this->sphinx_connection);
}
}
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 . ';');
}
}
if ($q) {
while ($a = mysql_fetch_assoc($q)) {
$results[] = $a;
}
if (count((array) $results) == 0) {
$this->_save_empty_results_log($sql);
}
}
$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 (empty($query_error) && $this->sphinx_connection) {
cache_set($CACHE_NAME, [$results, $meta, $warnings, $query_error, $describe, $time_wo_cache], $this->CACHE_TTL);
}
return $results;
}
public function _connect()
{
if (isset($this->sphinx_connection)) {
return $this->sphinx_connection;
}
if ($this->_connects_tried > $this->RECONNECT_TRIES) {
return false;
}
if (DEBUG_MODE) {
$time = microtime(true);
}
// For sphinxsearch we need small connect timeout, also save default for later restore
if ($this->CONNECT_TIMEOUT) {
$orig_connect_timeout = ini_get('mysql.connect_timeout');
if ($orig_connect_timeout != $this->CONNECT_TIMEOUT) {
ini_set('mysql.connect_timeout', $this->CONNECT_TIMEOUT);
}
}
$this->sphinx_connection = mysql_connect($this->HOST, '', '', $new_link = true);
$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 = mysql_connect($this->HOST, '', '', $new_link = true);
$this->_connects_tried++;
}
}
if ( ! $this->sphinx_connection) {
$query_error = mysql_error($this->sphinx_connection);
$q_error_num = mysql_errno($this->sphinx_connection);
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,
]);
}
// Revert default mysql connect timeout
if ($this->CONNECT_TIMEOUT && $orig_connect_timeout != $this->CONNECT_TIMEOUT) {
ini_set('mysql.connect_timeout', $orig_connect_timeout);
}
return $this->sphinx_connection;
}
/**
* @param mixed $string
*/
public function escape_string($string)
{
$from = ['\\', '(', ')', '|', '-', '!', '@', '~', '"', '&', '/', '^', '$', '='];
$to = ['\\\\', '\(', '\)', '\|', '\-', '\!', '\@', '\~', '\"', '\&', '\/', '\^', '\$', '\='];
return str_replace($from, $to, $string);
}
/**
* Get latest query internal details.
*/
public function _get_latest_meta()
{
if ( ! $this->sphinx_connection) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
return false;
}
$q = mysql_query('SHOW META', $this->sphinx_connection);
if ( ! is_bool($q)) {
while ($a = mysql_fetch_row($q)) {
$meta[$a[0]] = $a[1];
}
}
$GLOBALS['_SPHINX_META'] = $meta;
return $meta;
}
/**
* Get latest query warnings.
*/
public function _get_latest_warnings()
{
if ( ! $this->sphinx_connection) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
return false;
}
$q = mysql_query('SHOW WARNINGS', $this->sphinx_connection);
if ( ! is_bool($q)) {
while ($a = mysql_fetch_row($q)) {
$warnings[$a[0]] = $a[1];
}
}
$GLOBALS['_SPHINX_WARNINGS'] = $warnings;
return $warnings;
}
/**
* @param mixed $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 = mysql_query('DESCRIBE ' . $m[1], $this->sphinx_connection);
if ( ! is_bool($q)) {
while ($a = mysql_fetch_row($q)) {
$describe[$a[0]] = $a[1];
}
}
}
return $describe;
}
public function _get_server_status()
{
if ( ! $this->sphinx_connection) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
return false;
}
$status = [];
$q = mysql_query('SHOW STATUS', $this->sphinx_connection);
if ( ! is_bool($q)) {
while ($a = mysql_fetch_row($q)) {
$status[$a[0]] = $a[1];
}
}
return $status;
}
public function _get_server_version()
{
if ( ! $this->sphinx_connection) {
$this->_connect();
}
if ( ! $this->sphinx_connection) {
return false;
}
return mysql_get_server_info($this->sphinx_connection);
}
public function _get_host()
{
return $this->HOST;
}
/**
* @param mixed $sql
*/
public function _save_empty_results_log($sql)
{
if ( ! $this->EMPTY_RESULTS_LOG_PATH) {
return false;
}
$out = implode('#|#', [
date('YmdH'),
conf('CUR_DOMAIN_SHORT'),
common()->_db_escape($_SERVER['HTTP_REFERER']),
common()->_db_escape($_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']),
common()->_db_escape($_SERVER['HTTP_USER_AGENT']),
common()->_db_escape($sql),
]) . PHP_EOL;
return file_put_contents($this->EMPTY_RESULTS_LOG_PATH, $out, FILE_APPEND);
}
/**
* @param mixed $a
*/
public function _set_query_debug($a)
{
debug('sphinxsearch[]', [
'query' => $a['query'],
'results' => $a['results'],
'count' => is_array($a['results']) ? (int) (count((array) $a['results'])) : '',
'meta' => $a['meta'],
'error' => $a['error'],
'warnings' => $a['warnings'],
'describe' => $a['describe'],
'trace' => $a['trace'] ?: main()->trace_string(),
'cached' => (int) $a['cached'],
'time' => round(microtime(true) - $a['time'], 5),
'time_wo_cache' => $a['time_wo_cache'] ? round($a['time_wo_cache'], 5) : '',
]);
}
}