PageRenderTime 55ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/application/protected/components/DGSphinxSearch.php

https://bitbucket.org/dinhtrung/yiicorecms/
PHP | 509 lines | 230 code | 41 blank | 238 comment | 46 complexity | 90355b76bc2c5bd66d38ebc5c6b2a67d MD5 | raw file
Possible License(s): GPL-3.0, BSD-3-Clause, CC0-1.0, BSD-2-Clause, GPL-2.0, LGPL-2.1, LGPL-3.0
  1. <?php
  2. /**
  3. * DGSphinxSearch extension wrapper to communicate with Sphinx full-text search engine
  4. * For More documentation please see:
  5. * http://sphinxsearch.com/
  6. */
  7. /**
  8. * @defgroup DGSphinxSearchComponent
  9. */
  10. /**
  11. * @class DGSphinxSearchException
  12. * @brief extends default CException
  13. */
  14. class DGSphinxSearchException extends CException
  15. {
  16. }
  17. /**
  18. * @class DGSphinxSearch
  19. * @brief Implements Sphinx Search
  20. * @details Wrapper for sphinx searchd client class
  21. *
  22. * "A" - Team:
  23. * @author Andrey Evsyukov <thaheless@gmail.com>
  24. * @author Alexey Spiridonov <a.spiridonov@2gis.ru>
  25. * @author Alexey Papulovskiy <a.papulovskiyv@2gis.ru>
  26. * @author Alexander Biryukov <a.biryukov@2gis.ru>
  27. * @author Alexander Radionov <alex.radionov@gmail.com>
  28. * @author Andrey Trofimenko <a.trofimenko@2gis.ru>
  29. * @author Artem Kudzev <a.kiudzev@2gis.ru>
  30. *
  31. * @link http://www.2gis.ru
  32. * @copyright 2GIS
  33. * @license http://www.yiiframework.com/license/
  34. *
  35. * Requirements:
  36. * --------------
  37. * - Yii 1.1.x or above
  38. * - SphinxClient php library
  39. *
  40. * Usage:
  41. * --------------
  42. *
  43. * Search by criteria Object:
  44. *
  45. * $searchCriteria = new stdClass();
  46. * $pages = new CPagination();
  47. * $pages->pageSize = Yii::app()->params['firmPerPage'];
  48. * $searchCriteria->select = 'project_id';
  49. * $searchCriteria->filters = array('project_id' => $project_id);
  50. * $searchCriteria->query = '@name '.$query.'*';
  51. * $searchCriteria->paginator = $pages;
  52. * $searchCriteria->groupby = $groupby;
  53. * $searchCriteria->orders = array('f_name' => 'ASC');
  54. * $searchCriteria->from = 'firm';
  55. * $resIterator = Yii::App()->search->search($searchCriteria); // interator result
  56. * or
  57. * $resArray = Yii::App()->search->searchRaw($searchCriteria); // array result
  58. *
  59. *
  60. * Search by SQL-like syntax:
  61. *
  62. * $search->select('*')->
  63. * from($indexName)->
  64. * where($expression)->
  65. * filters(array('project_id' => $this->_city->id))->
  66. * groupby($groupby)->
  67. * orderby(array('f_name' => 'ASC'))->
  68. * limit(0, 30);
  69. * $resIterator = $search->search(); // interator result
  70. * or
  71. * $resArray = $search->searchRaw(); // array result
  72. *
  73. * Search by SphinxClient syntax:
  74. *
  75. * $search = Yii::App()->search;
  76. * $search->setSelect('*');
  77. * $search->setArrayResult(false);
  78. * $search->setMatchMode(SPH_MATCH_EXTENDED);
  79. * $search->setFieldWeights($fieldWeights)
  80. * $search->query( $query, $indexName);
  81. *
  82. *
  83. * Combined Method:
  84. *
  85. * $search = Yii::App()->search->
  86. * setArrayResult(false)->
  87. * setMatchMode(SPH_MATCH_EXTENDED);
  88. * $search->select('field_1, field_2')->search($searchCriteria);
  89. * ;
  90. */
  91. if (!class_exists('SphinxClient', false)) {
  92. include_once(dirname(__FILE__) . '/sphinxapi.php');
  93. }
  94. class DGSphinxSearch extends CApplicationComponent
  95. {
  96. /**
  97. * @var string
  98. * @brief sphinx server
  99. */
  100. public $server = 'localhost';
  101. /**
  102. * @var integer
  103. * @brief sphinx server port
  104. */
  105. public $port = 6712;
  106. /**
  107. * @var integer
  108. * @brief sphinx default match mode
  109. */
  110. public $matchMode = SPH_MATCH_ANY;
  111. /**
  112. * @var integer
  113. * @brief sphinx max exec time
  114. */
  115. public $maxQueryTime = 3000;
  116. /**
  117. * @var array
  118. * @brief default field weights
  119. */
  120. public $fieldWeights = array();
  121. /**
  122. * @var boolean
  123. * @brief enable Yii profiling
  124. */
  125. public $enableProfiling = false;
  126. /**
  127. * @var boolean
  128. * @brief enable Yii tracing
  129. */
  130. public $enableResultTrace = false;
  131. /**
  132. * @var stdClass
  133. * @brief current search criteria
  134. */
  135. protected $criteria;
  136. /**
  137. * @var SphinxClient
  138. * @brief Sphinx client object
  139. */
  140. private $client;
  141. public function init()
  142. {
  143. parent::init();
  144. include_once(dirname(__FILE__) . '/models/DGSphinxSearchResult.php');
  145. $this->client = new SphinxClient;
  146. $this->client->setServer($this->server, $this->port);
  147. $this->client->setMaxQueryTime($this->maxQueryTime);
  148. $this->resetCriteria();
  149. }
  150. /**
  151. * @brief connect to searchd server, run given search query through given indexes,
  152. * and return the search results
  153. * @details Mapped from SphinxClient directly
  154. * @param string $query
  155. * @param string $index
  156. * @param string $comment
  157. * @return array
  158. */
  159. public function query($query, $index='*', $comment='')
  160. {
  161. return $this->doSearch($index, $query, $comment);
  162. }
  163. /**
  164. * @brief full text search system query
  165. * @details send query to full text search system
  166. * @param object criteria
  167. * @return DGSphinxSearchResult
  168. */
  169. public function search($criteria = null)
  170. {
  171. if ($criteria === null) {
  172. $res = $this->doSearch($this->criteria->from, $this->criteria->query);
  173. } else {
  174. $res = $this->searchByCriteria($criteria);
  175. }
  176. return $this->initIterator($res, $this->criteria);
  177. }
  178. /**
  179. * @brief full text search system query
  180. * @details send query to full text search system
  181. * @param object criteria
  182. * @return array
  183. */
  184. public function searchRaw($criteria = null)
  185. {
  186. if ($criteria === null) {
  187. $res = $this->doSearch($this->criteria->from, $this->criteria->query);
  188. } else {
  189. $res = $this->searchByCriteria($criteria);
  190. }
  191. return $res;
  192. }
  193. /**
  194. * @brief set select-list (attributes or expressions), SQL-like syntax - 'expression'
  195. * @param string $select
  196. * @return $this chain
  197. */
  198. public function select($select)
  199. {
  200. $this->criteria->select = $select;
  201. $this->client->SetSelect($select);
  202. return $this;
  203. }
  204. /**
  205. * @brief set index name for search, SQL-like syntax - 'table_reference'
  206. * @param string $index
  207. * @return $this chain
  208. */
  209. public function from($index)
  210. {
  211. $this->criteria->from = $index;
  212. return $this;
  213. }
  214. /**
  215. * @brief set search query, SQL-like syntax - 'where_condition'
  216. * @param string $query
  217. * @return $this chain
  218. */
  219. public function where($query)
  220. {
  221. $this->criteria->query = $query;
  222. return $this;
  223. }
  224. /**
  225. * @brief set query filters, SQL-like syntax - 'additional where_condition'
  226. * @param array $filters
  227. * @return $this chain
  228. */
  229. public function filters($filters)
  230. {
  231. $this->criteria->filters = $filters;
  232. //set filters
  233. if ($filters && is_array($filters)) {
  234. foreach ($filters as $fil => $vol) {
  235. // geo filter
  236. if ($fil == 'geo') {
  237. $min = (float) (isset($vol['min']) ? $vol['min'] : 10);
  238. $point = explode(' ', str_replace('POINT(', '', trim($vol['point'], ')')));
  239. $this->client->setGeoAnchor('latitude', 'longitude', (float) $point[1] * ( pi() / 180 ), (float) $point[0] * ( pi() / 180 ));
  240. $this->client->setFilterFloatRange('@geodist', $min, (float) $vol['buffer']);
  241. // usual filter
  242. } else if ($vol) {
  243. $this->client->SetFilter($fil, (is_array($vol)) ? $vol : array($vol));
  244. }
  245. }
  246. }
  247. return $this;
  248. }
  249. /**
  250. * @brief set grouping attribute and function, SQL-like syntax - 'group_by'
  251. * @param array $groupby
  252. * @return $this chain
  253. */
  254. public function groupby($groupby = null)
  255. {
  256. $this->criteria->groupby = $groupby;
  257. // set groupby
  258. if ($groupby && is_array($groupby)) {
  259. $this->client->setGroupBy($groupby['field'], $groupby['mode'], $groupby['order']);
  260. }
  261. return $this;
  262. }
  263. /**
  264. * @brief set matches sorting, SQL-like syntax - 'order_by expression'
  265. * @param string|CSort $orders
  266. * @return $this chain
  267. */
  268. public function orderby($orders = null)
  269. {
  270. $this->criteria->orders = $orders;
  271. /*
  272. * TODO Remove this code after refactoring CSort
  273. */
  274. // set order by string or CSort object
  275. if ($orders) {
  276. if (is_string($orders)) {
  277. $this->client->SetSortMode(SPH_SORT_EXTENDED, $orders);
  278. } else if ($orders instanceOf CSort) {
  279. $this->client->SetSortMode(SPH_SORT_EXTENDED, $orders->getOrderBy());
  280. }
  281. }
  282. return $this;
  283. }
  284. /**
  285. * @brief set offset and count into result set, SQL-like syntax - 'limit $offset, $count'
  286. * @param integer $offset
  287. * @param integer $limit
  288. * @return $this chain
  289. */
  290. public function limit($offset=null, $limit=null)
  291. {
  292. $this->criteria->limit = array(
  293. 'offset' => $offset,
  294. 'limit' => $limit
  295. );
  296. if (isset($offset) && isset($limit)) {
  297. $this->client->setLimits($offset, $limit);
  298. }
  299. return $this;
  300. }
  301. /**
  302. * @brief returns errors if any
  303. */
  304. public function getLastError()
  305. {
  306. return $this->client->getLastError();
  307. }
  308. /**
  309. * @brief reset search criteria to default
  310. * @details reset conditions and set default search options
  311. */
  312. public function resetCriteria()
  313. {
  314. $this->criteria = new stdClass();
  315. $this->client->resetFilters();
  316. $this->client->resetGroupBy();
  317. $this->client->setArrayResult(false);
  318. $this->client->setMatchMode(SPH_MATCH_EXTENDED);
  319. $this->client->setLimits(0, 1000000, 2000);
  320. if (!empty($this->fieldWeights)) {
  321. $this->client->setFieldWeights($this->fieldWeights);
  322. }
  323. }
  324. /**
  325. * @brief handle given search criteria. set them to current object
  326. * @param object $criteria
  327. */
  328. public function setCriteria($criteria)
  329. {
  330. if (!is_object($criteria)) {
  331. throw new DGSphinxSearchException('Criteria does not set.');
  332. }
  333. if (isset($criteria->paginator)) {
  334. if (!is_object($criteria->paginator)) {
  335. throw new DGSphinxSearchException('Criteria paginator invalid.');
  336. }
  337. $this->criteria->paginator = $criteria->paginator;
  338. }
  339. // set select expression
  340. if (isset($criteria->select)) {
  341. $this->select($criteria->select);
  342. }
  343. // set from criteria
  344. if (isset($criteria->from)) {
  345. $this->from($criteria->from);
  346. }
  347. // set where criteria
  348. if (isset($criteria->query)) {
  349. $this->where($criteria->query);
  350. }
  351. // set grouping
  352. if (isset($criteria->groupby)) {
  353. $this->groupby($criteria->groupby);
  354. }
  355. // set filters
  356. if (isset($criteria->filters)) {
  357. $this->filters($criteria->filters);
  358. }
  359. // set field ordering
  360. if (isset($criteria->orders)) {
  361. $this->orderby($criteria->orders);
  362. }
  363. }
  364. /**
  365. * @brief get current search criteria
  366. * @return object criteria
  367. */
  368. public function getCriteria()
  369. {
  370. return $this->criteria;
  371. }
  372. /**
  373. * @brief magic for wrap SphinxClient functions
  374. * @param string $name
  375. * @param array $parameters
  376. * @return DGSphinxSearch
  377. */
  378. public function __call($name, $parameters)
  379. {
  380. $res = null;
  381. if (method_exists($this->client, $name)) {
  382. $res = call_user_func_array(array($this->client, $name), $parameters);
  383. } else {
  384. $res = parent::__call($name, $parameters);
  385. }
  386. // if setter or resetter then return chain
  387. if (strtolower(substr($name, 0, 3)) === 'set' || strtolower(substr($name, 0, 5)) === 'reset') {
  388. $res = $this;
  389. }
  390. return $res;
  391. }
  392. /**
  393. * @brief Performs actual query through Sphinx Connector
  394. * @details Profiles $this->client->query($query, $index);
  395. * @param string $index
  396. * @param string $query
  397. * @param string $comment
  398. * @return array
  399. */
  400. protected function doSearch($index, $query = '', $comment = '')
  401. {
  402. if (!$query) {
  403. throw new DGSphinxSearchException('Query search criteria invalid');
  404. }
  405. if (!$index) {
  406. throw new DGSphinxSearchException('Index search criteria invalid');
  407. }
  408. if ($this->enableResultTrace) {
  409. Yii::trace("Query '$query' is performed for index '$index'", 'CEXT.DGSphinxSearch.doSearch');
  410. }
  411. if ($this->enableProfiling) {
  412. Yii::beginProfile("Search query: '{$query}' in index: '{$index}'", 'CEXT.DGSphinxSearch.doSearch');
  413. }
  414. $res = $this->client->query($query, $index, $comment);
  415. if ($this->getLastError()) {
  416. throw new DGSphinxSearchException($this->getLastError());
  417. }
  418. if ($this->enableProfiling) {
  419. Yii::endProfile("Search query: '{$query}' in index: '{$index}'", 'CEXT.DGSphinxSearch.doSearch');
  420. }
  421. if ($this->enableResultTrace) {
  422. Yii::trace("Query result: " . print_r($res, true), 'CEXT.DGSphinxSearch.doSearch');
  423. }
  424. if (!isset($res['matches'])) {
  425. $res['matches'] = array();
  426. }
  427. $this->resetCriteria();
  428. return $res;
  429. }
  430. /**
  431. * @brief full text search system query by given criteria object
  432. * @details send query to full text search system
  433. * @param object criteria
  434. * @return array
  435. */
  436. protected function searchByCriteria($criteria)
  437. {
  438. if (!is_object($criteria)) {
  439. throw new DGSphinxSearchException('Criteria does not set.');
  440. }
  441. // handle given criteria
  442. $this->setCriteria($criteria);
  443. // process search
  444. $res = $this->doSearch($this->criteria->from, $this->criteria->query);
  445. //ugly hack
  446. if ($criteria->paginator) {
  447. if (isset($res['total'])) {
  448. $criteria->paginator->setItemCount($res['total']);
  449. } else {
  450. $criteria->paginator->setItemCount(0);
  451. }
  452. }
  453. return $res;
  454. }
  455. /**
  456. * @brief init DGSphinxSearchResult interator for search results
  457. * @param array $data
  458. * @param stdObject $criteria
  459. * @return DGSphinxSearchResult
  460. */
  461. protected function initIterator(array $data, $criteria = NULL)
  462. {
  463. $iterator = new DGSphinxSearchResult($data, $criteria);
  464. $iterator->enableProfiling = $this->enableProfiling;
  465. $iterator->enableResultTrace = $this->enableResultTrace;
  466. return $iterator;
  467. }
  468. }