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