XSDocument.class.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. <?php
  2. /**
  3. * XSDocument 类定义文件
  4. *
  5. * @author hightman
  6. * @link http://www.xunsearch.com/
  7. * @copyright Copyright &copy; 2011 HangZhou YunSheng Network Technology Co., Ltd.
  8. * @license http://www.xunsearch.com/license/
  9. * @version $Id$
  10. */
  11. /**
  12. * 文档用于描述检索/索引的基础对象, 包含一组字段及其值, 相当于常规SQL数据表中的一行记录.
  13. * 通过魔术方法, 每个字段名都是文档的虚拟属性, 可直接赋值或取值, 也支持数组方式访问文档字段.
  14. * <pre>
  15. * $doc = new XSDocument;
  16. * $doc->name = 'value'; // 用对象属性方式进行赋值、取值
  17. * $doc['name'] = 'value'; // 用数组下标方式进行赋值、取值
  18. * $value = $doc->f('name'); // 用函数方式进行取值
  19. * $doc->setField('name', 'value'); // 用函数方式进行赋值
  20. * $doc->setFields(array('name' => 'value', 'name2' => 'value2')); // 用数组进行批量赋值
  21. *
  22. * // 迭代方式取所有字段值
  23. * foreach($doc as $name => $value)
  24. * {
  25. * echo "$name: $value\n";
  26. * }
  27. * </pre>
  28. * 如果有特殊需求, 可以自行扩展本类, 重写 beforeSubmit() 及 afterSubmit() 方法以定义在索引
  29. * 提交前后的行为
  30. *
  31. * @method int docid() docid(void) 取得搜索结果文档的 docid 值 (实际数据库的id)
  32. * @method int rank() rank(void) 取得搜索结果文档的序号值 (第X条结果)
  33. * @method int percent() percent(void) 取得搜索结果文档的匹配百分比 (结果匹配度, 1~100)
  34. * @method float weight() weight(void) 取得搜索结果文档的权重值 (浮点数)
  35. * @method int ccount() ccount(void) 取得搜索结果折叠的数量 (按字段折叠搜索时)
  36. * @method array matched() matched(void) 取得搜索结果文档中匹配查询的词汇 (数组)
  37. *
  38. * @author hightman <hightman@twomice.net>
  39. * @version 1.0.0
  40. * @package XS
  41. */
  42. class XSDocument implements ArrayAccess, IteratorAggregate
  43. {
  44. private $_data;
  45. private $_terms, $_texts;
  46. private $_charset, $_meta;
  47. private static $_resSize = 20;
  48. private static $_resFormat = 'Idocid/Irank/Iccount/ipercent/fweight';
  49. /**
  50. * 构造函数
  51. * @param mixed $p 字符串表示索引文档的编码或搜索结果文档的 meta 数据, 数组则表示或索引文档的初始字段数据
  52. * @param string $d 可选参数, 当 $p 不为编码时, 本参数表示数据编码
  53. */
  54. public function __construct($p = null, $d = null)
  55. {
  56. $this->_data = array();
  57. if (is_array($p)) {
  58. $this->_data = $p;
  59. } elseif (is_string($p)) {
  60. if (strlen($p) !== self::$_resSize) {
  61. $this->setCharset($p);
  62. return;
  63. }
  64. $this->_meta = unpack(self::$_resFormat, $p);
  65. }
  66. if ($d !== null && is_string($d)) {
  67. $this->setCharset($d);
  68. }
  69. }
  70. /**
  71. * 魔术方法 __get
  72. * 实现以对象属性方式获取文档字段值
  73. * @param string $name 字段名称
  74. * @return mixed 字段值, 若不存在返回 null
  75. */
  76. public function __get($name)
  77. {
  78. if (!isset($this->_data[$name])) {
  79. return null;
  80. }
  81. return $this->autoConvert($this->_data[$name]);
  82. }
  83. /**
  84. * 魔术方法 __set
  85. * 实现以对象属性方式设置文档字段值
  86. * @param string $name 字段名称
  87. * @param mixed $value 字段值
  88. */
  89. public function __set($name, $value)
  90. {
  91. if ($this->_meta !== null) {
  92. throw new XSException('Magick property of result document is read-only');
  93. }
  94. $this->setField($name, $value);
  95. }
  96. /**
  97. * 魔术方法 __call
  98. * 实现以函数调用访问搜索结果元数据, 支持: docid, rank, percent, weight, ccount
  99. * @param string $name 方法名称
  100. * @param array $args 调用时的参数列表 (此处无用)
  101. * @throw XSException 若不存在相应元数据则抛出方法未定义的异常
  102. */
  103. public function __call($name, $args)
  104. {
  105. if ($this->_meta !== null) {
  106. $name = strtolower($name);
  107. if (isset($this->_meta[$name])) {
  108. return $this->_meta[$name];
  109. }
  110. }
  111. throw new XSException('Call to undefined method `' . get_class($this) . '::' . $name . '()\'');
  112. }
  113. /**
  114. * 获取文档字符集
  115. * @return string 当前设定的字符集(已大写), 若未曾设置则返回 null
  116. */
  117. public function getCharset()
  118. {
  119. return $this->_charset;
  120. }
  121. /**
  122. * 设置文档字符集
  123. * @param string $charset 设置文档字符集
  124. */
  125. public function setCharset($charset)
  126. {
  127. $this->_charset = strtoupper($charset);
  128. if ($this->_charset == 'UTF8') {
  129. $this->_charset = 'UTF-8';
  130. }
  131. }
  132. /**
  133. * 获取字段值
  134. * @return array 返回已设置的字段键值数组
  135. */
  136. public function getFields()
  137. {
  138. return $this->_data;
  139. }
  140. /**
  141. * 批量设置字段值
  142. * 这里是以合并方式赋值, 即不会清空已赋值并且不在参数中的字段.
  143. * @param array $data 字段名及其值组成的数组
  144. */
  145. public function setFields($data)
  146. {
  147. if ($data === null) {
  148. $this->_data = array();
  149. $this->_meta = $this->_terms = $this->_texts = null;
  150. } else {
  151. $this->_data = array_merge($this->_data, $data);
  152. }
  153. }
  154. /**
  155. * 设置某个字段的值
  156. * @param string $name 字段名称
  157. * @param mixed $value 字段值
  158. * @param bool $isMeta 是否为元数据字段
  159. */
  160. public function setField($name, $value, $isMeta = false)
  161. {
  162. if ($value === null) {
  163. if ($isMeta) {
  164. unset($this->_meta[$name]);
  165. } else {
  166. unset($this->_data[$name]);
  167. }
  168. } else {
  169. if ($isMeta) {
  170. $this->_meta[$name] = $value;
  171. } else {
  172. $this->_data[$name] = $value;
  173. }
  174. }
  175. }
  176. /**
  177. * 获取文档字段的值
  178. * @param string $name 字段名称
  179. * @return mixed 字段值, 若不存在则返回 null
  180. */
  181. public function f($name)
  182. {
  183. return $this->__get(strval($name));
  184. }
  185. /**
  186. * 获取字段的附加索引词列表 (仅限索引文档)
  187. * @param string $field 字段名称
  188. * @return array 索引词列表(词为键, 词重为值), 若无则返回 null
  189. */
  190. public function getAddTerms($field)
  191. {
  192. $field = strval($field);
  193. if ($this->_terms === null || !isset($this->_terms[$field])) {
  194. return null;
  195. }
  196. $terms = array();
  197. foreach ($this->_terms[$field] as $term => $weight) {
  198. $term = $this->autoConvert($term);
  199. $terms[$term] = $weight;
  200. }
  201. return $terms;
  202. }
  203. /**
  204. * 获取字段的附加索引文本 (仅限索引文档)
  205. * @param string $field 字段名称
  206. * @return string 文本内容, 若无则返回 null
  207. */
  208. public function getAddIndex($field)
  209. {
  210. $field = strval($field);
  211. if ($this->_texts === null || !isset($this->_texts[$field])) {
  212. return null;
  213. }
  214. return $this->autoConvert($this->_texts[$field]);
  215. }
  216. /**
  217. * 给字段增加索引词 (仅限索引文档)
  218. * @param string $field 词条所属字段名称
  219. * @param string $term 词条内容, 不超过 255字节
  220. * @param int $weight 词重, 默认为 1
  221. */
  222. public function addTerm($field, $term, $weight = 1)
  223. {
  224. $field = strval($field);
  225. if (!is_array($this->_terms)) {
  226. $this->_terms = array();
  227. }
  228. if (!isset($this->_terms[$field])) {
  229. $this->_terms[$field] = array($term => $weight);
  230. } elseif (!isset($this->_terms[$field][$term])) {
  231. $this->_terms[$field][$term] = $weight;
  232. } else {
  233. $this->_terms[$field][$term] += $weight;
  234. }
  235. }
  236. /**
  237. * 给字段增加索引文本 (仅限索引文档)
  238. * @param string $field 文本所属的字段名称
  239. * @param string $text 文本内容
  240. */
  241. public function addIndex($field, $text)
  242. {
  243. $field = strval($field);
  244. if (!is_array($this->_texts)) {
  245. $this->_texts = array();
  246. }
  247. if (!isset($this->_texts[$field])) {
  248. $this->_texts[$field] = strval($text);
  249. } else {
  250. $this->_texts[$field] .= "\n" . strval($text);
  251. }
  252. }
  253. /**
  254. * IteratorAggregate 接口, 以支持 foreach 遍历访问字段列表
  255. */
  256. public function getIterator()
  257. {
  258. if ($this->_charset !== null && $this->_charset !== 'UTF-8') {
  259. $from = $this->_meta === null ? $this->_charset : 'UTF-8';
  260. $to = $this->_meta === null ? 'UTF-8' : $this->_charset;
  261. return new ArrayIterator(XS::convert($this->_data, $to, $from));
  262. }
  263. return new ArrayIterator($this->_data);
  264. }
  265. /**
  266. * ArrayAccess 接口, 判断字段是否存在, 勿直接调用
  267. * @param string $name 字段名称
  268. * @return bool 存在返回 true, 若不存在返回 false
  269. */
  270. public function offsetExists($name)
  271. {
  272. return isset($this->_data[$name]);
  273. }
  274. /**
  275. * ArrayAccess 接口, 取得字段值, 勿直接调用
  276. * @param string $name 字段名称
  277. * @return mixed 字段值, 若不存在返回 null
  278. * @see __get
  279. */
  280. public function offsetGet($name)
  281. {
  282. return $this->__get($name);
  283. }
  284. /**
  285. * ArrayAccess 接口, 设置字段值, 勿直接调用
  286. * @param string $name 字段名称
  287. * @param mixed $value 字段值
  288. * @see __set
  289. */
  290. public function offsetSet($name, $value)
  291. {
  292. if (!is_null($name)) {
  293. $this->__set(strval($name), $value);
  294. }
  295. }
  296. /**
  297. * ArrayAccess 接口, 删除字段值, 勿直接调用
  298. * @param string $name 字段名称
  299. */
  300. public function offsetUnset($name)
  301. {
  302. unset($this->_data[$name]);
  303. }
  304. /**
  305. * 重写接口, 在文档提交到索引服务器前调用
  306. * 继承此类进行重写该方法时, 必须调用 parent::beforeSave($index) 以确保正确
  307. * @param XSIndex $index 索引操作对象
  308. * @return bool 默认返回 true, 若返回 false 将阻止该文档提交到索引服务器
  309. */
  310. public function beforeSubmit(XSIndex $index)
  311. {
  312. if ($this->_charset === null) {
  313. $this->_charset = $index->xs->getDefaultCharset();
  314. }
  315. return true;
  316. }
  317. /**
  318. * 重写接口, 在文档成功提交到索引服务器后调用
  319. * 继承此类进行重写该方法时, 强烈建议要调用 parent::afterSave($index) 以确保完整.
  320. * @param XSIndex $index 索引操作对象
  321. */
  322. public function afterSubmit($index)
  323. {
  324. }
  325. /**
  326. * 智能字符集编码转换
  327. * 将 XS 内部用的 UTF-8 与指定的文档编码按需相互转换
  328. * 索引文档: ... -> UTF-8, 搜索结果文档: ... <-- UTF-8
  329. * @param string $value 要转换的字符串
  330. * @return string 转好的字符串
  331. * @see setCharset
  332. */
  333. private function autoConvert($value)
  334. {
  335. // Is the value need to convert
  336. if ($this->_charset === null || $this->_charset == 'UTF-8'
  337. || !is_string($value) || !preg_match('/[\x81-\xfe]/', $value)) {
  338. return $value;
  339. }
  340. // _meta === null ? index document : search result document
  341. $from = $this->_meta === null ? $this->_charset : 'UTF-8';
  342. $to = $this->_meta === null ? 'UTF-8' : $this->_charset;
  343. return XS::convert($value, $to, $from);
  344. }
  345. }