Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.61% covered (warning)
82.61%
57 / 69
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryZObjectLabels
82.61% covered (warning)
82.61%
57 / 69
0.00% covered (danger)
0.00%
0 / 1
20.90
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 run
82.61% covered (warning)
82.61%
57 / 69
0.00% covered (danger)
0.00%
0 / 1
17.35
 getAllowedParams
n/a
0 / 0
n/a
0 / 0
1
 getExamplesMessages
n/a
0 / 0
n/a
0 / 0
1
1<?php
2/**
3 * WikiLambda ZObject labels helper for the query API
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\WikiLambda\ActionAPI;
12
13use MediaWiki\Api\ApiBase;
14use MediaWiki\Api\ApiQuery;
15use MediaWiki\Extension\WikiLambda\HttpStatus;
16use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
17use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
18use MediaWiki\Extension\WikiLambda\ZErrorFactory;
19use MediaWiki\Extension\WikiLambda\ZObjectUtils;
20use MediaWiki\MediaWikiServices;
21use MediaWiki\Title\Title;
22use Wikimedia\ParamValidator\ParamValidator;
23use Wikimedia\ParamValidator\TypeDef\IntegerDef;
24
25class ApiQueryZObjectLabels extends WikiLambdaApiQueryGeneratorBase {
26
27    /**
28     * @codeCoverageIgnore
29     */
30    public function __construct( ApiQuery $query, string $moduleName ) {
31        parent::__construct( $query, $moduleName, 'wikilambdasearch_' );
32    }
33
34    /**
35     * @inheritDoc
36     */
37    protected function run( $resultPageSet = null ) {
38        // Exit if we're running in non-repo mode (e.g. on a client wiki)
39        if ( !$this->getConfig()->get( 'WikiLambdaEnableRepoMode' ) ) {
40            WikiLambdaApiBase::dieWithZError(
41                ZErrorFactory::createZErrorInstance(
42                    ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_RUN,
43                    []
44                ),
45                HttpStatus::BAD_REQUEST
46            );
47        }
48
49        [
50            'search' => $searchTerm,
51            'exact' => $exact,
52            'language' => $language,
53            'type' => $types,
54            'return_type' => $returnTypes,
55            'limit' => $limit,
56            'continue' => $continue,
57        ] = $this->extractRequestParams();
58
59        // TODO (T348545): We can reduce this control limit to 100 when we have
60        // have a system to return results already pre-ranked from the DB.
61        $controlLimit = 5000;
62
63        $zObjectStore = WikiLambdaServices::getZObjectStore();
64        $res = $zObjectStore->searchZObjectLabels(
65            $searchTerm,
66            $exact,
67            [],
68            $types ?? [],
69            $returnTypes ?? [],
70            null,
71            $controlLimit
72        );
73
74        // 1. Set match_rate for every entry and eliminate duplicates with lower match rates
75        // TODO (T349583): Improve this result sorting algorithm; e.g. should we prioritize matches with primary labels?
76        $matches = [];
77        $hasSearchTerm = ( $searchTerm !== '' );
78        $matchField = ZObjectUtils::isValidZObjectReference( $searchTerm ) ? 'wlzl_zobject_zid' : 'wlzl_label';
79
80        foreach ( $res as $row ) {
81            $matchRate = $hasSearchTerm ? self::getMatchRate( $searchTerm, $row->{ $matchField }, $exact ) : 0;
82
83            // If the current row is new or a better match, keep. Else, ignore.
84            if ( !array_key_exists( $row->wlzl_zobject_zid, $matches ) ||
85                ( $matches[ $row->wlzl_zobject_zid ][ 'match_rate' ] < $matchRate ) ) {
86                $matches[ $row->wlzl_zobject_zid ] = [
87                    // TODO (T338248): Implement, otherwise the generator won't work.
88                    'page_id' => 0,
89                    // TODO (T258915): When we support redirects, implement.
90                    'page_is_redirect' => false,
91                    'page_namespace' => NS_MAIN,
92                    'page_content_model' => CONTENT_MODEL_ZOBJECT,
93                    'page_title' => $row->wlzl_zobject_zid,
94                    'page_type' => $row->wlzl_type,
95                    'match_label' => $hasSearchTerm ? $row->{ $matchField } : null,
96                    'match_is_primary' => $hasSearchTerm ? $row->wlzl_label_primary : null,
97                    'match_lang' => $hasSearchTerm ? $row->wlzl_language : null,
98                    'match_rate' => $matchRate,
99                    // Labels in the user language will be set after selecting the page
100                    'label' => null,
101                    'type_label' => null,
102                ];
103            }
104        }
105
106        // 2. Sort all results by match_rate to get best hits
107        usort( $matches, static function ( $a, $b ) {
108            return $b[ 'match_rate' ] <=> $a[ 'match_rate' ];
109        } );
110
111        // 3. Prune the result set to the limit, slice to requested page, and set continue
112        $continue = $continue === null ? 0 : intval( $continue );
113        $hits = array_slice( $matches, $continue * $limit, $limit );
114        $pageSize = count( $matches ) - ( $continue * $limit );
115        if ( $pageSize > $limit ) {
116            $this->setContinueEnumParameter( 'continue', strval( $continue + 1 ) );
117        }
118
119        // 4. Add relevant user language labels to each hit: This will be the main
120        // name shown in the selector, while the match_label set above will be used
121        // as supporting text when the search text has matched an alias or a label in
122        // a different language.
123        foreach ( $hits as $index => $hit ) {
124            $hits[ $index ][ 'label' ] = $zObjectStore->fetchZObjectLabel( $hit[ 'page_title' ], $language );
125            $hits[ $index ][ 'type_label' ] = $zObjectStore->fetchZObjectLabel( $hit[ 'page_type' ], $language );
126        }
127
128        if ( $resultPageSet ) {
129            // TODO (T362192): This needs to be an IResultWrapper, not an array of assoc. objects, irritatingly.
130            // $resultPageSet->populateFromQueryResult( $dbr, $hits );
131            foreach ( $hits as $index => $entry ) {
132                $resultPageSet->setGeneratorData(
133                    Title::makeTitle( $entry['page_namespace'], $entry['page_title'] ),
134                    [ 'index' => $index + $continue + 1 ]
135                );
136            }
137        } else {
138            $result = $this->getResult();
139            foreach ( $hits as $entry ) {
140                $result->addValue( [ 'query', $this->getModuleName() ], null, $entry );
141            }
142        }
143    }
144
145    /**
146     * @inheritDoc
147     * @codeCoverageIgnore
148     */
149    protected function getAllowedParams(): array {
150        return [
151            'search' => [
152                ParamValidator::PARAM_TYPE => 'string',
153                ParamValidator::PARAM_DEFAULT => '',
154            ],
155            'language' => [
156                ParamValidator::PARAM_TYPE => array_keys(
157                    // TODO (T330033): Consider injecting this service rather than just fetching from main
158                    MediaWikiServices::getInstance()->getLanguageNameUtils()->getLanguageNames()
159                ),
160                ParamValidator::PARAM_REQUIRED => true,
161            ],
162            // This is the wrong way around logically, but MediaWiki's Action API doesn't allow for
163            // default-true boolean flags to ever be set false.
164            'nofallback' => [
165                ParamValidator::PARAM_TYPE => 'boolean',
166                ParamValidator::PARAM_DEFAULT => false,
167            ],
168            'exact' => [
169                ParamValidator::PARAM_TYPE => 'boolean',
170                ParamValidator::PARAM_DEFAULT => false,
171            ],
172            'type' => [
173                ParamValidator::PARAM_TYPE => 'string',
174                ParamValidator::PARAM_ISMULTI => true,
175            ],
176            'return_type' => [
177                ParamValidator::PARAM_TYPE => 'string',
178                ParamValidator::PARAM_ISMULTI => true,
179            ],
180            'limit' => [
181                ParamValidator::PARAM_TYPE => 'limit',
182                ParamValidator::PARAM_DEFAULT => 10,
183                IntegerDef::PARAM_MIN => 1,
184                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
185                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
186            ],
187            'continue' => [
188                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
189            ],
190        ];
191    }
192
193    /**
194     * @see ApiBase::getExamplesMessages()
195     * @return array
196     * @codeCoverageIgnore
197     */
198    protected function getExamplesMessages() {
199        return [
200            // search "foo" in language "en"
201            'action=query&list=wikilambdasearch_labels&'
202            . ' wikilambdasearch_search=foo&'
203            . ' wikilambdasearch_language=en' => 'apihelp-query+wikilambda-example-simple',
204            // search "foo" in language "fr" without fallbacks
205            'action=query&list=wikilambdasearch_labels&'
206            . 'wikilambdasearch_search=foo&'
207            . 'wikilambdasearch_language=fr&'
208            . 'wikilambdasearch_nofallback=true' => 'apihelp-query+wikilambda-example-nofallback',
209            // Search for objects of type "Z4"
210            'action=query&list=wikilambdasearch_labels&'
211            . 'wikilambdasearch_type=Z4&'
212            . 'wikilambdasearch_language=en' => 'apihelp-query+wikilambda-example-type',
213            // Search for objects that resolve to "Z40"
214            'action=query&list=wikilambdasearch_labels&'
215            . 'wikilambdasearch_return_type=Z40&'
216            . 'wikilambdasearch_language=en' => 'apihelp-query+wikilambda-example-return-type',
217            // Search for functions that output "Z40" or "Z1"
218            'action=query&list=wikilambdasearch_labels&'
219            . 'wikilambdasearch_type=Z8&'
220            . 'wikilambdasearch_return_type=Z40|Z1&'
221            . 'wikilambdasearch_language=en' => 'apihelp-query+wikilambda-example-type-and-return-types',
222            // Search for function calls equivalent to "Z4" or literal "Z4" objects
223            'action=query&list=wikilambdasearch_labels&'
224            . 'wikilambdasearch_type=Z4|Z7&'
225            . 'wikilambdasearch_return_type=Z4&'
226            . 'wikilambdasearch_language=en' => 'apihelp-query+wikilambda-example-types-and-return-type',
227        ];
228    }
229}