Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.92% covered (success)
97.92%
47 / 48
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikiLambdaApiQueryGeneratorBase
97.92% covered (success)
97.92%
47 / 48
87.50% covered (warning)
87.50%
7 / 8
16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 executeGenerator
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
1.12
 run
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMatchRate
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
8
 getSubstringMatchRate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\Extension\WikiLambda\ActionAPI;
4
5use MediaWiki\Api\ApiPageSet;
6use MediaWiki\Api\ApiQuery;
7use MediaWiki\Api\ApiQueryGeneratorBase;
8use MediaWiki\Extension\WikiLambda\HttpStatus;
9use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
10use MediaWiki\Extension\WikiLambda\ZErrorFactory;
11use MediaWiki\Extension\WikiLambda\ZObjectUtils;
12use MediaWiki\Logger\LoggerFactory;
13use Psr\Log\LoggerAwareInterface;
14use Psr\Log\LoggerInterface;
15
16/**
17 * WikiLambda Query Generator Base API util
18 *
19 * This abstract class extends the Wikimedia's ApiBase class
20 * and provides specific additional methods.
21 *
22 * @stable to extend
23 *
24 * @ingroup API
25 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
26 * @license MIT
27 */
28abstract class WikiLambdaApiQueryGeneratorBase extends ApiQueryGeneratorBase implements LoggerAwareInterface {
29
30    use WikiLambdaApiModeGuardTrait;
31
32    protected LoggerInterface $logger;
33
34    /**
35     * Set a default logger for all subclasses. The $logger property has a type and no default
36     * value, thus a getLogger() call before setLogger() is a fatal error. Subclasses can replace
37     * the default logger with setLogger().
38     *
39     * @inheritDoc
40     */
41    public function __construct( ApiQuery $query, string $moduleName, string $paramPrefix = '' ) {
42        parent::__construct( $query, $moduleName, $paramPrefix );
43
44        $this->setLogger( LoggerFactory::getInstance( 'WikiLambda' ) );
45    }
46
47    /**
48     * @inheritDoc
49     */
50    public function execute() {
51        $this->dieIfNotRepoMode();
52        $this->run( null );
53    }
54
55    /**
56     * @inheritDoc
57     */
58    public function executeGenerator( $resultPageSet ) {
59        $this->dieIfNotRepoMode();
60        $this->run( $resultPageSet );
61    }
62
63    /**
64     * @param ApiPageSet|null $resultPageSet
65     */
66    protected function run( $resultPageSet ) {
67        // Throw, not implemented
68        WikiLambdaApiBase::dieWithZError(
69            ZErrorFactory::createZErrorInstance(
70                ZErrorTypeRegistry::Z_ERROR_UNKNOWN,
71                [ 'message' => 'You must implement your run() method when using WikiLambdaApiBase' ]
72            ),
73            HttpStatus::NOT_IMPLEMENTED
74        );
75    }
76
77    /** @inheritDoc */
78    public function setLogger( LoggerInterface $logger ): void {
79        $this->logger = $logger;
80    }
81
82    /** @inheritDoc */
83    public function getLogger(): LoggerInterface {
84        return $this->logger;
85    }
86
87    private const PREFIX_BOOST_FACTOR = 1.5;
88    private const POSITION_WEIGHT_FACTOR = 3.5;
89    private const COVERAGE_FACTOR = 0.3;
90
91    /**
92     * Return the float match rate between a substring and the returned hit.
93     * The match rate is an aggregation of the match rates of each substring token.
94     *
95     * @param string $substring
96     * @param string $hit
97     * @param bool $exact
98     * @return float
99     */
100    protected static function getMatchRate( $substring, $hit, $exact = false ) {
101        if ( !$exact ) {
102            $substring = ZObjectUtils::comparableString( $substring );
103            $hit = ZObjectUtils::comparableString( $hit );
104        }
105
106        // Try match rate of full search term if substring is part of the hit
107        if ( strpos( $hit, $substring ) !== false ) {
108            return self::getSubstringMatchRate( $substring, $hit );
109        }
110
111        // Tokenize substring and calculate match rate for each token
112        $tokens = preg_split( '/\s+/', trim( $substring ), -1, PREG_SPLIT_NO_EMPTY );
113        if ( count( $tokens ) === 0 ) {
114            return 0.0;
115        }
116
117        $weightedSum = 0;
118        $totalWeight = 0;
119        $matchedTokens = 0;
120
121        // Aggregate weighted token's rates
122        foreach ( $tokens as $index => $token ) {
123            $rate = self::getSubstringMatchRate( $token, $hit );
124
125            // Count number of matched tokens to promote full token coverate
126            if ( $rate > 0 ) {
127                $matchedTokens++;
128            }
129
130            // Boost if first token matches the first characters of the hit
131            if ( $index === 0 && strpos( $hit, $token ) === 0 ) {
132                $rate = min( 1.0, $rate * self::PREFIX_BOOST_FACTOR );
133            }
134
135            // Weight by token length
136            $lengthWeight = strlen( $token );
137            // Weight by token position
138            $positionWeight = ( 1 / ( $index + 1 ) ) * self::POSITION_WEIGHT_FACTOR;
139            // Combined weights
140            $weight = $lengthWeight + $positionWeight;
141
142            $weightedSum += $rate * $weight;
143            $totalWeight += $weight;
144        }
145
146        $baseScore = ( $weightedSum / $totalWeight ) * ( 1 - self::COVERAGE_FACTOR );
147        $coverageScore = ( $matchedTokens / count( $tokens ) ) * self::COVERAGE_FACTOR;
148
149        return $baseScore + $coverageScore;
150    }
151
152    /**
153     * Calculates the float match rate between a substring token and the returned hit.
154     * The match rate is calculated considering:
155     * * Levenshtein distance
156     * * Position score
157     *
158     * @param string $substring
159     * @param string $hit
160     * @return float
161     */
162    private static function getSubstringMatchRate( $substring, $hit ) {
163        // Zero score if token not present at all
164        if ( strpos( $hit, $substring ) === false ) {
165            return 0.0;
166        }
167
168        // Calculate the base match rate with the Levenshtein distance
169        $distance = levenshtein( $substring, $hit );
170        $max = max( strlen( $substring ), strlen( $hit ) );
171        $baseMatchRate = ( $max - $distance ) / $max;
172
173        // Find the position of the substring in the hit
174        $position = strpos( $hit, $substring );
175
176        // Normalize the position to a score (earlier positions get higher weight)
177        $positionScore = 1 - ( $position / strlen( $hit ) );
178
179        // Combine the base match rate and position score
180        return $baseMatchRate * 0.5 + $positionScore * 0.5;
181    }
182}