Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
25.00% |
1 / 4 |
CRAP | |
76.71% |
56 / 73 |
FunctionScoreChain | |
0.00% |
0 / 1 |
|
25.00% |
1 / 4 |
41.37 | |
76.71% |
56 / 73 |
__construct | |
0.00% |
0 / 1 |
3.01 | |
91.67% |
11 / 12 |
|||
applyOverrides | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
buildRescoreQuery | |
0.00% |
0 / 1 |
6.73 | |
72.73% |
8 / 11 |
|||
getImplementation | |
0.00% |
0 / 1 |
27.95 | |
72.92% |
35 / 48 |
<?php | |
namespace CirrusSearch\Search\Rescore; | |
use CirrusSearch\CirrusSearchHookRunner; | |
use CirrusSearch\Profile\ArrayPathSetter; | |
use CirrusSearch\Profile\SearchProfileService; | |
use CirrusSearch\Search\SearchContext; | |
use Elastica\Query\FunctionScore; | |
/** | |
* This program is free software; you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License as published by | |
* the Free Software Foundation; either version 2 of the License, or | |
* (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License along | |
* with this program; if not, write to the Free Software Foundation, Inc., | |
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
* http://www.gnu.org/copyleft/gpl.html | |
*/ | |
class FunctionScoreChain { | |
/** | |
* List of allowed function_score param | |
* we keep boost and boost_mode even if they do not make sense | |
* here since we do not allow to specify the query param. | |
* The query will be MatchAll with a score to 1. | |
* | |
* @var string[] | |
*/ | |
private static $functionScoreParams = [ | |
'boost', | |
'boost_mode', | |
'max_boost', | |
'score_mode', | |
'min_score' | |
]; | |
/** | |
* @var SearchContext | |
*/ | |
private $context; | |
/** | |
* @var FunctionScoreDecorator | |
*/ | |
private $functionScore; | |
/** | |
* @var array the function score chain | |
*/ | |
private $chain; | |
/** | |
* @var string the name of the chain | |
*/ | |
private $chainName; | |
/** | |
* @var CirrusSearchHookRunner | |
*/ | |
private $cirrusSearchHookRunner; | |
/** | |
* Builds a new function score chain. | |
* | |
* @param SearchContext $context | |
* @param string $chainName the name of the chain (must be a valid | |
* chain in wgCirrusSearchRescoreFunctionScoreChains) | |
* @param array $overrides Parameter overrides | |
* @param CirrusSearchHookRunner $cirrusSearchHookRunner | |
*/ | |
public function __construct( SearchContext $context, $chainName, $overrides, CirrusSearchHookRunner $cirrusSearchHookRunner ) { | |
$this->chainName = $chainName; | |
$this->context = $context; | |
$this->functionScore = new FunctionScoreDecorator(); | |
$chain = $context->getConfig() | |
->getProfileService() | |
->loadProfileByName( SearchProfileService::RESCORE_FUNCTION_CHAINS, $chainName ); | |
$this->chain = $overrides ? $this->applyOverrides( $chain, $overrides ) : $chain; | |
$params = array_intersect_key( $this->chain, array_flip( self::$functionScoreParams ) ); | |
foreach ( $params as $param => $value ) { | |
$this->functionScore->setParam( $param, $value ); | |
} | |
$this->cirrusSearchHookRunner = $cirrusSearchHookRunner; | |
} | |
private function applyOverrides( array $chain, array $overrides ) { | |
$transformer = new ArrayPathSetter( $overrides ); | |
return $transformer->transform( $chain ); | |
} | |
/** | |
* @return FunctionScore|null the rescore query or null none of functions were | |
* needed. | |
* @throws InvalidRescoreProfileException | |
*/ | |
public function buildRescoreQuery() { | |
if ( !isset( $this->chain['functions'] ) ) { | |
throw new InvalidRescoreProfileException( "No functions defined in chain {$this->chainName}." ); | |
} | |
foreach ( $this->chain['functions'] as $func ) { | |
$impl = $this->getImplementation( $func ); | |
$impl->append( $this->functionScore ); | |
} | |
// Add extensions | |
if ( !empty( $this->chain['add_extensions'] ) ) { | |
foreach ( $this->context->getExtraScoreBuilders() as $extBuilder ) { | |
$extBuilder->append( $this->functionScore ); | |
} | |
} | |
if ( !$this->functionScore->isEmptyFunction() ) { | |
return $this->functionScore; | |
} | |
return null; | |
} | |
/** | |
* @param array $func | |
* @return BoostFunctionBuilder | |
* @throws InvalidRescoreProfileException | |
*/ | |
private function getImplementation( $func ) { | |
$weight = $func['weight'] ?? 1; | |
$config = $this->context->getConfig(); | |
switch ( $func['type'] ) { | |
case 'boostlinks': | |
return new IncomingLinksFunctionScoreBuilder(); | |
case 'recency': | |
foreach ( $this->context->getExtraScoreBuilders() as $boostFunctionBuilder ) { | |
if ( $boostFunctionBuilder instanceof PreferRecentFunctionScoreBuilder ) { | |
// If prefer-recent was used as a keyword we don't send the one | |
// from the profile | |
return new BoostedQueriesFunction( [], [] ); | |
} | |
} | |
$preferRecentDecayPortion = $config->get( 'CirrusSearchPreferRecentDefaultDecayPortion' ); | |
$preferRecentHalfLife = 0; | |
if ( $preferRecentDecayPortion > 0 ) { | |
$preferRecentHalfLife = $config->get( 'CirrusSearchPreferRecentDefaultHalfLife' ); | |
} | |
return new PreferRecentFunctionScoreBuilder( $config, $weight, | |
$preferRecentHalfLife, $preferRecentDecayPortion ); | |
case 'templates': | |
$withDefaultBoosts = true; | |
foreach ( $this->context->getExtraScoreBuilders() as $boostFunctionBuilder ) { | |
if ( $boostFunctionBuilder instanceof ByKeywordTemplateBoostFunction ) { | |
$withDefaultBoosts = false; | |
break; | |
} | |
} | |
return new BoostTemplatesFunctionScoreBuilder( $config, $this->context->getNamespaces(), | |
$this->context->getLimitSearchToLocalWiki(), $withDefaultBoosts, $weight ); | |
case 'namespaces': | |
return new NamespacesFunctionScoreBuilder( $config, $this->context->getNamespaces(), $weight ); | |
case 'language': | |
return new LangWeightFunctionScoreBuilder( $config, $weight ); | |
case 'custom_field': | |
return new CustomFieldFunctionScoreBuilder( $config, $weight, $func['params'] ); | |
case 'script': | |
return new ScriptScoreFunctionScoreBuilder( $config, $weight, $func['script'] ); | |
case 'logscale_boost': | |
return new LogScaleBoostFunctionScoreBuilder( $config, $weight, $func['params'] ); | |
case 'satu': | |
return new SatuFunctionScoreBuilder( $config, $weight, $func['params'] ); | |
case 'log_multi': | |
return new LogMultFunctionScoreBuilder( $config, $weight, $func['params'] ); | |
case 'geomean': | |
return new GeoMeanFunctionScoreBuilder( $config, $weight, $func['params'] ); | |
case 'term_boost': | |
return new TermBoostScoreBuilder( $config, $weight, $func['params'] ); | |
default: | |
$builder = null; | |
$this->cirrusSearchHookRunner->onCirrusSearchScoreBuilder( $func, $this->context, $builder ); | |
if ( !$builder ) { | |
throw new InvalidRescoreProfileException( "Unknown function score type {$func['type']}." ); | |
} | |
if ( !( $builder instanceof BoostFunctionBuilder ) ) { | |
throw new InvalidRescoreProfileException( "Invalid function score type {$func['type']}: expected " . | |
BoostFunctionBuilder::class . " but was " . get_class( $builder ) ); | |
} | |
/** | |
* @var $builder BoostFunctionBuilder | |
*/ | |
return $builder; | |
} | |
} | |
} |