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