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