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