yf_sphinxsearch class

Sphinx search querying.

Fileplugins/search/classes/yf_sphinxsearch.class.php
Use_class('sphinxsearch')

Public properties

A site sets them in its config with $PROJECT_CONF['sphinxsearch']['NAME'] = ...;

PropertyDefaultDescription
$CACHE_TTL 300
$CACHE_WRITES
bool
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 methods 16

escape_string

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);
    }

execute

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;
    }

query

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;
    }

Methods starting with _ hooks and helpers; not routable as page actions

__call

public function __call($name, $args)

Catch missing method call.

    public function __call($name, $args)
    {
        return main()->extend_call($this, $name, $args);
    }

_connect

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;
    }

_get_host

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) : '';
    }

_init

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;
        }
    }

_is_cacheable

public function _is_cacheable($sql)
  • $sql string

Returns bool

    public function _is_cacheable($sql)
    {
        if ($this->CACHE_WRITES) {
            return true;
        }
        return $this->_is_read($sql);
    }

_is_read

public function _is_read($sql)
  • $sql string

Returns 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) : '',
        ]);
    }