Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.71% covered (warning)
85.71%
30 / 35
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
RemoteSearchTaskSuggester
85.71% covered (warning)
85.71%
30 / 35
50.00% covered (danger)
50.00%
1 / 2
7.14
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 search
83.33% covered (warning)
83.33%
25 / 30
0.00% covered (danger)
0.00%
0 / 1
6.17
1<?php
2
3namespace GrowthExperiments\NewcomerTasks\TaskSuggester;
4
5use FauxSearchResultSet;
6use GrowthExperiments\NewcomerTasks\FauxSearchResultWithScore;
7use GrowthExperiments\NewcomerTasks\NewcomerTasksUserOptionsLookup;
8use GrowthExperiments\NewcomerTasks\TaskSuggester\SearchStrategy\SearchQuery;
9use GrowthExperiments\NewcomerTasks\TaskSuggester\SearchStrategy\SearchStrategy;
10use GrowthExperiments\NewcomerTasks\TaskType\TaskType;
11use GrowthExperiments\NewcomerTasks\TaskType\TaskTypeHandlerRegistry;
12use GrowthExperiments\NewcomerTasks\Topic\Topic;
13use GrowthExperiments\Util;
14use MediaWiki\Cache\LinkBatchFactory;
15use MediaWiki\Http\HttpRequestFactory;
16use MediaWiki\Title\TitleFactory;
17
18/**
19 * Suggest edits based on searching a wiki (potentially a different one) via the API.
20 * Mainly meant for testing and development; it can in theory be used in production but
21 * it is less efficient than using SearchEngine internally.
22 */
23class RemoteSearchTaskSuggester extends SearchTaskSuggester {
24
25    /** @var HttpRequestFactory */
26    private $requestFactory;
27
28    /** @var TitleFactory */
29    private $titleFactory;
30
31    /** @var string Remote API URL including api.php */
32    private $apiUrl;
33
34    /**
35     * @param TaskTypeHandlerRegistry $taskTypeHandlerRegistry
36     * @param SearchStrategy $searchStrategy
37     * @param NewcomerTasksUserOptionsLookup $newcomerTasksUserOptionsLookup
38     * @param LinkBatchFactory $linkBatchFactory
39     * @param HttpRequestFactory $requestFactory
40     * @param TitleFactory $titleFactory
41     * @param string $apiUrl Remote API URL including api.php
42     * @param TaskType[] $taskTypes
43     * @param Topic[] $topics
44     */
45    public function __construct(
46        TaskTypeHandlerRegistry $taskTypeHandlerRegistry,
47        SearchStrategy $searchStrategy,
48        NewcomerTasksUserOptionsLookup $newcomerTasksUserOptionsLookup,
49        LinkBatchFactory $linkBatchFactory,
50        HttpRequestFactory $requestFactory,
51        TitleFactory $titleFactory,
52        $apiUrl,
53        array $taskTypes,
54        array $topics
55    ) {
56        parent::__construct( $taskTypeHandlerRegistry, $searchStrategy, $newcomerTasksUserOptionsLookup,
57            $linkBatchFactory, $taskTypes, $topics );
58        $this->requestFactory = $requestFactory;
59        $this->titleFactory = $titleFactory;
60        $this->apiUrl = $apiUrl;
61    }
62
63    /** @inheritDoc */
64    protected function search(
65        SearchQuery $query,
66        int $limit,
67        int $offset,
68        bool $debug
69    ) {
70        // We randomize the results so offsets are meaningless.
71        // TODO use fixed random seed.
72        $params = [
73            'action' => 'query',
74            'list' => 'search',
75            'srsearch' => $query->getQueryString(),
76            'srnamespace' => 0,
77            'srlimit' => $limit,
78            'srinfo' => 'totalhits',
79            'srprop' => '',
80            'srqiprofile' => $query->getRescoreProfile() ?? 'classic_noboostlinks',
81            // Convenient for debugging. Production setups should use LocalSearchTaskSuggester anyway.
82            'errorlang' => 'en',
83        ];
84        // FIXME quick fix: don't randomize if we use morelike, seems to conflict
85        if ( $query->getSort() ) {
86            $params['srsort'] = $query->getSort();
87        }
88        $status = Util::getApiUrl( $this->requestFactory, $this->apiUrl, $params );
89        if ( !$status->isOK() ) {
90            return $status;
91        }
92        $data = $status->getValue();
93
94        $results = [];
95        foreach ( $data['query']['search'] ?? [] as $i => $result ) {
96            $title = $this->titleFactory->newFromText( $result['title'], $result['ns'] );
97            if ( !$title ) {
98                continue;
99            }
100            // The search API does not expose scores :( Put in something fake, just to ease testing.
101            $results[] = new FauxSearchResultWithScore( $title, 100 / ( $i + 1 ) );
102        }
103        $resultSet = new FauxSearchResultSet( $results, (int)$data['query']['searchinfo']['totalhits'] );
104
105        if ( $debug ) {
106            // Add Cirrus debug dump URLs which show the details of how the scores were calculated.
107            $query->setDebugUrl( $this->apiUrl . '?' . wfArrayToCgi( $params, [
108                'cirrusDumpResult' => 1,
109                'cirrusExplain' => 'pretty',
110            ] ) );
111        }
112
113        return $resultSet;
114    }
115
116}