2005-05-23 08:42:20 +00:00
|
|
|
<?php
|
WARNING: HUGE COMMIT
Doxygen documentation update:
* Changed alls @addtogroup to @ingroup. @addtogroup adds the comment to the group description, but doesn't add the file, class, function, ... to the group like @ingroup does. See for example http://svn.wikimedia.org/doc/group__SpecialPage.html where it's impossible to see related files, classes, ... that should belong to that group.
* Added @file to file description, it seems that it should be explicitely decalred for file descriptions, otherwise doxygen will think that the comment document the first class, variabled, function, ... that is in that file.
* Removed some empty comments
* Removed some ?>
Added following groups:
* ExternalStorage
* JobQueue
* MaintenanceLanguage
One more thing: there are still a lot of warnings when generating the doc.
2008-05-20 17:13:28 +00:00
|
|
|
/**
|
2010-08-08 10:44:59 +00:00
|
|
|
* MySQL search engine
|
|
|
|
|
*
|
|
|
|
|
* Copyright (C) 2004 Brion Vibber <brion@pobox.com>
|
2014-03-13 22:23:56 +00:00
|
|
|
* https://www.mediawiki.org/
|
2010-08-08 10:44:59 +00:00
|
|
|
*
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
WARNING: HUGE COMMIT
Doxygen documentation update:
* Changed alls @addtogroup to @ingroup. @addtogroup adds the comment to the group description, but doesn't add the file, class, function, ... to the group like @ingroup does. See for example http://svn.wikimedia.org/doc/group__SpecialPage.html where it's impossible to see related files, classes, ... that should belong to that group.
* Added @file to file description, it seems that it should be explicitely decalred for file descriptions, otherwise doxygen will think that the comment document the first class, variabled, function, ... that is in that file.
* Removed some empty comments
* Removed some ?>
Added following groups:
* ExternalStorage
* JobQueue
* MaintenanceLanguage
One more thing: there are still a lot of warnings when generating the doc.
2008-05-20 17:13:28 +00:00
|
|
|
* @file
|
|
|
|
|
* @ingroup Search
|
|
|
|
|
*/
|
|
|
|
|
|
2018-07-29 12:24:54 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
|
|
|
|
|
2005-05-23 08:42:20 +00:00
|
|
|
/**
|
2021-01-30 22:06:09 +00:00
|
|
|
* Search engine hook for MySQL
|
WARNING: HUGE COMMIT
Doxygen documentation update:
* Changed alls @addtogroup to @ingroup. @addtogroup adds the comment to the group description, but doesn't add the file, class, function, ... to the group like @ingroup does. See for example http://svn.wikimedia.org/doc/group__SpecialPage.html where it's impossible to see related files, classes, ... that should belong to that group.
* Added @file to file description, it seems that it should be explicitely decalred for file descriptions, otherwise doxygen will think that the comment document the first class, variabled, function, ... that is in that file.
* Removed some empty comments
* Removed some ?>
Added following groups:
* ExternalStorage
* JobQueue
* MaintenanceLanguage
One more thing: there are still a lot of warnings when generating the doc.
2008-05-20 17:13:28 +00:00
|
|
|
* @ingroup Search
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2014-01-18 00:43:40 +00:00
|
|
|
class SearchMySQL extends SearchDatabase {
|
2014-05-09 20:32:17 +00:00
|
|
|
protected $strictMatching = true;
|
|
|
|
|
|
|
|
|
|
private static $mMinSearchLength;
|
2008-03-18 23:50:05 +00:00
|
|
|
|
2011-06-02 19:32:45 +00:00
|
|
|
/**
|
2018-05-01 20:44:27 +00:00
|
|
|
* Parse the user's query and transform it into two SQL fragments:
|
|
|
|
|
* a WHERE condition and an ORDER BY expression
|
2011-05-28 19:00:01 +00:00
|
|
|
*
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param string $filteredText
|
|
|
|
|
* @param string $fulltext
|
2011-05-29 14:25:20 +00:00
|
|
|
*
|
2018-05-01 20:44:27 +00:00
|
|
|
* @return array
|
2008-12-22 12:31:15 +00:00
|
|
|
*/
|
2018-05-09 09:56:27 +00:00
|
|
|
private function parseQuery( $filteredText, $fulltext ) {
|
2017-06-29 08:29:13 +00:00
|
|
|
$lc = $this->legalSearchChars( self::CHARS_NO_SYNTAX ); // Minus syntax chars (" and *)
|
2008-03-18 23:50:05 +00:00
|
|
|
$searchon = '';
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->searchTerms = [];
|
2008-03-18 23:50:05 +00:00
|
|
|
|
2011-05-17 22:03:20 +00:00
|
|
|
# @todo FIXME: This doesn't handle parenthetical expressions.
|
2016-02-17 09:09:32 +00:00
|
|
|
$m = [];
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
|
2013-02-03 18:30:03 +00:00
|
|
|
$filteredText, $m, PREG_SET_ORDER ) ) {
|
2013-04-20 15:38:24 +00:00
|
|
|
foreach ( $m as $bits ) {
|
2018-02-10 07:52:26 +00:00
|
|
|
Wikimedia\suppressWarnings();
|
2014-05-09 20:32:17 +00:00
|
|
|
list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
|
2018-02-10 07:52:26 +00:00
|
|
|
Wikimedia\restoreWarnings();
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( $nonQuoted != '' ) {
|
2009-06-24 02:27:51 +00:00
|
|
|
$term = $nonQuoted;
|
|
|
|
|
$quote = '';
|
|
|
|
|
} else {
|
|
|
|
|
$term = str_replace( '"', '', $term );
|
|
|
|
|
$quote = '"';
|
|
|
|
|
}
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( $searchon !== '' ) {
|
|
|
|
|
$searchon .= ' ';
|
|
|
|
|
}
|
|
|
|
|
if ( $this->strictMatching && ( $modifier == '' ) ) {
|
2009-06-24 02:27:51 +00:00
|
|
|
// If we leave this out, boolean op defaults to OR which is rarely helpful.
|
|
|
|
|
$modifier = '+';
|
2008-03-18 23:50:05 +00:00
|
|
|
}
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2009-06-24 02:27:51 +00:00
|
|
|
// Some languages such as Serbian store the input form in the search index,
|
|
|
|
|
// so we may need to search for matches in multiple writing system variants.
|
2018-07-29 12:24:54 +00:00
|
|
|
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
|
|
|
|
|
$convertedVariants = $contLang->autoConvertToAllVariants( $term );
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( is_array( $convertedVariants ) ) {
|
2009-06-24 02:27:51 +00:00
|
|
|
$variants = array_unique( array_values( $convertedVariants ) );
|
|
|
|
|
} else {
|
2016-02-17 09:09:32 +00:00
|
|
|
$variants = [ $term ];
|
2009-06-09 19:19:14 +00:00
|
|
|
}
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2009-06-24 02:27:51 +00:00
|
|
|
// The low-level search index does some processing on input to work
|
|
|
|
|
// around problems with minimum lengths and encoding in MySQL's
|
|
|
|
|
// fulltext engine.
|
|
|
|
|
// For Chinese this also inserts spaces between adjacent Han characters.
|
2018-07-29 12:24:54 +00:00
|
|
|
$strippedVariants = array_map( [ $contLang, 'normalizeForSearch' ], $variants );
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2009-06-24 02:27:51 +00:00
|
|
|
// Some languages such as Chinese force all variants to a canonical
|
|
|
|
|
// form when stripping to the low-level search index, so to be sure
|
|
|
|
|
// let's check our variants list for unique items after stripping.
|
|
|
|
|
$strippedVariants = array_unique( $strippedVariants );
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2009-06-24 02:27:51 +00:00
|
|
|
$searchon .= $modifier;
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( count( $strippedVariants ) > 1 ) {
|
2009-06-24 02:27:51 +00:00
|
|
|
$searchon .= '(';
|
2013-04-20 15:38:24 +00:00
|
|
|
}
|
|
|
|
|
foreach ( $strippedVariants as $stripped ) {
|
2010-01-22 20:36:26 +00:00
|
|
|
$stripped = $this->normalizeText( $stripped );
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( $nonQuoted && strpos( $stripped, ' ' ) !== false ) {
|
2009-06-24 02:27:51 +00:00
|
|
|
// Hack for Chinese: we need to toss in quotes for
|
2010-02-02 15:09:01 +00:00
|
|
|
// multiple-character phrases since normalizeForSearch()
|
2009-06-24 02:27:51 +00:00
|
|
|
// added spaces between them to make word breaks.
|
|
|
|
|
$stripped = '"' . trim( $stripped ) . '"';
|
2008-12-19 01:50:07 +00:00
|
|
|
}
|
2009-06-24 02:27:51 +00:00
|
|
|
$searchon .= "$quote$stripped$quote$wildcard ";
|
2008-03-18 23:50:05 +00:00
|
|
|
}
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( count( $strippedVariants ) > 1 ) {
|
2009-06-24 02:27:51 +00:00
|
|
|
$searchon .= ')';
|
2013-04-20 15:38:24 +00:00
|
|
|
}
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2009-06-24 02:27:51 +00:00
|
|
|
// Match individual terms or quoted phrase in result highlighting...
|
|
|
|
|
// Note that variants will be introduced in a later stage for highlighting!
|
|
|
|
|
$regexp = $this->regexTerm( $term, $wildcard );
|
2008-05-04 15:31:03 +00:00
|
|
|
$this->searchTerms[] = $regexp;
|
2008-03-18 23:50:05 +00:00
|
|
|
}
|
2020-06-01 05:00:39 +00:00
|
|
|
wfDebug( __METHOD__ . ": Would search with '$searchon'" );
|
|
|
|
|
wfDebug( __METHOD__ . ': Match with /' . implode( '|', $this->searchTerms ) . "/" );
|
2008-03-18 23:50:05 +00:00
|
|
|
} else {
|
2020-06-01 05:00:39 +00:00
|
|
|
wfDebug( __METHOD__ . ": Can't understand search query '{$filteredText}'" );
|
2008-03-18 23:50:05 +00:00
|
|
|
}
|
|
|
|
|
|
2019-04-11 04:45:53 +00:00
|
|
|
$dbr = $this->lb->getConnectionRef( DB_REPLICA );
|
|
|
|
|
$searchon = $dbr->addQuotes( $searchon );
|
2008-03-18 23:50:05 +00:00
|
|
|
$field = $this->getIndexField( $fulltext );
|
2018-05-01 20:44:27 +00:00
|
|
|
return [
|
|
|
|
|
" MATCH($field) AGAINST($searchon IN BOOLEAN MODE) ",
|
|
|
|
|
" MATCH($field) AGAINST($searchon IN NATURAL LANGUAGE MODE) DESC "
|
|
|
|
|
];
|
2008-03-18 23:50:05 +00:00
|
|
|
}
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2018-05-09 09:56:27 +00:00
|
|
|
private function regexTerm( $string, $wildcard ) {
|
2009-06-24 02:27:51 +00:00
|
|
|
$regex = preg_quote( $string, '/' );
|
2018-07-29 12:24:54 +00:00
|
|
|
if ( MediaWikiServices::getInstance()->getContentLanguage()->hasWordBreaks() ) {
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( $wildcard ) {
|
2009-06-24 02:27:51 +00:00
|
|
|
// Don't cut off the final bit!
|
|
|
|
|
$regex = "\b$regex";
|
|
|
|
|
} else {
|
|
|
|
|
$regex = "\b$regex\b";
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// For Chinese, words may legitimately abut other words in the text literal.
|
|
|
|
|
// Don't add \b boundary checks... note this could cause false positives
|
2018-10-05 16:37:53 +00:00
|
|
|
// for Latin chars.
|
2009-06-24 02:27:51 +00:00
|
|
|
}
|
|
|
|
|
return $regex;
|
|
|
|
|
}
|
2008-03-18 23:50:05 +00:00
|
|
|
|
2019-04-12 00:54:26 +00:00
|
|
|
public function legalSearchChars( $type = self::CHARS_ALL ) {
|
2017-06-29 08:29:13 +00:00
|
|
|
$searchChars = parent::legalSearchChars( $type );
|
|
|
|
|
if ( $type === self::CHARS_ALL ) {
|
|
|
|
|
// " for phrase, * for wildcard
|
|
|
|
|
$searchChars = "\"*" . $searchChars;
|
|
|
|
|
}
|
|
|
|
|
return $searchChars;
|
2008-03-18 23:50:05 +00:00
|
|
|
}
|
|
|
|
|
|
2005-05-23 08:42:20 +00:00
|
|
|
/**
|
|
|
|
|
* Perform a full text search query and return a result set.
|
|
|
|
|
*
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param string $term Raw search term
|
2019-07-05 20:14:51 +00:00
|
|
|
* @return SqlSearchResultSet|null
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2018-07-10 12:10:09 +00:00
|
|
|
protected function doSearchTextInDB( $term ) {
|
2009-07-28 20:49:01 +00:00
|
|
|
return $this->searchInternal( $term, true );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Perform a title-only search query and return a result set.
|
|
|
|
|
*
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param string $term Raw search term
|
2019-07-05 20:14:51 +00:00
|
|
|
* @return SqlSearchResultSet|null
|
2009-07-28 20:49:01 +00:00
|
|
|
*/
|
2018-07-10 12:10:09 +00:00
|
|
|
protected function doSearchTitleInDB( $term ) {
|
2009-07-28 20:49:01 +00:00
|
|
|
return $this->searchInternal( $term, false );
|
|
|
|
|
}
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2009-07-28 20:49:01 +00:00
|
|
|
protected function searchInternal( $term, $fulltext ) {
|
2011-04-22 15:16:37 +00:00
|
|
|
// This seems out of place, why is this called with empty term?
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( trim( $term ) === '' ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2011-04-22 13:28:03 +00:00
|
|
|
|
2009-07-21 23:20:00 +00:00
|
|
|
$filteredTerm = $this->filter( $term );
|
2011-04-22 13:28:03 +00:00
|
|
|
$query = $this->getQuery( $filteredTerm, $fulltext );
|
2019-04-11 04:45:53 +00:00
|
|
|
$dbr = $this->lb->getConnectionRef( DB_REPLICA );
|
|
|
|
|
$resultSet = $dbr->select(
|
2011-04-22 13:28:03 +00:00
|
|
|
$query['tables'], $query['fields'], $query['conds'],
|
|
|
|
|
__METHOD__, $query['options'], $query['joins']
|
|
|
|
|
);
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2009-07-21 23:20:00 +00:00
|
|
|
$total = null;
|
2014-07-08 06:35:56 +00:00
|
|
|
$query = $this->getCountQuery( $filteredTerm, $fulltext );
|
2019-04-11 04:45:53 +00:00
|
|
|
$totalResult = $dbr->select(
|
2014-07-08 06:35:56 +00:00
|
|
|
$query['tables'], $query['fields'], $query['conds'],
|
|
|
|
|
__METHOD__, $query['options'], $query['joins']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$row = $totalResult->fetchObject();
|
|
|
|
|
if ( $row ) {
|
|
|
|
|
$total = intval( $row->c );
|
2009-07-21 23:20:00 +00:00
|
|
|
}
|
2014-07-08 06:35:56 +00:00
|
|
|
$totalResult->free();
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2014-04-22 07:52:19 +00:00
|
|
|
return new SqlSearchResultSet( $resultSet, $this->searchTerms, $total );
|
2005-05-23 08:42:20 +00:00
|
|
|
}
|
|
|
|
|
|
2011-04-22 16:13:58 +00:00
|
|
|
public function supports( $feature ) {
|
2013-04-26 14:42:31 +00:00
|
|
|
switch ( $feature ) {
|
2017-12-11 03:07:50 +00:00
|
|
|
case 'title-suffix-filter':
|
|
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return parent::supports( $feature );
|
2011-04-22 16:13:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
2006-01-07 13:31:29 +00:00
|
|
|
|
2005-05-23 08:42:20 +00:00
|
|
|
/**
|
2011-04-22 16:13:58 +00:00
|
|
|
* Add special conditions
|
2017-08-11 00:23:16 +00:00
|
|
|
* @param array &$query
|
2011-04-22 16:13:58 +00:00
|
|
|
* @since 1.18
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2011-04-22 16:13:58 +00:00
|
|
|
protected function queryFeatures( &$query ) {
|
|
|
|
|
foreach ( $this->features as $feature => $value ) {
|
2014-03-13 20:39:47 +00:00
|
|
|
if ( $feature === 'title-suffix-filter' && $value ) {
|
2019-04-11 04:45:53 +00:00
|
|
|
$dbr = $this->lb->getConnectionRef( DB_REPLICA );
|
|
|
|
|
$query['conds'][] = 'page_title' . $dbr->buildLike( $dbr->anyString(), $value );
|
2011-04-22 16:13:58 +00:00
|
|
|
}
|
2005-05-23 08:42:20 +00:00
|
|
|
}
|
|
|
|
|
}
|
2006-01-07 13:31:29 +00:00
|
|
|
|
2005-05-23 08:42:20 +00:00
|
|
|
/**
|
2011-04-22 13:28:03 +00:00
|
|
|
* Add namespace conditions
|
2017-08-11 00:23:16 +00:00
|
|
|
* @param array &$query
|
2011-04-22 13:28:03 +00:00
|
|
|
* @since 1.18 (changed)
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2020-05-17 22:39:57 +00:00
|
|
|
private function queryNamespaces( &$query ) {
|
2011-04-22 13:28:03 +00:00
|
|
|
if ( is_array( $this->namespaces ) ) {
|
|
|
|
|
if ( count( $this->namespaces ) === 0 ) {
|
|
|
|
|
$this->namespaces[] = '0';
|
|
|
|
|
}
|
|
|
|
|
$query['conds']['page_namespace'] = $this->namespaces;
|
2005-05-23 08:42:20 +00:00
|
|
|
}
|
|
|
|
|
}
|
2006-01-07 13:31:29 +00:00
|
|
|
|
2005-05-23 08:42:20 +00:00
|
|
|
/**
|
2011-04-22 13:28:03 +00:00
|
|
|
* Add limit options
|
2017-08-11 00:23:16 +00:00
|
|
|
* @param array &$query
|
2011-04-22 13:28:03 +00:00
|
|
|
* @since 1.18
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2011-04-22 13:28:03 +00:00
|
|
|
protected function limitResult( &$query ) {
|
|
|
|
|
$query['options']['LIMIT'] = $this->limit;
|
|
|
|
|
$query['options']['OFFSET'] = $this->offset;
|
2005-05-23 08:42:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2011-04-22 13:28:03 +00:00
|
|
|
* Construct the SQL query to do the search.
|
2005-05-23 08:42:20 +00:00
|
|
|
* The guts shoulds be constructed in queryMain()
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param string $filteredTerm
|
|
|
|
|
* @param bool $fulltext
|
|
|
|
|
* @return array
|
2011-04-22 13:28:03 +00:00
|
|
|
* @since 1.18 (changed)
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2018-05-09 09:56:27 +00:00
|
|
|
private function getQuery( $filteredTerm, $fulltext ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$query = [
|
|
|
|
|
'tables' => [],
|
|
|
|
|
'fields' => [],
|
|
|
|
|
'conds' => [],
|
|
|
|
|
'options' => [],
|
|
|
|
|
'joins' => [],
|
|
|
|
|
];
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2011-04-22 13:28:03 +00:00
|
|
|
$this->queryMain( $query, $filteredTerm, $fulltext );
|
2011-04-22 16:13:58 +00:00
|
|
|
$this->queryFeatures( $query );
|
2011-04-22 13:28:03 +00:00
|
|
|
$this->queryNamespaces( $query );
|
|
|
|
|
$this->limitResult( $query );
|
2011-01-25 19:56:58 +00:00
|
|
|
|
|
|
|
|
return $query;
|
2005-05-23 08:42:20 +00:00
|
|
|
}
|
2011-01-25 19:56:58 +00:00
|
|
|
|
2005-05-23 08:42:20 +00:00
|
|
|
/**
|
|
|
|
|
* Picks which field to index on, depending on what type of query.
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param bool $fulltext
|
|
|
|
|
* @return string
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2018-05-09 09:56:27 +00:00
|
|
|
private function getIndexField( $fulltext ) {
|
2005-05-23 08:42:20 +00:00
|
|
|
return $fulltext ? 'si_text' : 'si_title';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the base part of the search query.
|
|
|
|
|
*
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param array &$query Search query array
|
|
|
|
|
* @param string $filteredTerm
|
|
|
|
|
* @param bool $fulltext
|
2011-04-22 13:28:03 +00:00
|
|
|
* @since 1.18 (changed)
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2018-05-09 09:56:27 +00:00
|
|
|
private function queryMain( &$query, $filteredTerm, $fulltext ) {
|
2005-05-23 08:42:20 +00:00
|
|
|
$match = $this->parseQuery( $filteredTerm, $fulltext );
|
2011-04-22 13:28:03 +00:00
|
|
|
$query['tables'][] = 'page';
|
|
|
|
|
$query['tables'][] = 'searchindex';
|
|
|
|
|
$query['fields'][] = 'page_id';
|
|
|
|
|
$query['fields'][] = 'page_namespace';
|
|
|
|
|
$query['fields'][] = 'page_title';
|
|
|
|
|
$query['conds'][] = 'page_id=si_page';
|
2018-05-01 20:44:27 +00:00
|
|
|
$query['conds'][] = $match[0];
|
|
|
|
|
$query['options']['ORDER BY'] = $match[1];
|
2005-05-23 08:42:20 +00:00
|
|
|
}
|
|
|
|
|
|
2011-04-22 13:28:03 +00:00
|
|
|
/**
|
|
|
|
|
* @since 1.18 (changed)
|
2014-08-14 19:34:55 +00:00
|
|
|
* @param string $filteredTerm
|
|
|
|
|
* @param bool $fulltext
|
2012-02-09 21:36:14 +00:00
|
|
|
* @return array
|
2011-04-22 13:28:03 +00:00
|
|
|
*/
|
2018-05-09 09:56:27 +00:00
|
|
|
private function getCountQuery( $filteredTerm, $fulltext ) {
|
2009-07-21 23:20:00 +00:00
|
|
|
$match = $this->parseQuery( $filteredTerm, $fulltext );
|
2011-01-24 13:59:22 +00:00
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$query = [
|
|
|
|
|
'tables' => [ 'page', 'searchindex' ],
|
|
|
|
|
'fields' => [ 'COUNT(*) as c' ],
|
2018-05-01 20:44:27 +00:00
|
|
|
'conds' => [ 'page_id=si_page', $match[0] ],
|
2016-02-17 09:09:32 +00:00
|
|
|
'options' => [],
|
|
|
|
|
'joins' => [],
|
|
|
|
|
];
|
2011-04-22 13:28:03 +00:00
|
|
|
|
2011-04-22 16:13:58 +00:00
|
|
|
$this->queryFeatures( $query );
|
2011-04-22 13:28:03 +00:00
|
|
|
$this->queryNamespaces( $query );
|
|
|
|
|
|
|
|
|
|
return $query;
|
2009-07-21 23:20:00 +00:00
|
|
|
}
|
|
|
|
|
|
2005-05-23 08:42:20 +00:00
|
|
|
/**
|
|
|
|
|
* Create or update the search index record for the given page.
|
|
|
|
|
* Title and text should be pre-processed.
|
|
|
|
|
*
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param int $id
|
|
|
|
|
* @param string $title
|
|
|
|
|
* @param string $text
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2020-05-17 02:07:58 +00:00
|
|
|
public function update( $id, $title, $text ) {
|
2019-04-11 04:45:53 +00:00
|
|
|
$dbw = $this->lb->getConnectionRef( DB_MASTER );
|
2020-03-24 22:28:16 +00:00
|
|
|
$dbw->replace(
|
|
|
|
|
'searchindex',
|
|
|
|
|
'si_page',
|
2016-02-17 09:09:32 +00:00
|
|
|
[
|
2005-05-23 08:42:20 +00:00
|
|
|
'si_page' => $id,
|
2010-01-22 20:36:26 +00:00
|
|
|
'si_title' => $this->normalizeText( $title ),
|
|
|
|
|
'si_text' => $this->normalizeText( $text )
|
2020-03-24 22:28:16 +00:00
|
|
|
],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
2005-05-23 08:42:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update a search index record's title only.
|
|
|
|
|
* Title should be pre-processed.
|
|
|
|
|
*
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param int $id
|
|
|
|
|
* @param string $title
|
2005-05-23 08:42:20 +00:00
|
|
|
*/
|
2020-05-17 02:07:58 +00:00
|
|
|
public function updateTitle( $id, $title ) {
|
2019-04-11 04:45:53 +00:00
|
|
|
$dbw = $this->lb->getConnectionRef( DB_MASTER );
|
2005-05-24 14:56:07 +00:00
|
|
|
$dbw->update( 'searchindex',
|
2016-02-17 09:09:32 +00:00
|
|
|
[ 'si_title' => $this->normalizeText( $title ) ],
|
|
|
|
|
[ 'si_page' => $id ],
|
2019-04-11 04:45:53 +00:00
|
|
|
__METHOD__
|
|
|
|
|
);
|
2005-05-23 08:42:20 +00:00
|
|
|
}
|
2010-01-22 20:36:26 +00:00
|
|
|
|
2013-06-18 20:06:49 +00:00
|
|
|
/**
|
|
|
|
|
* Delete an indexed page
|
|
|
|
|
* Title should be pre-processed.
|
|
|
|
|
*
|
2014-04-20 21:33:05 +00:00
|
|
|
* @param int $id Page id that was deleted
|
|
|
|
|
* @param string $title Title of page that was deleted
|
2013-06-18 20:06:49 +00:00
|
|
|
*/
|
2020-05-17 22:39:57 +00:00
|
|
|
public function delete( $id, $title ) {
|
2019-04-11 04:45:53 +00:00
|
|
|
$dbw = $this->lb->getConnectionRef( DB_MASTER );
|
2016-02-17 09:09:32 +00:00
|
|
|
$dbw->delete( 'searchindex', [ 'si_page' => $id ], __METHOD__ );
|
2013-06-18 20:06:49 +00:00
|
|
|
}
|
|
|
|
|
|
2010-01-22 20:36:26 +00:00
|
|
|
/**
|
|
|
|
|
* Converts some characters for MySQL's indexing to grok it correctly,
|
|
|
|
|
* and pads short words to overcome limitations.
|
2014-08-14 19:34:55 +00:00
|
|
|
* @param string $string
|
2012-02-09 21:36:14 +00:00
|
|
|
* @return mixed|string
|
2010-01-22 20:36:26 +00:00
|
|
|
*/
|
2020-05-17 22:39:57 +00:00
|
|
|
public function normalizeText( $string ) {
|
2010-03-09 04:19:55 +00:00
|
|
|
$out = parent::normalizeText( $string );
|
2010-01-22 20:36:26 +00:00
|
|
|
|
|
|
|
|
// MySQL fulltext index doesn't grok utf-8, so we
|
|
|
|
|
// need to fold cases and convert to hex
|
|
|
|
|
$out = preg_replace_callback(
|
|
|
|
|
"/([\\xc0-\\xff][\\x80-\\xbf]*)/",
|
2016-02-17 09:09:32 +00:00
|
|
|
[ $this, 'stripForSearchCallback' ],
|
2018-07-29 12:24:54 +00:00
|
|
|
MediaWikiServices::getInstance()->getContentLanguage()->lc( $out ) );
|
2010-01-22 20:36:26 +00:00
|
|
|
|
|
|
|
|
// And to add insult to injury, the default indexing
|
|
|
|
|
// ignores short words... Pad them so we can pass them
|
|
|
|
|
// through without reconfiguring the server...
|
|
|
|
|
$minLength = $this->minSearchLength();
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( $minLength > 1 ) {
|
2010-01-22 20:36:26 +00:00
|
|
|
$n = $minLength - 1;
|
|
|
|
|
$out = preg_replace(
|
|
|
|
|
"/\b(\w{1,$n})\b/",
|
|
|
|
|
"$1u800",
|
|
|
|
|
$out );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Periods within things like hostnames and IP addresses
|
|
|
|
|
// are also important -- we want a search for "example.com"
|
|
|
|
|
// or "192.168.1.1" to work sanely.
|
|
|
|
|
// MySQL's search seems to ignore them, so you'd match on
|
|
|
|
|
// "example.wikipedia.com" and "192.168.83.1" as well.
|
|
|
|
|
$out = preg_replace(
|
|
|
|
|
"/(\w)\.(\w|\*)/u",
|
|
|
|
|
"$1u82e$2",
|
|
|
|
|
$out );
|
|
|
|
|
|
|
|
|
|
return $out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Armor a case-folded UTF-8 string to get through MySQL's
|
|
|
|
|
* fulltext search without being mucked up by funny charset
|
|
|
|
|
* settings or anything else of the sort.
|
2014-08-14 19:34:55 +00:00
|
|
|
* @param array $matches
|
2012-02-09 21:36:14 +00:00
|
|
|
* @return string
|
2010-01-22 20:36:26 +00:00
|
|
|
*/
|
|
|
|
|
protected function stripForSearchCallback( $matches ) {
|
|
|
|
|
return 'u8' . bin2hex( $matches[1] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check MySQL server's ft_min_word_len setting so we know
|
|
|
|
|
* if we need to pad short words...
|
2011-06-02 19:32:45 +00:00
|
|
|
*
|
2010-01-22 20:36:26 +00:00
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
protected function minSearchLength() {
|
2020-01-09 23:48:34 +00:00
|
|
|
if ( self::$mMinSearchLength === null ) {
|
2010-01-22 20:36:26 +00:00
|
|
|
$sql = "SHOW GLOBAL VARIABLES LIKE 'ft\\_min\\_word\\_len'";
|
|
|
|
|
|
2019-04-11 04:45:53 +00:00
|
|
|
$dbr = $this->lb->getConnectionRef( DB_REPLICA );
|
2015-07-12 20:05:37 +00:00
|
|
|
$result = $dbr->query( $sql, __METHOD__ );
|
2010-01-22 20:36:26 +00:00
|
|
|
$row = $result->fetchObject();
|
|
|
|
|
$result->free();
|
|
|
|
|
|
2013-04-20 15:38:24 +00:00
|
|
|
if ( $row && $row->Variable_name == 'ft_min_word_len' ) {
|
2010-01-22 20:36:26 +00:00
|
|
|
self::$mMinSearchLength = intval( $row->Value );
|
|
|
|
|
} else {
|
|
|
|
|
self::$mMinSearchLength = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return self::$mMinSearchLength;
|
|
|
|
|
}
|
2005-05-23 08:42:20 +00:00
|
|
|
}
|