Verzeichnisstruktur phpBB-3.3.16
- Veröffentlicht
- 27.04.2026
So funktioniert es
|
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
fulltext_sphinx.php
0001 <?php
0002 /**
0003 *
0004 * This file is part of the phpBB Forum Software package.
0005 *
0006 * @copyright (c) phpBB Limited <https://www.phpbb.com>
0007 * @license GNU General Public License, version 2 (GPL-2.0)
0008 *
0009 * For full copyright and license information, please see
0010 * the docs/CREDITS.txt file.
0011 *
0012 */
0013
0014 namespace phpbb\search;
0015
0016 define('SPHINX_MAX_MATCHES', 20000);
0017 define('SPHINX_CONNECT_RETRIES', 3);
0018 define('SPHINX_CONNECT_WAIT_TIME', 300);
0019
0020 /**
0021 * Fulltext search based on the sphinx search daemon
0022 */
0023 class fulltext_sphinx
0024 {
0025 /**
0026 * Associative array holding index stats
0027 * @var array
0028 */
0029 protected $stats = array();
0030
0031 /**
0032 * Holds the words entered by user, obtained by splitting the entered query on whitespace
0033 * @var array
0034 */
0035 protected $split_words = array();
0036
0037 /**
0038 * Holds unique sphinx id
0039 * @var string
0040 */
0041 protected $id;
0042
0043 /**
0044 * Stores the names of both main and delta sphinx indexes
0045 * separated by a semicolon
0046 * @var string
0047 */
0048 protected $indexes;
0049
0050 /**
0051 * Sphinx searchd client object
0052 * @var SphinxClient
0053 */
0054 protected $sphinx;
0055
0056 /**
0057 * Relative path to board root
0058 * @var string
0059 */
0060 protected $phpbb_root_path;
0061
0062 /**
0063 * PHP Extension
0064 * @var string
0065 */
0066 protected $php_ext;
0067
0068 /**
0069 * Auth object
0070 * @var \phpbb\auth\auth
0071 */
0072 protected $auth;
0073
0074 /**
0075 * Config object
0076 * @var \phpbb\config\config
0077 */
0078 protected $config;
0079
0080 /**
0081 * Database connection
0082 * @var \phpbb\db\driver\driver_interface
0083 */
0084 protected $db;
0085
0086 /**
0087 * Database Tools object
0088 * @var \phpbb\db\tools\tools_interface
0089 */
0090 protected $db_tools;
0091
0092 /**
0093 * Stores the database type if supported by sphinx
0094 * @var string
0095 */
0096 protected $dbtype;
0097
0098 /**
0099 * phpBB event dispatcher object
0100 * @var \phpbb\event\dispatcher_interface
0101 */
0102 protected $phpbb_dispatcher;
0103
0104 /**
0105 * User object
0106 * @var \phpbb\user
0107 */
0108 protected $user;
0109
0110 /**
0111 * Stores the generated content of the sphinx config file
0112 * @var string
0113 */
0114 protected $config_file_data = '';
0115
0116 /**
0117 * Contains tidied search query.
0118 * Operators are prefixed in search query and common words excluded
0119 * @var string
0120 */
0121 protected $search_query;
0122
0123 /**
0124 * Constructor
0125 * Creates a new \phpbb\search\fulltext_postgres, which is used as a search backend
0126 *
0127 * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
0128 * @param string $phpbb_root_path Relative path to phpBB root
0129 * @param string $phpEx PHP file extension
0130 * @param \phpbb\auth\auth $auth Auth object
0131 * @param \phpbb\config\config $config Config object
0132 * @param \phpbb\db\driver\driver_interface $db Database object
0133 * @param \phpbb\user $user User object
0134 * @param \phpbb\event\dispatcher_interface $phpbb_dispatcher Event dispatcher object
0135 */
0136 public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
0137 {
0138 $this->phpbb_root_path = $phpbb_root_path;
0139 $this->php_ext = $phpEx;
0140 $this->config = $config;
0141 $this->phpbb_dispatcher = $phpbb_dispatcher;
0142 $this->user = $user;
0143 $this->db = $db;
0144 $this->auth = $auth;
0145
0146 // Initialize \phpbb\db\tools\tools object
0147 global $phpbb_container; // TODO inject into object
0148 $this->db_tools = $phpbb_container->get('dbal.tools');
0149
0150 if (!$this->config['fulltext_sphinx_id'])
0151 {
0152 $this->config->set('fulltext_sphinx_id', unique_id());
0153 }
0154 $this->id = $this->config['fulltext_sphinx_id'];
0155 $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main';
0156
0157 if (!class_exists('SphinxClient'))
0158 {
0159 require($this->phpbb_root_path . 'includes/sphinxapi.' . $this->php_ext);
0160 }
0161
0162 // Initialize sphinx client
0163 $this->sphinx = new \SphinxClient();
0164
0165 $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));
0166
0167 $error = false;
0168 }
0169
0170 /**
0171 * Returns the name of this search backend to be displayed to administrators
0172 *
0173 * @return string Name
0174 */
0175 public function get_name()
0176 {
0177 return 'Sphinx Fulltext';
0178 }
0179
0180 /**
0181 * Returns the search_query
0182 *
0183 * @return string search query
0184 */
0185 public function get_search_query()
0186 {
0187 return $this->search_query;
0188 }
0189
0190 /**
0191 * Returns false as there is no word_len array
0192 *
0193 * @return false
0194 */
0195 public function get_word_length()
0196 {
0197 return false;
0198 }
0199
0200 /**
0201 * Returns an empty array as there are no common_words
0202 *
0203 * @return array common words that are ignored by search backend
0204 */
0205 public function get_common_words()
0206 {
0207 return array();
0208 }
0209
0210 /**
0211 * Checks permissions and paths, if everything is correct it generates the config file
0212 *
0213 * @return string|bool Language key of the error/incompatibility encountered, or false if successful
0214 */
0215 public function init()
0216 {
0217 if ($this->db->get_sql_layer() != 'mysqli' && $this->db->get_sql_layer() != 'postgres')
0218 {
0219 return $this->user->lang['FULLTEXT_SPHINX_WRONG_DATABASE'];
0220 }
0221
0222 // Move delta to main index each hour
0223 $this->config->set('search_gc', 3600);
0224
0225 return false;
0226 }
0227
0228 /**
0229 * Generates content of sphinx.conf
0230 *
0231 * @return bool True if sphinx.conf content is correctly generated, false otherwise
0232 */
0233 protected function config_generate()
0234 {
0235 // Check if Database is supported by Sphinx
0236 if ($this->db->get_sql_layer() == 'mysqli')
0237 {
0238 $this->dbtype = 'mysql';
0239 }
0240 else if ($this->db->get_sql_layer() == 'postgres')
0241 {
0242 $this->dbtype = 'pgsql';
0243 }
0244 else
0245 {
0246 $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_WRONG_DATABASE');
0247 return false;
0248 }
0249
0250 // Check if directory paths have been filled
0251 if (!$this->config['fulltext_sphinx_data_path'])
0252 {
0253 $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_NO_CONFIG_DATA');
0254 return false;
0255 }
0256
0257 include($this->phpbb_root_path . 'config.' . $this->php_ext);
0258
0259 /* Now that we're sure everything was entered correctly,
0260 generate a config for the index. We use a config value
0261 fulltext_sphinx_id for this, as it should be unique. */
0262 $config_object = new \phpbb\search\sphinx\config($this->config_file_data);
0263 $config_data = array(
0264 'source source_phpbb_' . $this->id . '_main' => array(
0265 array('type', $this->dbtype . ' # mysql or pgsql'),
0266 // This config value sql_host needs to be changed incase sphinx and sql are on different servers
0267 array('sql_host', $dbhost . ' # SQL server host sphinx connects to'),
0268 array('sql_user', '[dbuser]'),
0269 array('sql_pass', '[dbpassword]'),
0270 array('sql_db', $dbname),
0271 array('sql_port', $dbport . ' # optional, default is 3306 for mysql and 5432 for pgsql'),
0272 array('sql_query_pre', 'SET NAMES \'utf8\''),
0273 array('sql_query_pre', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = (SELECT MAX(post_id) FROM ' . POSTS_TABLE . ') WHERE counter_id = 1'),
0274 array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''),
0275 array('sql_range_step', '5000'),
0276 array('sql_query', 'SELECT
0277 p.post_id AS id,
0278 p.forum_id,
0279 p.topic_id,
0280 p.poster_id,
0281 p.post_visibility,
0282 CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
0283 p.post_time,
0284 p.post_subject,
0285 p.post_subject as title,
0286 p.post_text as data,
0287 t.topic_last_post_time,
0288 0 as deleted
0289 FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
0290 WHERE
0291 p.topic_id = t.topic_id
0292 AND p.post_id >= $start AND p.post_id <= $end'),
0293 array('sql_query_post', ''),
0294 array('sql_query_post_index', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = $maxid WHERE counter_id = 1'),
0295 array('sql_attr_uint', 'forum_id'),
0296 array('sql_attr_uint', 'topic_id'),
0297 array('sql_attr_uint', 'poster_id'),
0298 array('sql_attr_uint', 'post_visibility'),
0299 array('sql_attr_bool', 'topic_first_post'),
0300 array('sql_attr_bool', 'deleted'),
0301 array('sql_attr_timestamp', 'post_time'),
0302 array('sql_attr_timestamp', 'topic_last_post_time'),
0303 array('sql_attr_string', 'post_subject'),
0304 ),
0305 'source source_phpbb_' . $this->id . '_delta : source_phpbb_' . $this->id . '_main' => array(
0306 array('sql_query_pre', 'SET NAMES \'utf8\''),
0307 array('sql_query_range', ''),
0308 array('sql_range_step', ''),
0309 array('sql_query', 'SELECT
0310 p.post_id AS id,
0311 p.forum_id,
0312 p.topic_id,
0313 p.poster_id,
0314 p.post_visibility,
0315 CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
0316 p.post_time,
0317 p.post_subject,
0318 p.post_subject as title,
0319 p.post_text as data,
0320 t.topic_last_post_time,
0321 0 as deleted
0322 FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
0323 WHERE
0324 p.topic_id = t.topic_id
0325 AND p.post_id >= ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'),
0326 array('sql_query_post_index', ''),
0327 ),
0328 'index index_phpbb_' . $this->id . '_main' => array(
0329 array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'),
0330 array('source', 'source_phpbb_' . $this->id . '_main'),
0331 array('docinfo', 'extern'),
0332 array('morphology', 'none'),
0333 array('stopwords', ''),
0334 array('wordforms', ' # optional, specify path to wordforms file. See ./docs/sphinx_wordforms.txt for example'),
0335 array('exceptions', ' # optional, specify path to exceptions file. See ./docs/sphinx_exceptions.txt for example'),
0336 array('min_word_len', '2'),
0337 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'),
0338 array('ignore_chars', 'U+0027, U+002C'),
0339 array('min_prefix_len', '3 # Minimum number of characters for wildcard searches by prefix (min 1). Default is 3. If specified, set min_infix_len to 0'),
0340 array('min_infix_len', '0 # Minimum number of characters for wildcard searches by infix (min 2). If specified, set min_prefix_len to 0'),
0341 array('html_strip', '1'),
0342 array('index_exact_words', '0 # Set to 1 to enable exact search operator. Requires wordforms or morphology'),
0343 array('blend_chars', 'U+23, U+24, U+25, U+26, U+40'),
0344 ),
0345 'index index_phpbb_' . $this->id . '_delta : index_phpbb_' . $this->id . '_main' => array(
0346 array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'),
0347 array('source', 'source_phpbb_' . $this->id . '_delta'),
0348 ),
0349 'indexer' => array(
0350 array('mem_limit', $this->config['fulltext_sphinx_indexer_mem_limit'] . 'M'),
0351 ),
0352 'searchd' => array(
0353 array('listen' , ($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost') . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '9312')),
0354 array('log', $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'),
0355 array('query_log', $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'),
0356 array('read_timeout', '5'),
0357 array('max_children', '30'),
0358 array('pid_file', $this->config['fulltext_sphinx_data_path'] . 'searchd.pid'),
0359 array('binlog_path', rtrim($this->config['fulltext_sphinx_data_path'], '/\\')), // Trim trailing slash
0360 ),
0361 );
0362
0363 $non_unique = array('sql_query_pre' => true, 'sql_attr_uint' => true, 'sql_attr_timestamp' => true, 'sql_attr_str2ordinal' => true, 'sql_attr_bool' => true);
0364 $delete = array('sql_group_column' => true, 'sql_date_column' => true, 'sql_str2ordinal_column' => true);
0365
0366 /**
0367 * Allow adding/changing the Sphinx configuration data
0368 *
0369 * @event core.search_sphinx_modify_config_data
0370 * @var array config_data Array with the Sphinx configuration data
0371 * @var array non_unique Array with the Sphinx non-unique variables to delete
0372 * @var array delete Array with the Sphinx variables to delete
0373 * @since 3.1.7-RC1
0374 */
0375 $vars = array(
0376 'config_data',
0377 'non_unique',
0378 'delete',
0379 );
0380 extract($this->phpbb_dispatcher->trigger_event('core.search_sphinx_modify_config_data', compact($vars)));
0381
0382 foreach ($config_data as $section_name => $section_data)
0383 {
0384 $section = $config_object->get_section_by_name($section_name);
0385 if (!$section)
0386 {
0387 $section = $config_object->add_section($section_name);
0388 }
0389
0390 foreach ($delete as $key => $void)
0391 {
0392 $section->delete_variables_by_name($key);
0393 }
0394
0395 foreach ($non_unique as $key => $void)
0396 {
0397 $section->delete_variables_by_name($key);
0398 }
0399
0400 foreach ($section_data as $entry)
0401 {
0402 $key = $entry[0];
0403 $value = $entry[1];
0404
0405 if (!isset($non_unique[$key]))
0406 {
0407 $variable = $section->get_variable_by_name($key);
0408 if (!$variable)
0409 {
0410 $section->create_variable($key, $value);
0411 }
0412 else
0413 {
0414 $variable->set_value($value);
0415 }
0416 }
0417 else
0418 {
0419 $section->create_variable($key, $value);
0420 }
0421 }
0422 }
0423 $this->config_file_data = $config_object->get_data();
0424
0425 return true;
0426 }
0427
0428 /**
0429 * Splits keywords entered by a user into an array of words stored in $this->split_words
0430 * Stores the tidied search query in $this->search_query
0431 *
0432 * @param string $keywords Contains the keyword as entered by the user
0433 * @param string $terms is either 'all' or 'any'
0434 * @return false if no valid keywords were found and otherwise true
0435 */
0436 public function split_keywords(&$keywords, $terms)
0437 {
0438 // Keep quotes and new lines
0439 $keywords = str_replace(['"', "\n"], ['"', ' '], trim($keywords));
0440
0441 if ($terms == 'all')
0442 {
0443 // Replaces verbal operators OR and NOT with special characters | and -, unless appearing within quotation marks
0444 $match = ['#\sor\s(?=([^"]*"[^"]*")*[^"]*$)#i', '#\snot\s(?=([^"]*"[^"]*")*[^"]*$)#i'];
0445 $replace = [' | ', ' -'];
0446
0447 $keywords = preg_replace($match, $replace, $keywords);
0448 $this->sphinx->SetMatchMode(SPH_MATCH_EXTENDED);
0449 }
0450 else
0451 {
0452 $match = ['\\', '(',')', '|', '!', '@', '~', '/', '^', '$', '=', '&', '<', '>'];
0453
0454 $keywords = str_replace($match, ' ', $keywords);
0455 $this->sphinx->SetMatchMode(SPH_MATCH_ANY);
0456 }
0457
0458 // Split words
0459 $split_keywords = preg_replace('#([^\p{L}\p{N}\'*"()])#u', '$1$1', str_replace('\'\'', '\' \'', trim($keywords)));
0460 $matches = array();
0461 preg_match_all('#(?:[^\p{L}\p{N}*"()]|^)([+\-|]?(?:[\p{L}\p{N}*"()]+\'?)*[\p{L}\p{N}*"()])(?:[^\p{L}\p{N}*"()]|$)#u', $split_keywords, $matches);
0462 $this->split_words = $matches[1];
0463
0464 if ($terms == 'any')
0465 {
0466 $this->search_query = '';
0467 foreach ($this->split_words as $word)
0468 {
0469 if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0) || (strpos($word, '|') === 0))
0470 {
0471 $word = substr($word, 1);
0472 }
0473 $this->search_query .= $word . ' ';
0474 }
0475 }
0476 else
0477 {
0478 $this->search_query = '';
0479 foreach ($this->split_words as $word)
0480 {
0481 if ((strpos($word, '+') === 0) || (strpos($word, '-') === 0))
0482 {
0483 $this->search_query .= $word . ' ';
0484 }
0485 else if (strpos($word, '|') === 0)
0486 {
0487 $this->search_query .= substr($word, 1) . ' ';
0488 }
0489 else
0490 {
0491 $this->search_query .= '+' . $word . ' ';
0492 }
0493 }
0494 }
0495
0496 if ($this->search_query)
0497 {
0498 $this->search_query = str_replace('"', '"', $this->search_query);
0499 return true;
0500 }
0501
0502 return false;
0503 }
0504
0505 /**
0506 * Cleans search query passed into Sphinx search engine, as follows:
0507 * 1. Hyphenated words are replaced with keyword search for either the exact phrase with spaces
0508 * or as a single word without spaces eg search for "know-it-all" becomes ("know it all"|"knowitall*")
0509 * 2. Words with apostrophes are contracted eg "it's" becomes "its"
0510 * 3. <, >, " and & are decoded from HTML entities.
0511 * 4. Following special characters used as search operators in Sphinx are preserved when used with correct syntax:
0512 * (a) quorum matching: "the world is a wonderful place"/3
0513 * Finds 3 of the words within the phrase. Number must be between 1 and 9.
0514 * (b) proximity search: "hello world"~10
0515 * Finds hello and world within 10 words of each other. Number can be between 1 and 99.
0516 * (c) strict word order: aaa << bbb << ccc
0517 * Finds "aaa" only where it appears before "bbb" and only where "bbb" appears before "ccc".
0518 * (d) exact match operator: if lemmatizer or stemming enabled,
0519 * search will find exact match only and ignore other grammatical forms of the same word stem.
0520 * eg. raining =cats and =dogs
0521 * will not return "raining cat and dog"
0522 * eg. ="search this exact phrase"
0523 * will not return "searched this exact phrase", "searching these exact phrases".
0524 * 5. Special characters /, ~, << and = not complying with the correct syntax
0525 * and other reserved operators are escaped and searched literally.
0526 * Special characters not explicitly listed in charset_table or blend_chars in sphinx.conf
0527 * will not be indexed and keywords containing them will be ignored by Sphinx.
0528 * By default, only $, %, & and @ characters are indexed and searchable.
0529 * String transformation is in backend only and not visible to the end user
0530 * nor reflected in the results page URL or keyword highlighting.
0531 *
0532 * @param string $search_string
0533 * @return string
0534 */
0535 public function sphinx_clean_search_string($search_string)
0536 {
0537 $from = ['@', '^', '$', '!', '<', '>', '"', '&', '\''];
0538 $to = ['\@', '\^', '\$', '\!', '<', '>', '"', '&', ''];
0539
0540 $search_string = str_replace($from, $to, $search_string);
0541
0542 $search_string = strrev($search_string);
0543 $search_string = preg_replace(['#\/(?!"[^"]+")#', '#~(?!"[^"]+")#'], ['/\\', '~\\'], $search_string);
0544 $search_string = strrev($search_string);
0545
0546 $match = ['#(/|\\\\/)(?)#', '#(~|\\\\~)(?!\d{1,2}(\s|$))#', '#((?:\p{L}|\p{N})+)-((?:\p{L}|\p{N})+)(?:-((?:\p{L}|\p{N})+))?(?:-((?:\p{L}|\p{N})+))?#i', '#<<\s*$#', '#(\S\K=|=(?=\s)|=$)#'];
0547 $replace = ['\/', '\~', '("$1 $2 $3 $4"|$1$2$3$4*)', '\<\<', '\='];
0548
0549 $search_string = preg_replace($match, $replace, $search_string);
0550 $search_string = preg_replace('#\s+"\|#', '"|', $search_string);
0551
0552 /**
0553 * OPTIONAL: Thousands separator stripped from numbers, eg search for '90,000' is queried as '90000'.
0554 * By default commas are stripped from search index so that '90,000' is indexed as '90000'
0555 */
0556 // $search_string = preg_replace('#[0-9]{1,3}\K,(?=[0-9]{3})#', '', $search_string);
0557
0558 return $search_string;
0559 }
0560
0561 /**
0562 * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
0563 *
0564 * @param string $type contains either posts or topics depending on what should be searched for
0565 * @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)
0566 * @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)
0567 * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
0568 * @param string $sort_key is the key of $sort_by_sql for the selected sorting
0569 * @param string $sort_dir is either a or d representing ASC and DESC
0570 * @param string $sort_days specifies the maximum amount of days a post may be old
0571 * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
0572 * @param string $post_visibility specifies which types of posts the user can view in which forums
0573 * @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
0574 * @param array $author_ary an array of author ids if the author should be ignored during the search the array is empty
0575 * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
0576 * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
0577 * @param int $start indicates the first index of the page
0578 * @param int $per_page number of ids each page is supposed to contain
0579 * @return boolean|int total number of results
0580 */
0581 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)
0582 {
0583 global $user, $phpbb_log;
0584
0585 // No keywords? No posts.
0586 if (!strlen($this->search_query) && !count($author_ary))
0587 {
0588 return false;
0589 }
0590
0591 $id_ary = array();
0592
0593 // Sorting
0594
0595 if ($type == 'topics')
0596 {
0597 switch ($sort_key)
0598 {
0599 case 'a':
0600 $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'poster_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
0601 break;
0602
0603 case 'f':
0604 $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'forum_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
0605 break;
0606
0607 case 'i':
0608
0609 case 's':
0610 $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'post_subject ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
0611 break;
0612
0613 case 't':
0614
0615 default:
0616 $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'topic_last_post_time ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
0617 break;
0618 }
0619 }
0620 else
0621 {
0622 switch ($sort_key)
0623 {
0624 case 'a':
0625 $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'poster_id');
0626 break;
0627
0628 case 'f':
0629 $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'forum_id');
0630 break;
0631
0632 case 'i':
0633
0634 case 's':
0635 $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_subject');
0636 break;
0637
0638 case 't':
0639
0640 default:
0641 $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_time');
0642 break;
0643 }
0644 }
0645
0646 // Most narrow filters first
0647 if ($topic_id)
0648 {
0649 $this->sphinx->SetFilter('topic_id', array($topic_id));
0650 }
0651
0652 /**
0653 * Allow modifying the Sphinx search options
0654 *
0655 * @event core.search_sphinx_keywords_modify_options
0656 * @var string type Searching type ('posts', 'topics')
0657 * @var string fields Searching fields ('titleonly', 'msgonly', 'firstpost', 'all')
0658 * @var string terms Searching terms ('all', 'any')
0659 * @var int sort_days Time, in days, of the oldest possible post to list
0660 * @var string sort_key The sort type used from the possible sort types
0661 * @var int topic_id Limit the search to this topic_id only
0662 * @var array ex_fid_ary Which forums not to search on
0663 * @var string post_visibility Post visibility data
0664 * @var array author_ary Array of user_id containing the users to filter the results to
0665 * @var string author_name The username to search on
0666 * @var object sphinx The Sphinx searchd client object
0667 * @since 3.1.7-RC1
0668 */
0669 $sphinx = $this->sphinx;
0670 $vars = array(
0671 'type',
0672 'fields',
0673 'terms',
0674 'sort_days',
0675 'sort_key',
0676 'topic_id',
0677 'ex_fid_ary',
0678 'post_visibility',
0679 'author_ary',
0680 'author_name',
0681 'sphinx',
0682 );
0683 extract($this->phpbb_dispatcher->trigger_event('core.search_sphinx_keywords_modify_options', compact($vars)));
0684 $this->sphinx = $sphinx;
0685 unset($sphinx);
0686
0687 $search_query_prefix = '';
0688
0689 switch ($fields)
0690 {
0691 case 'titleonly':
0692 // Only search the title
0693 if ($terms == 'all')
0694 {
0695 $search_query_prefix = '@title ';
0696 }
0697 // Weight for the title
0698 $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
0699 // 1 is first_post, 0 is not first post
0700 $this->sphinx->SetFilter('topic_first_post', array(1));
0701 break;
0702
0703 case 'msgonly':
0704 // Only search the body
0705 if ($terms == 'all')
0706 {
0707 $search_query_prefix = '@data ';
0708 }
0709 // Weight for the body
0710 $this->sphinx->SetFieldWeights(array("title" => 1, "data" => 5));
0711 break;
0712
0713 case 'firstpost':
0714 // More relative weight for the title, also search the body
0715 $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
0716 // 1 is first_post, 0 is not first post
0717 $this->sphinx->SetFilter('topic_first_post', array(1));
0718 break;
0719
0720 default:
0721 // More relative weight for the title, also search the body
0722 $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
0723 break;
0724 }
0725
0726 if (count($author_ary))
0727 {
0728 $this->sphinx->SetFilter('poster_id', $author_ary);
0729 }
0730
0731 // As this is not simply possible at the moment, we limit the result to approved posts.
0732 // This will make it impossible for moderators to search unapproved and softdeleted posts,
0733 // but at least it will also cause the same for normal users.
0734 $this->sphinx->SetFilter('post_visibility', array(ITEM_APPROVED));
0735
0736 if (count($ex_fid_ary))
0737 {
0738 // All forums that a user is allowed to access
0739 $fid_ary = array_unique(array_intersect(array_keys($this->auth->acl_getf('f_read', true)), array_keys($this->auth->acl_getf('f_search', true))));
0740 // All forums that the user wants to and can search in
0741 $search_forums = array_diff($fid_ary, $ex_fid_ary);
0742
0743 if (count($search_forums))
0744 {
0745 $this->sphinx->SetFilter('forum_id', $search_forums);
0746 }
0747 }
0748
0749 $this->sphinx->SetFilter('deleted', array(0));
0750
0751 $this->sphinx->SetLimits((int) $start, (int) $per_page, max(SPHINX_MAX_MATCHES, (int) $start + $per_page));
0752 $result = $this->sphinx->Query($search_query_prefix . $this->sphinx_clean_search_string(str_replace('"', '"', $this->search_query)), $this->indexes);
0753
0754 // Could be connection to localhost:9312 failed (errno=111,
0755 // msg=Connection refused) during rotate, retry if so
0756 $retries = SPHINX_CONNECT_RETRIES;
0757 while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
0758 {
0759 usleep(SPHINX_CONNECT_WAIT_TIME);
0760 $result = $this->sphinx->Query($search_query_prefix . $this->sphinx_clean_search_string(str_replace('"', '"', $this->search_query)), $this->indexes);
0761 }
0762
0763 if ($this->sphinx->GetLastError())
0764 {
0765 $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_SPHINX_ERROR', false, array($this->sphinx->GetLastError()));
0766 if ($this->auth->acl_get('a_'))
0767 {
0768 trigger_error($this->user->lang('SPHINX_SEARCH_FAILED', $this->sphinx->GetLastError()));
0769 }
0770 else
0771 {
0772 trigger_error($this->user->lang('SPHINX_SEARCH_FAILED_LOG'));
0773 }
0774 }
0775
0776 $result_count = $result['total_found'];
0777
0778 if ($result_count && $start >= $result_count)
0779 {
0780 $start = floor(($result_count - 1) / $per_page) * $per_page;
0781
0782 $this->sphinx->SetLimits((int) $start, (int) $per_page, max(SPHINX_MAX_MATCHES, (int) $start + $per_page));
0783 $result = $this->sphinx->Query($search_query_prefix . $this->sphinx_clean_search_string(str_replace('"', '"', $this->search_query)), $this->indexes);
0784
0785 // Could be connection to localhost:9312 failed (errno=111,
0786 // msg=Connection refused) during rotate, retry if so
0787 $retries = SPHINX_CONNECT_RETRIES;
0788 while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
0789 {
0790 usleep(SPHINX_CONNECT_WAIT_TIME);
0791 $result = $this->sphinx->Query($search_query_prefix . $this->sphinx_clean_search_string(str_replace('"', '"', $this->search_query)), $this->indexes);
0792 }
0793 }
0794
0795 $id_ary = array();
0796 if (isset($result['matches']))
0797 {
0798 if ($type == 'posts')
0799 {
0800 $id_ary = array_keys($result['matches']);
0801 }
0802 else
0803 {
0804 foreach ($result['matches'] as $key => $value)
0805 {
0806 $id_ary[] = $value['attrs']['topic_id'];
0807 }
0808 }
0809 }
0810 else
0811 {
0812 return false;
0813 }
0814
0815 $id_ary = array_slice($id_ary, 0, (int) $per_page);
0816
0817 return $result_count;
0818 }
0819
0820 /**
0821 * Performs a search on an author's posts without caring about message contents. Depends on display specific params
0822 *
0823 * @param string $type contains either posts or topics depending on what should be searched for
0824 * @param boolean $firstpost_only if true, only topic starting posts will be considered
0825 * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
0826 * @param string $sort_key is the key of $sort_by_sql for the selected sorting
0827 * @param string $sort_dir is either a or d representing ASC and DESC
0828 * @param string $sort_days specifies the maximum amount of days a post may be old
0829 * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
0830 * @param string $post_visibility specifies which types of posts the user can view in which forums
0831 * @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
0832 * @param array $author_ary an array of author ids
0833 * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
0834 * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
0835 * @param int $start indicates the first index of the page
0836 * @param int $per_page number of ids each page is supposed to contain
0837 * @return boolean|int total number of results
0838 */
0839 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)
0840 {
0841 $this->search_query = '';
0842
0843 $this->sphinx->SetMatchMode(SPH_MATCH_FULLSCAN);
0844 $fields = ($firstpost_only) ? 'firstpost' : 'all';
0845 $terms = 'all';
0846 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);
0847 }
0848
0849 /**
0850 * Updates wordlist and wordmatch tables when a message is posted or changed
0851 *
0852 * @param string $mode Contains the post mode: edit, post, reply, quote
0853 * @param int $post_id The id of the post which is modified/created
0854 * @param string &$message New or updated post content
0855 * @param string &$subject New or updated post subject
0856 * @param int $poster_id Post author's user id
0857 * @param int $forum_id The id of the forum in which the post is located
0858 */
0859 public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
0860 {
0861 /**
0862 * Event to modify method arguments before the Sphinx search index is updated
0863 *
0864 * @event core.search_sphinx_index_before
0865 * @var string mode Contains the post mode: edit, post, reply, quote
0866 * @var int post_id The id of the post which is modified/created
0867 * @var string message New or updated post content
0868 * @var string subject New or updated post subject
0869 * @var int poster_id Post author's user id
0870 * @var int forum_id The id of the forum in which the post is located
0871 * @since 3.2.3-RC1
0872 */
0873 $vars = array(
0874 'mode',
0875 'post_id',
0876 'message',
0877 'subject',
0878 'poster_id',
0879 'forum_id',
0880 );
0881 extract($this->phpbb_dispatcher->trigger_event('core.search_sphinx_index_before', compact($vars)));
0882
0883 if ($mode == 'edit')
0884 {
0885 $this->sphinx->UpdateAttributes($this->indexes, array('forum_id', 'poster_id'), array((int) $post_id => array((int) $forum_id, (int) $poster_id)));
0886 }
0887 else if ($mode != 'post' && $post_id)
0888 {
0889 // Update topic_last_post_time for full topic
0890 $sql_array = array(
0891 'SELECT' => 'p1.post_id',
0892 'FROM' => array(
0893 POSTS_TABLE => 'p1',
0894 ),
0895 'LEFT_JOIN' => array(array(
0896 'FROM' => array(
0897 POSTS_TABLE => 'p2'
0898 ),
0899 'ON' => 'p1.topic_id = p2.topic_id',
0900 )),
0901 'WHERE' => 'p2.post_id = ' . ((int) $post_id),
0902 );
0903
0904 $sql = $this->db->sql_build_query('SELECT', $sql_array);
0905 $result = $this->db->sql_query($sql);
0906
0907 $post_updates = array();
0908 $post_time = time();
0909 while ($row = $this->db->sql_fetchrow($result))
0910 {
0911 $post_updates[(int) $row['post_id']] = array($post_time);
0912 }
0913 $this->db->sql_freeresult($result);
0914
0915 if (count($post_updates))
0916 {
0917 $this->sphinx->UpdateAttributes($this->indexes, array('topic_last_post_time'), $post_updates);
0918 }
0919 }
0920 }
0921
0922 /**
0923 * Delete a post from the index after it was deleted
0924 */
0925 public function index_remove($post_ids, $author_ids, $forum_ids)
0926 {
0927 $values = array();
0928 foreach ($post_ids as $post_id)
0929 {
0930 $values[$post_id] = array(1);
0931 }
0932
0933 $this->sphinx->UpdateAttributes($this->indexes, array('deleted'), $values);
0934 }
0935
0936 /**
0937 * Nothing needs to be destroyed
0938 */
0939 public function tidy($create = false)
0940 {
0941 $this->config->set('search_last_gc', time(), false);
0942 }
0943
0944 /**
0945 * Create sphinx table
0946 *
0947 * @return string|bool error string is returned incase of errors otherwise false
0948 */
0949 public function create_index($acp_module, $u_action)
0950 {
0951 if (!$this->index_created())
0952 {
0953 $table_data = array(
0954 'COLUMNS' => array(
0955 'counter_id' => array('UINT', 0),
0956 'max_doc_id' => array('UINT', 0),
0957 ),
0958 'PRIMARY_KEY' => 'counter_id',
0959 );
0960 $this->db_tools->sql_create_table(SPHINX_TABLE, $table_data);
0961
0962 $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE;
0963 $this->db->sql_query($sql);
0964
0965 $data = array(
0966 'counter_id' => '1',
0967 'max_doc_id' => '0',
0968 );
0969 $sql = 'INSERT INTO ' . SPHINX_TABLE . ' ' . $this->db->sql_build_array('INSERT', $data);
0970 $this->db->sql_query($sql);
0971 }
0972
0973 return false;
0974 }
0975
0976 /**
0977 * Drop sphinx table
0978 *
0979 * @return string|bool error string is returned incase of errors otherwise false
0980 */
0981 public function delete_index($acp_module, $u_action)
0982 {
0983 if (!$this->index_created())
0984 {
0985 return false;
0986 }
0987
0988 $this->db_tools->sql_table_drop(SPHINX_TABLE);
0989
0990 return false;
0991 }
0992
0993 /**
0994 * Returns true if the sphinx table was created
0995 *
0996 * @return bool true if sphinx table was created
0997 */
0998 public function index_created($allow_new_files = true)
0999 {
1000 $created = false;
1001
1002 if ($this->db_tools->sql_table_exists(SPHINX_TABLE))
1003 {
1004 $created = true;
1005 }
1006
1007 return $created;
1008 }
1009
1010 /**
1011 * Returns an associative array containing information about the indexes
1012 *
1013 * @return string|bool Language string of error false otherwise
1014 */
1015 public function index_stats()
1016 {
1017 if (empty($this->stats))
1018 {
1019 $this->get_stats();
1020 }
1021
1022 return array(
1023 $this->user->lang['FULLTEXT_SPHINX_MAIN_POSTS'] => ($this->index_created()) ? $this->stats['main_posts'] : 0,
1024 $this->user->lang['FULLTEXT_SPHINX_DELTA_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0,
1025 $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0,
1026 );
1027 }
1028
1029 /**
1030 * Collects stats that can be displayed on the index maintenance page
1031 */
1032 protected function get_stats()
1033 {
1034 if ($this->index_created())
1035 {
1036 $sql = 'SELECT COUNT(post_id) as total_posts
1037 FROM ' . POSTS_TABLE;
1038 $result = $this->db->sql_query($sql);
1039 $this->stats['total_posts'] = (int) $this->db->sql_fetchfield('total_posts');
1040 $this->db->sql_freeresult($result);
1041
1042 $sql = 'SELECT COUNT(p.post_id) as main_posts
1043 FROM ' . POSTS_TABLE . ' p, ' . SPHINX_TABLE . ' m
1044 WHERE p.post_id <= m.max_doc_id
1045 AND m.counter_id = 1';
1046 $result = $this->db->sql_query($sql);
1047 $this->stats['main_posts'] = (int) $this->db->sql_fetchfield('main_posts');
1048 $this->db->sql_freeresult($result);
1049 }
1050 }
1051
1052 /**
1053 * Returns a list of options for the ACP to display
1054 *
1055 * @return associative array containing template and config variables
1056 */
1057 public function acp()
1058 {
1059 $config_vars = array(
1060 'fulltext_sphinx_data_path' => 'string',
1061 'fulltext_sphinx_host' => 'string',
1062 'fulltext_sphinx_port' => 'string',
1063 'fulltext_sphinx_indexer_mem_limit' => 'int',
1064 );
1065
1066 $tpl = '
1067 <span class="error">' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE']. '</span>
1068 <dl>
1069 <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>
1070 <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>
1071 </dl>
1072 <dl>
1073 <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>
1074 <dd><input id="fulltext_sphinx_host" type="text" size="40" maxlength="255" name="config[fulltext_sphinx_host]" value="' . $this->config['fulltext_sphinx_host'] . '" /></dd>
1075 </dl>
1076 <dl>
1077 <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>
1078 <dd><input id="fulltext_sphinx_port" type="number" min="0" max="9999999999" name="config[fulltext_sphinx_port]" value="' . $this->config['fulltext_sphinx_port'] . '" /></dd>
1079 </dl>
1080 <dl>
1081 <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>
1082 <dd><input id="fulltext_sphinx_indexer_mem_limit" type="number" min="0" max="9999999999" name="config[fulltext_sphinx_indexer_mem_limit]" value="' . $this->config['fulltext_sphinx_indexer_mem_limit'] . '" /> ' . $this->user->lang['MIB'] . '</dd>
1083 </dl>
1084 <dl>
1085 <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>
1086 <dd>' . (($this->config_generate()) ? '<textarea readonly="readonly" rows="6" id="sphinx_config_data">' . htmlspecialchars($this->config_file_data, ENT_COMPAT) . '</textarea>' : $this->config_file_data) . '</dd>
1087 <dl>
1088 ';
1089
1090 // These are fields required in the config table
1091 return array(
1092 'tpl' => $tpl,
1093 'config' => $config_vars
1094 );
1095 }
1096 }
1097