PageRenderTime 56ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/phpBB/includes/search/fulltext_mysql.php

http://github.com/phpbb/phpbb3
PHP | 939 lines | 676 code | 126 blank | 137 comment | 131 complexity | 664feb041033b8cae16cf442e4be2d7f MD5 | raw file
Possible License(s): AGPL-1.0
  1. <?php
  2. /**
  3. *
  4. * @package search
  5. * @copyright (c) 2005 phpBB Group
  6. * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2
  7. *
  8. */
  9. /**
  10. * @ignore
  11. */
  12. if (!defined('IN_PHPBB'))
  13. {
  14. exit;
  15. }
  16. /**
  17. * fulltext_mysql
  18. * Fulltext search for MySQL
  19. * @package search
  20. */
  21. class phpbb_search_fulltext_mysql extends phpbb_search_base
  22. {
  23. var $stats = array();
  24. var $word_length = array();
  25. var $split_words = array();
  26. var $search_query;
  27. var $common_words = array();
  28. var $pcre_properties = false;
  29. var $mbstring_regex = false;
  30. public function __construct(&$error)
  31. {
  32. global $config;
  33. $this->word_length = array('min' => $config['fulltext_mysql_min_word_len'], 'max' => $config['fulltext_mysql_max_word_len']);
  34. // PHP may not be linked with the bundled PCRE lib and instead with an older version
  35. if (phpbb_pcre_utf8_support())
  36. {
  37. $this->pcre_properties = true;
  38. }
  39. if (function_exists('mb_ereg'))
  40. {
  41. $this->mbstring_regex = true;
  42. mb_regex_encoding('UTF-8');
  43. }
  44. $error = false;
  45. }
  46. /**
  47. * Returns the name of this search backend to be displayed to administrators
  48. *
  49. * @return string Name
  50. */
  51. public function get_name()
  52. {
  53. return 'MySQL Fulltext';
  54. }
  55. /**
  56. * Checks for correct MySQL version and stores min/max word length in the config
  57. */
  58. function init()
  59. {
  60. global $db, $user;
  61. if ($db->sql_layer != 'mysql4' && $db->sql_layer != 'mysqli')
  62. {
  63. return $user->lang['FULLTEXT_MYSQL_INCOMPATIBLE_VERSION'];
  64. }
  65. $result = $db->sql_query('SHOW TABLE STATUS LIKE \'' . POSTS_TABLE . '\'');
  66. $info = $db->sql_fetchrow($result);
  67. $db->sql_freeresult($result);
  68. $engine = '';
  69. if (isset($info['Engine']))
  70. {
  71. $engine = $info['Engine'];
  72. }
  73. else if (isset($info['Type']))
  74. {
  75. $engine = $info['Type'];
  76. }
  77. if ($engine != 'MyISAM')
  78. {
  79. return $user->lang['FULLTEXT_MYSQL_NOT_MYISAM'];
  80. }
  81. $sql = 'SHOW VARIABLES
  82. LIKE \'ft\_%\'';
  83. $result = $db->sql_query($sql);
  84. $mysql_info = array();
  85. while ($row = $db->sql_fetchrow($result))
  86. {
  87. $mysql_info[$row['Variable_name']] = $row['Value'];
  88. }
  89. $db->sql_freeresult($result);
  90. set_config('fulltext_mysql_max_word_len', $mysql_info['ft_max_word_len']);
  91. set_config('fulltext_mysql_min_word_len', $mysql_info['ft_min_word_len']);
  92. return false;
  93. }
  94. /**
  95. * Splits keywords entered by a user into an array of words stored in $this->split_words
  96. * Stores the tidied search query in $this->search_query
  97. *
  98. * @param string &$keywords Contains the keyword as entered by the user
  99. * @param string $terms is either 'all' or 'any'
  100. * @return bool false if no valid keywords were found and otherwise true
  101. */
  102. function split_keywords(&$keywords, $terms)
  103. {
  104. global $config, $user;
  105. if ($terms == 'all')
  106. {
  107. $match = array('#\sand\s#iu', '#\sor\s#iu', '#\snot\s#iu', '#(^|\s)\+#', '#(^|\s)-#', '#(^|\s)\|#');
  108. $replace = array(' +', ' |', ' -', ' +', ' -', ' |');
  109. $keywords = preg_replace($match, $replace, $keywords);
  110. }
  111. // Filter out as above
  112. $split_keywords = preg_replace("#[\n\r\t]+#", ' ', trim(htmlspecialchars_decode($keywords)));
  113. // Split words
  114. if ($this->pcre_properties)
  115. {
  116. $split_keywords = preg_replace('#([^\p{L}\p{N}\'*"()])#u', '$1$1', str_replace('\'\'', '\' \'', trim($split_keywords)));
  117. }
  118. else if ($this->mbstring_regex)
  119. {
  120. $split_keywords = mb_ereg_replace('([^\w\'*"()])', '\\1\\1', str_replace('\'\'', '\' \'', trim($split_keywords)));
  121. }
  122. else
  123. {
  124. $split_keywords = preg_replace('#([^\w\'*"()])#u', '$1$1', str_replace('\'\'', '\' \'', trim($split_keywords)));
  125. }
  126. if ($this->pcre_properties)
  127. {
  128. $matches = array();
  129. preg_match_all('#(?:[^\p{L}\p{N}*"()]|^)([+\-|]?(?:[\p{L}\p{N}*"()]+\'?)*[\p{L}\p{N}*"()])(?:[^\p{L}\p{N}*"()]|$)#u', $split_keywords, $matches);
  130. $this->split_words = $matches[1];
  131. }
  132. else if ($this->mbstring_regex)
  133. {
  134. mb_ereg_search_init($split_keywords, '(?:[^\w*"()]|^)([+\-|]?(?:[\w*"()]+\'?)*[\w*"()])(?:[^\w*"()]|$)');
  135. while (($word = mb_ereg_search_regs()))
  136. {
  137. $this->split_words[] = $word[1];
  138. }
  139. }
  140. else
  141. {
  142. $matches = array();
  143. preg_match_all('#(?:[^\w*"()]|^)([+\-|]?(?:[\w*"()]+\'?)*[\w*"()])(?:[^\w*"()]|$)#u', $split_keywords, $matches);
  144. $this->split_words = $matches[1];
  145. }
  146. // We limit the number of allowed keywords to minimize load on the database
  147. if ($config['max_num_search_keywords'] && sizeof($this->split_words) > $config['max_num_search_keywords'])
  148. {
  149. trigger_error($user->lang('MAX_NUM_SEARCH_KEYWORDS_REFINE', $config['max_num_search_keywords'], sizeof($this->split_words)));
  150. }
  151. // to allow phrase search, we need to concatenate quoted words
  152. $tmp_split_words = array();
  153. $phrase = '';
  154. foreach ($this->split_words as $word)
  155. {
  156. if ($phrase)
  157. {
  158. $phrase .= ' ' . $word;
  159. if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
  160. {
  161. $tmp_split_words[] = $phrase;
  162. $phrase = '';
  163. }
  164. }
  165. else if (strpos($word, '"') !== false && substr_count($word, '"') % 2 == 1)
  166. {
  167. $phrase = $word;
  168. }
  169. else
  170. {
  171. $tmp_split_words[] = $word . ' ';
  172. }
  173. }
  174. if ($phrase)
  175. {
  176. $tmp_split_words[] = $phrase;
  177. }
  178. $this->split_words = $tmp_split_words;
  179. unset($tmp_split_words);
  180. unset($phrase);
  181. foreach ($this->split_words as $i => $word)
  182. {
  183. $clean_word = preg_replace('#^[+\-|"]#', '', $word);
  184. // check word length
  185. $clean_len = utf8_strlen(str_replace('*', '', $clean_word));
  186. if (($clean_len < $config['fulltext_mysql_min_word_len']) || ($clean_len > $config['fulltext_mysql_max_word_len']))
  187. {
  188. $this->common_words[] = $word;
  189. unset($this->split_words[$i]);
  190. }
  191. }
  192. if ($terms == 'any')
  193. {
  194. $this->search_query = '';
  195. foreach ($this->split_words as $word)
  196. {
  197. if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0) || (strpos($word, '|') === 0))
  198. {
  199. $word = substr($word, 1);
  200. }
  201. $this->search_query .= $word . ' ';
  202. }
  203. }
  204. else
  205. {
  206. $this->search_query = '';
  207. foreach ($this->split_words as $word)
  208. {
  209. if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0))
  210. {
  211. $this->search_query .= $word . ' ';
  212. }
  213. else if (strpos($word, '|') === 0)
  214. {
  215. $this->search_query .= substr($word, 1) . ' ';
  216. }
  217. else
  218. {
  219. $this->search_query .= '+' . $word . ' ';
  220. }
  221. }
  222. }
  223. $this->search_query = utf8_htmlspecialchars($this->search_query);
  224. if ($this->search_query)
  225. {
  226. $this->split_words = array_values($this->split_words);
  227. sort($this->split_words);
  228. return true;
  229. }
  230. return false;
  231. }
  232. /**
  233. * Turns text into an array of words
  234. */
  235. function split_message($text)
  236. {
  237. global $config;
  238. // Split words
  239. if ($this->pcre_properties)
  240. {
  241. $text = preg_replace('#([^\p{L}\p{N}\'*])#u', '$1$1', str_replace('\'\'', '\' \'', trim($text)));
  242. }
  243. else if ($this->mbstring_regex)
  244. {
  245. $text = mb_ereg_replace('([^\w\'*])', '\\1\\1', str_replace('\'\'', '\' \'', trim($text)));
  246. }
  247. else
  248. {
  249. $text = preg_replace('#([^\w\'*])#u', '$1$1', str_replace('\'\'', '\' \'', trim($text)));
  250. }
  251. if ($this->pcre_properties)
  252. {
  253. $matches = array();
  254. preg_match_all('#(?:[^\p{L}\p{N}*]|^)([+\-|]?(?:[\p{L}\p{N}*]+\'?)*[\p{L}\p{N}*])(?:[^\p{L}\p{N}*]|$)#u', $text, $matches);
  255. $text = $matches[1];
  256. }
  257. else if ($this->mbstring_regex)
  258. {
  259. mb_ereg_search_init($text, '(?:[^\w*]|^)([+\-|]?(?:[\w*]+\'?)*[\w*])(?:[^\w*]|$)');
  260. $text = array();
  261. while (($word = mb_ereg_search_regs()))
  262. {
  263. $text[] = $word[1];
  264. }
  265. }
  266. else
  267. {
  268. $matches = array();
  269. preg_match_all('#(?:[^\w*]|^)([+\-|]?(?:[\w*]+\'?)*[\w*])(?:[^\w*]|$)#u', $text, $matches);
  270. $text = $matches[1];
  271. }
  272. // remove too short or too long words
  273. $text = array_values($text);
  274. for ($i = 0, $n = sizeof($text); $i < $n; $i++)
  275. {
  276. $text[$i] = trim($text[$i]);
  277. if (utf8_strlen($text[$i]) < $config['fulltext_mysql_min_word_len'] || utf8_strlen($text[$i]) > $config['fulltext_mysql_max_word_len'])
  278. {
  279. unset($text[$i]);
  280. }
  281. }
  282. return array_values($text);
  283. }
  284. /**
  285. * Performs a search on keywords depending on display specific params. You have to run split_keywords() first.
  286. *
  287. * @param string $type contains either posts or topics depending on what should be searched for
  288. * @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)
  289. * @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)
  290. * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
  291. * @param string $sort_key is the key of $sort_by_sql for the selected sorting
  292. * @param string $sort_dir is either a or d representing ASC and DESC
  293. * @param string $sort_days specifies the maximum amount of days a post may be old
  294. * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
  295. * @param array $m_approve_fid_ary specifies an array of forum ids in which the searcher is allowed to view unapproved posts
  296. * @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
  297. * @param array $author_ary an array of author ids if the author should be ignored during the search the array is empty
  298. * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
  299. * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
  300. * @param int $start indicates the first index of the page
  301. * @param int $per_page number of ids each page is supposed to contain
  302. * @return boolean|int total number of results
  303. *
  304. * @access public
  305. */
  306. function keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $m_approve_fid_ary, $topic_id, $author_ary, $author_name, &$id_ary, $start, $per_page)
  307. {
  308. global $config, $db;
  309. // No keywords? No posts.
  310. if (!$this->search_query)
  311. {
  312. return false;
  313. }
  314. // generate a search_key from all the options to identify the results
  315. $search_key = md5(implode('#', array(
  316. implode(', ', $this->split_words),
  317. $type,
  318. $fields,
  319. $terms,
  320. $sort_days,
  321. $sort_key,
  322. $topic_id,
  323. implode(',', $ex_fid_ary),
  324. implode(',', $m_approve_fid_ary),
  325. implode(',', $author_ary)
  326. )));
  327. // try reading the results from cache
  328. $result_count = 0;
  329. if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
  330. {
  331. return $result_count;
  332. }
  333. $id_ary = array();
  334. $join_topic = ($type == 'posts') ? false : true;
  335. // Build sql strings for sorting
  336. $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
  337. $sql_sort_table = $sql_sort_join = '';
  338. switch ($sql_sort[0])
  339. {
  340. case 'u':
  341. $sql_sort_table = USERS_TABLE . ' u, ';
  342. $sql_sort_join = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
  343. break;
  344. case 't':
  345. $join_topic = true;
  346. break;
  347. case 'f':
  348. $sql_sort_table = FORUMS_TABLE . ' f, ';
  349. $sql_sort_join = ' AND f.forum_id = p.forum_id ';
  350. break;
  351. }
  352. // Build some display specific sql strings
  353. switch ($fields)
  354. {
  355. case 'titleonly':
  356. $sql_match = 'p.post_subject';
  357. $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
  358. $join_topic = true;
  359. break;
  360. case 'msgonly':
  361. $sql_match = 'p.post_text';
  362. $sql_match_where = '';
  363. break;
  364. case 'firstpost':
  365. $sql_match = 'p.post_subject, p.post_text';
  366. $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
  367. $join_topic = true;
  368. break;
  369. default:
  370. $sql_match = 'p.post_subject, p.post_text';
  371. $sql_match_where = '';
  372. break;
  373. }
  374. if (!sizeof($m_approve_fid_ary))
  375. {
  376. $m_approve_fid_sql = ' AND p.post_approved = 1';
  377. }
  378. else if ($m_approve_fid_ary === array(-1))
  379. {
  380. $m_approve_fid_sql = '';
  381. }
  382. else
  383. {
  384. $m_approve_fid_sql = ' AND (p.post_approved = 1 OR ' . $db->sql_in_set('p.forum_id', $m_approve_fid_ary, true) . ')';
  385. }
  386. $sql_select = (!$result_count) ? 'SQL_CALC_FOUND_ROWS ' : '';
  387. $sql_select = ($type == 'posts') ? $sql_select . 'p.post_id' : 'DISTINCT ' . $sql_select . 't.topic_id';
  388. $sql_from = ($join_topic) ? TOPICS_TABLE . ' t, ' : '';
  389. $field = ($type == 'posts') ? 'post_id' : 'topic_id';
  390. if (sizeof($author_ary) && $author_name)
  391. {
  392. // first one matches post of registered users, second one guests and deleted users
  393. $sql_author = ' AND (' . $db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
  394. }
  395. else if (sizeof($author_ary))
  396. {
  397. $sql_author = ' AND ' . $db->sql_in_set('p.poster_id', $author_ary);
  398. }
  399. else
  400. {
  401. $sql_author = '';
  402. }
  403. $sql_where_options = $sql_sort_join;
  404. $sql_where_options .= ($topic_id) ? ' AND p.topic_id = ' . $topic_id : '';
  405. $sql_where_options .= ($join_topic) ? ' AND t.topic_id = p.topic_id' : '';
  406. $sql_where_options .= (sizeof($ex_fid_ary)) ? ' AND ' . $db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
  407. $sql_where_options .= $m_approve_fid_sql;
  408. $sql_where_options .= $sql_author;
  409. $sql_where_options .= ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
  410. $sql_where_options .= $sql_match_where;
  411. $sql = "SELECT $sql_select
  412. FROM $sql_from$sql_sort_table" . POSTS_TABLE . " p
  413. WHERE MATCH ($sql_match) AGAINST ('" . $db->sql_escape(htmlspecialchars_decode($this->search_query)) . "' IN BOOLEAN MODE)
  414. $sql_where_options
  415. ORDER BY $sql_sort";
  416. $result = $db->sql_query_limit($sql, $config['search_block_size'], $start);
  417. while ($row = $db->sql_fetchrow($result))
  418. {
  419. $id_ary[] = (int) $row[$field];
  420. }
  421. $db->sql_freeresult($result);
  422. $id_ary = array_unique($id_ary);
  423. if (!sizeof($id_ary))
  424. {
  425. return false;
  426. }
  427. // if the total result count is not cached yet, retrieve it from the db
  428. if (!$result_count)
  429. {
  430. $sql = 'SELECT FOUND_ROWS() as result_count';
  431. $result = $db->sql_query($sql);
  432. $result_count = (int) $db->sql_fetchfield('result_count');
  433. $db->sql_freeresult($result);
  434. if (!$result_count)
  435. {
  436. return false;
  437. }
  438. }
  439. // store the ids, from start on then delete anything that isn't on the current page because we only need ids for one page
  440. $this->save_ids($search_key, implode(' ', $this->split_words), $author_ary, $result_count, $id_ary, $start, $sort_dir);
  441. $id_ary = array_slice($id_ary, 0, (int) $per_page);
  442. return $result_count;
  443. }
  444. /**
  445. * Performs a search on an author's posts without caring about message contents. Depends on display specific params
  446. *
  447. * @param string $type contains either posts or topics depending on what should be searched for
  448. * @param boolean $firstpost_only if true, only topic starting posts will be considered
  449. * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
  450. * @param string $sort_key is the key of $sort_by_sql for the selected sorting
  451. * @param string $sort_dir is either a or d representing ASC and DESC
  452. * @param string $sort_days specifies the maximum amount of days a post may be old
  453. * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
  454. * @param array $m_approve_fid_ary specifies an array of forum ids in which the searcher is allowed to view unapproved posts
  455. * @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
  456. * @param array $author_ary an array of author ids
  457. * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
  458. * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
  459. * @param int $start indicates the first index of the page
  460. * @param int $per_page number of ids each page is supposed to contain
  461. * @return boolean|int total number of results
  462. *
  463. * @access public
  464. */
  465. function author_search($type, $firstpost_only, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $m_approve_fid_ary, $topic_id, $author_ary, $author_name, &$id_ary, $start, $per_page)
  466. {
  467. global $config, $db;
  468. // No author? No posts.
  469. if (!sizeof($author_ary))
  470. {
  471. return 0;
  472. }
  473. // generate a search_key from all the options to identify the results
  474. $search_key = md5(implode('#', array(
  475. '',
  476. $type,
  477. ($firstpost_only) ? 'firstpost' : '',
  478. '',
  479. '',
  480. $sort_days,
  481. $sort_key,
  482. $topic_id,
  483. implode(',', $ex_fid_ary),
  484. implode(',', $m_approve_fid_ary),
  485. implode(',', $author_ary),
  486. $author_name,
  487. )));
  488. // try reading the results from cache
  489. $result_count = 0;
  490. if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
  491. {
  492. return $result_count;
  493. }
  494. $id_ary = array();
  495. // Create some display specific sql strings
  496. if ($author_name)
  497. {
  498. // first one matches post of registered users, second one guests and deleted users
  499. $sql_author = '(' . $db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
  500. }
  501. else
  502. {
  503. $sql_author = $db->sql_in_set('p.poster_id', $author_ary);
  504. }
  505. $sql_fora = (sizeof($ex_fid_ary)) ? ' AND ' . $db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
  506. $sql_topic_id = ($topic_id) ? ' AND p.topic_id = ' . (int) $topic_id : '';
  507. $sql_time = ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
  508. $sql_firstpost = ($firstpost_only) ? ' AND p.post_id = t.topic_first_post_id' : '';
  509. // Build sql strings for sorting
  510. $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
  511. $sql_sort_table = $sql_sort_join = '';
  512. switch ($sql_sort[0])
  513. {
  514. case 'u':
  515. $sql_sort_table = USERS_TABLE . ' u, ';
  516. $sql_sort_join = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
  517. break;
  518. case 't':
  519. $sql_sort_table = ($type == 'posts' && !$firstpost_only) ? TOPICS_TABLE . ' t, ' : '';
  520. $sql_sort_join = ($type == 'posts' && !$firstpost_only) ? ' AND t.topic_id = p.topic_id ' : '';
  521. break;
  522. case 'f':
  523. $sql_sort_table = FORUMS_TABLE . ' f, ';
  524. $sql_sort_join = ' AND f.forum_id = p.forum_id ';
  525. break;
  526. }
  527. if (!sizeof($m_approve_fid_ary))
  528. {
  529. $m_approve_fid_sql = ' AND p.post_approved = 1';
  530. }
  531. else if ($m_approve_fid_ary == array(-1))
  532. {
  533. $m_approve_fid_sql = '';
  534. }
  535. else
  536. {
  537. $m_approve_fid_sql = ' AND (p.post_approved = 1 OR ' . $db->sql_in_set('p.forum_id', $m_approve_fid_ary, true) . ')';
  538. }
  539. // If the cache was completely empty count the results
  540. $calc_results = ($result_count) ? '' : 'SQL_CALC_FOUND_ROWS ';
  541. // Build the query for really selecting the post_ids
  542. if ($type == 'posts')
  543. {
  544. $sql = "SELECT {$calc_results}p.post_id
  545. FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
  546. WHERE $sql_author
  547. $sql_topic_id
  548. $sql_firstpost
  549. $m_approve_fid_sql
  550. $sql_fora
  551. $sql_sort_join
  552. $sql_time
  553. ORDER BY $sql_sort";
  554. $field = 'post_id';
  555. }
  556. else
  557. {
  558. $sql = "SELECT {$calc_results}t.topic_id
  559. FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
  560. WHERE $sql_author
  561. $sql_topic_id
  562. $sql_firstpost
  563. $m_approve_fid_sql
  564. $sql_fora
  565. AND t.topic_id = p.topic_id
  566. $sql_sort_join
  567. $sql_time
  568. GROUP BY t.topic_id
  569. ORDER BY $sql_sort";
  570. $field = 'topic_id';
  571. }
  572. // Only read one block of posts from the db and then cache it
  573. $result = $db->sql_query_limit($sql, $config['search_block_size'], $start);
  574. while ($row = $db->sql_fetchrow($result))
  575. {
  576. $id_ary[] = (int) $row[$field];
  577. }
  578. $db->sql_freeresult($result);
  579. // retrieve the total result count if needed
  580. if (!$result_count)
  581. {
  582. $sql = 'SELECT FOUND_ROWS() as result_count';
  583. $result = $db->sql_query($sql);
  584. $result_count = (int) $db->sql_fetchfield('result_count');
  585. $db->sql_freeresult($result);
  586. if (!$result_count)
  587. {
  588. return false;
  589. }
  590. }
  591. if (sizeof($id_ary))
  592. {
  593. $this->save_ids($search_key, '', $author_ary, $result_count, $id_ary, $start, $sort_dir);
  594. $id_ary = array_slice($id_ary, 0, $per_page);
  595. return $result_count;
  596. }
  597. return false;
  598. }
  599. /**
  600. * Destroys cached search results, that contained one of the new words in a post so the results won't be outdated.
  601. *
  602. * @param string $mode contains the post mode: edit, post, reply, quote ...
  603. */
  604. function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
  605. {
  606. global $db;
  607. // Split old and new post/subject to obtain array of words
  608. $split_text = $this->split_message($message);
  609. $split_title = ($subject) ? $this->split_message($subject) : array();
  610. $words = array_unique(array_merge($split_text, $split_title));
  611. unset($split_text);
  612. unset($split_title);
  613. // destroy cached search results containing any of the words removed or added
  614. $this->destroy_cache($words, array($poster_id));
  615. unset($words);
  616. }
  617. /**
  618. * Destroy cached results, that might be outdated after deleting a post
  619. */
  620. function index_remove($post_ids, $author_ids, $forum_ids)
  621. {
  622. $this->destroy_cache(array(), $author_ids);
  623. }
  624. /**
  625. * Destroy old cache entries
  626. */
  627. function tidy()
  628. {
  629. global $db, $config;
  630. // destroy too old cached search results
  631. $this->destroy_cache(array());
  632. set_config('search_last_gc', time(), true);
  633. }
  634. /**
  635. * Create fulltext index
  636. */
  637. function create_index($acp_module, $u_action)
  638. {
  639. global $db;
  640. // Make sure we can actually use MySQL with fulltext indexes
  641. if ($error = $this->init())
  642. {
  643. return $error;
  644. }
  645. if (empty($this->stats))
  646. {
  647. $this->get_stats();
  648. }
  649. $alter = array();
  650. if (!isset($this->stats['post_subject']))
  651. {
  652. if ($db->sql_layer == 'mysqli' || version_compare($db->sql_server_info(true), '4.1.3', '>='))
  653. {
  654. //$alter[] = 'MODIFY post_subject varchar(100) COLLATE utf8_unicode_ci DEFAULT \'\' NOT NULL';
  655. }
  656. else
  657. {
  658. $alter[] = 'MODIFY post_subject text NOT NULL';
  659. }
  660. $alter[] = 'ADD FULLTEXT (post_subject)';
  661. }
  662. if (!isset($this->stats['post_text']))
  663. {
  664. if ($db->sql_layer == 'mysqli' || version_compare($db->sql_server_info(true), '4.1.3', '>='))
  665. {
  666. $alter[] = 'MODIFY post_text mediumtext COLLATE utf8_unicode_ci NOT NULL';
  667. }
  668. else
  669. {
  670. $alter[] = 'MODIFY post_text mediumtext NOT NULL';
  671. }
  672. $alter[] = 'ADD FULLTEXT (post_text)';
  673. }
  674. if (!isset($this->stats['post_content']))
  675. {
  676. $alter[] = 'ADD FULLTEXT post_content (post_subject, post_text)';
  677. }
  678. if (sizeof($alter))
  679. {
  680. $db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
  681. }
  682. $db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
  683. return false;
  684. }
  685. /**
  686. * Drop fulltext index
  687. */
  688. function delete_index($acp_module, $u_action)
  689. {
  690. global $db;
  691. // Make sure we can actually use MySQL with fulltext indexes
  692. if ($error = $this->init())
  693. {
  694. return $error;
  695. }
  696. if (empty($this->stats))
  697. {
  698. $this->get_stats();
  699. }
  700. $alter = array();
  701. if (isset($this->stats['post_subject']))
  702. {
  703. $alter[] = 'DROP INDEX post_subject';
  704. }
  705. if (isset($this->stats['post_text']))
  706. {
  707. $alter[] = 'DROP INDEX post_text';
  708. }
  709. if (isset($this->stats['post_content']))
  710. {
  711. $alter[] = 'DROP INDEX post_content';
  712. }
  713. if (sizeof($alter))
  714. {
  715. $db->sql_query('ALTER TABLE ' . POSTS_TABLE . ' ' . implode(', ', $alter));
  716. }
  717. $db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
  718. return false;
  719. }
  720. /**
  721. * Returns true if both FULLTEXT indexes exist
  722. */
  723. function index_created()
  724. {
  725. if (empty($this->stats))
  726. {
  727. $this->get_stats();
  728. }
  729. return (isset($this->stats['post_text']) && isset($this->stats['post_subject']) && isset($this->stats['post_content'])) ? true : false;
  730. }
  731. /**
  732. * Returns an associative array containing information about the indexes
  733. */
  734. function index_stats()
  735. {
  736. global $user;
  737. if (empty($this->stats))
  738. {
  739. $this->get_stats();
  740. }
  741. return array(
  742. $user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0,
  743. );
  744. }
  745. function get_stats()
  746. {
  747. global $db;
  748. if (strpos($db->sql_layer, 'mysql') === false)
  749. {
  750. $this->stats = array();
  751. return;
  752. }
  753. $sql = 'SHOW INDEX
  754. FROM ' . POSTS_TABLE;
  755. $result = $db->sql_query($sql);
  756. while ($row = $db->sql_fetchrow($result))
  757. {
  758. // deal with older MySQL versions which didn't use Index_type
  759. $index_type = (isset($row['Index_type'])) ? $row['Index_type'] : $row['Comment'];
  760. if ($index_type == 'FULLTEXT')
  761. {
  762. if ($row['Key_name'] == 'post_text')
  763. {
  764. $this->stats['post_text'] = $row;
  765. }
  766. else if ($row['Key_name'] == 'post_subject')
  767. {
  768. $this->stats['post_subject'] = $row;
  769. }
  770. else if ($row['Key_name'] == 'post_content')
  771. {
  772. $this->stats['post_content'] = $row;
  773. }
  774. }
  775. }
  776. $db->sql_freeresult($result);
  777. $sql = 'SELECT COUNT(post_id) as total_posts
  778. FROM ' . POSTS_TABLE;
  779. $result = $db->sql_query($sql);
  780. $this->stats['total_posts'] = (int) $db->sql_fetchfield('total_posts');
  781. $db->sql_freeresult($result);
  782. }
  783. /**
  784. * Display a note, that UTF-8 support is not available with certain versions of PHP
  785. */
  786. function acp()
  787. {
  788. global $user, $config;
  789. $tpl = '
  790. <dl>
  791. <dt><label>' . $user->lang['FULLTEXT_MYSQL_PCRE'] . '</label><br /><span>' . $user->lang['FULLTEXT_MYSQL_PCRE_EXPLAIN'] . '</span></dt>
  792. <dd>' . (($this->pcre_properties) ? $user->lang['YES'] : $user->lang['NO']) . ' (PHP ' . PHP_VERSION . ')</dd>
  793. </dl>
  794. <dl>
  795. <dt><label>' . $user->lang['FULLTEXT_MYSQL_MBSTRING'] . '</label><br /><span>' . $user->lang['FULLTEXT_MYSQL_MBSTRING_EXPLAIN'] . '</span></dt>
  796. <dd>' . (($this->mbstring_regex) ? $user->lang['YES'] : $user->lang['NO']). '</dd>
  797. </dl>
  798. <dl>
  799. <dt><label>' . $user->lang['MIN_SEARCH_CHARS'] . ':</label><br /><span>' . $user->lang['FULLTEXT_MYSQL_MIN_SEARCH_CHARS_EXPLAIN'] . '</span></dt>
  800. <dd>' . $config['fulltext_mysql_min_word_len'] . '</dd>
  801. </dl>
  802. <dl>
  803. <dt><label>' . $user->lang['MAX_SEARCH_CHARS'] . ':</label><br /><span>' . $user->lang['FULLTEXT_MYSQL_MAX_SEARCH_CHARS_EXPLAIN'] . '</span></dt>
  804. <dd>' . $config['fulltext_mysql_max_word_len'] . '</dd>
  805. </dl>
  806. ';
  807. // These are fields required in the config table
  808. return array(
  809. 'tpl' => $tpl,
  810. 'config' => array()
  811. );
  812. }
  813. }