Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.39% covered (warning)
59.39%
98 / 165
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
SchemaDump
59.39% covered (warning)
59.39%
98 / 165
11.11% covered (danger)
11.11%
1 / 9
85.31
0.00% covered (danger)
0.00%
0 / 1
 execute
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
3.33
 fetchFromCluster
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 buildFromCode
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
3.33
 getBuildContext
79.31% covered (warning)
79.31%
23 / 29
0.00% covered (danger)
0.00%
0 / 1
5.22
 buildSchemaForIndex
55.26% covered (warning)
55.26%
21 / 38
0.00% covered (danger)
0.00%
0 / 1
3.81
 buildCompleteSettings
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
7.01
 getAllowedParams
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace CirrusSearch\Api;
4
5use CirrusSearch\Connection;
6use CirrusSearch\Maintenance\AnalysisConfigBuilder;
7use CirrusSearch\Maintenance\ArchiveMappingConfigBuilder;
8use CirrusSearch\Maintenance\ConfigUtils;
9use CirrusSearch\Maintenance\MappingConfigBuilder;
10use CirrusSearch\Maintenance\NullPrinter;
11use CirrusSearch\Maintenance\SuggesterAnalysisConfigBuilder;
12use CirrusSearch\Maintenance\SuggesterMappingConfigBuilder;
13use CirrusSearch\SearchConfig;
14use MediaWiki\Api\ApiBase;
15use Wikimedia\ParamValidator\ParamValidator;
16
17/**
18 * Dumps CirrusSearch mappings for easy viewing.
19 *
20 * @license GPL-2.0-or-later
21 */
22class SchemaDump extends ApiBase {
23    use ApiTrait;
24
25    public function execute() {
26        $build = $this->getParameter( 'build' );
27        $conn = $this->getCirrusConnection();
28        $indexPrefix = $this->getSearchConfig()->get( SearchConfig::INDEX_BASE_NAME );
29
30        // Get all index suffixes to process
31        $indexSuffixes = $conn->getAllIndexSuffixes( null );
32
33        if ( $build ) {
34            try {
35                $this->buildFromCode( $conn, $indexPrefix, $indexSuffixes );
36            } catch ( \InvalidArgumentException $e ) {
37                $this->dieWithException( $e );
38            }
39        } else {
40            $this->fetchFromCluster( $conn, $indexPrefix, $indexSuffixes );
41        }
42    }
43
44    /**
45     * Fetch schema (settings and mappings) from live cluster indices
46     *
47     * @param Connection $conn CirrusSearch connection
48     * @param string $indexPrefix Index prefix (typically wiki ID)
49     * @param array $indexSuffixes Index suffixes to process
50     */
51    private function fetchFromCluster( Connection $conn, string $indexPrefix, array $indexSuffixes ): void {
52        foreach ( $indexSuffixes as $suffix ) {
53            $index = $conn->getIndex( $indexPrefix, $suffix );
54
55            if ( !$index->exists() ) {
56                continue;
57            }
58
59            $settings = $index->getSettings()->get();
60            $mappings = $index->getMapping();
61
62            $this->getResult()->addValue(
63                null,
64                $suffix,
65                [
66                    'settings' => [ 'index' => $settings ],
67                    'mappings' => $mappings
68                ]
69            );
70        }
71
72        // Handle completion suggester if enabled
73        if ( $this->getSearchConfig()->isCompletionSuggesterEnabled() ) {
74            $index = $conn->getIndex( $indexPrefix, Connection::TITLE_SUGGEST_INDEX_SUFFIX );
75            if ( $index->exists() ) {
76                $settings = $index->getSettings()->get();
77                $mappings = $index->getMapping();
78
79                $this->getResult()->addValue(
80                    null,
81                    Connection::TITLE_SUGGEST_INDEX_SUFFIX,
82                    [
83                        'settings' => [ 'index' => $settings ],
84                        'mappings' => $mappings
85                    ]
86                );
87            }
88        }
89    }
90
91    /**
92     * Build schema (settings and mappings) from code and configuration
93     *
94     * @param Connection $conn CirrusSearch connection
95     * @param string $indexPrefix Index prefix (typically wiki ID)
96     * @param array $indexSuffixes Index suffixes to process
97     */
98    private function buildFromCode( Connection $conn, string $indexPrefix, array $indexSuffixes ): void {
99        $buildContext = $this->getBuildContext( $conn );
100
101        foreach ( $indexSuffixes as $suffix ) {
102            $schema = $this->buildSchemaForIndex( $suffix, $buildContext );
103            $indexName = $indexPrefix . '_' . $suffix;
104
105            $this->getResult()->addValue( null, $suffix, $schema );
106        }
107
108        // Handle completion suggester if enabled
109        if ( $this->getSearchConfig()->isCompletionSuggesterEnabled() ) {
110            $suffix = Connection::TITLE_SUGGEST_INDEX_SUFFIX;
111            $schema = $this->buildSchemaForIndex( $suffix, $buildContext );
112
113            $this->getResult()->addValue( null, $suffix, $schema );
114        }
115    }
116
117    /**
118     * Gather all context needed for building schema from code
119     *
120     * @param Connection $conn CirrusSearch connection
121     * @return array Build context with language code, plugins, flags, settings
122     */
123    private function getBuildContext( Connection $conn ): array {
124        $config = $this->getSearchConfig();
125
126        // Get language code
127        $langCode = $config->get( 'LanguageCode' );
128
129        // Check if plugins were provided via API parameter
130        $pluginsParam = $this->getParameter( 'plugins' );
131
132        $utils = new ConfigUtils( $conn->getClient(), new NullPrinter() );
133        $bannedPlugins = $config->get( 'CirrusSearchBannedPlugins' );
134        if ( $pluginsParam !== null ) {
135            $plugins = $utils->removeBannedPlugins( $pluginsParam, $bannedPlugins );
136        } else {
137            // Fall back to scanning cluster
138            $pluginStatus = $utils->scanAvailablePlugins( $bannedPlugins );
139            if ( !$pluginStatus->isOK() ) {
140                $this->dieStatus( $pluginStatus );
141            }
142            $plugins = $pluginStatus->getValue();
143        }
144
145        // Get config flags
146        $flags = 0;
147        if ( $config->get( 'CirrusSearchPrefixSearchStartsWithAnyWord' ) ) {
148            $flags |= MappingConfigBuilder::PREFIX_START_WITH_ANY;
149        }
150        if ( $config->get( 'CirrusSearchPhraseSuggestUseText' ) ) {
151            $flags |= MappingConfigBuilder::PHRASE_SUGGEST_USE_TEXT;
152        }
153
154        $optimizeForHighlighter = $config->get( 'CirrusSearchOptimizeIndexForExperimentalHighlighter' );
155
156        // Get replica and refresh settings
157        $replicas = $config->get( 'CirrusSearchReplicas' );
158        $refreshInterval = $config->get( 'CirrusSearchRefreshInterval' );
159
160        return [
161            'langCode' => $langCode,
162            'plugins' => $plugins,
163            'flags' => $flags,
164            'optimizeForHighlighter' => $optimizeForHighlighter,
165            'replicas' => $replicas,
166            'refreshInterval' => $refreshInterval,
167            'config' => $config,
168            'conn' => $conn,
169        ];
170    }
171
172    /**
173     * Build complete schema for a specific index type
174     *
175     * @param string $indexSuffix Index suffix (content, general, archive, titlesuggest)
176     * @param array $context Build context from getBuildContext()
177     * @return array Schema with 'settings' and 'mappings' keys
178     */
179    private function buildSchemaForIndex( string $indexSuffix, array $context ): array {
180        $config = $context['config'];
181
182        // Select appropriate builders based on index type
183        if ( $indexSuffix === Connection::TITLE_SUGGEST_INDEX_SUFFIX ) {
184            $analysisBuilder = new SuggesterAnalysisConfigBuilder(
185                $context['langCode'],
186                $context['plugins'],
187                $config
188            );
189            $mappingBuilder = new SuggesterMappingConfigBuilder( $config );
190        } elseif ( $indexSuffix === Connection::ARCHIVE_INDEX_SUFFIX ) {
191            $analysisBuilder = new AnalysisConfigBuilder(
192                $context['langCode'],
193                $context['plugins'],
194                $config
195            );
196            $mappingBuilder = new ArchiveMappingConfigBuilder(
197                $context['optimizeForHighlighter'],
198                $context['plugins'],
199                $context['flags'],
200                $config
201            );
202        } else {
203            // Content or general index
204            $analysisBuilder = new AnalysisConfigBuilder(
205                $context['langCode'],
206                $context['plugins'],
207                $config
208            );
209            $mappingBuilder = new MappingConfigBuilder(
210                $context['optimizeForHighlighter'],
211                $context['plugins'],
212                $context['flags'],
213                $config
214            );
215        }
216
217        // Build analysis and mappings
218        $analysisConfig = $analysisBuilder->buildConfig();
219        $mappings = $mappingBuilder->buildConfig();
220
221        // Build complete settings structure
222        $settings = $this->buildCompleteSettings( $analysisConfig, $indexSuffix, $context );
223
224        return [
225            'settings' => $settings,
226            'mappings' => $mappings
227        ];
228    }
229
230    /**
231     * Construct complete settings structure matching IndexCreator
232     *
233     * @param array $analysisConfig Analysis configuration (analyzers, tokenizers, filters)
234     * @param string $indexSuffix Index suffix for getting type-specific settings
235     * @param array $context Build context from getBuildContext()
236     * @return array Complete settings structure
237     */
238    private function buildCompleteSettings( array $analysisConfig, string $indexSuffix, array $context ): array {
239        $config = $context['config'];
240        $conn = $context['conn'];
241
242        // Get shard count for this specific index type
243        $shardCount = $conn->getSettings()->getShardCount( $indexSuffix );
244
245        // Get replica count for this index type
246        $replicas = $context['replicas'];
247        $replicaCount = is_array( $replicas ) && isset( $replicas[$indexSuffix] )
248            ? $replicas[$indexSuffix]
249            : '0-2';
250
251        // Build similarity config
252        $analysisBuilder = new AnalysisConfigBuilder(
253            $context['langCode'],
254            $context['plugins'],
255            $config
256        );
257        $similarityConfig = $analysisBuilder->buildSimilarityConfig();
258
259        // Base settings structure
260        $indexSettings = [
261            'number_of_shards' => $shardCount,
262            'auto_expand_replicas' => $replicaCount,
263            'refresh_interval' => $context['refreshInterval'] . 's',
264            'analysis' => $analysisConfig,
265            'query' => [
266                'default_field' => 'all'
267            ],
268        ];
269
270        // Add similarity if present
271        if ( $similarityConfig ) {
272            $indexSettings['similarity'] = $similarityConfig;
273        }
274
275        // Add merge settings if configured
276        $mergeSettings = $config->get( 'CirrusSearchMergeSettings' );
277        if ( is_array( $mergeSettings ) && isset( $mergeSettings[$indexSuffix] ) ) {
278            $indexSettings['merge'] = [ 'policy' => $mergeSettings[$indexSuffix] ];
279        }
280
281        // Wrap in 'index' key
282        $settings = [ 'index' => $indexSettings ];
283
284        // Add extra settings if configured
285        $extraSettings = $config->get( 'CirrusSearchExtraIndexSettings' );
286        if ( is_array( $extraSettings ) ) {
287            $settings = array_merge( $settings, $extraSettings );
288        }
289
290        return $settings;
291    }
292
293    /** @inheritDoc */
294    public function getAllowedParams() {
295        return [
296            'build' => [
297                ParamValidator::PARAM_DEFAULT => false,
298                ParamValidator::PARAM_TYPE => 'boolean',
299            ],
300            'plugins' => [
301                ParamValidator::PARAM_TYPE => 'string',
302                ParamValidator::PARAM_ISMULTI => true,
303                ParamValidator::PARAM_DEFAULT => null,
304                ParamValidator::PARAM_ALLOW_DUPLICATES => false,
305            ],
306        ];
307    }
308
309    /**
310     * Mark as internal. This isn't meant to be used by normal api users
311     * @return bool
312     */
313    public function isInternal() {
314        return true;
315    }
316
317    /**
318     * @see ApiBase::getExamplesMessages
319     * @return array
320     */
321    protected function getExamplesMessages() {
322        return [
323            'action=cirrus-schema-dump' =>
324                'apihelp-cirrus-schema-dump-example',
325            'action=cirrus-schema-dump&build=true' =>
326                'apihelp-cirrus-schema-dump-example-build',
327            'action=cirrus-schema-dump&build=true&plugins=analysis-icu|extra-analysis-textify' =>
328                'apihelp-cirrus-schema-dump-example-build-plugins',
329        ];
330    }
331
332}