if (!extension_loaded('mysqli')) { error('Mysqli Extension not loaded.'); } class SphinxQL extends mysqli { private static $Connections = array(); private $Server; private $Port; private $Socket; private $Ident; private $Connected = false; public static $Queries = array(); public static $Time = 0.0; /** * Initialize SphinxQL object * * @param string $Server server address or hostname * @param int $Port listening port * @param string $Socket Unix socket address, overrides $Server:$Port */ public function __construct($Server, $Port, $Socket) { $this->Server = $Server; $this->Port = $Port; $this->Socket = $Socket; $this->Ident = $this->get_ident($Server, $Port, $Socket); } /** * Create server ident based on connection information * * @param string $Server server address or hostname * @param int $Port listening port * @param string $Socket Unix socket address, overrides $Server:$Port * @return identification string */ private function get_ident($Server, $Port, $Socket) { if($Socket) { return $Socket; } else { return "$Server:$Port"; } } /** * Create SphinxQL object or return existing one * * @param string $Server server address or hostname * @param int $Port listening port * @param string $Socket Unix socket address, overrides $Server:$Port * @return SphinxQL object */ public static function init_connection($Server, $Port, $Socket) { $Ident = self::get_ident($Server, $Port, $Socket); if(!isset(self::$Connections[$Ident])) { self::$Connections[$Ident] = new SphinxQL($Server, $Port, $Socket); } return self::$Connections[$Ident]; } /** * Connect the SphinxQL object to the Sphinx server */ public function connect() { if(!$this->Connected) { global $Debug; $Debug->set_flag('Connecting to Sphinx server '.$this->Ident); parent::__construct($this->Server, '', '', '', $this->Port, $this->Socket); if($this->connect_error) { $Errno = $this->connect_errno; $Error = $this->connect_error; $this->error("Connection failed. ".strval($Errno)." (".strval($Error).")"); } $Debug->set_flag('Connected to Sphinx server '.$this->Ident); $this->Connected = true; } } /** * Print a message to privileged users and optionally halt page processing * * @param string $Msg message to display * @param bool $Halt halt page processing. Default is to continue processing the page * @return SphinxQL object */ public function error($Msg, $Halt = false) { global $Debug; $ErrorMsg = 'SphinxQL ('.$this->Ident.'): '.strval($Msg); $Debug->analysis('SphinxQL Error', $ErrorMsg, 3600*24); if($Halt === true && (DEBUG_MODE || check_perms('site_debug'))) { echo '
'.display_str($ErrorMsg).''; die(); } elseif($Halt === true) { error('-1'); } } /** * Escape special characters before sending them to the Sphinx server. * Two escapes needed because the first one is eaten up by the mysql driver. * * @param string $String string to escape * @return escaped string */ public function escape_string($String) { return strtr($String, array( '('=>'\\\\(', ')'=>'\\\\)', '|'=>'\\\\|', '-'=>'\\\\-', '@'=>'\\\\@', '~'=>'\\\\~', '&'=>'\\\\&', '\''=>'\\\'', '<'=>'\\\\<', '!'=>'\\\\!', '"'=>'\\\\"', '/'=>'\\\\/', '\\'=>'\\\\\\\\') ); } /** * Register sent queries globally for later retrieval by debug functions * * @param string $QueryString query text * @param param $QueryProcessTime time building and processing the query */ public function register_query($QueryString, $QueryProcessTime) { SphinxQL::$Queries[] = array($QueryString, $QueryProcessTime); SphinxQL::$Time += $QueryProcessTime; } } class SphinxQL_Query { private $SphinxQL; private $Expressions; private $Filters; private $GroupBy; private $Indexes; private $Limits; private $Options; private $QueryString; private $Select; private $SortBy; private $SortGroupBy; /** * Initialize SphinxQL object * * @param string $Server server address or hostname * @param int $Port listening port * @param string $Socket Unix socket address, overrides $Server:$Port */ public function __construct($Server = SPHINXQL_HOST, $Port = SPHINXQL_PORT, $Socket = SPHINXQL_SOCK) { $this->SphinxQL = SphinxQL::init_connection($Server, $Port, $Socket); $this->reset(); } /** * Specify what data the Sphinx query is supposed to return * * @param string $Fields Attributes and expressions * @return current SphinxQL query object */ public function select($Fields) { $this->Select = $Fields; return $this; } /** * Specify the indexes to use in the search * * @param string $Indexes comma separated list of indexes * @return current SphinxQL query object */ public function from($Indexes) { $this->Indexes = $Indexes; return $this; } /** * Add attribute filter. Calling this function multiple times results in boolean AND between each condition * * @param string $Attribute attribute which the filter will apply to * @param mixed $Values scalar or array of numerical values. Array uses boolean OR in query condition * @param bool $Exclude whether to exclude or include matching documents. Default mode is to include matches * @return current SphinxQL query object */ public function where($Attribute, $Values, $Exclude = false) { if(empty($Attribute) && empty($Values)) { return false; } $Filters = array(); if(is_array($Values)) { foreach($Values as $Value) { if(!is_number($Value)) { $this->error("Filters require numeric values"); } } if($Exclude) { $Filters[] = "$Attribute NOT IN (".implode(",", $Value).")"; } else { $Filters[] = "$Attribute IN (".implode(",", $Value).")"; } } else { if(!is_number($Values)) { $this->error("Filters require numeric values"); } if($Exclude) { $Filters[] = "$Attribute != $Values"; } else { $Filters[] = "$Attribute = $Values"; } } $this->Filters[] = implode(" AND ", $Filters); return $this; } /** * Add attribute range filter. Calling this function multiple times results in boolean AND between each condition * * @param string $Attribute attribute which the filter will apply to * @param array $Values pair of numerical values that defines the filter range * @return current SphinxQL query object */ public function where_between($Attribute, $Values) { if(empty($Attribute) || empty($Values) || count($Values) != 2 || !is_number($Values[0]) || !is_number($Values[1])) { $this->error("Filter range requires array of two numerical boundaries as values."); } $this->Filters[] = "$Attribute BETWEEN $Values[0] AND $Values[1]"; return $this; } /** * Add fulltext query expression. Calling this function multiple times results in boolean AND between each condition. * Query expression is escaped automatically * * @param string $Expr query expression * @param string $Field field to match $Expr against. Default is *, which means all available fields * @return current SphinxQL query object */ public function where_match($Expr, $Field = '*') { if(empty($Expr)) { return $this; } $this->Expressions[] = "@$Field ".SphinxQL::escape_string($Expr); return $this; } /** * Specify the order of the matches. Calling this function multiple times sets secondary priorities * * @param string $Attribute attribute to use for sorting * @param string $Mode sort method to apply to the selected attribute * @return current SphinxQL query object */ public function order_by($Attribute, $Mode) { $this->SortBy[] = "$Attribute $Mode"; return $this; } /** * Specify how the results are grouped * * @param string $Attribute group matches with the same $Attribute value * @return current SphinxQL query object */ public function group_by($Attribute) { $this->GroupBy = "$Attribute"; return $this; } /** * Specify the order of the results within groups * * @param string $Attribute attribute to use for sorting * @param string $Mode sort method to apply to the selected attribute * @return current SphinxQL query object */ public function order_group_by($Attribute, $Mode) { $this->SortGroupBy = "$Attribute $Mode"; return $this; } /** * Specify the offset and amount of matches to return * * @param int $Offset number of matches to discard * @param int $Limit number of matches to return * @param int $MaxMatches number of results to store in the Sphinx server's memory. Must be >= ($Offset+$Limit) * @return current SphinxQL query object */ public function limit($Offset, $Limit, $MaxMatches = SPHINX_MATCHES_START) { $this->Limits = "$Offset, $Limit"; $this->set('max_matches', $MaxMatches); return $this; } /** * Tweak the settings to use for the query. Sanity checking shouldn't be needed as Sphinx already does it * * @param string $Name setting name * @param mixed $Value value * @return current SphinxQL query object */ public function set($Name, $Value) { $this->Options[$Name] = $Value; return $this; } /** * Combine the query options into a valid Sphinx query segment * * @return string of options */ private function build_options() { $Options = array(); foreach($this->Options as $Option => $Value) { $Options[] = "$Option = $Value"; } return implode(", ", $Options); } /** * Combine the query conditions into a valid Sphinx query segment */ private function build_query() { if(!$this->Indexes) { $this->error('Index name is required.'); } $this->QueryString = "SELECT $this->Select FROM $this->Indexes"; if(!empty($this->Expressions)) { $this->Filters['expr'] = "MATCH('".implode(" ", $this->Expressions)."')"; } if(!empty($this->Filters)) { $this->QueryString .= "\nWHERE ".implode("\n\tAND ", $this->Filters); } if(!empty($this->GroupBy)) { $this->QueryString .= "\nGROUP BY $this->GroupBy"; } if(!empty($this->SortGroupBy)) { $this->QueryString .= "\nWITHIN GROUP ORDER BY $this->SortGroupBy"; } if(!empty($this->SortBy)) { $this->QueryString .= "\nORDER BY ".implode(", ", $this->SortBy); } if(!empty($this->Limits)) { $this->QueryString .= "\nLIMIT $this->Limits"; } if(!empty($this->Options)) { $Options = $this->build_options(); $this->QueryString .= "\nOPTION $Options"; } } /** * Construct and send the query. Register the query in the global SphinxQL object * * @param bool GetMeta whether to fetch meta data for the executed query. Default is yes * @return SphinxQL result object */ public function query($GetMeta = true) { $QueryStartTime = microtime(true); $this->build_query(); $QueryString = $this->QueryString; $Result = $this->send_query($GetMeta); $QueryProcessTime = (microtime(true) - $QueryStartTime)*1000; SphinxQL::register_query($QueryString, $QueryProcessTime); return $Result; } /** * Run a manually constructed query * * @param string Query query expression * @param bool GetMeta whether to fetch meta data for the executed query. Default is yes * @return SphinxQL result object */ public function raw_query($Query, $GetMeta = true) { $this->QueryString = $Query; return $this->send_query($GetMeta); } /** * Run a pre-processed query. Only used internally * * @param bool GetMeta whether to fetch meta data for the executed query * @return SphinxQL result object */ private function send_query($GetMeta) { if(!$this->QueryString) { return false; } $this->SphinxQL->connect(); $Result = $this->SphinxQL->query($this->QueryString); if($Result === false) { $Errno = $this->SphinxQL->errno; $Error = $this->SphinxQL->error; $this->error("Query returned error $Errno ($Error).\n$this->QueryString"); } else { $Meta = $GetMeta ? $this->get_meta() : null; } return new SphinxQL_Result($Result, $Meta, $Errno, $Error); } /** * Reset all query options and conditions */ public function reset() { $this->Expressions = array(); $this->Filters = array(); $this->GroupBy = ''; $this->Indexes = ''; $this->Limits = array(); $this->Options = array('ranker' => 'none'); $this->QueryString = ''; $this->Select = '*'; $this->SortBy = array(); $this->SortGroupBy = ''; } /** * Fetch and store meta data for the last executed query * * @return meta data */ private function get_meta() { return $this->raw_query("SHOW META", false)->to_pair(0, 1); } /** * Wrapper for the current SphinxQL connection's error function */ private function error($Msg, $Halt = false) { $this->SphinxQL->error($Msg, $Halt); } } class SphinxQL_Result { private $Result; private $Meta; public $Errno; public $Error; /** * Create SphinxQL result object * * @param mysqli_result $Result query results * @param array $Meta meta data for the query * @param int $Errno error code returned by the query upon failure * @param string $Error error message returned by the query upon failure */ public function __construct($Result, $Meta, $Errno, $Error) { $this->Result = $Result; $this->Meta = $Meta; $this->Errno = $Errno; $this->Error = $Error; } /** * Redirect to the Mysqli result object if a nonexistent method is called * * @param string $Name method name * @param array $Arguments arguments used in the function call * @return whatever the parent function returns */ public function __call($Name, $Arguments) { return call_user_func_array(array($this->Result, $Name), $Arguments); } /** * Collect and return the specified key of all results as a list * * @param string $Key key containing the desired data * @return array with the $Key value of all results */ public function collect($Key) { $Return = array(); while($Row = $this->fetch_array()) { $Return[] = $Row[$Key]; } $this->data_seek(0); return $Return; } /** * Collect and return all available data for the matches optionally indexed by a specified key * * @param string $Key key to use as indexing value * @param string $ResultType method to use when fetching data from the mysqli_result object. Default is MYSQLI_ASSOC * @return array with all available data for the matches */ public function to_array($Key, $ResultType = MYSQLI_ASSOC) { $Return = array(); while($Row = $this->fetch_array($ResultType)) { if($Key !== false) { $Return[$Row[$Key]] = $Row; } else { $Return[] = $Row; } } $this->data_seek(0); return $Return; } /** * Collect pairs of keys for all matches * * @param string $Key1 key to use as indexing value * @param string $Key2 key to use as value * @return array with $Key1 => $Key2 pairs for matches */ public function to_pair($Key1, $Key2) { $Return = array(); while($Row = $this->fetch_row()) { $Return[$Row[$Key1]] = $Row[$Key2]; } $this->data_seek(0); return $Return; } /** * Return specified portions of the current SphinxQL result object's meta data * * @param mixed $Keys scalar or array with keys to return. Default is false, which returns all meta data * @return array with meta data */ public function get_meta($Keys = false) { if($Keys !== false) { if(is_array($Keys)) { $Return = array(); foreach($Keys as $Key) { if(!isset($this->Meta[$Key])) { continue; } $Return[$Key] = $this->Meta[$Key]; } return $Return; } else { return isset($this->Meta[$Keys]) ? $this->Meta[$Keys] : false; } } else { return $this->Meta; } } /** * Return specified portions of the current Mysqli result object's information * * @param mixed $Keys scalar or array with keys to return. Default is false, which returns all available information * @return array with result information */ public function get_result_info($Keys = false) { if($Keys !== false) { if(is_array($Keys)) { $Return = array(); foreach($Keys as $Key) { if(!isset($this->Result->$Key)) { continue; } $Return[$Key] = $this->Result->$Key; } return $Return; } else { return isset($this->Result->$Keys) ? $this->Result->$Keys : false; } } else { return $this->Result; } } }