PageRenderTime 61ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/wwwroot/phpbb/phpbb/search/fulltext_sphinx.php

https://github.com/spring/spring-website
PHP | 921 lines | 566 code | 109 blank | 246 comment | 75 complexity | 40588ab126d8b7d68efe88ac6cead433 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, Apache-2.0, LGPL-3.0, BSD-3-Clause
  1. <?php
  2. /**
  3. *
  4. * This file is part of the phpBB Forum Software package.
  5. *
  6. * @copyright (c) phpBB Limited <https://www.phpbb.com>
  7. * @license GNU General Public License, version 2 (GPL-2.0)
  8. *
  9. * For full copyright and license information, please see
  10. * the docs/CREDITS.txt file.
  11. *
  12. */
  13. namespace phpbb\search;
  14. define('SPHINX_MAX_MATCHES', 20000);
  15. define('SPHINX_CONNECT_RETRIES', 3);
  16. define('SPHINX_CONNECT_WAIT_TIME', 300);
  17. /**
  18. * Fulltext search based on the sphinx search deamon
  19. */
  20. class fulltext_sphinx
  21. {
  22. /**
  23. * Associative array holding index stats
  24. * @var array
  25. */
  26. protected $stats = array();
  27. /**
  28. * Holds the words entered by user, obtained by splitting the entered query on whitespace
  29. * @var array
  30. */
  31. protected $split_words = array();
  32. /**
  33. * Holds unique sphinx id
  34. * @var string
  35. */
  36. protected $id;
  37. /**
  38. * Stores the names of both main and delta sphinx indexes
  39. * separated by a semicolon
  40. * @var string
  41. */
  42. protected $indexes;
  43. /**
  44. * Sphinx searchd client object
  45. * @var SphinxClient
  46. */
  47. protected $sphinx;
  48. /**
  49. * Relative path to board root
  50. * @var string
  51. */
  52. protected $phpbb_root_path;
  53. /**
  54. * PHP Extension
  55. * @var string
  56. */
  57. protected $php_ext;
  58. /**
  59. * Auth object
  60. * @var \phpbb\auth\auth
  61. */
  62. protected $auth;
  63. /**
  64. * Config object
  65. * @var \phpbb\config\config
  66. */
  67. protected $config;
  68. /**
  69. * Database connection
  70. * @var \phpbb\db\driver\driver_interface
  71. */
  72. protected $db;
  73. /**
  74. * Database Tools object
  75. * @var \phpbb\db\tools
  76. */
  77. protected $db_tools;
  78. /**
  79. * Stores the database type if supported by sphinx
  80. * @var string
  81. */
  82. protected $dbtype;
  83. /**
  84. * phpBB event dispatcher object
  85. * @var \phpbb\event\dispatcher_interface
  86. */
  87. protected $phpbb_dispatcher;
  88. /**
  89. * User object
  90. * @var \phpbb\user
  91. */
  92. protected $user;
  93. /**
  94. * Stores the generated content of the sphinx config file
  95. * @var string
  96. */
  97. protected $config_file_data = '';
  98. /**
  99. * Contains tidied search query.
  100. * Operators are prefixed in search query and common words excluded
  101. * @var string
  102. */
  103. protected $search_query;
  104. /**
  105. * Constructor
  106. * Creates a new \phpbb\search\fulltext_postgres, which is used as a search backend
  107. *
  108. * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
  109. * @param string $phpbb_root_path Relative path to phpBB root
  110. * @param string $phpEx PHP file extension
  111. * @param \phpbb\auth\auth $auth Auth object
  112. * @param \phpbb\config\config $config Config object
  113. * @param \phpbb\db\driver\driver_interface Database object
  114. * @param \phpbb\user $user User object
  115. * @param \phpbb\event\dispatcher_interface $phpbb_dispatcher Event dispatcher object
  116. */
  117. public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
  118. {
  119. $this->phpbb_root_path = $phpbb_root_path;
  120. $this->php_ext = $phpEx;
  121. $this->config = $config;
  122. $this->phpbb_dispatcher = $phpbb_dispatcher;
  123. $this->user = $user;
  124. $this->db = $db;
  125. $this->auth = $auth;
  126. // Initialize \phpbb\db\tools object
  127. $this->db_tools = new \phpbb\db\tools($this->db);
  128. if(!$this->config['fulltext_sphinx_id'])
  129. {
  130. set_config('fulltext_sphinx_id', unique_id());
  131. }
  132. $this->id = $this->config['fulltext_sphinx_id'];
  133. $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main';
  134. if (!class_exists('SphinxClient'))
  135. {
  136. require($this->phpbb_root_path . 'includes/sphinxapi.' . $this->php_ext);
  137. }
  138. // Initialize sphinx client
  139. $this->sphinx = new \SphinxClient();
  140. $this->sphinx->SetServer(($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost'), ($this->config['fulltext_sphinx_port'] ? (int) $this->config['fulltext_sphinx_port'] : 9312));
  141. $error = false;
  142. }
  143. /**
  144. * Returns the name of this search backend to be displayed to administrators
  145. *
  146. * @return string Name
  147. */
  148. public function get_name()
  149. {
  150. return 'Sphinx Fulltext';
  151. }
  152. /**
  153. * Returns the search_query
  154. *
  155. * @return string search query
  156. */
  157. public function get_search_query()
  158. {
  159. return $this->search_query;
  160. }
  161. /**
  162. * Returns false as there is no word_len array
  163. *
  164. * @return false
  165. */
  166. public function get_word_length()
  167. {
  168. return false;
  169. }
  170. /**
  171. * Returns an empty array as there are no common_words
  172. *
  173. * @return array common words that are ignored by search backend
  174. */
  175. public function get_common_words()
  176. {
  177. return array();
  178. }
  179. /**
  180. * Checks permissions and paths, if everything is correct it generates the config file
  181. *
  182. * @return string|bool Language key of the error/incompatiblity encountered, or false if successful
  183. */
  184. public function init()
  185. {
  186. if ($this->db->get_sql_layer() != 'mysql' && $this->db->get_sql_layer() != 'mysql4' && $this->db->get_sql_layer() != 'mysqli' && $this->db->get_sql_layer() != 'postgres')
  187. {
  188. return $this->user->lang['FULLTEXT_SPHINX_WRONG_DATABASE'];
  189. }
  190. // Move delta to main index each hour
  191. set_config('search_gc', 3600);
  192. return false;
  193. }
  194. /**
  195. * Generates content of sphinx.conf
  196. *
  197. * @return bool True if sphinx.conf content is correctly generated, false otherwise
  198. */
  199. protected function config_generate()
  200. {
  201. // Check if Database is supported by Sphinx
  202. if ($this->db->get_sql_layer() =='mysql' || $this->db->get_sql_layer() == 'mysql4' || $this->db->get_sql_layer() == 'mysqli')
  203. {
  204. $this->dbtype = 'mysql';
  205. }
  206. else if ($this->db->get_sql_layer() == 'postgres')
  207. {
  208. $this->dbtype = 'pgsql';
  209. }
  210. else
  211. {
  212. $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_WRONG_DATABASE');
  213. return false;
  214. }
  215. // Check if directory paths have been filled
  216. if (!$this->config['fulltext_sphinx_data_path'])
  217. {
  218. $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_NO_CONFIG_DATA');
  219. return false;
  220. }
  221. include($this->phpbb_root_path . 'config.' . $this->php_ext);
  222. /* Now that we're sure everything was entered correctly,
  223. generate a config for the index. We use a config value
  224. fulltext_sphinx_id for this, as it should be unique. */
  225. $config_object = new \phpbb\search\sphinx\config($this->config_file_data);
  226. $config_data = array(
  227. 'source source_phpbb_' . $this->id . '_main' => array(
  228. array('type', $this->dbtype . ' # mysql or pgsql'),
  229. // This config value sql_host needs to be changed incase sphinx and sql are on different servers
  230. array('sql_host', $dbhost . ' # SQL server host sphinx connects to'),
  231. array('sql_user', '[dbuser]'),
  232. array('sql_pass', '[dbpassword]'),
  233. array('sql_db', $dbname),
  234. array('sql_port', $dbport . ' # optional, default is 3306 for mysql and 5432 for pgsql'),
  235. array('sql_query_pre', 'SET NAMES \'utf8\''),
  236. array('sql_query_pre', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = (SELECT MAX(post_id) FROM ' . POSTS_TABLE . ') WHERE counter_id = 1'),
  237. array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''),
  238. array('sql_range_step', '5000'),
  239. array('sql_query', 'SELECT
  240. p.post_id AS id,
  241. p.forum_id,
  242. p.topic_id,
  243. p.poster_id,
  244. p.post_visibility,
  245. CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
  246. p.post_time,
  247. p.post_subject,
  248. p.post_subject as title,
  249. p.post_text as data,
  250. t.topic_last_post_time,
  251. 0 as deleted
  252. FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
  253. WHERE
  254. p.topic_id = t.topic_id
  255. AND p.post_id >= $start AND p.post_id <= $end'),
  256. array('sql_query_post', ''),
  257. array('sql_query_post_index', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = $maxid WHERE counter_id = 1'),
  258. array('sql_query_info', 'SELECT * FROM ' . POSTS_TABLE . ' WHERE post_id = $id'),
  259. array('sql_attr_uint', 'forum_id'),
  260. array('sql_attr_uint', 'topic_id'),
  261. array('sql_attr_uint', 'poster_id'),
  262. array('sql_attr_uint', 'post_visibility'),
  263. array('sql_attr_bool', 'topic_first_post'),
  264. array('sql_attr_bool', 'deleted'),
  265. array('sql_attr_timestamp', 'post_time'),
  266. array('sql_attr_timestamp', 'topic_last_post_time'),
  267. array('sql_attr_string', 'post_subject'),
  268. ),
  269. 'source source_phpbb_' . $this->id . '_delta : source_phpbb_' . $this->id . '_main' => array(
  270. array('sql_query_pre', ''),
  271. array('sql_query_range', ''),
  272. array('sql_range_step', ''),
  273. array('sql_query', 'SELECT
  274. p.post_id AS id,
  275. p.forum_id,
  276. p.topic_id,
  277. p.poster_id,
  278. p.post_visibility,
  279. CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
  280. p.post_time,
  281. p.post_subject,
  282. p.post_subject as title,
  283. p.post_text as data,
  284. t.topic_last_post_time,
  285. 0 as deleted
  286. FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
  287. WHERE
  288. p.topic_id = t.topic_id
  289. AND p.post_id >= ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'),
  290. ),
  291. 'index index_phpbb_' . $this->id . '_main' => array(
  292. array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'),
  293. array('source', 'source_phpbb_' . $this->id . '_main'),
  294. array('docinfo', 'extern'),
  295. array('morphology', 'none'),
  296. array('stopwords', ''),
  297. array('min_word_len', '2'),
  298. array('charset_type', 'utf-8'),
  299. array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+0410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'),
  300. array('min_prefix_len', '0'),
  301. array('min_infix_len', '0'),
  302. ),
  303. 'index index_phpbb_' . $this->id . '_delta : index_phpbb_' . $this->id . '_main' => array(
  304. array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'),
  305. array('source', 'source_phpbb_' . $this->id . '_delta'),
  306. ),
  307. 'indexer' => array(
  308. array('mem_limit', $this->config['fulltext_sphinx_indexer_mem_limit'] . 'M'),
  309. ),
  310. 'searchd' => array(
  311. array('compat_sphinxql_magics' , '0'),
  312. array('listen' , ($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost') . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '9312')),
  313. array('log', $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'),
  314. array('query_log', $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'),
  315. array('read_timeout', '5'),
  316. array('max_children', '30'),
  317. array('pid_file', $this->config['fulltext_sphinx_data_path'] . 'searchd.pid'),
  318. array('max_matches', (string) SPHINX_MAX_MATCHES),
  319. array('binlog_path', $this->config['fulltext_sphinx_data_path']),
  320. ),
  321. );
  322. $non_unique = array('sql_query_pre' => true, 'sql_attr_uint' => true, 'sql_attr_timestamp' => true, 'sql_attr_str2ordinal' => true, 'sql_attr_bool' => true);
  323. $delete = array('sql_group_column' => true, 'sql_date_column' => true, 'sql_str2ordinal_column' => true);
  324. foreach ($config_data as $section_name => $section_data)
  325. {
  326. $section = $config_object->get_section_by_name($section_name);
  327. if (!$section)
  328. {
  329. $section = $config_object->add_section($section_name);
  330. }
  331. foreach ($delete as $key => $void)
  332. {
  333. $section->delete_variables_by_name($key);
  334. }
  335. foreach ($non_unique as $key => $void)
  336. {
  337. $section->delete_variables_by_name($key);
  338. }
  339. foreach ($section_data as $entry)
  340. {
  341. $key = $entry[0];
  342. $value = $entry[1];
  343. if (!isset($non_unique[$key]))
  344. {
  345. $variable = $section->get_variable_by_name($key);
  346. if (!$variable)
  347. {
  348. $variable = $section->create_variable($key, $value);
  349. }
  350. else
  351. {
  352. $variable->set_value($value);
  353. }
  354. }
  355. else
  356. {
  357. $variable = $section->create_variable($key, $value);
  358. }
  359. }
  360. }
  361. $this->config_file_data = $config_object->get_data();
  362. return true;
  363. }
  364. /**
  365. * Splits keywords entered by a user into an array of words stored in $this->split_words
  366. * Stores the tidied search query in $this->search_query
  367. *
  368. * @param string $keywords Contains the keyword as entered by the user
  369. * @param string $terms is either 'all' or 'any'
  370. * @return false if no valid keywords were found and otherwise true
  371. */
  372. public function split_keywords(&$keywords, $terms)
  373. {
  374. if ($terms == 'all')
  375. {
  376. $match = array('#\sand\s#i', '#\sor\s#i', '#\snot\s#i', '#\+#', '#-#', '#\|#', '#@#');
  377. $replace = array(' & ', ' | ', ' - ', ' +', ' -', ' |', '');
  378. $replacements = 0;
  379. $keywords = preg_replace($match, $replace, $keywords);
  380. $this->sphinx->SetMatchMode(SPH_MATCH_EXTENDED);
  381. }
  382. else
  383. {
  384. $this->sphinx->SetMatchMode(SPH_MATCH_ANY);
  385. }
  386. // Keep quotes and new lines
  387. $keywords = str_replace(array('&quot;', "\n"), array('"', ' '), trim($keywords));
  388. if (strlen($keywords) > 0)
  389. {
  390. $this->search_query = str_replace('"', '&quot;', $keywords);
  391. return true;
  392. }
  393. return false;
  394. }
  395. /**
  396. * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
  397. *
  398. * @param string $type contains either posts or topics depending on what should be searched for
  399. * @param string $fields contains either titleonly (topic titles should be searched), msgonly (only message bodies should be searched), firstpost (only subject and body of the first post should be searched) or all (all post bodies and subjects should be searched)
  400. * @param string $terms is either 'all' (use query as entered, words without prefix should default to "have to be in field") or 'any' (ignore search query parts and just return all posts that contain any of the specified words)
  401. * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
  402. * @param string $sort_key is the key of $sort_by_sql for the selected sorting
  403. * @param string $sort_dir is either a or d representing ASC and DESC
  404. * @param string $sort_days specifies the maximum amount of days a post may be old
  405. * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
  406. * @param string $post_visibility specifies which types of posts the user can view in which forums
  407. * @param int $topic_id is set to 0 or a topic id, if it is not 0 then only posts in this topic should be searched
  408. * @param array $author_ary an array of author ids if the author should be ignored during the search the array is empty
  409. * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
  410. * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
  411. * @param int $start indicates the first index of the page
  412. * @param int $per_page number of ids each page is supposed to contain
  413. * @return boolean|int total number of results
  414. */
  415. public function keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $post_visibility, $topic_id, $author_ary, $author_name, &$id_ary, &$start, $per_page)
  416. {
  417. // No keywords? No posts.
  418. if (!strlen($this->search_query) && !sizeof($author_ary))
  419. {
  420. return false;
  421. }
  422. $id_ary = array();
  423. $join_topic = ($type != 'posts');
  424. // Sorting
  425. if ($type == 'topics')
  426. {
  427. switch ($sort_key)
  428. {
  429. case 'a':
  430. $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'poster_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
  431. break;
  432. case 'f':
  433. $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'forum_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
  434. break;
  435. case 'i':
  436. case 's':
  437. $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'post_subject ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
  438. break;
  439. case 't':
  440. default:
  441. $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'topic_last_post_time ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
  442. break;
  443. }
  444. }
  445. else
  446. {
  447. switch ($sort_key)
  448. {
  449. case 'a':
  450. $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'poster_id');
  451. break;
  452. case 'f':
  453. $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'forum_id');
  454. break;
  455. case 'i':
  456. case 's':
  457. $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_subject');
  458. break;
  459. case 't':
  460. default:
  461. $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_time');
  462. break;
  463. }
  464. }
  465. // Most narrow filters first
  466. if ($topic_id)
  467. {
  468. $this->sphinx->SetFilter('topic_id', array($topic_id));
  469. }
  470. $search_query_prefix = '';
  471. switch ($fields)
  472. {
  473. case 'titleonly':
  474. // Only search the title
  475. if ($terms == 'all')
  476. {
  477. $search_query_prefix = '@title ';
  478. }
  479. // Weight for the title
  480. $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
  481. // 1 is first_post, 0 is not first post
  482. $this->sphinx->SetFilter('topic_first_post', array(1));
  483. break;
  484. case 'msgonly':
  485. // Only search the body
  486. if ($terms == 'all')
  487. {
  488. $search_query_prefix = '@data ';
  489. }
  490. // Weight for the body
  491. $this->sphinx->SetFieldWeights(array("title" => 1, "data" => 5));
  492. break;
  493. case 'firstpost':
  494. // More relative weight for the title, also search the body
  495. $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
  496. // 1 is first_post, 0 is not first post
  497. $this->sphinx->SetFilter('topic_first_post', array(1));
  498. break;
  499. default:
  500. // More relative weight for the title, also search the body
  501. $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
  502. break;
  503. }
  504. if (sizeof($author_ary))
  505. {
  506. $this->sphinx->SetFilter('poster_id', $author_ary);
  507. }
  508. // As this is not simply possible at the moment, we limit the result to approved posts.
  509. // This will make it impossible for moderators to search unapproved and softdeleted posts,
  510. // but at least it will also cause the same for normal users.
  511. $this->sphinx->SetFilter('post_visibility', array(ITEM_APPROVED));
  512. if (sizeof($ex_fid_ary))
  513. {
  514. // All forums that a user is allowed to access
  515. $fid_ary = array_unique(array_intersect(array_keys($this->auth->acl_getf('f_read', true)), array_keys($this->auth->acl_getf('f_search', true))));
  516. // All forums that the user wants to and can search in
  517. $search_forums = array_diff($fid_ary, $ex_fid_ary);
  518. if (sizeof($search_forums))
  519. {
  520. $this->sphinx->SetFilter('forum_id', $search_forums);
  521. }
  522. }
  523. $this->sphinx->SetFilter('deleted', array(0));
  524. $this->sphinx->SetLimits($start, (int) $per_page, SPHINX_MAX_MATCHES);
  525. $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
  526. // Could be connection to localhost:9312 failed (errno=111,
  527. // msg=Connection refused) during rotate, retry if so
  528. $retries = SPHINX_CONNECT_RETRIES;
  529. while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
  530. {
  531. usleep(SPHINX_CONNECT_WAIT_TIME);
  532. $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
  533. }
  534. if ($this->sphinx->GetLastError())
  535. {
  536. add_log('critical', 'LOG_SPHINX_ERROR', $this->sphinx->GetLastError());
  537. if ($this->auth->acl_get('a_'))
  538. {
  539. trigger_error($this->user->lang('SPHINX_SEARCH_FAILED', $this->sphinx->GetLastError()));
  540. }
  541. else
  542. {
  543. trigger_error($this->user->lang('SPHINX_SEARCH_FAILED_LOG'));
  544. }
  545. }
  546. $result_count = $result['total_found'];
  547. if ($result_count && $start >= $result_count)
  548. {
  549. $start = floor(($result_count - 1) / $per_page) * $per_page;
  550. $this->sphinx->SetLimits((int) $start, (int) $per_page, SPHINX_MAX_MATCHES);
  551. $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
  552. // Could be connection to localhost:9312 failed (errno=111,
  553. // msg=Connection refused) during rotate, retry if so
  554. $retries = SPHINX_CONNECT_RETRIES;
  555. while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
  556. {
  557. usleep(SPHINX_CONNECT_WAIT_TIME);
  558. $result = $this->sphinx->Query($search_query_prefix . str_replace('&quot;', '"', $this->search_query), $this->indexes);
  559. }
  560. }
  561. $id_ary = array();
  562. if (isset($result['matches']))
  563. {
  564. if ($type == 'posts')
  565. {
  566. $id_ary = array_keys($result['matches']);
  567. }
  568. else
  569. {
  570. foreach ($result['matches'] as $key => $value)
  571. {
  572. $id_ary[] = $value['attrs']['topic_id'];
  573. }
  574. }
  575. }
  576. else
  577. {
  578. return false;
  579. }
  580. $id_ary = array_slice($id_ary, 0, (int) $per_page);
  581. return $result_count;
  582. }
  583. /**
  584. * Performs a search on an author's posts without caring about message contents. Depends on display specific params
  585. *
  586. * @param string $type contains either posts or topics depending on what should be searched for
  587. * @param boolean $firstpost_only if true, only topic starting posts will be considered
  588. * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
  589. * @param string $sort_key is the key of $sort_by_sql for the selected sorting
  590. * @param string $sort_dir is either a or d representing ASC and DESC
  591. * @param string $sort_days specifies the maximum amount of days a post may be old
  592. * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
  593. * @param string $post_visibility specifies which types of posts the user can view in which forums
  594. * @param int $topic_id is set to 0 or a topic id, if it is not 0 then only posts in this topic should be searched
  595. * @param array $author_ary an array of author ids
  596. * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
  597. * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
  598. * @param int $start indicates the first index of the page
  599. * @param int $per_page number of ids each page is supposed to contain
  600. * @return boolean|int total number of results
  601. */
  602. public function author_search($type, $firstpost_only, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $post_visibility, $topic_id, $author_ary, $author_name, &$id_ary, $start, $per_page)
  603. {
  604. $this->search_query = '';
  605. $this->sphinx->SetMatchMode(SPH_MATCH_FULLSCAN);
  606. $fields = ($firstpost_only) ? 'firstpost' : 'all';
  607. $terms = 'all';
  608. return $this->keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $post_visibility, $topic_id, $author_ary, $author_name, $id_ary, $start, $per_page);
  609. }
  610. /**
  611. * Updates wordlist and wordmatch tables when a message is posted or changed
  612. *
  613. * @param string $mode Contains the post mode: edit, post, reply, quote
  614. * @param int $post_id The id of the post which is modified/created
  615. * @param string &$message New or updated post content
  616. * @param string &$subject New or updated post subject
  617. * @param int $poster_id Post author's user id
  618. * @param int $forum_id The id of the forum in which the post is located
  619. */
  620. public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
  621. {
  622. if ($mode == 'edit')
  623. {
  624. $this->sphinx->UpdateAttributes($this->indexes, array('forum_id', 'poster_id'), array((int) $post_id => array((int) $forum_id, (int) $poster_id)));
  625. }
  626. else if ($mode != 'post' && $post_id)
  627. {
  628. // Update topic_last_post_time for full topic
  629. $sql_array = array(
  630. 'SELECT' => 'p1.post_id',
  631. 'FROM' => array(
  632. POSTS_TABLE => 'p1',
  633. ),
  634. 'LEFT_JOIN' => array(array(
  635. 'FROM' => array(
  636. POSTS_TABLE => 'p2'
  637. ),
  638. 'ON' => 'p1.topic_id = p2.topic_id',
  639. )),
  640. 'WHERE' => 'p2.post_id = ' . ((int) $post_id),
  641. );
  642. $sql = $this->db->sql_build_query('SELECT', $sql_array);
  643. $result = $this->db->sql_query($sql);
  644. $post_updates = array();
  645. $post_time = time();
  646. while ($row = $this->db->sql_fetchrow($result))
  647. {
  648. $post_updates[(int) $row['post_id']] = array($post_time);
  649. }
  650. $this->db->sql_freeresult($result);
  651. if (sizeof($post_updates))
  652. {
  653. $this->sphinx->UpdateAttributes($this->indexes, array('topic_last_post_time'), $post_updates);
  654. }
  655. }
  656. }
  657. /**
  658. * Delete a post from the index after it was deleted
  659. */
  660. public function index_remove($post_ids, $author_ids, $forum_ids)
  661. {
  662. $values = array();
  663. foreach ($post_ids as $post_id)
  664. {
  665. $values[$post_id] = array(1);
  666. }
  667. $this->sphinx->UpdateAttributes($this->indexes, array('deleted'), $values);
  668. }
  669. /**
  670. * Nothing needs to be destroyed
  671. */
  672. public function tidy($create = false)
  673. {
  674. set_config('search_last_gc', time(), true);
  675. }
  676. /**
  677. * Create sphinx table
  678. *
  679. * @return string|bool error string is returned incase of errors otherwise false
  680. */
  681. public function create_index($acp_module, $u_action)
  682. {
  683. if (!$this->index_created())
  684. {
  685. $table_data = array(
  686. 'COLUMNS' => array(
  687. 'counter_id' => array('UINT', 0),
  688. 'max_doc_id' => array('UINT', 0),
  689. ),
  690. 'PRIMARY_KEY' => 'counter_id',
  691. );
  692. $this->db_tools->sql_create_table(SPHINX_TABLE, $table_data);
  693. $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE;
  694. $this->db->sql_query($sql);
  695. $data = array(
  696. 'counter_id' => '1',
  697. 'max_doc_id' => '0',
  698. );
  699. $sql = 'INSERT INTO ' . SPHINX_TABLE . ' ' . $this->db->sql_build_array('INSERT', $data);
  700. $this->db->sql_query($sql);
  701. }
  702. return false;
  703. }
  704. /**
  705. * Drop sphinx table
  706. *
  707. * @return string|bool error string is returned incase of errors otherwise false
  708. */
  709. public function delete_index($acp_module, $u_action)
  710. {
  711. if (!$this->index_created())
  712. {
  713. return false;
  714. }
  715. $this->db_tools->sql_table_drop(SPHINX_TABLE);
  716. return false;
  717. }
  718. /**
  719. * Returns true if the sphinx table was created
  720. *
  721. * @return bool true if sphinx table was created
  722. */
  723. public function index_created($allow_new_files = true)
  724. {
  725. $created = false;
  726. if ($this->db_tools->sql_table_exists(SPHINX_TABLE))
  727. {
  728. $created = true;
  729. }
  730. return $created;
  731. }
  732. /**
  733. * Returns an associative array containing information about the indexes
  734. *
  735. * @return string|bool Language string of error false otherwise
  736. */
  737. public function index_stats()
  738. {
  739. if (empty($this->stats))
  740. {
  741. $this->get_stats();
  742. }
  743. return array(
  744. $this->user->lang['FULLTEXT_SPHINX_MAIN_POSTS'] => ($this->index_created()) ? $this->stats['main_posts'] : 0,
  745. $this->user->lang['FULLTEXT_SPHINX_DELTA_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0,
  746. $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0,
  747. );
  748. }
  749. /**
  750. * Collects stats that can be displayed on the index maintenance page
  751. */
  752. protected function get_stats()
  753. {
  754. if ($this->index_created())
  755. {
  756. $sql = 'SELECT COUNT(post_id) as total_posts
  757. FROM ' . POSTS_TABLE;
  758. $result = $this->db->sql_query($sql);
  759. $this->stats['total_posts'] = (int) $this->db->sql_fetchfield('total_posts');
  760. $this->db->sql_freeresult($result);
  761. $sql = 'SELECT COUNT(p.post_id) as main_posts
  762. FROM ' . POSTS_TABLE . ' p, ' . SPHINX_TABLE . ' m
  763. WHERE p.post_id <= m.max_doc_id
  764. AND m.counter_id = 1';
  765. $result = $this->db->sql_query($sql);
  766. $this->stats['main_posts'] = (int) $this->db->sql_fetchfield('main_posts');
  767. $this->db->sql_freeresult($result);
  768. }
  769. }
  770. /**
  771. * Returns a list of options for the ACP to display
  772. *
  773. * @return associative array containing template and config variables
  774. */
  775. public function acp()
  776. {
  777. $config_vars = array(
  778. 'fulltext_sphinx_data_path' => 'string',
  779. 'fulltext_sphinx_host' => 'string',
  780. 'fulltext_sphinx_port' => 'string',
  781. 'fulltext_sphinx_indexer_mem_limit' => 'int',
  782. );
  783. $tpl = '
  784. <span class="error">' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE']. '</span>
  785. <dl>
  786. <dt><label for="fulltext_sphinx_data_path">' . $this->user->lang['FULLTEXT_SPHINX_DATA_PATH'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_DATA_PATH_EXPLAIN'] . '</span></dt>
  787. <dd><input id="fulltext_sphinx_data_path" type="text" size="40" maxlength="255" name="config[fulltext_sphinx_data_path]" value="' . $this->config['fulltext_sphinx_data_path'] . '" /></dd>
  788. </dl>
  789. <dl>
  790. <dt><label for="fulltext_sphinx_host">' . $this->user->lang['FULLTEXT_SPHINX_HOST'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_HOST_EXPLAIN'] . '</span></dt>
  791. <dd><input id="fulltext_sphinx_host" type="text" size="40" maxlength="255" name="config[fulltext_sphinx_host]" value="' . $this->config['fulltext_sphinx_host'] . '" /></dd>
  792. </dl>
  793. <dl>
  794. <dt><label for="fulltext_sphinx_port">' . $this->user->lang['FULLTEXT_SPHINX_PORT'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_PORT_EXPLAIN'] . '</span></dt>
  795. <dd><input id="fulltext_sphinx_port" type="number" size="4" maxlength="10" name="config[fulltext_sphinx_port]" value="' . $this->config['fulltext_sphinx_port'] . '" /></dd>
  796. </dl>
  797. <dl>
  798. <dt><label for="fulltext_sphinx_indexer_mem_limit">' . $this->user->lang['FULLTEXT_SPHINX_INDEXER_MEM_LIMIT'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN'] . '</span></dt>
  799. <dd><input id="fulltext_sphinx_indexer_mem_limit" type="number" size="4" maxlength="10" name="config[fulltext_sphinx_indexer_mem_limit]" value="' . $this->config['fulltext_sphinx_indexer_mem_limit'] . '" /> ' . $this->user->lang['MIB'] . '</dd>
  800. </dl>
  801. <dl>
  802. <dt><label for="fulltext_sphinx_config_file">' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_FILE'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN'] . '</span></dt>
  803. <dd>' . (($this->config_generate()) ? '<textarea readonly="readonly" rows="6" id="sphinx_config_data">' . htmlspecialchars($this->config_file_data) . '</textarea>' : $this->config_file_data) . '</dd>
  804. <dl>
  805. ';
  806. // These are fields required in the config table
  807. return array(
  808. 'tpl' => $tpl,
  809. 'config' => $config_vars
  810. );
  811. }
  812. }