3 * Rescores results from a prefix search/opensearch to make sure the
4 * exact match is the first result.
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
24 use MediaWiki\MediaWikiServices
;
25 use MediaWiki\Page\PageIdentity
;
26 use MediaWiki\Title\Title
;
29 * An utility class to rescore search results by looking for an exact match
30 * in the db and add the page found to the first position.
32 * NOTE: extracted from TitlePrefixSearch
35 class SearchExactMatchRescorer
{
37 * @var ?string set when a redirect returned from the engine is replaced by the exact match
39 private ?
string $replacedRedirect;
42 * Default search backend does proper prefix searching, but custom backends
43 * may sort based on other algorithms that may cause the exact title match
44 * to not be in the results or be lower down the list.
45 * @param string $search the query
46 * @param int[] $namespaces
47 * @param string[] $srchres results
48 * @param int $limit the max number of results to return
49 * @return string[] munged results
51 public function rescore( $search, $namespaces, $srchres, $limit ) {
52 $this->replacedRedirect
= null;
53 // Pick namespace (based on PrefixSearch::defaultSearchBackend)
54 $ns = in_array( NS_MAIN
, $namespaces ) ? NS_MAIN
: reset( $namespaces );
55 $t = Title
::newFromText( $search, $ns );
56 if ( !$t ||
!$t->exists() ) {
57 // No exact match so just return the search results
60 $string = $t->getPrefixedText();
61 $key = array_search( $string, $srchres );
62 if ( $key !== false ) {
63 // Exact match was in the results so just move it to the front
64 return $this->pullFront( $key, $srchres );
66 // Exact match not in the search results so check for some redirect handling cases
67 if ( $t->isRedirect() ) {
68 $target = $this->getRedirectTarget( $t );
69 $key = array_search( $target, $srchres );
70 if ( $key !== false ) {
71 // Exact match is a redirect to one of the returned matches so pull the
72 // returned match to the front. This might look odd but the alternative
73 // is to put the redirect in front and drop the match. The name of the
74 // found match is often more descriptive/better formed than the name of
75 // the redirect AND by definition they share a prefix. Hopefully this
76 // choice is less confusing and more helpful. But it might not be. But
77 // it is the choice we're going with for now.
78 return $this->pullFront( $key, $srchres );
80 $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres );
81 if ( isset( $redirectTargetsToRedirect[$target] ) ) {
82 // The exact match and something in the results list are both redirects
83 // to the same thing! In this case we prefer the match the user typed.
84 $this->replacedRedirect
= array_splice( $srchres, $redirectTargetsToRedirect[$target], 1 )[0];
85 array_unshift( $srchres, $string );
89 $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres );
90 if ( isset( $redirectTargetsToRedirect[$string] ) ) {
91 // The exact match is the target of a redirect already in the results list so remove
92 // the redirect from the results list and push the exact match to the front
93 array_splice( $srchres, $redirectTargetsToRedirect[$string], 1 );
94 array_unshift( $srchres, $string );
99 // Exact match is totally unique from the other results so just add it to the front
100 array_unshift( $srchres, $string );
101 // And roll one off the end if the results are too long
102 if ( count( $srchres ) > $limit ) {
103 array_pop( $srchres );
109 * Redirect initially returned by the search engine that got replaced by a better match:
110 * - exact match to a redirect to the same page
111 * - exact match to the target page
112 * @return string|null the replaced redirect or null if nothing was replaced
114 public function getReplacedRedirect(): ?
string {
115 return $this->replacedRedirect
;
119 * @param string[] $titles
120 * @return array redirect target prefixedText to index of title in titles
121 * that is a redirect to it.
123 private function redirectTargetsToRedirect( array $titles ) {
125 foreach ( $titles as $key => $titleText ) {
126 $title = Title
::newFromText( $titleText );
127 if ( !$title ||
!$title->isRedirect() ) {
130 $target = $this->getRedirectTarget( $title );
134 $result[$target] = $key;
140 * Returns an array where the element of $array at index $key becomes
142 * @param int $key key to pull to the front
143 * @param array $array
144 * @return array $array with the item at $key pulled to the front
146 private function pullFront( $key, array $array ) {
147 $cut = array_splice( $array, $key, 1 );
148 array_unshift( $array, $cut[0] );
153 * Get a redirect's destination from a title
154 * @param PageIdentity $page A page to redirect. It may not redirect or even exist
155 * @return null|string If title exists and redirects, get the destination's prefixed name
157 private function getRedirectTarget( PageIdentity
$page ) {
158 $redirectStore = MediaWikiServices
::getInstance()->getRedirectStore();
159 $redir = $redirectStore->getRedirectTarget( $page );
161 // Needed to get the text needed for display.
162 $redir = Title
::castFromLinkTarget( $redir );
163 return $redir ?
$redir->getPrefixedText() : null;