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    public function __construct( Config $config ) {
40        $this->halfLife = $config->get( 'CirrusSearchPreferRecentDefaultHalfLife' );
41        $this->unspecifiedDecay = $config->get( 'CirrusSearchPreferRecentUnspecifiedDecayPortion' );
42    }
43
44    /**
45     * @return string[] The list of keywords this feature is supposed to match
46     */
47    protected function getKeywords() {
48        return [ "prefer-recent" ];
49    }
50
51    /**
52     * @return bool
53     */
54    public function allowEmptyValue() {
55        return true;
56    }
57
58    /**
59     * @param string $key
60     * @param string $value
61     * @param string $quotedValue
62     * @param string $valueDelimiter
63     * @param string $suffix
64     * @param WarningCollector $warningCollector
65     * @return array|null|false
66     */
67    public function parseValue( $key, $value, $quotedValue, $valueDelimiter, $suffix, WarningCollector $warningCollector ) {
68        $matches = [];
69        $retValue = [];
70        // FIXME: we should probably no longer accept the empty string and simply return false
71        // instead of null
72        if ( preg_match( '/^(1|0?(?:\.\d+)?)?(?:,(\d*\.?\d+))?$/', $value, $matches ) === 1 ) {
73            if ( isset( $matches[1] ) && strlen( $matches[1] ) > 0 ) {
74                $retValue['decay'] = floatval( $matches[1] );
75            }
76
77            if ( isset( $matches[2] ) ) {
78                $retValue['halfLife'] = floatval( $matches[2] );
79            }
80            return $retValue !== [] ? $retValue : null;
81        }
82        return false;
83    }
84
85    /**
86     * @param KeywordFeatureNode $node
87     * @return CrossSearchStrategy
88     */
89    public function getCrossSearchStrategy( KeywordFeatureNode $node ) {
90        return CrossSearchStrategy::allWikisStrategy();
91    }
92
93    /**
94     * Applies the detected keyword from the search term. May apply changes
95     * either to $context directly, or return a filter to be added.
96     *
97     * @param SearchContext $context
98     * @param string $key The keyword
99     * @param string $value The value attached to the keyword with quotes stripped and escaped
100     *  quotes un-escaped.
101     * @param string $quotedValue The original value in the search string, including quotes if used
102     * @param bool $negated Is the search negated? Not used to generate the returned AbstractQuery,
103     *  that will be negated as necessary. Used for any other building/context necessary.
104     * @return array Two element array, first an AbstractQuery or null to apply to the
105     *  query. Second a boolean indicating if the quotedValue should be kept in the search
106     *  string.
107     */
108    protected function doApply( SearchContext $context, $key, $value, $quotedValue, $negated ) {
109        $parsedValue = $this->parseValue( $key, $value, $quotedValue, '', '', $context );
110        $context->addCustomRescoreComponent( $this->buildBoost( $parsedValue, $context->getConfig() ) );
111        return [ null, $parsedValue === false ];
112    }
113
114    /**
115     * @param KeywordFeatureNode $node
116     * @param QueryBuildingContext $context
117     * @return BoostFunctionBuilder|null
118     */
119    public function getBoostFunctionBuilder( KeywordFeatureNode $node, QueryBuildingContext $context ) {
120        return $this->buildBoost( $node->getParsedValue(), $context->getSearchConfig() );
121    }
122
123    /**
124     * @param array|null|false $parsedValue
125     * @param SearchConfig $config
126     * @return PreferRecentFunctionScoreBuilder
127     */
128    private function buildBoost( $parsedValue, SearchConfig $config ) {
129        $halfLife = $this->halfLife;
130        $decay = $this->unspecifiedDecay;
131        if ( is_array( $parsedValue ) ) {
132            if ( isset( $parsedValue['halfLife'] ) ) {
133                $halfLife = $parsedValue['halfLife'];
134            }
135            if ( isset( $parsedValue['decay'] ) ) {
136                $decay = $parsedValue['decay'];
137            }
138        }
139        return new PreferRecentFunctionScoreBuilder( $config, 1, $halfLife, $decay );
140    }
141}