Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.15% |
25 / 26 |
|
87.50% |
7 / 8 |
CRAP | |
0.00% |
0 / 1 |
PreferRecentFeature | |
96.15% |
25 / 26 |
|
87.50% |
7 / 8 |
16 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getKeywords | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
allowEmptyValue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parseValue | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
getCrossSearchStrategy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doApply | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getBoostFunctionBuilder | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
buildBoost | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | |
3 | namespace CirrusSearch\Query; |
4 | |
5 | use CirrusSearch\CrossSearchStrategy; |
6 | use CirrusSearch\Parser\AST\KeywordFeatureNode; |
7 | use CirrusSearch\Query\Builder\QueryBuildingContext; |
8 | use CirrusSearch\Search\Rescore\BoostFunctionBuilder; |
9 | use CirrusSearch\Search\Rescore\PreferRecentFunctionScoreBuilder; |
10 | use CirrusSearch\Search\SearchContext; |
11 | use CirrusSearch\SearchConfig; |
12 | use CirrusSearch\WarningCollector; |
13 | use 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 | */ |
25 | class 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 | } |