Verzeichnisstruktur phpBB-3.2.0
- Veröffentlicht
- 06.01.2017
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
001 <?php
002 /**
003 *
004 * This file is part of the phpBB Forum Software package.
005 *
006 * @copyright (c) phpBB Limited <https://www.phpbb.com>
007 * @license GNU General Public License, version 2 (GPL-2.0)
008 *
009 * For full copyright and license information, please see
010 * the docs/CREDITS.txt file.
011 *
012 */
013
014 namespace phpbb\search;
015
016 define('SPHINX_MAX_MATCHES', 20000);
017 define('SPHINX_CONNECT_RETRIES', 3);
018 define('SPHINX_CONNECT_WAIT_TIME', 300);
019
020 /**
021 * Fulltext search based on the sphinx search deamon
022 */
023 class fulltext_sphinx
024 {
025 /**
026 * Associative array holding index stats
027 * @var array
028 */
029 protected $stats = array();
030
031 /**
032 * Holds the words entered by user, obtained by splitting the entered query on whitespace
033 * @var array
034 */
035 protected $split_words = array();
036
037 /**
038 * Holds unique sphinx id
039 * @var string
040 */
041 protected $id;
042
043 /**
044 * Stores the names of both main and delta sphinx indexes
045 * separated by a semicolon
046 * @var string
047 */
048 protected $indexes;
049
050 /**
051 * Sphinx searchd client object
052 * @var SphinxClient
053 */
054 protected $sphinx;
055
056 /**
057 * Relative path to board root
058 * @var string
059 */
060 protected $phpbb_root_path;
061
062 /**
063 * PHP Extension
064 * @var string
065 */
066 protected $php_ext;
067
068 /**
069 * Auth object
070 * @var \phpbb\auth\auth
071 */
072 protected $auth;
073
074 /**
075 * Config object
076 * @var \phpbb\config\config
077 */
078 protected $config;
079
080 /**
081 * Database connection
082 * @var \phpbb\db\driver\driver_interface
083 */
084 protected $db;
085
086 /**
087 * Database Tools object
088 * @var \phpbb\db\tools\tools_interface
089 */
090 protected $db_tools;
091
092 /**
093 * Stores the database type if supported by sphinx
094 * @var string
095 */
096 protected $dbtype;
097
098 /**
099 * phpBB event dispatcher object
100 * @var \phpbb\event\dispatcher_interface
101 */
102 protected $phpbb_dispatcher;
103
104 /**
105 * User object
106 * @var \phpbb\user
107 */
108 protected $user;
109
110 /**
111 * Stores the generated content of the sphinx config file
112 * @var string
113 */
114 protected $config_file_data = '';
115
116 /**
117 * Contains tidied search query.
118 * Operators are prefixed in search query and common words excluded
119 * @var string
120 */
121 protected $search_query;
122
123 /**
124 * Constructor
125 * Creates a new \phpbb\search\fulltext_postgres, which is used as a search backend
126 *
127 * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false
128 * @param string $phpbb_root_path Relative path to phpBB root
129 * @param string $phpEx PHP file extension
130 * @param \phpbb\auth\auth $auth Auth object
131 * @param \phpbb\config\config $config Config object
132 * @param \phpbb\db\driver\driver_interface Database object
133 * @param \phpbb\user $user User object
134 * @param \phpbb\event\dispatcher_interface $phpbb_dispatcher Event dispatcher object
135 */
136 public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
137 {
138 $this->phpbb_root_path = $phpbb_root_path;
139 $this->php_ext = $phpEx;
140 $this->config = $config;
141 $this->phpbb_dispatcher = $phpbb_dispatcher;
142 $this->user = $user;
143 $this->db = $db;
144 $this->auth = $auth;
145
146 // Initialize \phpbb\db\tools\tools object
147 global $phpbb_container; // TODO inject into object
148 $this->db_tools = $phpbb_container->get('dbal.tools');
149
150 if (!$this->config['fulltext_sphinx_id'])
151 {
152 $this->config->set('fulltext_sphinx_id', unique_id());
153 }
154 $this->id = $this->config['fulltext_sphinx_id'];
155 $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main';
156
157 if (!class_exists('SphinxClient'))
158 {
159 require($this->phpbb_root_path . 'includes/sphinxapi.' . $this->php_ext);
160 }
161
162 // Initialize sphinx client
163 $this->sphinx = new \SphinxClient();
164
165 $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));
166
167 $error = false;
168 }
169
170 /**
171 * Returns the name of this search backend to be displayed to administrators
172 *
173 * @return string Name
174 */
175 public function get_name()
176 {
177 return 'Sphinx Fulltext';
178 }
179
180 /**
181 * Returns the search_query
182 *
183 * @return string search query
184 */
185 public function get_search_query()
186 {
187 return $this->search_query;
188 }
189
190 /**
191 * Returns false as there is no word_len array
192 *
193 * @return false
194 */
195 public function get_word_length()
196 {
197 return false;
198 }
199
200 /**
201 * Returns an empty array as there are no common_words
202 *
203 * @return array common words that are ignored by search backend
204 */
205 public function get_common_words()
206 {
207 return array();
208 }
209
210 /**
211 * Checks permissions and paths, if everything is correct it generates the config file
212 *
213 * @return string|bool Language key of the error/incompatiblity encountered, or false if successful
214 */
215 public function init()
216 {
217 if ($this->db->get_sql_layer() != 'mysql' && $this->db->get_sql_layer() != 'mysql4' && $this->db->get_sql_layer() != 'mysqli' && $this->db->get_sql_layer() != 'postgres')
218 {
219 return $this->user->lang['FULLTEXT_SPHINX_WRONG_DATABASE'];
220 }
221
222 // Move delta to main index each hour
223 $this->config->set('search_gc', 3600);
224
225 return false;
226 }
227
228 /**
229 * Generates content of sphinx.conf
230 *
231 * @return bool True if sphinx.conf content is correctly generated, false otherwise
232 */
233 protected function config_generate()
234 {
235 // Check if Database is supported by Sphinx
236 if ($this->db->get_sql_layer() =='mysql' || $this->db->get_sql_layer() == 'mysql4' || $this->db->get_sql_layer() == 'mysqli')
237 {
238 $this->dbtype = 'mysql';
239 }
240 else if ($this->db->get_sql_layer() == 'postgres')
241 {
242 $this->dbtype = 'pgsql';
243 }
244 else
245 {
246 $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_WRONG_DATABASE');
247 return false;
248 }
249
250 // Check if directory paths have been filled
251 if (!$this->config['fulltext_sphinx_data_path'])
252 {
253 $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_NO_CONFIG_DATA');
254 return false;
255 }
256
257 include($this->phpbb_root_path . 'config.' . $this->php_ext);
258
259 /* Now that we're sure everything was entered correctly,
260 generate a config for the index. We use a config value
261 fulltext_sphinx_id for this, as it should be unique. */
262 $config_object = new \phpbb\search\sphinx\config($this->config_file_data);
263 $config_data = array(
264 'source source_phpbb_' . $this->id . '_main' => array(
265 array('type', $this->dbtype . ' # mysql or pgsql'),
266 // This config value sql_host needs to be changed incase sphinx and sql are on different servers
267 array('sql_host', $dbhost . ' # SQL server host sphinx connects to'),
268 array('sql_user', '[dbuser]'),
269 array('sql_pass', '[dbpassword]'),
270 array('sql_db', $dbname),
271 array('sql_port', $dbport . ' # optional, default is 3306 for mysql and 5432 for pgsql'),
272 array('sql_query_pre', 'SET NAMES \'utf8\''),
273 array('sql_query_pre', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = (SELECT MAX(post_id) FROM ' . POSTS_TABLE . ') WHERE counter_id = 1'),
274 array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''),
275 array('sql_range_step', '5000'),
276 array('sql_query', 'SELECT
277 p.post_id AS id,
278 p.forum_id,
279 p.topic_id,
280 p.poster_id,
281 p.post_visibility,
282 CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
283 p.post_time,
284 p.post_subject,
285 p.post_subject as title,
286 p.post_text as data,
287 t.topic_last_post_time,
288 0 as deleted
289 FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
290 WHERE
291 p.topic_id = t.topic_id
292 AND p.post_id >= $start AND p.post_id <= $end'),
293 array('sql_query_post', ''),
294 array('sql_query_post_index', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = $maxid WHERE counter_id = 1'),
295 array('sql_query_info', 'SELECT * FROM ' . POSTS_TABLE . ' WHERE post_id = $id'),
296 array('sql_attr_uint', 'forum_id'),
297 array('sql_attr_uint', 'topic_id'),
298 array('sql_attr_uint', 'poster_id'),
299 array('sql_attr_uint', 'post_visibility'),
300 array('sql_attr_bool', 'topic_first_post'),
301 array('sql_attr_bool', 'deleted'),
302 array('sql_attr_timestamp', 'post_time'),
303 array('sql_attr_timestamp', 'topic_last_post_time'),
304 array('sql_attr_string', 'post_subject'),
305 ),
306 'source source_phpbb_' . $this->id . '_delta : source_phpbb_' . $this->id . '_main' => array(
307 array('sql_query_pre', ''),
308 array('sql_query_range', ''),
309 array('sql_range_step', ''),
310 array('sql_query', 'SELECT
311 p.post_id AS id,
312 p.forum_id,
313 p.topic_id,
314 p.poster_id,
315 p.post_visibility,
316 CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post,
317 p.post_time,
318 p.post_subject,
319 p.post_subject as title,
320 p.post_text as data,
321 t.topic_last_post_time,
322 0 as deleted
323 FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t
324 WHERE
325 p.topic_id = t.topic_id
326 AND p.post_id >= ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'),
327 ),
328 'index index_phpbb_' . $this->id . '_main' => array(
329 array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'),
330 array('source', 'source_phpbb_' . $this->id . '_main'),
331 array('docinfo', 'extern'),
332 array('morphology', 'none'),
333 array('stopwords', ''),
334 array('min_word_len', '2'),
335 array('charset_type', 'utf-8'),
336 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'),
337 array('min_prefix_len', '0'),
338 array('min_infix_len', '0'),
339 ),
340 'index index_phpbb_' . $this->id . '_delta : index_phpbb_' . $this->id . '_main' => array(
341 array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'),
342 array('source', 'source_phpbb_' . $this->id . '_delta'),
343 ),
344 'indexer' => array(
345 array('mem_limit', $this->config['fulltext_sphinx_indexer_mem_limit'] . 'M'),
346 ),
347 'searchd' => array(
348 array('compat_sphinxql_magics' , '0'),
349 array('listen' , ($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost') . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '9312')),
350 array('log', $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'),
351 array('query_log', $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'),
352 array('read_timeout', '5'),
353 array('max_children', '30'),
354 array('pid_file', $this->config['fulltext_sphinx_data_path'] . 'searchd.pid'),
355 array('max_matches', (string) SPHINX_MAX_MATCHES),
356 array('binlog_path', $this->config['fulltext_sphinx_data_path']),
357 ),
358 );
359
360 $non_unique = array('sql_query_pre' => true, 'sql_attr_uint' => true, 'sql_attr_timestamp' => true, 'sql_attr_str2ordinal' => true, 'sql_attr_bool' => true);
361 $delete = array('sql_group_column' => true, 'sql_date_column' => true, 'sql_str2ordinal_column' => true);
362
363 /**
364 * Allow adding/changing the Sphinx configuration data
365 *
366 * @event core.search_sphinx_modify_config_data
367 * @var array config_data Array with the Sphinx configuration data
368 * @var array non_unique Array with the Sphinx non-unique variables to delete
369 * @var array delete Array with the Sphinx variables to delete
370 * @since 3.1.7-RC1
371 */
372 $vars = array(
373 'config_data',
374 'non_unique',
375 'delete',
376 );
377 extract($this->phpbb_dispatcher->trigger_event('core.search_sphinx_modify_config_data', compact($vars)));
378
379 foreach ($config_data as $section_name => $section_data)
380 {
381 $section = $config_object->get_section_by_name($section_name);
382 if (!$section)
383 {
384 $section = $config_object->add_section($section_name);
385 }
386
387 foreach ($delete as $key => $void)
388 {
389 $section->delete_variables_by_name($key);
390 }
391
392 foreach ($non_unique as $key => $void)
393 {
394 $section->delete_variables_by_name($key);
395 }
396
397 foreach ($section_data as $entry)
398 {
399 $key = $entry[0];
400 $value = $entry[1];
401
402 if (!isset($non_unique[$key]))
403 {
404 $variable = $section->get_variable_by_name($key);
405 if (!$variable)
406 {
407 $section->create_variable($key, $value);
408 }
409 else
410 {
411 $variable->set_value($value);
412 }
413 }
414 else
415 {
416 $section->create_variable($key, $value);
417 }
418 }
419 }
420 $this->config_file_data = $config_object->get_data();
421
422 return true;
423 }
424
425 /**
426 * Splits keywords entered by a user into an array of words stored in $this->split_words
427 * Stores the tidied search query in $this->search_query
428 *
429 * @param string $keywords Contains the keyword as entered by the user
430 * @param string $terms is either 'all' or 'any'
431 * @return false if no valid keywords were found and otherwise true
432 */
433 public function split_keywords(&$keywords, $terms)
434 {
435 if ($terms == 'all')
436 {
437 $match = array('#\sand\s#i', '#\sor\s#i', '#\snot\s#i', '#\+#', '#-#', '#\|#', '#@#');
438 $replace = array(' & ', ' | ', ' - ', ' +', ' -', ' |', '');
439
440 $keywords = preg_replace($match, $replace, $keywords);
441 $this->sphinx->SetMatchMode(SPH_MATCH_EXTENDED);
442 }
443 else
444 {
445 $this->sphinx->SetMatchMode(SPH_MATCH_ANY);
446 }
447
448 // Keep quotes and new lines
449 $keywords = str_replace(array('"', "\n"), array('"', ' '), trim($keywords));
450
451 if (strlen($keywords) > 0)
452 {
453 $this->search_query = str_replace('"', '"', $keywords);
454 return true;
455 }
456
457 return false;
458 }
459
460 /**
461 * Performs a search on keywords depending on display specific params. You have to run split_keywords() first
462 *
463 * @param string $type contains either posts or topics depending on what should be searched for
464 * @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)
465 * @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)
466 * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
467 * @param string $sort_key is the key of $sort_by_sql for the selected sorting
468 * @param string $sort_dir is either a or d representing ASC and DESC
469 * @param string $sort_days specifies the maximum amount of days a post may be old
470 * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
471 * @param string $post_visibility specifies which types of posts the user can view in which forums
472 * @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
473 * @param array $author_ary an array of author ids if the author should be ignored during the search the array is empty
474 * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
475 * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
476 * @param int $start indicates the first index of the page
477 * @param int $per_page number of ids each page is supposed to contain
478 * @return boolean|int total number of results
479 */
480 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)
481 {
482 global $user, $phpbb_log;
483
484 // No keywords? No posts.
485 if (!strlen($this->search_query) && !sizeof($author_ary))
486 {
487 return false;
488 }
489
490 $id_ary = array();
491
492 // Sorting
493
494 if ($type == 'topics')
495 {
496 switch ($sort_key)
497 {
498 case 'a':
499 $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'poster_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
500 break;
501
502 case 'f':
503 $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'forum_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
504 break;
505
506 case 'i':
507
508 case 's':
509 $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'post_subject ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
510 break;
511
512 case 't':
513
514 default:
515 $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'topic_last_post_time ' . (($sort_dir == 'a') ? 'ASC' : 'DESC'));
516 break;
517 }
518 }
519 else
520 {
521 switch ($sort_key)
522 {
523 case 'a':
524 $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'poster_id');
525 break;
526
527 case 'f':
528 $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'forum_id');
529 break;
530
531 case 'i':
532
533 case 's':
534 $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_subject');
535 break;
536
537 case 't':
538
539 default:
540 $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_time');
541 break;
542 }
543 }
544
545 // Most narrow filters first
546 if ($topic_id)
547 {
548 $this->sphinx->SetFilter('topic_id', array($topic_id));
549 }
550
551 /**
552 * Allow modifying the Sphinx search options
553 *
554 * @event core.search_sphinx_keywords_modify_options
555 * @var string type Searching type ('posts', 'topics')
556 * @var string fields Searching fields ('titleonly', 'msgonly', 'firstpost', 'all')
557 * @var string terms Searching terms ('all', 'any')
558 * @var int sort_days Time, in days, of the oldest possible post to list
559 * @var string sort_key The sort type used from the possible sort types
560 * @var int topic_id Limit the search to this topic_id only
561 * @var array ex_fid_ary Which forums not to search on
562 * @var string post_visibility Post visibility data
563 * @var array author_ary Array of user_id containing the users to filter the results to
564 * @var string author_name The username to search on
565 * @var object sphinx The Sphinx searchd client object
566 * @since 3.1.7-RC1
567 */
568 $sphinx = $this->sphinx;
569 $vars = array(
570 'type',
571 'fields',
572 'terms',
573 'sort_days',
574 'sort_key',
575 'topic_id',
576 'ex_fid_ary',
577 'post_visibility',
578 'author_ary',
579 'author_name',
580 'sphinx',
581 );
582 extract($this->phpbb_dispatcher->trigger_event('core.search_sphinx_keywords_modify_options', compact($vars)));
583 $this->sphinx = $sphinx;
584 unset($sphinx);
585
586 $search_query_prefix = '';
587
588 switch ($fields)
589 {
590 case 'titleonly':
591 // Only search the title
592 if ($terms == 'all')
593 {
594 $search_query_prefix = '@title ';
595 }
596 // Weight for the title
597 $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
598 // 1 is first_post, 0 is not first post
599 $this->sphinx->SetFilter('topic_first_post', array(1));
600 break;
601
602 case 'msgonly':
603 // Only search the body
604 if ($terms == 'all')
605 {
606 $search_query_prefix = '@data ';
607 }
608 // Weight for the body
609 $this->sphinx->SetFieldWeights(array("title" => 1, "data" => 5));
610 break;
611
612 case 'firstpost':
613 // More relative weight for the title, also search the body
614 $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
615 // 1 is first_post, 0 is not first post
616 $this->sphinx->SetFilter('topic_first_post', array(1));
617 break;
618
619 default:
620 // More relative weight for the title, also search the body
621 $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1));
622 break;
623 }
624
625 if (sizeof($author_ary))
626 {
627 $this->sphinx->SetFilter('poster_id', $author_ary);
628 }
629
630 // As this is not simply possible at the moment, we limit the result to approved posts.
631 // This will make it impossible for moderators to search unapproved and softdeleted posts,
632 // but at least it will also cause the same for normal users.
633 $this->sphinx->SetFilter('post_visibility', array(ITEM_APPROVED));
634
635 if (sizeof($ex_fid_ary))
636 {
637 // All forums that a user is allowed to access
638 $fid_ary = array_unique(array_intersect(array_keys($this->auth->acl_getf('f_read', true)), array_keys($this->auth->acl_getf('f_search', true))));
639 // All forums that the user wants to and can search in
640 $search_forums = array_diff($fid_ary, $ex_fid_ary);
641
642 if (sizeof($search_forums))
643 {
644 $this->sphinx->SetFilter('forum_id', $search_forums);
645 }
646 }
647
648 $this->sphinx->SetFilter('deleted', array(0));
649
650 $this->sphinx->SetLimits($start, (int) $per_page, SPHINX_MAX_MATCHES);
651 $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes);
652
653 // Could be connection to localhost:9312 failed (errno=111,
654 // msg=Connection refused) during rotate, retry if so
655 $retries = SPHINX_CONNECT_RETRIES;
656 while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
657 {
658 usleep(SPHINX_CONNECT_WAIT_TIME);
659 $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes);
660 }
661
662 if ($this->sphinx->GetLastError())
663 {
664 $phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_SPHINX_ERROR', false, array($this->sphinx->GetLastError()));
665 if ($this->auth->acl_get('a_'))
666 {
667 trigger_error($this->user->lang('SPHINX_SEARCH_FAILED', $this->sphinx->GetLastError()));
668 }
669 else
670 {
671 trigger_error($this->user->lang('SPHINX_SEARCH_FAILED_LOG'));
672 }
673 }
674
675 $result_count = $result['total_found'];
676
677 if ($result_count && $start >= $result_count)
678 {
679 $start = floor(($result_count - 1) / $per_page) * $per_page;
680
681 $this->sphinx->SetLimits((int) $start, (int) $per_page, SPHINX_MAX_MATCHES);
682 $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes);
683
684 // Could be connection to localhost:9312 failed (errno=111,
685 // msg=Connection refused) during rotate, retry if so
686 $retries = SPHINX_CONNECT_RETRIES;
687 while (!$result && (strpos($this->sphinx->GetLastError(), "errno=111,") !== false) && $retries--)
688 {
689 usleep(SPHINX_CONNECT_WAIT_TIME);
690 $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes);
691 }
692 }
693
694 $id_ary = array();
695 if (isset($result['matches']))
696 {
697 if ($type == 'posts')
698 {
699 $id_ary = array_keys($result['matches']);
700 }
701 else
702 {
703 foreach ($result['matches'] as $key => $value)
704 {
705 $id_ary[] = $value['attrs']['topic_id'];
706 }
707 }
708 }
709 else
710 {
711 return false;
712 }
713
714 $id_ary = array_slice($id_ary, 0, (int) $per_page);
715
716 return $result_count;
717 }
718
719 /**
720 * Performs a search on an author's posts without caring about message contents. Depends on display specific params
721 *
722 * @param string $type contains either posts or topics depending on what should be searched for
723 * @param boolean $firstpost_only if true, only topic starting posts will be considered
724 * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query
725 * @param string $sort_key is the key of $sort_by_sql for the selected sorting
726 * @param string $sort_dir is either a or d representing ASC and DESC
727 * @param string $sort_days specifies the maximum amount of days a post may be old
728 * @param array $ex_fid_ary specifies an array of forum ids which should not be searched
729 * @param string $post_visibility specifies which types of posts the user can view in which forums
730 * @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
731 * @param array $author_ary an array of author ids
732 * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match
733 * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered
734 * @param int $start indicates the first index of the page
735 * @param int $per_page number of ids each page is supposed to contain
736 * @return boolean|int total number of results
737 */
738 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)
739 {
740 $this->search_query = '';
741
742 $this->sphinx->SetMatchMode(SPH_MATCH_FULLSCAN);
743 $fields = ($firstpost_only) ? 'firstpost' : 'all';
744 $terms = 'all';
745 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);
746 }
747
748 /**
749 * Updates wordlist and wordmatch tables when a message is posted or changed
750 *
751 * @param string $mode Contains the post mode: edit, post, reply, quote
752 * @param int $post_id The id of the post which is modified/created
753 * @param string &$message New or updated post content
754 * @param string &$subject New or updated post subject
755 * @param int $poster_id Post author's user id
756 * @param int $forum_id The id of the forum in which the post is located
757 */
758 public function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id)
759 {
760 if ($mode == 'edit')
761 {
762 $this->sphinx->UpdateAttributes($this->indexes, array('forum_id', 'poster_id'), array((int) $post_id => array((int) $forum_id, (int) $poster_id)));
763 }
764 else if ($mode != 'post' && $post_id)
765 {
766 // Update topic_last_post_time for full topic
767 $sql_array = array(
768 'SELECT' => 'p1.post_id',
769 'FROM' => array(
770 POSTS_TABLE => 'p1',
771 ),
772 'LEFT_JOIN' => array(array(
773 'FROM' => array(
774 POSTS_TABLE => 'p2'
775 ),
776 'ON' => 'p1.topic_id = p2.topic_id',
777 )),
778 'WHERE' => 'p2.post_id = ' . ((int) $post_id),
779 );
780
781 $sql = $this->db->sql_build_query('SELECT', $sql_array);
782 $result = $this->db->sql_query($sql);
783
784 $post_updates = array();
785 $post_time = time();
786 while ($row = $this->db->sql_fetchrow($result))
787 {
788 $post_updates[(int) $row['post_id']] = array($post_time);
789 }
790 $this->db->sql_freeresult($result);
791
792 if (sizeof($post_updates))
793 {
794 $this->sphinx->UpdateAttributes($this->indexes, array('topic_last_post_time'), $post_updates);
795 }
796 }
797 }
798
799 /**
800 * Delete a post from the index after it was deleted
801 */
802 public function index_remove($post_ids, $author_ids, $forum_ids)
803 {
804 $values = array();
805 foreach ($post_ids as $post_id)
806 {
807 $values[$post_id] = array(1);
808 }
809
810 $this->sphinx->UpdateAttributes($this->indexes, array('deleted'), $values);
811 }
812
813 /**
814 * Nothing needs to be destroyed
815 */
816 public function tidy($create = false)
817 {
818 $this->config->set('search_last_gc', time(), false);
819 }
820
821 /**
822 * Create sphinx table
823 *
824 * @return string|bool error string is returned incase of errors otherwise false
825 */
826 public function create_index($acp_module, $u_action)
827 {
828 if (!$this->index_created())
829 {
830 $table_data = array(
831 'COLUMNS' => array(
832 'counter_id' => array('UINT', 0),
833 'max_doc_id' => array('UINT', 0),
834 ),
835 'PRIMARY_KEY' => 'counter_id',
836 );
837 $this->db_tools->sql_create_table(SPHINX_TABLE, $table_data);
838
839 $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE;
840 $this->db->sql_query($sql);
841
842 $data = array(
843 'counter_id' => '1',
844 'max_doc_id' => '0',
845 );
846 $sql = 'INSERT INTO ' . SPHINX_TABLE . ' ' . $this->db->sql_build_array('INSERT', $data);
847 $this->db->sql_query($sql);
848 }
849
850 return false;
851 }
852
853 /**
854 * Drop sphinx table
855 *
856 * @return string|bool error string is returned incase of errors otherwise false
857 */
858 public function delete_index($acp_module, $u_action)
859 {
860 if (!$this->index_created())
861 {
862 return false;
863 }
864
865 $this->db_tools->sql_table_drop(SPHINX_TABLE);
866
867 return false;
868 }
869
870 /**
871 * Returns true if the sphinx table was created
872 *
873 * @return bool true if sphinx table was created
874 */
875 public function index_created($allow_new_files = true)
876 {
877 $created = false;
878
879 if ($this->db_tools->sql_table_exists(SPHINX_TABLE))
880 {
881 $created = true;
882 }
883
884 return $created;
885 }
886
887 /**
888 * Returns an associative array containing information about the indexes
889 *
890 * @return string|bool Language string of error false otherwise
891 */
892 public function index_stats()
893 {
894 if (empty($this->stats))
895 {
896 $this->get_stats();
897 }
898
899 return array(
900 $this->user->lang['FULLTEXT_SPHINX_MAIN_POSTS'] => ($this->index_created()) ? $this->stats['main_posts'] : 0,
901 $this->user->lang['FULLTEXT_SPHINX_DELTA_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0,
902 $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0,
903 );
904 }
905
906 /**
907 * Collects stats that can be displayed on the index maintenance page
908 */
909 protected function get_stats()
910 {
911 if ($this->index_created())
912 {
913 $sql = 'SELECT COUNT(post_id) as total_posts
914 FROM ' . POSTS_TABLE;
915 $result = $this->db->sql_query($sql);
916 $this->stats['total_posts'] = (int) $this->db->sql_fetchfield('total_posts');
917 $this->db->sql_freeresult($result);
918
919 $sql = 'SELECT COUNT(p.post_id) as main_posts
920 FROM ' . POSTS_TABLE . ' p, ' . SPHINX_TABLE . ' m
921 WHERE p.post_id <= m.max_doc_id
922 AND m.counter_id = 1';
923 $result = $this->db->sql_query($sql);
924 $this->stats['main_posts'] = (int) $this->db->sql_fetchfield('main_posts');
925 $this->db->sql_freeresult($result);
926 }
927 }
928
929 /**
930 * Returns a list of options for the ACP to display
931 *
932 * @return associative array containing template and config variables
933 */
934 public function acp()
935 {
936 $config_vars = array(
937 'fulltext_sphinx_data_path' => 'string',
938 'fulltext_sphinx_host' => 'string',
939 'fulltext_sphinx_port' => 'string',
940 'fulltext_sphinx_indexer_mem_limit' => 'int',
941 );
942
943 $tpl = '
944 <span class="error">' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE']. '</span>
945 <dl>
946 <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>
947 <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>
948 </dl>
949 <dl>
950 <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>
951 <dd><input id="fulltext_sphinx_host" type="text" size="40" maxlength="255" name="config[fulltext_sphinx_host]" value="' . $this->config['fulltext_sphinx_host'] . '" /></dd>
952 </dl>
953 <dl>
954 <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>
955 <dd><input id="fulltext_sphinx_port" type="number" min="0" max="9999999999" name="config[fulltext_sphinx_port]" value="' . $this->config['fulltext_sphinx_port'] . '" /></dd>
956 </dl>
957 <dl>
958 <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>
959 <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>
960 </dl>
961 <dl>
962 <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>
963 <dd>' . (($this->config_generate()) ? '<textarea readonly="readonly" rows="6" id="sphinx_config_data">' . htmlspecialchars($this->config_file_data) . '</textarea>' : $this->config_file_data) . '</dd>
964 <dl>
965 ';
966
967 // These are fields required in the config table
968 return array(
969 'tpl' => $tpl,
970 'config' => $config_vars
971 );
972 }
973 }
974