Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
49.28% covered (danger)
49.28%
34 / 69
22.22% covered (danger)
22.22%
4 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchSuggestionSet
49.28% covered (danger)
49.28%
34 / 69
22.22% covered (danger)
22.22%
4 / 18
260.39
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 hasMoreResults
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSuggestions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 map
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 filter
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 append
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
6.56
 appendAll
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 rescore
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 prepend
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
6.56
 remove
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 getBestScore
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getWorstScore
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 shrink
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 buildPageMap
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 fromTitles
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 fromStrings
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 emptySuggestionSet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Search suggestion sets
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
22use MediaWiki\Title\Title;
23
24/**
25 * A set of search suggestions.
26 * The set is always ordered by score, with the best match first.
27 */
28class SearchSuggestionSet {
29    /**
30     * @var SearchSuggestion[]
31     */
32    private $suggestions = [];
33
34    /**
35     * @var array
36     */
37    private $pageMap = [];
38
39    /**
40     * @var bool Are more results available?
41     */
42    private $hasMoreResults;
43
44    /**
45     * Builds a new set of suggestions.
46     *
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.
52     *
53     * @param SearchSuggestion[] $suggestions (must be sorted by score)
54     * @param bool $hasMoreResults Are more results available?
55     */
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;
62            }
63            $this->suggestions[] = $suggestion;
64        }
65    }
66
67    /**
68     * @return bool Are more results available?
69     */
70    public function hasMoreResults() {
71        return $this->hasMoreResults;
72    }
73
74    /**
75     * Get the list of suggestions.
76     * @return SearchSuggestion[]
77     */
78    public function getSuggestions() {
79        return $this->suggestions;
80    }
81
82    /**
83     * Call array_map on the suggestions array
84     * @param callable $callback
85     * @return array
86     */
87    public function map( $callback ) {
88        return array_map( $callback, $this->suggestions );
89    }
90
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
96     */
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 );
101    }
102
103    /**
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).
107     */
108    public function append( SearchSuggestion $suggestion ) {
109        $pageID = $suggestion->getSuggestedTitleID();
110        if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
111            return;
112        }
113        if ( $this->getSize() > 0 && $suggestion->getScore() >= $this->getWorstScore() ) {
114            $suggestion->setScore( $this->getWorstScore() - 1 );
115        }
116        $this->suggestions[] = $suggestion;
117        if ( $pageID ) {
118            $this->pageMap[$pageID] = true;
119        }
120    }
121
122    /**
123     * Add suggestion set to the end of the current one.
124     */
125    public function appendAll( SearchSuggestionSet $set ) {
126        foreach ( $set->getSuggestions() as $sugg ) {
127            $this->append( $sugg );
128        }
129    }
130
131    /**
132     * Move the suggestion at index $key to the first position
133     * @param int $key
134     */
135    public function rescore( $key ) {
136        $removed = array_splice( $this->suggestions, $key, 1 );
137        unset( $this->pageMap[$removed[0]->getSuggestedTitleID()] );
138        $this->prepend( $removed[0] );
139    }
140
141    /**
142     * Add a new suggestion at the top. If the new suggestion score
143     * is lower than the best one its score will be updated (best + 1)
144     */
145    public function prepend( SearchSuggestion $suggestion ) {
146        $pageID = $suggestion->getSuggestedTitleID();
147        if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
148            return;
149        }
150        if ( $this->getSize() > 0 && $suggestion->getScore() <= $this->getBestScore() ) {
151            $suggestion->setScore( $this->getBestScore() + 1 );
152        }
153        array_unshift( $this->suggestions, $suggestion );
154        if ( $pageID ) {
155            $this->pageMap[$pageID] = true;
156        }
157    }
158
159    /**
160     * Remove a suggestion from the set.
161     * Removes the first suggestion that has the same article id or the same suggestion text.
162     * @param SearchSuggestion $suggestion
163     * @return bool true if something was removed
164     */
165    public function remove( SearchSuggestion $suggestion ): bool {
166        foreach ( $this->suggestions as $k => $s ) {
167            $titleId = $s->getSuggestedTitleID();
168            if ( ( $titleId != null && $titleId === $suggestion->getSuggestedTitleID() )
169                || $s->getText() === $suggestion->getText()
170            ) {
171                array_splice( $this->suggestions, $k, 1 );
172                unset( $this->pageMap[$s->getSuggestedTitleID()] );
173                return true;
174            }
175        }
176        return false;
177    }
178
179    /**
180     * @return float the best score in this suggestion set
181     */
182    public function getBestScore() {
183        if ( !$this->suggestions ) {
184            return 0;
185        }
186        return $this->suggestions[0]->getScore();
187    }
188
189    /**
190     * @return float the worst score in this set
191     */
192    public function getWorstScore() {
193        if ( !$this->suggestions ) {
194            return 0;
195        }
196        return end( $this->suggestions )->getScore();
197    }
198
199    /**
200     * @return int the number of suggestion in this set
201     */
202    public function getSize() {
203        return count( $this->suggestions );
204    }
205
206    /**
207     * Remove any extra elements in the suggestions set
208     * @param int $limit the max size of this set.
209     */
210    public function shrink( $limit ) {
211        if ( count( $this->suggestions ) > $limit ) {
212            $this->suggestions = array_slice( $this->suggestions, 0, $limit );
213            $this->pageMap = self::buildPageMap( $this->suggestions );
214            $this->hasMoreResults = true;
215        }
216    }
217
218    /**
219     * Build an array of true with the page ids present in $suggestion as keys.
220     *
221     * @param array $suggestions
222     * @return array<int,bool>
223     */
224    private static function buildPageMap( array $suggestions ): array {
225        $pageMap = [];
226        foreach ( $suggestions as $suggestion ) {
227            $pageID = $suggestion->getSuggestedTitleID();
228            if ( $pageID ) {
229                $pageMap[$pageID] = true;
230            }
231        }
232        return $pageMap;
233    }
234
235    /**
236     * Builds a new set of suggestion based on a title array.
237     * Useful when using a backend that supports only Titles.
238     *
239     * NOTE: Suggestion scores will be generated.
240     *
241     * @param Title[] $titles
242     * @param bool $hasMoreResults Are more results available?
243     * @return SearchSuggestionSet
244     */
245    public static function fromTitles( array $titles, $hasMoreResults = false ) {
246        $score = count( $titles );
247        $suggestions = array_map( static function ( $title ) use ( &$score ) {
248            return SearchSuggestion::fromTitle( $score--, $title );
249        }, $titles );
250        return new SearchSuggestionSet( $suggestions, $hasMoreResults );
251    }
252
253    /**
254     * Builds a new set of suggestion based on a string array.
255     *
256     * NOTE: Suggestion scores will be generated.
257     *
258     * @param string[] $titles
259     * @param bool $hasMoreResults Are more results available?
260     * @return SearchSuggestionSet
261     */
262    public static function fromStrings( array $titles, $hasMoreResults = false ) {
263        $score = count( $titles );
264        $suggestions = array_map( static function ( $title ) use ( &$score ) {
265            return SearchSuggestion::fromText( $score--, $title );
266        }, $titles );
267        return new SearchSuggestionSet( $suggestions, $hasMoreResults );
268    }
269
270    /**
271     * @return SearchSuggestionSet an empty suggestion set
272     */
273    public static function emptySuggestionSet() {
274        return new SearchSuggestionSet( [] );
275    }
276}