Merge "Bump wikimedia/parsoid to 0.21.0-a11"
[mediawiki.git] / includes / search / SearchSuggestionSet.php
blobefeb7124b637fd6d29921e916546fcfb93453bd3
1 <?php
3 /**
4 * Search suggestion sets
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
22 use MediaWiki\Title\Title;
24 /**
25 * A set of search suggestions.
26 * The set is always ordered by score, with the best match first.
28 class SearchSuggestionSet {
29 /**
30 * @var SearchSuggestion[]
32 private $suggestions = [];
34 /**
35 * @var array
37 private $pageMap = [];
39 /**
40 * @var bool Are more results available?
42 private $hasMoreResults;
44 /**
45 * Builds a new set of suggestions.
47 * NOTE: the array should be sorted by score (higher is better),
48 * in descending order.
49 * SearchSuggestionSet will not try to re-order this input array.
50 * Providing an unsorted input array is a mistake and will lead to
51 * unexpected behaviors.
53 * @param SearchSuggestion[] $suggestions (must be sorted by score)
54 * @param bool $hasMoreResults Are more results available?
56 public function __construct( array $suggestions, $hasMoreResults = false ) {
57 $this->hasMoreResults = $hasMoreResults;
58 foreach ( $suggestions as $suggestion ) {
59 $pageID = $suggestion->getSuggestedTitleID();
60 if ( $pageID && empty( $this->pageMap[$pageID] ) ) {
61 $this->pageMap[$pageID] = true;
63 $this->suggestions[] = $suggestion;
67 /**
68 * @return bool Are more results available?
70 public function hasMoreResults() {
71 return $this->hasMoreResults;
74 /**
75 * Get the list of suggestions.
76 * @return SearchSuggestion[]
78 public function getSuggestions() {
79 return $this->suggestions;
82 /**
83 * Call array_map on the suggestions array
84 * @param callable $callback
85 * @return array
87 public function map( $callback ) {
88 return array_map( $callback, $this->suggestions );
91 /**
92 * Filter the suggestions array
93 * @param callable $callback Callable accepting single SearchSuggestion
94 * instance returning bool false to remove the item.
95 * @return int The number of suggestions removed
97 public function filter( $callback ) {
98 $before = count( $this->suggestions );
99 $this->suggestions = array_values( array_filter( $this->suggestions, $callback ) );
100 return $before - count( $this->suggestions );
104 * Add a new suggestion at the end.
105 * If the score of the new suggestion is greater than the worst one,
106 * the new suggestion score will be updated (worst - 1).
108 * @param SearchSuggestion $suggestion
110 public function append( SearchSuggestion $suggestion ) {
111 $pageID = $suggestion->getSuggestedTitleID();
112 if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
113 return;
115 if ( $this->getSize() > 0 && $suggestion->getScore() >= $this->getWorstScore() ) {
116 $suggestion->setScore( $this->getWorstScore() - 1 );
118 $this->suggestions[] = $suggestion;
119 if ( $pageID ) {
120 $this->pageMap[$pageID] = true;
125 * Add suggestion set to the end of the current one.
126 * @param SearchSuggestionSet $set
128 public function appendAll( SearchSuggestionSet $set ) {
129 foreach ( $set->getSuggestions() as $sugg ) {
130 $this->append( $sugg );
135 * Move the suggestion at index $key to the first position
136 * @param int $key
138 public function rescore( $key ) {
139 $removed = array_splice( $this->suggestions, $key, 1 );
140 unset( $this->pageMap[$removed[0]->getSuggestedTitleID()] );
141 $this->prepend( $removed[0] );
145 * Add a new suggestion at the top. If the new suggestion score
146 * is lower than the best one its score will be updated (best + 1)
147 * @param SearchSuggestion $suggestion
149 public function prepend( SearchSuggestion $suggestion ) {
150 $pageID = $suggestion->getSuggestedTitleID();
151 if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
152 return;
154 if ( $this->getSize() > 0 && $suggestion->getScore() <= $this->getBestScore() ) {
155 $suggestion->setScore( $this->getBestScore() + 1 );
157 array_unshift( $this->suggestions, $suggestion );
158 if ( $pageID ) {
159 $this->pageMap[$pageID] = true;
164 * Remove a suggestion from the set.
165 * Removes the first suggestion that has the same article id or the same suggestion text.
166 * @param SearchSuggestion $suggestion
167 * @return bool true if something was removed
169 public function remove( SearchSuggestion $suggestion ): bool {
170 foreach ( $this->suggestions as $k => $s ) {
171 $titleId = $s->getSuggestedTitleID();
172 if ( ( $titleId != null && $titleId === $suggestion->getSuggestedTitleID() )
173 || $s->getText() === $suggestion->getText()
175 array_splice( $this->suggestions, $k, 1 );
176 unset( $this->pageMap[$s->getSuggestedTitleID()] );
177 return true;
180 return false;
184 * @return float the best score in this suggestion set
186 public function getBestScore() {
187 if ( !$this->suggestions ) {
188 return 0;
190 return $this->suggestions[0]->getScore();
194 * @return float the worst score in this set
196 public function getWorstScore() {
197 if ( !$this->suggestions ) {
198 return 0;
200 return end( $this->suggestions )->getScore();
204 * @return int the number of suggestion in this set
206 public function getSize() {
207 return count( $this->suggestions );
211 * Remove any extra elements in the suggestions set
212 * @param int $limit the max size of this set.
214 public function shrink( $limit ) {
215 if ( count( $this->suggestions ) > $limit ) {
216 $this->suggestions = array_slice( $this->suggestions, 0, $limit );
217 $this->pageMap = self::buildPageMap( $this->suggestions );
218 $this->hasMoreResults = true;
223 * Build an array of true with the page ids present in $suggestion as keys.
225 * @param array $suggestions
226 * @return array<int,bool>
228 private static function buildPageMap( array $suggestions ): array {
229 $pageMap = [];
230 foreach ( $suggestions as $suggestion ) {
231 $pageID = $suggestion->getSuggestedTitleID();
232 if ( $pageID ) {
233 $pageMap[$pageID] = true;
236 return $pageMap;
240 * Builds a new set of suggestion based on a title array.
241 * Useful when using a backend that supports only Titles.
243 * NOTE: Suggestion scores will be generated.
245 * @param Title[] $titles
246 * @param bool $hasMoreResults Are more results available?
247 * @return SearchSuggestionSet
249 public static function fromTitles( array $titles, $hasMoreResults = false ) {
250 $score = count( $titles );
251 $suggestions = array_map( static function ( $title ) use ( &$score ) {
252 return SearchSuggestion::fromTitle( $score--, $title );
253 }, $titles );
254 return new SearchSuggestionSet( $suggestions, $hasMoreResults );
258 * Builds a new set of suggestion based on a string array.
260 * NOTE: Suggestion scores will be generated.
262 * @param string[] $titles
263 * @param bool $hasMoreResults Are more results available?
264 * @return SearchSuggestionSet
266 public static function fromStrings( array $titles, $hasMoreResults = false ) {
267 $score = count( $titles );
268 $suggestions = array_map( static function ( $title ) use ( &$score ) {
269 return SearchSuggestion::fromText( $score--, $title );
270 }, $titles );
271 return new SearchSuggestionSet( $suggestions, $hasMoreResults );
275 * @return SearchSuggestionSet an empty suggestion set
277 public static function emptySuggestionSet() {
278 return new SearchSuggestionSet( [] );