123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715 |
- <?php
- /**
- * XS 主类定义文件
- *
- * @author hightman
- * @link http://www.xunsearch.com/
- * @copyright Copyright © 2011 HangZhou YunSheng Network Technology Co., Ltd.
- * @license http://www.xunsearch.com/license/
- * @version $Id$
- *
- * <ul>
- * <li>XS 是 XunSearch 的统一缩写, XS 是解决方案而不仅仅针对搜索, 还包括索引管理等</li>
- * <li>XS 运行环境要求 PHP 5.2.0 及以上版本, 带有 SPL 扩展</li>
- * <li>如果您的数据包含 utf-8 以外的编码(如: gbk), 则要求安装 mbstring 或 iconv 以便转换编码</li>
- * <li>对于 bool 类型函数/方法若无特别说明, 均表示成功返回 true, 失败返回 false</li>
- * <li>对于致命的异常情况均抛出类型为 XSException 的异常, 应将 xs 所有操作放入 try/catch 区块</li>
- * <li>这只是 XunSearch 项目客户端的 PHP 实现, 需要配合 xunsearch 服务端协同工作</li>
- * </ul>
- *
- * 用法简例:
- * <pre>
- * try {
- * // 创建 xs 实例 (包含3个字段 id, title, content)
- * $xs = new XS('etc/sample.ini');
- *
- * // 索引管理
- * $doc = new XSDocument('gbk');
- *
- * // 新增/根据主键更新数据
- * $doc->id = 123;
- * $doc->title = '您好, 世界!';
- * $doc->setFields(array('content' => '英文说法是: Hello, the world!'));
- * $xs->index->add($doc);
- *
- * $doc->title = '世界, 你好!';
- * $xs->index->update($doc);
- *
- * $xs->index->del(124); // 删除单条主键为 124 的数据
- * $xs->index->del(array(125, 126, 129)); // 批量删除 3条数据
- *
- * // 正常检索
- * // 快速检索取得结果
- * // 快速检索匹配数量(估算)
- *
- * } catch (XSException $e) {
- * echo $e . "<br />\n";
- * }
- * </pre>
- */
- define('XS_LIB_ROOT', dirname(__FILE__));
- include_once XS_LIB_ROOT . '/xs_cmd.inc.php';
- /**
- * XS 异常类定义, XS 所有操作过程发生异常均抛出该实例
- *
- * @author hightman <hightman@twomice.net>
- * @version 1.0.0
- * @package XS
- */
- class XSException extends Exception
- {
- /**
- * 将类对象转换成字符串
- * @return string 异常的简要描述信息
- */
- public function __toString()
- {
- $string = '[' . __CLASS__ . '] ' . $this->getRelPath($this->getFile()) . '(' . $this->getLine() . '): ';
- $string .= $this->getMessage() . ($this->getCode() > 0 ? '(S#' . $this->getCode() . ')' : '');
- return $string;
- }
- /**
- * 取得相对当前的文件路径
- * @param string $file 需要转换的绝对路径
- * @return string 转换后的相对路径
- */
- public static function getRelPath($file)
- {
- $from = getcwd();
- $file = realpath($file);
- if (is_dir($file)) {
- $pos = false;
- $to = $file;
- } else {
- $pos = strrpos($file, '/');
- $to = substr($file, 0, $pos);
- }
- for ($rel = '';; $rel .= '../') {
- if ($from === $to) {
- break;
- }
- if ($from === dirname($from)) {
- $rel .= substr($to, 1);
- break;
- }
- if (!strncmp($from . '/', $to, strlen($from) + 1)) {
- $rel .= substr($to, strlen($from) + 1);
- break;
- }
- $from = dirname($from);
- }
- if (substr($rel, -1, 1) === '/') {
- $rel = substr($rel, 0, -1);
- }
- if ($pos !== false) {
- $rel .= substr($file, $pos);
- }
- return $rel;
- }
- }
- /**
- * XS 错误异常类定义, XS 所有操作过程发生错误均抛出该实例
- *
- * @author hightman <hightman@twomice.net>
- * @version 1.0.0
- * @package XS
- */
- class XSErrorException extends XSException
- {
- private $_file, $_line;
- /**
- * 构造函数
- * 将 $file, $line 记录到私有属性在 __toString 中使用
- * @param int $code 出错代码
- * @param string $message 出错信息
- * @param string $file 出错所在文件
- * @param int $line 出错所在的行数
- * @param Exception $previous
- */
- public function __construct($code, $message, $file, $line, $previous = null)
- {
- $this->_file = $file;
- $this->_line = $line;
- if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
- parent::__construct($message, $code, $previous);
- } else {
- parent::__construct($message, $code);
- }
- }
- /**
- * 将类对象转换成字符串
- * @return string 异常的简要描述信息
- */
- public function __toString()
- {
- $string = '[' . __CLASS__ . '] ' . $this->getRelPath($this->_file) . '(' . $this->_line . '): ';
- $string .= $this->getMessage() . '(' . $this->getCode() . ')';
- return $string;
- }
- }
- /**
- * XS 组件基类
- * 封装一些魔术方法, 以实现支持模拟属性
- *
- * 模拟属性通过定义读取函数, 写入函数来实现, 允许两者缺少其中一个
- * 这类属性可以跟正常定义的属性一样存取, 但是这类属性名称不区分大小写. 例:
- * <pre>
- * $a = $obj->text; // $a 值等于 $obj->getText() 的返回值
- * $obj->text = $a; // 等同事调用 $obj->setText($a)
- * </pre>
- *
- * @author hightman <hightman@twomice.net>
- * @version 1.0.0
- * @package XS
- */
- class XSComponent
- {
- /**
- * 魔术方法 __get
- * 取得模拟属性的值, 内部实际调用 getXxx 方法的返回值
- * @param string $name 属性名称
- * @return mixed 属性值
- * @throw XSException 属性不存在或不可读时抛出异常
- */
- public function __get($name)
- {
- $getter = 'get' . $name;
- if (method_exists($this, $getter)) {
- return $this->$getter();
- }
- // throw exception
- $msg = method_exists($this, 'set' . $name) ? 'Write-only' : 'Undefined';
- $msg .= ' property: ' . get_class($this) . '::$' . $name;
- throw new XSException($msg);
- }
- /**
- * 魔术方法 __set
- * 设置模拟属性的值, 内部实际是调用 setXxx 方法
- * @param string $name 属性名称
- * @param mixed $value 属性值
- * @throw XSException 属性不存在或不可写入时抛出异常
- */
- public function __set($name, $value)
- {
- $setter = 'set' . $name;
- if (method_exists($this, $setter)) {
- return $this->$setter($value);
- }
- // throw exception
- $msg = method_exists($this, 'get' . $name) ? 'Read-only' : 'Undefined';
- $msg .= ' property: ' . get_class($this) . '::$' . $name;
- throw new XSException($msg);
- }
- /**
- * 魔术方法 __isset
- * 判断模拟属性是否存在并可读取
- * @param string $name 属性名称
- * @return bool 若存在为 true, 反之为 false
- */
- public function __isset($name)
- {
- return method_exists($this, 'get' . $name);
- }
- /**
- * 魔术方法 __unset
- * 删除、取消模拟属性, 相当于设置属性值为 null
- * @param string $name 属性名称
- */
- public function __unset($name)
- {
- $this->__set($name, null);
- }
- }
- /**
- * XS 搜索项目主类
- *
- * @property XSFieldScheme $scheme 当前在用的字段方案
- * @property string $defaultCharset 默认字符集编码
- * @property-read string $name 项目名称
- * @property-read XSIndex $index 索引操作对象
- * @property-read XSSearch $search 搜索操作对象
- * @property-read XSFieldMeta $idField 主键字段
- * @author hightman <hightman@twomice.net>
- * @version 1.0.0
- * @package XS
- */
- class XS extends XSComponent
- {
- /**
- * @var XSIndex 索引操作对象
- */
- private $_index;
- /**
- * @var XSSearch 搜索操作对象
- */
- private $_search;
- /**
- * @var XSServer scws 分词服务器
- */
- private $_scws;
- /**
- * @var XSFieldScheme 当前字段方案
- */
- private $_scheme, $_bindScheme;
- private $_config;
- /**
- * @var XS 最近创建的 XS 对象
- */
- private static $_lastXS;
- /**
- * 构造函数
- * 特别说明一个小技巧, 参数 $file 可以直接是配置文件的内容, 还可以是仅仅是文件名,
- * 如果只是文件名会自动查找 XS_LIB_ROOT/../app/$file.ini
- * @param string $file 要加载的项目配置文件
- */
- public function __construct($file)
- {
- if (strlen($file) < 255 && !is_file($file)) {
- $appRoot = getenv('XS_APP_ROOT');
- if ($appRoot === false) {
- $appRoot = defined('XS_APP_ROOT') ? XS_APP_ROOT : XS_LIB_ROOT . '/../app';
- }
- $file2 = $appRoot . '/' . $file . '.ini';
- if (is_file($file2)) {
- $file = $file2;
- }
- }
- $this->loadIniFile($file);
- self::$_lastXS = $this;
- }
- /**
- * 析构函数
- * 由于对象交叉引用, 如需提前销毁对象, 请强制调用该函数
- */
- public function __destruct()
- {
- $this->_index = null;
- $this->_search = null;
- }
- /**
- * 获取最新的 XS 实例
- * @return XS 最近创建的 XS 对象
- */
- public static function getLastXS()
- {
- return self::$_lastXS;
- }
- /**
- * 获取当前在用的字段方案
- * 通用于搜索结果文档和修改、添加的索引文档
- * @return XSFieldScheme 当前字段方案
- */
- public function getScheme()
- {
- return $this->_scheme;
- }
- /**
- * 设置当前在用的字段方案
- * @param XSFieldScheme $fs 一个有效的字段方案对象
- * @throw XSException 无效方案则直接抛出异常
- */
- public function setScheme(XSFieldScheme $fs)
- {
- $fs->checkValid(true);
- $this->_scheme = $fs;
- if ($this->_search !== null) {
- $this->_search->markResetScheme();
- }
- }
- /**
- * 还原字段方案为项目绑定方案
- */
- public function restoreScheme()
- {
- if ($this->_scheme !== $this->_bindScheme) {
- $this->_scheme = $this->_bindScheme;
- if ($this->_search !== null) {
- $this->_search->markResetScheme(true);
- }
- }
- }
- /**
- * @return array 获取配置原始数据
- */
- public function getConfig()
- {
- return $this->_config;
- }
- /**
- * 获取当前项目名称
- * @return string 当前项目名称
- */
- public function getName()
- {
- return $this->_config['project.name'];
- }
- /**
- * 修改当前项目名称
- * 注意,必须在 {@link getSearch} 和 {@link getIndex} 前调用才能起作用
- * @param string $name 项目名称
- */
- public function setName($name)
- {
- $this->_config['project.name'] = $name;
- }
- /**
- * 获取项目的默认字符集
- * @return string 默认字符集(已大写)
- */
- public function getDefaultCharset()
- {
- return isset($this->_config['project.default_charset']) ?
- strtoupper($this->_config['project.default_charset']) : 'UTF-8';
- }
- /**
- * 改变项目的默认字符集
- * @param string $charset 修改后的字符集
- */
- public function setDefaultCharset($charset)
- {
- $this->_config['project.default_charset'] = strtoupper($charset);
- }
- /**
- * 获取索引操作对象
- * @return XSIndex 索引操作对象
- */
- public function getIndex()
- {
- if ($this->_index === null) {
- $adds = array();
- $conn = isset($this->_config['server.index']) ? $this->_config['server.index'] : 8383;
- if (($pos = strpos($conn, ';')) !== false) {
- $adds = explode(';', substr($conn, $pos + 1));
- $conn = substr($conn, 0, $pos);
- }
- $this->_index = new XSIndex($conn, $this);
- $this->_index->setTimeout(0);
- foreach ($adds as $conn) {
- $conn = trim($conn);
- if ($conn !== '') {
- $this->_index->addServer($conn)->setTimeout(0);
- }
- }
- }
- return $this->_index;
- }
- /**
- * 获取搜索操作对象
- * @return XSSearch 搜索操作对象
- */
- public function getSearch()
- {
- if ($this->_search === null) {
- $conns = array();
- if (!isset($this->_config['server.search'])) {
- $conns[] = 8384;
- } else {
- foreach (explode(';', $this->_config['server.search']) as $conn) {
- $conn = trim($conn);
- if ($conn !== '') {
- $conns[] = $conn;
- }
- }
- }
- if (count($conns) > 1) {
- shuffle($conns);
- }
- for ($i = 0; $i < count($conns); $i++) {
- try {
- $this->_search = new XSSearch($conns[$i], $this);
- $this->_search->setCharset($this->getDefaultCharset());
- return $this->_search;
- } catch (XSException $e) {
- if (($i + 1) === count($conns)) {
- throw $e;
- }
- }
- }
- }
- return $this->_search;
- }
- /**
- * 创建 scws 分词连接
- * @return XSServer 分词服务器
- */
- public function getScwsServer()
- {
- if ($this->_scws === null) {
- $conn = isset($this->_config['server.search']) ? $this->_config['server.search'] : 8384;
- $this->_scws = new XSServer($conn, $this);
- }
- return $this->_scws;
- }
- /**
- * 获取当前主键字段
- * @return XSFieldMeta 类型为 ID 的字段
- * @see XSFieldScheme::getFieldId
- */
- public function getFieldId()
- {
- return $this->_scheme->getFieldId();
- }
- /**
- * 获取当前标题字段
- * @return XSFieldMeta 类型为 TITLE 的字段
- * @see XSFieldScheme::getFieldTitle
- */
- public function getFieldTitle()
- {
- return $this->_scheme->getFieldTitle();
- }
- /**
- * 获取当前内容字段
- * @return XSFieldMeta 类型为 BODY 的字段
- * @see XSFieldScheme::getFieldBody
- */
- public function getFieldBody()
- {
- return $this->_scheme->getFieldBody();
- }
- /**
- * 获取项目字段元数据
- * @param mixed $name 字段名称(string) 或字段序号(vno, int)
- * @param bool $throw 当字段不存在时是否抛出异常, 默认为 true
- * @return XSFieldMeta 字段元数据对象
- * @throw XSException 当字段不存在并且参数 throw 为 true 时抛出异常
- * @see XSFieldScheme::getField
- */
- public function getField($name, $throw = true)
- {
- return $this->_scheme->getField($name, $throw);
- }
- /**
- * 获取项目所有字段结构设置
- * @return XSFieldMeta[]
- */
- public function getAllFields()
- {
- return $this->_scheme->getAllFields();
- }
- /**
- * 智能加载类库文件
- * 要求以 Name.class.php 命名并与本文件存放在同一目录, 如: XSTokenizerXxx.class.php
- * @param string $name 类的名称
- */
- public static function autoload($name)
- {
- $file = XS_LIB_ROOT . '/' . $name . '.class.php';
- if (file_exists($file)) {
- require_once $file;
- }
- }
- /**
- * 字符集转换
- * 要求安装有 mbstring, iconv 中的一种
- * @param mixed $data 需要转换的数据, 支持 string 和 array, 数组会自动递归转换
- * @param string $to 转换后的字符集
- * @param string $from 转换前的字符集
- * @return mixed 转换后的数据
- * @throw XSEXception 如果没有合适的转换函数抛出异常
- */
- public static function convert($data, $to, $from)
- {
- // need not convert
- if ($to == $from) {
- return $data;
- }
- // array traverse
- if (is_array($data)) {
- foreach ($data as $key => $value) {
- $data[$key] = self::convert($value, $to, $from);
- }
- return $data;
- }
- // string contain 8bit characters
- if (is_string($data) && preg_match('/[\x81-\xfe]/', $data)) {
- // mbstring, iconv, throw ...
- if (function_exists('mb_convert_encoding')) {
- return mb_convert_encoding($data, $to, $from);
- } elseif (function_exists('iconv')) {
- return iconv($from, $to . '//TRANSLIT', $data);
- } else {
- throw new XSException('Cann\'t find the mbstring or iconv extension to convert encoding');
- }
- }
- return $data;
- }
- /**
- * 计算经纬度距离
- * @param float $lon1 原点经度
- * @param float $lat1 原点纬度
- * @param float $lon2 目标点经度
- * @param float $lat2 目标点纬度
- * @return float 两点大致距离,单位:米
- */
- public static function geoDistance($lon1, $lat1, $lon2, $lat2)
- {
- $dx = $lon1 - $lon2;
- $dy = $lat1 - $lat2;
- $b = ($lat1 + $lat2) / 2;
- $lx = 6367000.0 * deg2rad($dx) * cos(deg2rad($b));
- $ly = 6367000.0 * deg2rad($dy);
- return sqrt($lx * $lx + $ly * $ly);
- }
- /**
- * 解析INI配置文件
- * 由于 PHP 自带的 parse_ini_file 存在一些不兼容,故自行简易实现
- * @param string $data 文件内容
- * @return array 解析后的结果
- */
- private function parseIniData($data)
- {
- $ret = array();
- $cur = &$ret;
- $lines = explode("\n", $data);
- foreach ($lines as $line) {
- if ($line === '' || $line[0] == ';' || $line[0] == '#') {
- continue;
- }
- $line = trim($line);
- if ($line === '') {
- continue;
- }
- if ($line[0] === '[' && substr($line, -1, 1) === ']') {
- $sec = substr($line, 1, -1);
- $ret[$sec] = array();
- $cur = &$ret[$sec];
- continue;
- }
- if (($pos = strpos($line, '=')) === false) {
- continue;
- }
- $key = trim(substr($line, 0, $pos));
- $value = trim(substr($line, $pos + 1), " '\t\"");
- $cur[$key] = $value;
- }
- return $ret;
- }
- /**
- * 加载项目配置文件
- * @param string $file 配置文件路径
- * @throw XSException 出错时抛出异常
- * @see XSFieldMeta::fromConfig
- */
- private function loadIniFile($file)
- {
- // check cache
- $cache = false;
- $cache_write = '';
- if (strlen($file) < 255 && file_exists($file)) {
- $cache_key = md5(__CLASS__ . '::ini::' . realpath($file));
- if (function_exists('apc_fetch')) {
- $cache = apc_fetch($cache_key);
- $cache_write = 'apc_store';
- } elseif (function_exists('xcache_get') && php_sapi_name() !== 'cli') {
- $cache = xcache_get($cache_key);
- $cache_write = 'xcache_set';
- } elseif (function_exists('eaccelerator_get')) {
- $cache = eaccelerator_get($cache_key);
- $cache_write = 'eaccelerator_put';
- }
- if ($cache && isset($cache['mtime']) && isset($cache['scheme'])
- && filemtime($file) <= $cache['mtime']) {
- // cache HIT
- $this->_scheme = $this->_bindScheme = unserialize($cache['scheme']);
- $this->_config = $cache['config'];
- return;
- }
- $data = file_get_contents($file);
- } else {
- // parse ini string
- $data = $file;
- $file = substr(md5($file), 8, 8) . '.ini';
- }
- // parse ini file
- $this->_config = $this->parseIniData($data);
- if ($this->_config === false) {
- throw new XSException('Failed to parse project config file/string: \'' . substr($file, 0, 10) . '...\'');
- }
- // create the scheme object
- $scheme = new XSFieldScheme;
- foreach ($this->_config as $key => $value) {
- if (is_array($value)) {
- $scheme->addField($key, $value);
- }
- }
- $scheme->checkValid(true);
- // load default config
- if (!isset($this->_config['project.name'])) {
- $this->_config['project.name'] = basename($file, '.ini');
- }
- // save to cache
- $this->_scheme = $this->_bindScheme = $scheme;
- if ($cache_write != '') {
- $cache['mtime'] = filemtime($file);
- $cache['scheme'] = serialize($this->_scheme);
- $cache['config'] = $this->_config;
- call_user_func($cache_write, $cache_key, $cache);
- }
- }
- }
- /**
- * Add autoload handler to search classes on current directory
- * Class file should be named as Name.class.php
- */
- spl_autoload_register('XS::autoload', true, true);
- /**
- * 修改默认的错误处理函数
- * 把发生的错误修改为抛出异常, 方便统一处理
- */
- function xsErrorHandler($errno, $error, $file, $line)
- {
- if (($errno & ini_get('error_reporting')) && !strncmp($file, XS_LIB_ROOT, strlen(XS_LIB_ROOT))) {
- throw new XSErrorException($errno, $error, $file, $line);
- }
- return false;
- }
- set_error_handler('xsErrorHandler');
|