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     * @param SearchSuggestion $suggestion
109     */
110    public function append( SearchSuggestion $suggestion ) {
111        $pageID = $suggestion->getSuggestedTitleID();
112        if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
113            return;
114        }
115        if ( $this->getSize() > 0 && $suggestion->getScore() >= $this->getWorstScore() ) {
116            $suggestion->setScore( $this->getWorstScore() - 1 );
117        }
118        $this->suggestions[] = $suggestion;
119        if ( $pageID ) {
120            $this->pageMap[$pageID] = true;
121        }
122    }
123
124    /**
125     * Add suggestion set to the end of the current one.
126     * @param SearchSuggestionSet $set
127     */
128    public function appendAll( SearchSuggestionSet $set ) {
129        foreach ( $set->getSuggestions() as $sugg ) {
130            $this->append( $sugg );
131        }
132    }
133
134    /**
135     * Move the suggestion at index $key to the first position
136     * @param int $key
137     */
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] );
142    }
143
144    /**
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
148     */
149    public function prepend( SearchSuggestion $suggestion ) {
150        $pageID = $suggestion->getSuggestedTitleID();
151        if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
152            return;
153        }
154        if ( $this->getSize() > 0 && $suggestion->getScore() <= $this->getBestScore() ) {
155            $suggestion->setScore( $this->getBestScore() + 1 );
156        }
157        array_unshift( $this->suggestions, $suggestion );
158        if ( $pageID ) {
159            $this->pageMap[$pageID] = true;
160        }
161    }
162
163    /**
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
168     */
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()
174            ) {
175                array_splice( $this->suggestions, $k, 1 );
176                unset( $this->pageMap[$s->getSuggestedTitleID()] );
177                return true;
178            }
179        }
180        return false;
181    }
182
183    /**
184     * @return float the best score in this suggestion set
185     */
186    public function getBestScore() {
187        if ( !$this->suggestions ) {
188            return 0;
189        }
190        return $this->suggestions[0]->getScore();
191    }
192
193    /**
194     * @return float the worst score in this set
195     */
196    public function getWorstScore() {
197        if ( !$this->suggestions ) {
198            return 0;
199        }
200        return end( $this->suggestions )->getScore();
201    }
202
203    /**
204     * @return int the number of suggestion in this set
205     */
206    public function getSize() {
207        return count( $this->suggestions );
208    }
209
210    /**
211     * Remove any extra elements in the suggestions set
212     * @param int $limit the max size of this set.
213     */
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;
219        }
220    }
221
222    /**
223     * Build an array of true with the page ids present in $suggestion as keys.
224     *
225     * @param array $suggestions
226     * @return array<int,bool>
227     */
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;
234            }
235        }
236        return $pageMap;
237    }
238
239    /**
240     * Builds a new set of suggestion based on a title array.
241     * Useful when using a backend that supports only Titles.
242     *
243     * NOTE: Suggestion scores will be generated.
244     *
245     * @param Title[] $titles
246     * @param bool $hasMoreResults Are more results available?
247     * @return SearchSuggestionSet
248     */
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 );
255    }
256
257    /**
258     * Builds a new set of suggestion based on a string array.
259     *
260     * NOTE: Suggestion scores will be generated.
261     *
262     * @param string[] $titles
263     * @param bool $hasMoreResults Are more results available?
264     * @return SearchSuggestionSet
265     */
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 );
272    }
273
274    /**
275     * @return SearchSuggestionSet an empty suggestion set
276     */
277    public static function emptySuggestionSet() {
278        return new SearchSuggestionSet( [] );
279    }
280}