Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.15% covered (success)
96.15%
25 / 26
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
PreferRecentFeature
96.15% covered (success)
96.15%
25 / 26
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
 getKeywords
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allowEmptyValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseValue
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 getCrossSearchStrategy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doApply
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getBoostFunctionBuilder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildBoost
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace CirrusSearch\Query;
4
5use CirrusSearch\CrossSearchStrategy;
6use CirrusSearch\Parser\AST\KeywordFeatureNode;
7use CirrusSearch\Query\Builder\QueryBuildingContext;
8use CirrusSearch\Search\Rescore\BoostFunctionBuilder;
9use CirrusSearch\Search\Rescore\PreferRecentFunctionScoreBuilder;
10use CirrusSearch\Search\SearchContext;
11use CirrusSearch\SearchConfig;
12use CirrusSearch\WarningCollector;
13use MediaWiki\Config\Config;
14
15/**
16 * Matches "prefer-recent:" and then an optional floating point number <= 1 but
17 * >= 0 (decay portion) and then an optional comma followed by another floating
18 * point number >0 0 (half life).
19 *
20 * Examples:
21 *  prefer-recent:
22 *  prefer-recent:.6
23 *  prefer-recent:0.5,.0001
24 */
25class PreferRecentFeature extends SimpleKeywordFeature implements BoostFunctionFeature {
26    /**
27     * @var float Default number of days for the portion of the score effected
28     *  by this feature to be cut in half. Used when `prefer-recent:` is present
29     *  in the query without any arguments.
30     */
31    private $halfLife;
32
33    /**
34     * @var float Value between 0 and 1 indicating the default portion of the
35     *  score affected by this feature when not specified in the search term.
36     */
37    private $unspecifiedDecay;
38
39    /**
40     * @param Config $config
41     */
42    public function __construct( Config $config ) {
43        $this->halfLife = $config->get( 'CirrusSearchPreferRecentDefaultHalfLife' );
44        $this->unspecifiedDecay = $config->get( 'CirrusSearchPreferRecentUnspecifiedDecayPortion' );
45    }
46
47    /**
48     * @return string[] The list of keywords this feature is supposed to match
49     */
50    protected function getKeywords() {
51        return [ "prefer-recent" ];
52    }
53
54    /**
55     * @return bool
56     */
57    public function allowEmptyValue() {
58        return true;
59    }
60
61    /**
62     * @param string $key
63     * @param string $value
64     * @param string $quotedValue
65     * @param string $valueDelimiter
66     * @param string $suffix
67     * @param WarningCollector $warningCollector
68     * @return array|null|false
69     */
70    public function parseValue( $key, $value, $quotedValue, $valueDelimiter, $suffix, WarningCollector $warningCollector ) {
71        $matches = [];
72        $retValue = [];
73        // FIXME: we should probably no longer accept the empty string and simply return false
74        // instead of null
75        if ( preg_match( '/^(1|0?(?:\.\d+)?)?(?:,(\d*\.?\d+))?$/', $value, $matches ) === 1 ) {
76            if ( isset( $matches[1] ) && strlen( $matches[1] ) > 0 ) {
77                $retValue['decay'] = floatval( $matches[1] );
78            }
79
80            if ( isset( $matches[2] ) ) {
81                $retValue['halfLife'] = floatval( $matches[2] );
82            }
83            return $retValue !== [] ? $retValue : null;
84        }
85        return false;
86    }
87
88    /**
89     * @param KeywordFeatureNode $node
90     * @return CrossSearchStrategy
91     */
92    public function getCrossSearchStrategy( KeywordFeatureNode $node ) {
93        return CrossSearchStrategy::allWikisStrategy();
94    }
95
96    /**
97     * Applies the detected keyword from the search term. May apply changes
98     * either to $context directly, or return a filter to be added.
99     *
100     * @param SearchContext $context
101     * @param string $key The keyword
102     * @param string $value The value attached to the keyword with quotes stripped and escaped
103     *  quotes un-escaped.
104     * @param string $quotedValue The original value in the search string, including quotes if used
105     * @param bool $negated Is the search negated? Not used to generate the returned AbstractQuery,
106     *  that will be negated as necessary. Used for any other building/context necessary.
107     * @return array Two element array, first an AbstractQuery or null to apply to the
108     *  query. Second a boolean indicating if the quotedValue should be kept in the search
109     *  string.
110     */
111    protected function doApply( SearchContext $context, $key, $value, $quotedValue, $negated ) {
112        $parsedValue = $this->parseValue( $key, $value, $quotedValue, '', '', $context );
113        $context->addCustomRescoreComponent( $this->buildBoost( $parsedValue, $context->getConfig() ) );
114        return [ null, $parsedValue === false ];
115    }
116
117    /**
118     * @param KeywordFeatureNode $node
119     * @param QueryBuildingContext $context
120     * @return BoostFunctionBuilder|null
121     */
122    public function getBoostFunctionBuilder( KeywordFeatureNode $node, QueryBuildingContext $context ) {
123        return $this->buildBoost( $node->getParsedValue(), $context->getSearchConfig() );
124    }
125
126    /**
127     * @param array|null|false $parsedValue
128     * @param SearchConfig $config
129     * @return PreferRecentFunctionScoreBuilder
130     */
131    private function buildBoost( $parsedValue, SearchConfig $config ) {
132        $halfLife = $this->halfLife;
133        $decay = $this->unspecifiedDecay;
134        if ( is_array( $parsedValue ) ) {
135            if ( isset( $parsedValue['halfLife'] ) ) {
136                $halfLife = $parsedValue['halfLife'];
137            }
138            if ( isset( $parsedValue['decay'] ) ) {
139                $decay = $parsedValue['decay'];
140            }
141        }
142        return new PreferRecentFunctionScoreBuilder( $config, 1, $halfLife, $decay );
143    }
144}