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