Merge "mw.Feedback: If the message is posted remotely, link the title correctly"
[lhc/web/wiklou.git] / includes / search / SearchExactMatchRescorer.php
1 <?php
2 /**
3 * Rescores results from a prefix search/opensearch to make sure the
4 * exact match is the first result.
5 *
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.
10 *
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.
15 *
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
20 *
21 * @file
22 */
23
24 /**
25 * An utility class to rescore search results by looking for an exact match
26 * in the db and add the page found to the first position.
27 *
28 * NOTE: extracted from TitlePrefixSearch
29 * @ingroup Search
30 */
31 class SearchExactMatchRescorer {
32 /**
33 * Default search backend does proper prefix searching, but custom backends
34 * may sort based on other algorithms that may cause the exact title match
35 * to not be in the results or be lower down the list.
36 * @param string $search the query
37 * @param int[] $namespaces
38 * @param string[] $srchres results
39 * @param int $limit the max number of results to return
40 * @return string[] munged results
41 */
42 public function rescore( $search, $namespaces, $srchres, $limit ) {
43 // Pick namespace (based on PrefixSearch::defaultSearchBackend)
44 $ns = in_array( NS_MAIN, $namespaces ) ? NS_MAIN : reset( $namespaces );
45 $t = Title::newFromText( $search, $ns );
46 if ( !$t || !$t->exists() ) {
47 // No exact match so just return the search results
48 return $srchres;
49 }
50 $string = $t->getPrefixedText();
51 $key = array_search( $string, $srchres );
52 if ( $key !== false ) {
53 // Exact match was in the results so just move it to the front
54 return $this->pullFront( $key, $srchres );
55 }
56 // Exact match not in the search results so check for some redirect handling cases
57 if ( $t->isRedirect() ) {
58 $target = $this->getRedirectTarget( $t );
59 $key = array_search( $target, $srchres );
60 if ( $key !== false ) {
61 // Exact match is a redirect to one of the returned matches so pull the
62 // returned match to the front. This might look odd but the alternative
63 // is to put the redirect in front and drop the match. The name of the
64 // found match is often more descriptive/better formed than the name of
65 // the redirect AND by definition they share a prefix. Hopefully this
66 // choice is less confusing and more helpful. But it might not be. But
67 // it is the choice we're going with for now.
68 return $this->pullFront( $key, $srchres );
69 }
70 $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres );
71 if ( isset( $redirectTargetsToRedirect[$target] ) ) {
72 // The exact match and something in the results list are both redirects
73 // to the same thing! In this case we'll pull the returned match to the
74 // top following the same logic above. Again, it might not be a perfect
75 // choice but it'll do.
76 return $this->pullFront( $redirectTargetsToRedirect[$target], $srchres );
77 }
78 } else {
79 $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres );
80 if ( isset( $redirectTargetsToRedirect[$string] ) ) {
81 // The exact match is the target of a redirect already in the results list so remove
82 // the redirect from the results list and push the exact match to the front
83 array_splice( $srchres, $redirectTargetsToRedirect[$string], 1 );
84 array_unshift( $srchres, $string );
85 return $srchres;
86 }
87 }
88
89 // Exact match is totally unique from the other results so just add it to the front
90 array_unshift( $srchres, $string );
91 // And roll one off the end if the results are too long
92 if ( count( $srchres ) > $limit ) {
93 array_pop( $srchres );
94 }
95 return $srchres;
96 }
97
98 /**
99 * @param string[] $titles
100 * @return array redirect target prefixedText to index of title in titles
101 * that is a redirect to it.
102 */
103 private function redirectTargetsToRedirect( array $titles ) {
104 $result = [];
105 foreach ( $titles as $key => $titleText ) {
106 $title = Title::newFromText( $titleText );
107 if ( !$title || !$title->isRedirect() ) {
108 continue;
109 }
110 $target = $this->getRedirectTarget( $title );
111 if ( !$target ) {
112 continue;
113 }
114 $result[$target] = $key;
115 }
116 return $result;
117 }
118
119 /**
120 * Returns an array where the element of $array at index $key becomes
121 * the first element.
122 * @param int $key key to pull to the front
123 * @return array $array with the item at $key pulled to the front
124 */
125 private function pullFront( $key, array $array ) {
126 $cut = array_splice( $array, $key, 1 );
127 array_unshift( $array, $cut[0] );
128 return $array;
129 }
130
131 /**
132 * Get a redirect's destination from a title
133 * @param Title $title A title to redirect. It may not redirect or even exist
134 * @return null|string If title exists and redirects, get the destination's prefixed name
135 */
136 private function getRedirectTarget( $title ) {
137 $page = WikiPage::factory( $title );
138 if ( !$page->exists() ) {
139 return null;
140 }
141 $redir = $page->getRedirectTarget();
142 return $redir ? $redir->getPrefixedText() : null;
143 }
144 }