[ Index ]

PHP Cross Reference of phpBB-3.1.12-deutsch

title

Body

[close]

/phpbb/search/ -> fulltext_postgres.php (source)

   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  
  14  namespace phpbb\search;
  15  
  16  /**
  17  * Fulltext search for PostgreSQL
  18  */
  19  class fulltext_postgres extends \phpbb\search\base
  20  {
  21      /**
  22       * Associative array holding index stats
  23       * @var array
  24       */
  25      protected $stats = array();
  26  
  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      /**
  34       * Stores the tsearch query
  35       * @var string
  36       */
  37      protected $tsearch_query;
  38  
  39      /**
  40       * True if phrase search is supported.
  41       * PostgreSQL fulltext currently doesn't support it
  42       * @var boolean
  43       */
  44      protected $phrase_search = false;
  45  
  46      /**
  47       * Config object
  48       * @var \phpbb\config\config
  49       */
  50      protected $config;
  51  
  52      /**
  53       * Database connection
  54       * @var \phpbb\db\driver\driver_interface
  55       */
  56      protected $db;
  57  
  58      /**
  59       * phpBB event dispatcher object
  60       * @var \phpbb\event\dispatcher_interface
  61       */
  62      protected $phpbb_dispatcher;
  63  
  64      /**
  65       * User object
  66       * @var \phpbb\user
  67       */
  68      protected $user;
  69  
  70      /**
  71       * Contains tidied search query.
  72       * Operators are prefixed in search query and common words excluded
  73       * @var string
  74       */
  75      protected $search_query;
  76  
  77      /**
  78       * Contains common words.
  79       * Common words are words with length less/more than min/max length
  80       * @var array
  81       */
  82      protected $common_words = array();
  83  
  84      /**
  85       * Associative array stores the min and max word length to be searched
  86       * @var array
  87       */
  88      protected $word_length = array();
  89  
  90      /**
  91       * Constructor
  92       * Creates a new \phpbb\search\fulltext_postgres, which is used as a search backend
  93       *
  94       * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
  95       * @param string $phpbb_root_path Relative path to phpBB root
  96       * @param string $phpEx PHP file extension
  97       * @param \phpbb\auth\auth $auth Auth object
  98       * @param \phpbb\config\config $config Config object
  99       * @param \phpbb\db\driver\driver_interface Database object
 100       * @param \phpbb\user $user User object
 101       * @param \phpbb\event\dispatcher_interface    $phpbb_dispatcher    Event dispatcher object
 102       */
 103  	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
 104      {
 105          $this->config = $config;
 106          $this->db = $db;
 107          $this->phpbb_dispatcher = $phpbb_dispatcher;
 108          $this->user = $user;
 109  
 110          $this->word_length = array('min' => $this->config['fulltext_postgres_min_word_len'], 'max' => $this->config['fulltext_postgres_max_word_len']);
 111  
 112          /**
 113           * Load the UTF tools
 114           */
 115          if (!function_exists('utf8_strlen'))
 116          {
 117              include($phpbb_root_path . 'includes/utf/utf_tools.' . $phpEx);
 118          }
 119  
 120          $error = false;
 121      }
 122  
 123      /**
 124      * Returns the name of this search backend to be displayed to administrators
 125      *
 126      * @return string Name
 127      */
 128  	public function get_name()
 129      {
 130          return 'PostgreSQL Fulltext';
 131      }
 132  
 133      /**
 134       * Returns the search_query
 135       *
 136       * @return string search query
 137       */
 138  	public function get_search_query()
 139      {
 140          return $this->search_query;
 141      }
 142  
 143      /**
 144       * Returns the common_words array
 145       *
 146       * @return array common words that are ignored by search backend
 147       */
 148  	public function get_common_words()
 149      {
 150          return $this->common_words;
 151      }
 152  
 153      /**
 154       * Returns the word_length array
 155       *
 156       * @return array min and max word length for searching
 157       */
 158  	public function get_word_length()
 159      {
 160          return $this->word_length;
 161      }
 162  
 163      /**
 164       * Returns if phrase search is supported or not
 165       *
 166       * @return bool
 167       */
 168  	public function supports_phrase_search()
 169      {
 170          return $this->phrase_search;
 171      }
 172  
 173      /**
 174      * Checks for correct PostgreSQL version and stores min/max word length in the config
 175      *
 176      * @return string|bool Language key of the error/incompatiblity occurred
 177      */
 178  	public function init()
 179      {
 180          if ($this->db->get_sql_layer() != 'postgres')
 181          {
 182              return $this->user->lang['FULLTEXT_POSTGRES_INCOMPATIBLE_DATABASE'];
 183          }
 184  
 185          return false;
 186      }
 187  
 188      /**
 189      * Splits keywords entered by a user into an array of words stored in $this->split_words
 190      * Stores the tidied search query in $this->search_query
 191      *
 192      * @param    string    &$keywords    Contains the keyword as entered by the user
 193      * @param    string    $terms    is either 'all' or 'any'
 194      * @return    bool    false    if no valid keywords were found and otherwise true
 195      */
 196  	public function split_keywords(&$keywords, $terms)
 197      {
 198          if ($terms == 'all')
 199          {
 200              $match        = array('#\sand\s#iu', '#\sor\s#iu', '#\snot\s#iu', '#(^|\s)\+#', '#(^|\s)-#', '#(^|\s)\|#');
 201              $replace    = array(' +', ' |', ' -', ' +', ' -', ' |');
 202  
 203              $keywords = preg_replace($match, $replace, $keywords);
 204          }
 205  
 206          // Filter out as above
 207          $split_keywords = preg_replace("#[\"\n\r\t]+#", ' ', trim(htmlspecialchars_decode($keywords)));
 208  
 209          // Split words
 210          $split_keywords = preg_replace('#([^\p{L}\p{N}\'*"()])#u', '$1$1', str_replace('\'\'', '\' \'', trim($split_keywords)));
 211          $matches = array();
 212          preg_match_all('#(?:[^\p{L}\p{N}*"()]|^)([+\-|]?(?:[\p{L}\p{N}*"()]+\'?)*[\p{L}\p{N}*"()])(?:[^\p{L}\p{N}*"()]|$)#u', $split_keywords, $matches);
 213          $this->split_words = $matches[1];
 214  
 215          foreach ($this->split_words as $i => $word)
 216          {
 217              $clean_word = preg_replace('#^[+\-|"]#', '', $word);
 218  
 219              // check word length
 220              $clean_len = utf8_strlen(str_replace('*', '', $clean_word));
 221              if (($clean_len < $this->config['fulltext_postgres_min_word_len']) || ($clean_len > $this->config['fulltext_postgres_max_word_len']))
 222              {
 223                  $this->common_words[] = $word;
 224                  unset($this->split_words[$i]);
 225              }
 226          }
 227  
 228          if ($terms == 'any')
 229          {
 230              $this->search_query = '';
 231              $this->tsearch_query = '';
 232              foreach ($this->split_words as $word)
 233              {
 234                  if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0) || (strpos($word, '|') === 0))
 235                  {
 236                      $word = substr($word, 1);
 237                  }
 238                  $this->search_query .= $word . ' ';
 239                  $this->tsearch_query .= '|' . $word . ' ';
 240              }
 241          }
 242          else
 243          {
 244              $this->search_query = '';
 245              $this->tsearch_query = '';
 246              foreach ($this->split_words as $word)
 247              {
 248                  if (strpos($word, '+') === 0)
 249                  {
 250                      $this->search_query .= $word . ' ';
 251                      $this->tsearch_query .= '&' . substr($word, 1) . ' ';
 252                  }
 253                  else if (strpos($word, '-') === 0)
 254                  {
 255                      $this->search_query .= $word . ' ';
 256                      $this->tsearch_query .= '&!' . substr($word, 1) . ' ';
 257                  }
 258                  else if (strpos($word, '|') === 0)
 259                  {
 260                      $this->search_query .= $word . ' ';
 261                      $this->tsearch_query .= '|' . substr($word, 1) . ' ';
 262                  }
 263                  else
 264                  {
 265                      $this->search_query .= '+' . $word . ' ';
 266                      $this->tsearch_query .= '&' . $word . ' ';
 267                  }
 268              }
 269          }
 270  
 271          $this->tsearch_query = substr($this->tsearch_query, 1);
 272          $this->search_query = utf8_htmlspecialchars($this->search_query);
 273  
 274          if ($this->search_query)
 275          {
 276              $this->split_words = array_values($this->split_words);
 277              sort($this->split_words);
 278              return true;
 279          }
 280          return false;
 281      }
 282  
 283      /**
 284      * Turns text into an array of words
 285      * @param string $text contains post text/subject
 286      */
 287  	public function split_message($text)
 288      {
 289          // Split words
 290          $text = preg_replace('#([^\p{L}\p{N}\'*])#u', '$1$1', str_replace('\'\'', '\' \'', trim($text)));
 291          $matches = array();
 292          preg_match_all('#(?:[^\p{L}\p{N}*]|^)([+\-|]?(?:[\p{L}\p{N}*]+\'?)*[\p{L}\p{N}*])(?:[^\p{L}\p{N}*]|$)#u', $text, $matches);
 293          $text = $matches[1];
 294  
 295          // remove too short or too long words
 296          $text = array_values($text);
 297          for ($i = 0, $n = sizeof($text); $i < $n; $i++)
 298          {
 299              $text[$i] = trim($text[$i]);
 300              if (utf8_strlen($text[$i]) < $this->config['fulltext_postgres_min_word_len'] || utf8_strlen($text[$i]) > $this->config['fulltext_postgres_max_word_len'])
 301              {
 302                  unset($text[$i]);
 303              }
 304          }
 305  
 306          return array_values($text);
 307      }
 308  
 309      /**
 310      * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
 311      *
 312      * @param    string        $type                contains either posts or topics depending on what should be searched for
 313      * @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)
 314      * @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)
 315      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
 316      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
 317      * @param    string        $sort_dir            is either a or d representing ASC and DESC
 318      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
 319      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
 320      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
 321      * @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
 322      * @param    array        $author_ary            an array of author ids if the author should be ignored during the search the array is empty
 323      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
 324      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
 325      * @param    int            $start                indicates the first index of the page
 326      * @param    int            $per_page            number of ids each page is supposed to contain
 327      * @return    boolean|int                        total number of results
 328      */
 329  	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)
 330      {
 331          // No keywords? No posts
 332          if (!$this->search_query)
 333          {
 334              return false;
 335          }
 336  
 337          // When search query contains queries like -foo
 338          if (strpos($this->search_query, '+') === false)
 339          {
 340              return false;
 341          }
 342  
 343          // generate a search_key from all the options to identify the results
 344          $search_key_array = array(
 345              implode(', ', $this->split_words),
 346              $type,
 347              $fields,
 348              $terms,
 349              $sort_days,
 350              $sort_key,
 351              $topic_id,
 352              implode(',', $ex_fid_ary),
 353              $post_visibility,
 354              implode(',', $author_ary)
 355          );
 356  
 357          /**
 358          * Allow changing the search_key for cached results
 359          *
 360          * @event core.search_postgres_by_keyword_modify_search_key
 361          * @var    array    search_key_array    Array with search parameters to generate the search_key
 362          * @var    string    type                Searching type ('posts', 'topics')
 363          * @var    string    fields                Searching fields ('titleonly', 'msgonly', 'firstpost', 'all')
 364          * @var    string    terms                Searching terms ('all', 'any')
 365          * @var    int        sort_days            Time, in days, of the oldest possible post to list
 366          * @var    string    sort_key            The sort type used from the possible sort types
 367          * @var    int        topic_id            Limit the search to this topic_id only
 368          * @var    array    ex_fid_ary            Which forums not to search on
 369          * @var    string    post_visibility        Post visibility data
 370          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 371          * @since 3.1.7-RC1
 372          */
 373          $vars = array(
 374              'search_key_array',
 375              'type',
 376              'fields',
 377              'terms',
 378              'sort_days',
 379              'sort_key',
 380              'topic_id',
 381              'ex_fid_ary',
 382              'post_visibility',
 383              'author_ary',
 384          );
 385          extract($this->phpbb_dispatcher->trigger_event('core.search_postgres_by_keyword_modify_search_key', compact($vars)));
 386  
 387          $search_key = md5(implode('#', $search_key_array));
 388  
 389          if ($start < 0)
 390          {
 391              $start = 0;
 392          }
 393  
 394          // try reading the results from cache
 395          $result_count = 0;
 396          if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
 397          {
 398              return $result_count;
 399          }
 400  
 401          $id_ary = array();
 402  
 403          $join_topic = ($type == 'posts') ? false : true;
 404  
 405          // Build sql strings for sorting
 406          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
 407          $sql_sort_table = $sql_sort_join = '';
 408  
 409          switch ($sql_sort[0])
 410          {
 411              case 'u':
 412                  $sql_sort_table    = USERS_TABLE . ' u, ';
 413                  $sql_sort_join    = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
 414              break;
 415  
 416              case 't':
 417                  $join_topic = true;
 418              break;
 419  
 420              case 'f':
 421                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
 422                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
 423              break;
 424          }
 425  
 426          // Build some display specific sql strings
 427          switch ($fields)
 428          {
 429              case 'titleonly':
 430                  $sql_match = 'p.post_subject';
 431                  $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
 432                  $join_topic = true;
 433              break;
 434  
 435              case 'msgonly':
 436                  $sql_match = 'p.post_text';
 437                  $sql_match_where = '';
 438              break;
 439  
 440              case 'firstpost':
 441                  $sql_match = 'p.post_subject, p.post_text';
 442                  $sql_match_where = ' AND p.post_id = t.topic_first_post_id';
 443                  $join_topic = true;
 444              break;
 445  
 446              default:
 447                  $sql_match = 'p.post_subject, p.post_text';
 448                  $sql_match_where = '';
 449              break;
 450          }
 451  
 452          $tsearch_query = $this->tsearch_query;
 453  
 454          /**
 455          * Allow changing the query used to search for posts using fulltext_postgres
 456          *
 457          * @event core.search_postgres_keywords_main_query_before
 458          * @var    string    tsearch_query        The parsed keywords used for this search
 459          * @var    int        result_count        The previous result count for the format of the query.
 460          *                                    Set to 0 to force a re-count
 461          * @var    bool    join_topic            Weather or not TOPICS_TABLE should be CROSS JOIN'ED
 462          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 463          * @var    string    author_name            An extra username to search on (!empty(author_ary) must be true, to be relevant)
 464          * @var    array    ex_fid_ary            Which forums not to search on
 465          * @var    int        topic_id            Limit the search to this topic_id only
 466          * @var    string    sql_sort_table        Extra tables to include in the SQL query.
 467          *                                    Used in conjunction with sql_sort_join
 468          * @var    string    sql_sort_join        SQL conditions to join all the tables used together.
 469          *                                    Used in conjunction with sql_sort_table
 470          * @var    int        sort_days            Time, in days, of the oldest possible post to list
 471          * @var    string    sql_match            Which columns to do the search on.
 472          * @var    string    sql_match_where        Extra conditions to use to properly filter the matching process
 473          * @var    string    sort_by_sql            The possible predefined sort types
 474          * @var    string    sort_key            The sort type used from the possible sort types
 475          * @var    string    sort_dir            "a" for ASC or "d" dor DESC for the sort order used
 476          * @var    string    sql_sort            The result SQL when processing sort_by_sql + sort_key + sort_dir
 477          * @var    int        start                How many posts to skip in the search results (used for pagination)
 478          * @since 3.1.5-RC1
 479          */
 480          $vars = array(
 481              'tsearch_query',
 482              'result_count',
 483              'join_topic',
 484              'author_ary',
 485              'author_name',
 486              'ex_fid_ary',
 487              'topic_id',
 488              'sql_sort_table',
 489              'sql_sort_join',
 490              'sort_days',
 491              'sql_match',
 492              'sql_match_where',
 493              'sort_by_sql',
 494              'sort_key',
 495              'sort_dir',
 496              'sql_sort',
 497              'start',
 498          );
 499          extract($this->phpbb_dispatcher->trigger_event('core.search_postgres_keywords_main_query_before', compact($vars)));
 500  
 501          $sql_select            = ($type == 'posts') ? 'p.post_id' : 'DISTINCT t.topic_id';
 502          $sql_from            = ($join_topic) ? TOPICS_TABLE . ' t, ' : '';
 503          $field                = ($type == 'posts') ? 'post_id' : 'topic_id';
 504          $sql_author            = (sizeof($author_ary) == 1) ? ' = ' . $author_ary[0] : 'IN (' . implode(', ', $author_ary) . ')';
 505  
 506          if (sizeof($author_ary) && $author_name)
 507          {
 508              // first one matches post of registered users, second one guests and deleted users
 509              $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
 510          }
 511          else if (sizeof($author_ary))
 512          {
 513              $sql_author = ' AND ' . $this->db->sql_in_set('p.poster_id', $author_ary);
 514          }
 515          else
 516          {
 517              $sql_author = '';
 518          }
 519  
 520          $sql_where_options = $sql_sort_join;
 521          $sql_where_options .= ($topic_id) ? ' AND p.topic_id = ' . $topic_id : '';
 522          $sql_where_options .= ($join_topic) ? ' AND t.topic_id = p.topic_id' : '';
 523          $sql_where_options .= (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
 524          $sql_where_options .= ' AND ' . $post_visibility;
 525          $sql_where_options .= $sql_author;
 526          $sql_where_options .= ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
 527          $sql_where_options .= $sql_match_where;
 528  
 529          $tmp_sql_match = array();
 530          $sql_match = str_replace(',', " || ' ' ||", $sql_match);
 531          $tmp_sql_match = "to_tsvector ('" . $this->db->sql_escape($this->config['fulltext_postgres_ts_name']) . "', " . $sql_match . ") @@ to_tsquery ('" . $this->db->sql_escape($this->config['fulltext_postgres_ts_name']) . "', '" . $this->db->sql_escape($this->tsearch_query) . "')";
 532  
 533          $this->db->sql_transaction('begin');
 534  
 535          $sql_from = "FROM $sql_from$sql_sort_table" . POSTS_TABLE . " p";
 536          $sql_where = "WHERE (" . $tmp_sql_match . ")
 537              $sql_where_options";
 538          $sql = "SELECT $sql_select
 539              $sql_from
 540              $sql_where
 541              ORDER BY $sql_sort";
 542          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 543  
 544          while ($row = $this->db->sql_fetchrow($result))
 545          {
 546              $id_ary[] = $row[$field];
 547          }
 548          $this->db->sql_freeresult($result);
 549  
 550          $id_ary = array_unique($id_ary);
 551  
 552          // if the total result count is not cached yet, retrieve it from the db
 553          if (!$result_count)
 554          {
 555              $sql_count = "SELECT COUNT(*) as result_count
 556                  $sql_from
 557                  $sql_where";
 558              $result = $this->db->sql_query($sql_count);
 559              $result_count = (int) $this->db->sql_fetchfield('result_count');
 560              $this->db->sql_freeresult($result);
 561  
 562              if (!$result_count)
 563              {
 564                  return false;
 565              }
 566          }
 567  
 568          $this->db->sql_transaction('commit');
 569  
 570          if ($start >= $result_count)
 571          {
 572              $start = floor(($result_count - 1) / $per_page) * $per_page;
 573  
 574              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 575  
 576              while ($row = $this->db->sql_fetchrow($result))
 577              {
 578                  $id_ary[] = $row[$field];
 579              }
 580              $this->db->sql_freeresult($result);
 581  
 582              $id_ary = array_unique($id_ary);
 583          }
 584  
 585          // store the ids, from start on then delete anything that isn't on the current page because we only need ids for one page
 586          $this->save_ids($search_key, implode(' ', $this->split_words), $author_ary, $result_count, $id_ary, $start, $sort_dir);
 587          $id_ary = array_slice($id_ary, 0, (int) $per_page);
 588  
 589          return $result_count;
 590      }
 591  
 592      /**
 593      * Performs a search on an author's posts without caring about message contents. Depends on display specific params
 594      *
 595      * @param    string        $type                contains either posts or topics depending on what should be searched for
 596      * @param    boolean        $firstpost_only        if true, only topic starting posts will be considered
 597      * @param    array        $sort_by_sql        contains SQL code for the ORDER BY part of a query
 598      * @param    string        $sort_key            is the key of $sort_by_sql for the selected sorting
 599      * @param    string        $sort_dir            is either a or d representing ASC and DESC
 600      * @param    string        $sort_days            specifies the maximum amount of days a post may be old
 601      * @param    array        $ex_fid_ary            specifies an array of forum ids which should not be searched
 602      * @param    string        $post_visibility    specifies which types of posts the user can view in which forums
 603      * @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
 604      * @param    array        $author_ary            an array of author ids
 605      * @param    string        $author_name        specifies the author match, when ANONYMOUS is also a search-match
 606      * @param    array        &$id_ary            passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
 607      * @param    int            $start                indicates the first index of the page
 608      * @param    int            $per_page            number of ids each page is supposed to contain
 609      * @return    boolean|int                        total number of results
 610      */
 611  	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)
 612      {
 613          // No author? No posts
 614          if (!sizeof($author_ary))
 615          {
 616              return 0;
 617          }
 618  
 619          // generate a search_key from all the options to identify the results
 620          $search_key_array = array(
 621              '',
 622              $type,
 623              ($firstpost_only) ? 'firstpost' : '',
 624              '',
 625              '',
 626              $sort_days,
 627              $sort_key,
 628              $topic_id,
 629              implode(',', $ex_fid_ary),
 630              $post_visibility,
 631              implode(',', $author_ary),
 632              $author_name,
 633          );
 634  
 635          /**
 636          * Allow changing the search_key for cached results
 637          *
 638          * @event core.search_postgres_by_author_modify_search_key
 639          * @var    array    search_key_array    Array with search parameters to generate the search_key
 640          * @var    string    type                Searching type ('posts', 'topics')
 641          * @var    boolean    firstpost_only        Flag indicating if only topic starting posts are considered
 642          * @var    int        sort_days            Time, in days, of the oldest possible post to list
 643          * @var    string    sort_key            The sort type used from the possible sort types
 644          * @var    int        topic_id            Limit the search to this topic_id only
 645          * @var    array    ex_fid_ary            Which forums not to search on
 646          * @var    string    post_visibility        Post visibility data
 647          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 648          * @var    string    author_name            The username to search on
 649          * @since 3.1.7-RC1
 650          */
 651          $vars = array(
 652              'search_key_array',
 653              'type',
 654              'firstpost_only',
 655              'sort_days',
 656              'sort_key',
 657              'topic_id',
 658              'ex_fid_ary',
 659              'post_visibility',
 660              'author_ary',
 661              'author_name',
 662          );
 663          extract($this->phpbb_dispatcher->trigger_event('core.search_postgres_by_author_modify_search_key', compact($vars)));
 664  
 665          $search_key = md5(implode('#', $search_key_array));
 666  
 667          if ($start < 0)
 668          {
 669              $start = 0;
 670          }
 671  
 672          // try reading the results from cache
 673          $result_count = 0;
 674          if ($this->obtain_ids($search_key, $result_count, $id_ary, $start, $per_page, $sort_dir) == SEARCH_RESULT_IN_CACHE)
 675          {
 676              return $result_count;
 677          }
 678  
 679          $id_ary = array();
 680  
 681          // Create some display specific sql strings
 682          if ($author_name)
 683          {
 684              // first one matches post of registered users, second one guests and deleted users
 685              $sql_author = '(' . $this->db->sql_in_set('p.poster_id', array_diff($author_ary, array(ANONYMOUS)), false, true) . ' OR p.post_username ' . $author_name . ')';
 686          }
 687          else
 688          {
 689              $sql_author = $this->db->sql_in_set('p.poster_id', $author_ary);
 690          }
 691          $sql_fora        = (sizeof($ex_fid_ary)) ? ' AND ' . $this->db->sql_in_set('p.forum_id', $ex_fid_ary, true) : '';
 692          $sql_topic_id    = ($topic_id) ? ' AND p.topic_id = ' . (int) $topic_id : '';
 693          $sql_time        = ($sort_days) ? ' AND p.post_time >= ' . (time() - ($sort_days * 86400)) : '';
 694          $sql_firstpost = ($firstpost_only) ? ' AND p.post_id = t.topic_first_post_id' : '';
 695  
 696          // Build sql strings for sorting
 697          $sql_sort = $sort_by_sql[$sort_key] . (($sort_dir == 'a') ? ' ASC' : ' DESC');
 698          $sql_sort_table = $sql_sort_join = '';
 699          switch ($sql_sort[0])
 700          {
 701              case 'u':
 702                  $sql_sort_table    = USERS_TABLE . ' u, ';
 703                  $sql_sort_join    = ($type == 'posts') ? ' AND u.user_id = p.poster_id ' : ' AND u.user_id = t.topic_poster ';
 704              break;
 705  
 706              case 't':
 707                  $sql_sort_table    = ($type == 'posts' && !$firstpost_only) ? TOPICS_TABLE . ' t, ' : '';
 708                  $sql_sort_join    = ($type == 'posts' && !$firstpost_only) ? ' AND t.topic_id = p.topic_id ' : '';
 709              break;
 710  
 711              case 'f':
 712                  $sql_sort_table    = FORUMS_TABLE . ' f, ';
 713                  $sql_sort_join    = ' AND f.forum_id = p.forum_id ';
 714              break;
 715          }
 716  
 717          $m_approve_fid_sql = ' AND ' . $post_visibility;
 718  
 719          /**
 720          * Allow changing the query used to search for posts by author in fulltext_postgres
 721          *
 722          * @event core.search_postgres_author_count_query_before
 723          * @var    int        result_count        The previous result count for the format of the query.
 724          *                                    Set to 0 to force a re-count
 725          * @var    string    sql_sort_table        CROSS JOIN'ed table to allow doing the sort chosen
 726          * @var    string    sql_sort_join        Condition to define how to join the CROSS JOIN'ed table specifyed in sql_sort_table
 727          * @var    array    author_ary            Array of user_id containing the users to filter the results to
 728          * @var    string    author_name            An extra username to search on
 729          * @var    string    sql_author            SQL WHERE condition for the post author ids
 730          * @var    int        topic_id            Limit the search to this topic_id only
 731          * @var    string    sql_topic_id        SQL of topic_id
 732          * @var    string    sort_by_sql            The possible predefined sort types
 733          * @var    string    sort_key            The sort type used from the possible sort types
 734          * @var    string    sort_dir            "a" for ASC or "d" dor DESC for the sort order used
 735          * @var    string    sql_sort            The result SQL when processing sort_by_sql + sort_key + sort_dir
 736          * @var    string    sort_days            Time, in days, that the oldest post showing can have
 737          * @var    string    sql_time            The SQL to search on the time specifyed by sort_days
 738          * @var    bool    firstpost_only        Wether or not to search only on the first post of the topics
 739          * @var    array    ex_fid_ary            Forum ids that must not be searched on
 740          * @var    array    sql_fora            SQL query for ex_fid_ary
 741          * @var    string    m_approve_fid_sql    WHERE clause condition on post_visibility restrictions
 742          * @var    int        start                How many posts to skip in the search results (used for pagination)
 743          * @since 3.1.5-RC1
 744          */
 745          $vars = array(
 746              'result_count',
 747              'sql_sort_table',
 748              'sql_sort_join',
 749              'author_ary',
 750              'author_name',
 751              'sql_author',
 752              'topic_id',
 753              'sql_topic_id',
 754              'sort_by_sql',
 755              'sort_key',
 756              'sort_dir',
 757              'sql_sort',
 758              'sort_days',
 759              'sql_time',
 760              'firstpost_only',
 761              'ex_fid_ary',
 762              'sql_fora',
 763              'm_approve_fid_sql',
 764              'start',
 765          );
 766          extract($this->phpbb_dispatcher->trigger_event('core.search_postgres_author_count_query_before', compact($vars)));
 767  
 768          // Build the query for really selecting the post_ids
 769          if ($type == 'posts')
 770          {
 771              $sql = "SELECT p.post_id
 772                  FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
 773                  WHERE $sql_author
 774                      $sql_topic_id
 775                      $sql_firstpost
 776                      $m_approve_fid_sql
 777                      $sql_fora
 778                      $sql_sort_join
 779                      $sql_time
 780                  ORDER BY $sql_sort";
 781              $field = 'post_id';
 782          }
 783          else
 784          {
 785              $sql = "SELECT t.topic_id
 786                  FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
 787                  WHERE $sql_author
 788                      $sql_topic_id
 789                      $sql_firstpost
 790                      $m_approve_fid_sql
 791                      $sql_fora
 792                      AND t.topic_id = p.topic_id
 793                      $sql_sort_join
 794                      $sql_time
 795                  GROUP BY t.topic_id, $sort_by_sql[$sort_key]
 796                  ORDER BY $sql_sort";
 797              $field = 'topic_id';
 798          }
 799  
 800          $this->db->sql_transaction('begin');
 801  
 802          // Only read one block of posts from the db and then cache it
 803          $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 804  
 805          while ($row = $this->db->sql_fetchrow($result))
 806          {
 807              $id_ary[] = $row[$field];
 808          }
 809          $this->db->sql_freeresult($result);
 810  
 811          // retrieve the total result count if needed
 812          if (!$result_count)
 813          {
 814              if ($type == 'posts')
 815              {
 816                  $sql_count = "SELECT COUNT(*) as result_count
 817                      FROM " . $sql_sort_table . POSTS_TABLE . ' p' . (($firstpost_only) ? ', ' . TOPICS_TABLE . ' t ' : ' ') . "
 818                      WHERE $sql_author
 819                          $sql_topic_id
 820                          $sql_firstpost
 821                          $m_approve_fid_sql
 822                          $sql_fora
 823                          $sql_sort_join
 824                          $sql_time";
 825              }
 826              else
 827              {
 828                  $sql_count = "SELECT COUNT(*) as result_count
 829                      FROM " . $sql_sort_table . TOPICS_TABLE . ' t, ' . POSTS_TABLE . " p
 830                      WHERE $sql_author
 831                          $sql_topic_id
 832                          $sql_firstpost
 833                          $m_approve_fid_sql
 834                          $sql_fora
 835                          AND t.topic_id = p.topic_id
 836                          $sql_sort_join
 837                          $sql_time
 838                      GROUP BY t.topic_id, $sort_by_sql[$sort_key]";
 839              }
 840  
 841              $result = $this->db->sql_query($sql_count);
 842              $result_count = (int) $this->db->sql_fetchfield('result_count');
 843  
 844              if (!$result_count)
 845              {
 846                  return false;
 847              }
 848          }
 849  
 850          $this->db->sql_transaction('commit');
 851  
 852          if ($start >= $result_count)
 853          {
 854              $start = floor(($result_count - 1) / $per_page) * $per_page;
 855  
 856              $result = $this->db->sql_query_limit($sql, $this->config['search_block_size'], $start);
 857              while ($row = $this->db->sql_fetchrow($result))
 858              {
 859                  $id_ary[] = (int) $row[$field];
 860              }
 861              $this->db->sql_freeresult($result);
 862  
 863              $id_ary = array_unique($id_ary);
 864          }
 865  
 866          if (sizeof($id_ary))
 867          {
 868              $this->save_ids($search_key, '', $author_ary, $result_count, $id_ary, $start, $sort_dir);
 869              $id_ary = array_slice($id_ary, 0, $per_page);
 870  
 871              return $result_count;
 872          }
 873          return false;
 874      }
 875  
 876      /**
 877      * Destroys cached search results, that contained one of the new words in a post so the results won't be outdated
 878      *
 879      * @param    string        $mode        contains the post mode: edit, post, reply, quote ...
 880      * @param    int            $post_id    contains the post id of the post to index
 881      * @param    string        $message    contains the post text of the post
 882      * @param    string        $subject    contains the subject of the post to index
 883      * @param    int            $poster_id    contains the user id of the poster
 884      * @param    int            $forum_id    contains the forum id of parent forum of the post
 885      */
 886  	public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
 887      {
 888          // Split old and new post/subject to obtain array of words
 889          $split_text = $this->split_message($message);
 890          $split_title = ($subject) ? $this->split_message($subject) : array();
 891  
 892          $words = array_unique(array_merge($split_text, $split_title));
 893  
 894          unset($split_text);
 895          unset($split_title);
 896  
 897          // destroy cached search results containing any of the words removed or added
 898          $this->destroy_cache($words, array($poster_id));
 899  
 900          unset($words);
 901      }
 902  
 903      /**
 904      * Destroy cached results, that might be outdated after deleting a post
 905      */
 906  	public function index_remove($post_ids, $author_ids, $forum_ids)
 907      {
 908          $this->destroy_cache(array(), $author_ids);
 909      }
 910  
 911      /**
 912      * Destroy old cache entries
 913      */
 914  	public function tidy()
 915      {
 916          // destroy too old cached search results
 917          $this->destroy_cache(array());
 918  
 919          set_config('search_last_gc', time(), true);
 920      }
 921  
 922      /**
 923      * Create fulltext index
 924      *
 925      * @return string|bool error string is returned incase of errors otherwise false
 926      */
 927  	public function create_index($acp_module, $u_action)
 928      {
 929          // Make sure we can actually use PostgreSQL with fulltext indexes
 930          if ($error = $this->init())
 931          {
 932              return $error;
 933          }
 934  
 935          if (empty($this->stats))
 936          {
 937              $this->get_stats();
 938          }
 939  
 940          if (!isset($this->stats['post_subject']))
 941          {
 942              $this->db->sql_query("CREATE INDEX " . POSTS_TABLE . "_" . $this->config['fulltext_postgres_ts_name'] . "_post_subject ON " . POSTS_TABLE . " USING gin (to_tsvector ('" . $this->db->sql_escape($this->config['fulltext_postgres_ts_name']) . "', post_subject))");
 943          }
 944  
 945          if (!isset($this->stats['post_content']))
 946          {
 947              $this->db->sql_query("CREATE INDEX " . POSTS_TABLE . "_" . $this->config['fulltext_postgres_ts_name'] . "_post_content ON " . POSTS_TABLE . " USING gin (to_tsvector ('" . $this->db->sql_escape($this->config['fulltext_postgres_ts_name']) . "', post_text || ' ' || post_subject))");
 948          }
 949  
 950          $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
 951  
 952          return false;
 953      }
 954  
 955      /**
 956      * Drop fulltext index
 957      *
 958      * @return string|bool error string is returned incase of errors otherwise false
 959      */
 960  	public function delete_index($acp_module, $u_action)
 961      {
 962          // Make sure we can actually use PostgreSQL with fulltext indexes
 963          if ($error = $this->init())
 964          {
 965              return $error;
 966          }
 967  
 968          if (empty($this->stats))
 969          {
 970              $this->get_stats();
 971          }
 972  
 973          if (isset($this->stats['post_subject']))
 974          {
 975              $this->db->sql_query('DROP INDEX ' . $this->stats['post_subject']['relname']);
 976          }
 977  
 978          if (isset($this->stats['post_content']))
 979          {
 980              $this->db->sql_query('DROP INDEX ' . $this->stats['post_content']['relname']);
 981          }
 982  
 983          $this->db->sql_query('TRUNCATE TABLE ' . SEARCH_RESULTS_TABLE);
 984  
 985          return false;
 986      }
 987  
 988      /**
 989      * Returns true if both FULLTEXT indexes exist
 990      */
 991  	public function index_created()
 992      {
 993          if (empty($this->stats))
 994          {
 995              $this->get_stats();
 996          }
 997  
 998          return (isset($this->stats['post_subject']) && isset($this->stats['post_content'])) ? true : false;
 999      }
1000  
1001      /**
1002      * Returns an associative array containing information about the indexes
1003      */
1004  	public function index_stats()
1005      {
1006          if (empty($this->stats))
1007          {
1008              $this->get_stats();
1009          }
1010  
1011          return array(
1012              $this->user->lang['FULLTEXT_POSTGRES_TOTAL_POSTS']            => ($this->index_created()) ? $this->stats['total_posts'] : 0,
1013          );
1014      }
1015  
1016      /**
1017       * Computes the stats and store them in the $this->stats associative array
1018       */
1019  	protected function get_stats()
1020      {
1021          if ($this->db->get_sql_layer() != 'postgres')
1022          {
1023              $this->stats = array();
1024              return;
1025          }
1026  
1027          $sql = "SELECT c2.relname, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) AS indexdef
1028                FROM pg_catalog.pg_class c1, pg_catalog.pg_index i, pg_catalog.pg_class c2
1029               WHERE c1.relname = '" . POSTS_TABLE . "'
1030                 AND pg_catalog.pg_table_is_visible(c1.oid)
1031                 AND c1.oid = i.indrelid
1032                 AND i.indexrelid = c2.oid";
1033          $result = $this->db->sql_query($sql);
1034  
1035          while ($row = $this->db->sql_fetchrow($result))
1036          {
1037              // deal with older PostgreSQL versions which didn't use Index_type
1038              if (strpos($row['indexdef'], 'to_tsvector') !== false)
1039              {
1040                  if ($row['relname'] == POSTS_TABLE . '_' . $this->config['fulltext_postgres_ts_name'] . '_post_subject' || $row['relname'] == POSTS_TABLE . '_post_subject')
1041                  {
1042                      $this->stats['post_subject'] = $row;
1043                  }
1044                  else if ($row['relname'] == POSTS_TABLE . '_' . $this->config['fulltext_postgres_ts_name'] . '_post_content' || $row['relname'] == POSTS_TABLE . '_post_content')
1045                  {
1046                      $this->stats['post_content'] = $row;
1047                  }
1048              }
1049          }
1050          $this->db->sql_freeresult($result);
1051  
1052          $this->stats['total_posts'] = $this->config['num_posts'];
1053      }
1054  
1055      /**
1056      * Display various options that can be configured for the backend from the acp
1057      *
1058      * @return associative array containing template and config variables
1059      */
1060  	public function acp()
1061      {
1062          $tpl = '
1063          <dl>
1064              <dt><label>' . $this->user->lang['FULLTEXT_POSTGRES_VERSION_CHECK'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_POSTGRES_VERSION_CHECK_EXPLAIN'] . '</span></dt>
1065              <dd>' . (($this->db->get_sql_layer() == 'postgres') ? $this->user->lang['YES'] : $this->user->lang['NO']) . '</dd>
1066          </dl>
1067          <dl>
1068              <dt><label>' . $this->user->lang['FULLTEXT_POSTGRES_TS_NAME'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_POSTGRES_TS_NAME_EXPLAIN'] . '</span></dt>
1069              <dd><select name="config[fulltext_postgres_ts_name]">';
1070  
1071          if ($this->db->get_sql_layer() == 'postgres')
1072          {
1073              $sql = 'SELECT cfgname AS ts_name
1074                    FROM pg_ts_config';
1075              $result = $this->db->sql_query($sql);
1076  
1077              while ($row = $this->db->sql_fetchrow($result))
1078              {
1079                  $tpl .= '<option value="' . $row['ts_name'] . '"' . ($row['ts_name'] === $this->config['fulltext_postgres_ts_name'] ? ' selected="selected"' : '') . '>' . $row['ts_name'] . '</option>';
1080              }
1081              $this->db->sql_freeresult($result);
1082          }
1083          else
1084          {
1085              $tpl .= '<option value="' . $this->config['fulltext_postgres_ts_name'] . '" selected="selected">' . $this->config['fulltext_postgres_ts_name'] . '</option>';
1086          }
1087  
1088          $tpl .= '</select></dd>
1089          </dl>
1090                  <dl>
1091                          <dt><label for="fulltext_postgres_min_word_len">' . $this->user->lang['FULLTEXT_POSTGRES_MIN_WORD_LEN'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_POSTGRES_MIN_WORD_LEN_EXPLAIN'] . '</span></dt>
1092                          <dd><input id="fulltext_postgres_min_word_len" type="number" min="0" max="255" name="config[fulltext_postgres_min_word_len]" value="' . (int) $this->config['fulltext_postgres_min_word_len'] . '" /></dd>
1093                  </dl>
1094                  <dl>
1095                          <dt><label for="fulltext_postgres_max_word_len">' . $this->user->lang['FULLTEXT_POSTGRES_MAX_WORD_LEN'] . $this->user->lang['COLON'] . '</label><br /><span>' . $this->user->lang['FULLTEXT_POSTGRES_MAX_WORD_LEN_EXPLAIN'] . '</span></dt>
1096                          <dd><input id="fulltext_postgres_max_word_len" type="number" min="0" max="255" name="config[fulltext_postgres_max_word_len]" value="' . (int) $this->config['fulltext_postgres_max_word_len'] . '" /></dd>
1097                  </dl>
1098          ';
1099  
1100          // These are fields required in the config table
1101          return array(
1102              'tpl'        => $tpl,
1103              'config'    => array('fulltext_postgres_ts_name' => 'string', 'fulltext_postgres_min_word_len' => 'integer:0:255', 'fulltext_postgres_max_word_len' => 'integer:0:255')
1104          );
1105      }
1106  }


Generated: Thu Jan 11 00:25:41 2018 Cross-referenced by PHPXref 0.7.1