Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.44% covered (success)
94.44%
51 / 54
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
LangDetectFallbackMethod
94.44% covered (success)
94.44%
51 / 54
80.00% covered (warning)
80.00%
4 / 5
18.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 build
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 successApproximation
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
8
 rewrite
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
6.12
 getMetrics
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace CirrusSearch\Fallbacks;
4
5use CirrusSearch\CirrusConfigNames;
6use CirrusSearch\InterwikiResolver;
7use CirrusSearch\LanguageDetector\Detector;
8use CirrusSearch\LanguageDetector\LanguageDetectorFactory;
9use CirrusSearch\Parser\BasicQueryClassifier;
10use CirrusSearch\Search\CirrusSearchResultSet;
11use CirrusSearch\Search\SearchMetricsProvider;
12use CirrusSearch\Search\SearchQuery;
13use CirrusSearch\Search\SearchQueryBuilder;
14use CirrusSearch\SearchConfig;
15use CirrusSearch\Searcher;
16use MediaWiki\MainConfigNames;
17use Wikimedia\Assert\Assert;
18
19class LangDetectFallbackMethod implements FallbackMethod, SearchMetricsProvider {
20    use FallbackMethodTrait;
21
22    /**
23     * @var SearchQuery
24     */
25    private $query;
26
27    /**
28     * @var SearcherFactory
29     */
30    private $searcherFactory;
31
32    /**
33     * @var array|null
34     */
35    private $searchMetrics = [];
36
37    /**
38     * @var Detector[]
39     */
40    private $detectors;
41
42    /**
43     * @var InterwikiResolver
44     */
45    private $interwikiResolver;
46
47    /**
48     * @var SearchConfig|null
49     */
50    private $detectedLangWikiConfig;
51
52    /**
53     * @var int
54     */
55    private $threshold;
56
57    /**
58     * Do not use this constructor outside of tests!
59     * @param SearchQuery $query
60     * @param Detector[] $detectors
61     * @param InterwikiResolver $interwikiResolver
62     */
63    public function __construct(
64        SearchQuery $query,
65        array $detectors,
66        InterwikiResolver $interwikiResolver
67    ) {
68        Assert::precondition( $query->getCrossSearchStrategy()->isCrossLanguageSearchSupported(),
69            "Cross language search must be supported for this query" );
70        $this->query = $query;
71        $this->detectors = $detectors;
72        $this->interwikiResolver = $interwikiResolver;
73        $this->threshold = $query->getSearchConfig()->get( CirrusConfigNames::InterwikiThreshold );
74    }
75
76    /**
77     * @param SearchQuery $query
78     * @param array $params
79     * @param InterwikiResolver $interwikiResolver
80     * @return FallbackMethod|null
81     */
82    public static function build( SearchQuery $query, array $params, InterwikiResolver $interwikiResolver ) {
83        if ( !$query->getCrossSearchStrategy()->isCrossLanguageSearchSupported() ) {
84            return null;
85        }
86        $langDetectFactory = new LanguageDetectorFactory( $query->getSearchConfig() );
87        return new self( $query, $langDetectFactory->getDetectors(), $interwikiResolver );
88    }
89
90    /**
91     * @param FallbackRunnerContext $context
92     * @return float
93     */
94    public function successApproximation( FallbackRunnerContext $context ) {
95        $firstPassResults = $context->getInitialResultSet();
96        if ( !$this->query->isAllowRewrite() ) {
97            return 0.0;
98        }
99        if ( $this->resultsThreshold( $firstPassResults, $this->threshold ) ) {
100            return 0.0;
101        }
102        if ( !$this->query->getParsedQuery()->isQueryOfClass( BasicQueryClassifier::SIMPLE_BAG_OF_WORDS ) ) {
103            return 0.0;
104        }
105        foreach ( $this->detectors as $name => $detector ) {
106            $lang = $detector->detect( $this->query->getParsedQuery()->getRawQuery() );
107            if ( $lang === null ) {
108                continue;
109            }
110            if ( $lang === $this->query->getSearchConfig()->get( MainConfigNames::LanguageCode ) ) {
111                // The query is in the wiki language so we
112                // don't need to actually try another wiki.
113                // Note that this may not be very accurate for
114                // wikis that use deprecated language codes
115                // but the interwiki resolver should not return
116                // ourselves.
117                continue;
118            }
119            $iwPrefixAndConfig = $this->interwikiResolver->getSameProjectConfigByLang( $lang );
120            if ( $iwPrefixAndConfig ) {
121                // it might be more accurate to attach these to the 'next'
122                // log context? It would be inconsistent with the
123                // langdetect => false condition which does not have a next
124                // request though.
125                Searcher::appendLastLogPayload( 'langdetect', $name );
126                $prefix = key( $iwPrefixAndConfig );
127                $config = $iwPrefixAndConfig[$prefix];
128                $metric = [ $config->getWikiId(), $prefix ];
129                $this->detectedLangWikiConfig = $config;
130                return 0.5;
131            }
132        }
133        Searcher::appendLastLogPayload( 'langdetect', 'failed' );
134        return 0.0;
135    }
136
137    public function rewrite( FallbackRunnerContext $context ): FallbackStatus {
138        $previousSet = $context->getPreviousResultSet();
139        Assert::precondition( $this->detectedLangWikiConfig !== null,
140            'nothing has been detected, this should not even be tried.' );
141
142        if ( $this->resultsThreshold( $previousSet, $this->threshold ) ) {
143            return FallbackStatus::noSuggestion();
144        }
145
146        if ( !$context->costlyCallAllowed() ) {
147            return FallbackStatus::noSuggestion();
148        }
149
150        $crossLangQuery = SearchQueryBuilder::forCrossLanguageSearch( $this->detectedLangWikiConfig,
151            $this->query )->build();
152        $searcher = $context->makeSearcher( $crossLangQuery );
153        $status = $searcher->search( $crossLangQuery );
154        if ( !$status->isOK() ) {
155            return FallbackStatus::noSuggestion();
156        }
157        $crossLangResults = $status->getValue();
158        if ( !$crossLangResults instanceof CirrusSearchResultSet ) {
159            // NOTE: Can/should this happen?
160            return FallbackStatus::noSuggestion();
161        }
162        if ( $crossLangResults->numRows() > 0 ) {
163            return FallbackStatus::addInterwikiResults( $crossLangResults,
164                $this->detectedLangWikiConfig->getWikiId() );
165        }
166        return FallbackStatus::noSuggestion();
167    }
168
169    /** @inheritDoc */
170    public function getMetrics() {
171        return $this->searchMetrics;
172    }
173}