Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 288
0.00% covered (danger)
0.00%
0 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateOneSearchIndexConfig
0.00% covered (danger)
0.00%
0 / 281
0.00% covered (danger)
0.00%
0 / 30
2970
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 addSharedOptions
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
110
 updateVersions
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 validateIndex
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 createIndex
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexSettingsValidators
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 validateIndexSettings
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 validateAnalyzers
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 validateMapping
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 validateAlias
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 validateSpecificAlias
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
2
 validateAllAlias
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 validateIndexHasChanged
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getShardAllocationValidator
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 validateShardAllocation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pickAnalyzer
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 initMappingConfigBuilder
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getIndex
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSpecificIndexName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexAliasName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOldIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMergeSettings
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getShardCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReplicaCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMaxShardsPerNode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initAnalysisConfig
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 cleanupCreatedIndex
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 fatalError
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace CirrusSearch\Maintenance;
4
5use CirrusSearch\Connection;
6use CirrusSearch\ElasticaErrorHandler;
7use CirrusSearch\Maintenance\Validators\MappingValidator;
8use CirrusSearch\SearchConfig;
9use CirrusSearch\Util;
10use MediaWiki\Config\ConfigException;
11
12/**
13 * Update the search configuration on the search backend.
14 *
15 * This program is free software; you can redistribute it and/or modify
16 * it under the terms of the GNU General Public License as published by
17 * the Free Software Foundation; either version 2 of the License, or
18 * (at your option) any later version.
19 *
20 * This program is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * You should have received a copy of the GNU General Public License along
26 * with this program; if not, write to the Free Software Foundation, Inc.,
27 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
28 * http://www.gnu.org/copyleft/gpl.html
29 */
30
31$IP = getenv( 'MW_INSTALL_PATH' );
32if ( $IP === false ) {
33    $IP = __DIR__ . '/../../..';
34}
35require_once "$IP/maintenance/Maintenance.php";
36require_once __DIR__ . '/../includes/Maintenance/Maintenance.php';
37
38/**
39 * Update the elasticsearch configuration for this index.
40 */
41class UpdateOneSearchIndexConfig extends Maintenance {
42    /**
43     * @var string
44     */
45    private $indexSuffix;
46
47    /**
48     * @var bool Are we going to blow the index away and start from scratch?
49     */
50    private $startOver;
51
52    /**
53     * @var int
54     */
55    private $reindexChunkSize;
56
57    /**
58     * @var string
59     */
60    private $indexBaseName;
61
62    /**
63     * @var string
64     */
65    private $indexIdentifier;
66
67    /**
68     * @var bool
69     */
70    private $reindexAndRemoveOk;
71
72    /**
73     * @var int number of scan slices to use when reindexing
74     */
75    private $reindexSlices;
76
77    /**
78     * @var string language code we're building for
79     */
80    private $langCode;
81
82    /**
83     * @var bool prefix search on any term
84     */
85    private $prefixSearchStartsWithAny;
86
87    /**
88     * @var bool use suggestions on text fields
89     */
90    private $phraseSuggestUseText;
91
92    /**
93     * @var bool print config as it is being checked
94     */
95    private $printDebugCheckConfig;
96
97    /**
98     * @var float how much can the reindexed copy of an index is allowed to deviate from the current
99     * copy without triggering a reindex failure
100     */
101    private $reindexAcceptableCountDeviation;
102
103    /**
104     * @var array filtered analysis config
105     */
106    private $analysisConfig;
107
108    /**
109     * @var array list of available plugins
110     */
111    private $availablePlugins;
112
113    /**
114     * @var array
115     */
116    protected $bannedPlugins;
117
118    /**
119     * @var bool
120     */
121    protected $optimizeIndexForExperimentalHighlighter;
122
123    /**
124     * @var int
125     */
126    protected $refreshInterval;
127
128    /**
129     * @var string
130     */
131    protected $masterTimeout;
132
133    /**
134     * @var array
135     */
136    private $mapping = [];
137
138    /**
139     * @var array
140     */
141    private $similarityConfig;
142
143    /**
144     * @var bool true if the analysis config can be optimized
145     */
146    private $safeToOptimizeAnalysisConfig;
147
148    /** @var bool State flag indicating if we should attempt deleting the index we created */
149    private $canCleanupCreatedIndex = false;
150
151    public function __construct() {
152        parent::__construct();
153        $this->addDescription( "Update the configuration or contents of one search index. This always " .
154            "operates on a single cluster." );
155        $this->addOption( 'indexSuffix', 'Index to update.  Either content or general.', false, true );
156        $this->addOption( 'indexType', 'BC form of --indexSuffix', false, true );
157        self::addSharedOptions( $this );
158    }
159
160    /**
161     * @param Maintenance $maintenance
162     */
163    public static function addSharedOptions( $maintenance ) {
164        $maintenance->addOption( 'startOver', 'Blow away the identified index and rebuild it with ' .
165            'no data.' );
166        $maintenance->addOption( 'indexIdentifier', "Set the identifier of the index to work on.  " .
167            "You'll need this if you have an index in production serving queries and you have " .
168            "to alter some portion of its configuration that cannot safely be done without " .
169            "rebuilding it.  Once you specify a new indexIdentifier for this wiki you'll have to " .
170            "run this script with the same identifier each time.  Defaults to 'current' which " .
171            "infers the currently in use identifier.  You can also use 'now' to set the identifier " .
172            "to the current time in seconds which should give you a unique identifier.", false, true );
173        $maintenance->addOption( 'reindexAndRemoveOk', "If the alias is held by another index then " .
174            "reindex all documents from that index (via the alias) to this one, swing the " .
175            "alias to this index, and then remove other index.  Updates performed while this" .
176            "operation is in progress will be queued up in the job queue.  Defaults to false." );
177        $maintenance->addOption( 'reindexSlices', 'Number of slices to use in reindex. Roughly '
178            . 'equivalent to the level of indexing parallelism. Defaults to number of shards.', false, true );
179        $maintenance->addOption( 'reindexAcceptableCountDeviation', 'How much can the reindexed ' .
180            'copy of an index is allowed to deviate from the current copy without triggering a ' .
181            'reindex failure.  Defaults to 5%.', false, true );
182        $maintenance->addOption( 'reindexChunkSize', 'Documents per shard to reindex in a batch.   ' .
183            'Note when changing the number of shards that the old shard size is used, not the new ' .
184            'one.  If you see many errors submitting documents in bulk but the automatic retry as ' .
185            'singles works then lower this number.  Defaults to 100.', false, true );
186        $maintenance->addOption( 'baseName', 'What basename to use for all indexes, ' .
187            'defaults to wiki id', false, true );
188        $maintenance->addOption( 'debugCheckConfig', 'Print the configuration as it is checked ' .
189            'to help debug unexpected configuration mismatches.' );
190        $maintenance->addOption( 'justAllocation', 'Just validate the shard allocation settings.  Use ' .
191            "when you need to apply new cache warmers but want to be sure that you won't apply any other " .
192            'changes at an inopportune time.' );
193        $maintenance->addOption( 'fieldsToDelete', 'List of of comma separated field names to delete ' .
194            'while reindexing documents (defaults to empty)', false, true );
195        $maintenance->addOption( 'justMapping', 'Just try to update the mapping.' );
196        $maintenance->addOption( 'ignoreIndexChanged', 'Skip checking if the new index is different ' .
197            'from the old index.', false, false );
198    }
199
200    public function execute() {
201        $this->disablePoolCountersAndLogging();
202
203        $utils = new ConfigUtils( $this->getConnection()->getClient(), $this );
204
205        $this->indexSuffix = $this->getBackCompatOption( 'indexSuffix', 'indexType' );
206        $this->startOver = $this->getOption( 'startOver', false );
207        $this->indexBaseName = $this->getOption( 'baseName',
208            $this->getSearchConfig()->get( SearchConfig::INDEX_BASE_NAME ) );
209        $this->reindexAndRemoveOk = $this->getOption( 'reindexAndRemoveOk', false );
210        $this->reindexSlices = $this->getOption( 'reindexSlices', null );
211        $this->reindexAcceptableCountDeviation = Util::parsePotentialPercent(
212            $this->getOption( 'reindexAcceptableCountDeviation', '5%' ) );
213        $this->reindexChunkSize = $this->getOption( 'reindexChunkSize', 100 );
214        $this->printDebugCheckConfig = $this->getOption( 'debugCheckConfig', false );
215        $this->langCode = $this->getSearchConfig()->get( "LanguageCode" );
216        $this->prefixSearchStartsWithAny = $this->getSearchConfig()->get( "CirrusSearchPrefixSearchStartsWithAnyWord" );
217        $this->phraseSuggestUseText = $this->getSearchConfig()->get( "CirrusSearchPhraseSuggestUseText" );
218        $this->bannedPlugins = $this->getSearchConfig()->get( "CirrusSearchBannedPlugins" );
219        $this->optimizeIndexForExperimentalHighlighter = $this->getSearchConfig()
220            ->get( "CirrusSearchOptimizeIndexForExperimentalHighlighter" );
221        $this->masterTimeout = $this->getSearchConfig()->get( "CirrusSearchMasterTimeout" );
222        $this->refreshInterval = $this->getSearchConfig()->get( "CirrusSearchRefreshInterval" );
223
224        if ( $this->indexSuffix === Connection::ARCHIVE_INDEX_SUFFIX ) {
225            if ( !$this->getSearchConfig()->get( 'CirrusSearchEnableArchive' ) ) {
226                $this->output( "Warning: Not allowing {$this->indexSuffix}, archives are disabled\n" );
227                return true;
228            }
229            if ( !$this->getConnection()->getSettings()->isPrivateCluster() ) {
230                $this->output( "Warning: Not allowing {$this->indexSuffix} on a non-private cluster\n" );
231                return true;
232            }
233        }
234
235        $this->initMappingConfigBuilder();
236
237        try {
238            $indexSuffixes = $this->getConnection()->getAllIndexSuffixes( null );
239            if ( !in_array( $this->indexSuffix, $indexSuffixes ) ) {
240                $this->fatalError( 'indexSuffix option must be one of ' .
241                    implode( ', ', $indexSuffixes ) );
242            }
243
244            $this->unwrap( $utils->checkElasticsearchVersion() );
245            $this->availablePlugins = $this->unwrap( $utils->scanAvailablePlugins( $this->bannedPlugins ) );
246
247            if ( $this->getOption( 'justAllocation', false ) ) {
248                $this->validateShardAllocation();
249                return true;
250            }
251
252            if ( $this->getOption( 'justMapping', false ) ) {
253                $this->validateMapping();
254                return true;
255            }
256
257            $this->initAnalysisConfig();
258            $this->indexIdentifier = $this->unwrap( $utils->pickIndexIdentifierFromOption(
259                $this->getOption( 'indexIdentifier', 'current' ), $this->getIndexAliasName() ) );
260            // At this point everything is initialized and we start to mutate the cluster
261            // This creates the index if needed, such as when --indexIdentifier=now is provided.
262            $this->validateIndex();
263            // Compares analyzers against expected. If the index is newly
264            // created this should do nothing. If the index was not created
265            // this may fail the build if it needs to be recreated.
266            $this->validateAnalyzers();
267            // Compares mapping against expected. Same behavior as analyzers,
268            // but some mapping changes can be applied to a live index.
269            $this->validateMapping();
270            // If we have a replacement index, check that it is actually different
271            // from the live index in some way. If they are the same then do nothing.
272            if ( !$this->validateIndexHasChanged() ) {
273                $this->cleanupCreatedIndex( "Cleaning up unnecessary index" );
274                // Orchestration needs some way to know that we are refusing to
275                // create the index. Simplest way is to signal with an arbitrary
276                // exit code.
277                $this->fatalError( "Use --ignoreIndexChanged to do it anyways", 10 );
278            }
279            // Makes sure the index is part of the production aliases. This will
280            // reindex into the new index if necessary, promote the new index,
281            // and delete the old index.
282            $this->validateAlias();
283            // Flag the index version information in metadata
284            $this->updateVersions();
285        } catch ( \Elastica\Exception\Connection\HttpException $e ) {
286            $message = $e->getMessage();
287            $this->output( "\nUnexpected Elasticsearch failure.\n" );
288            $this->fatalError( "Http error communicating with Elasticsearch:  $message.\n" );
289        } catch ( \Elastica\Exception\ExceptionInterface $e ) {
290            $type = get_class( $e );
291            $message = ElasticaErrorHandler::extractMessage( $e );
292            /** @suppress PhanUndeclaredMethod ExceptionInterface has no methods */
293            $trace = $e->getTraceAsString();
294            $this->output( "\nUnexpected Elasticsearch failure.\n" );
295            $this->fatalError( "Elasticsearch failed in an unexpected way. " .
296                "This is always a bug in CirrusSearch.\n" .
297                "Error type: $type\n" .
298                "Message: $message\n" .
299                "Trace:\n" . $trace );
300        }
301
302        return true;
303    }
304
305    /**
306     * @suppress PhanUndeclaredMethod runChild technically returns a
307     *  \Maintenance instance but only \CirrusSearch\Maintenance\Maintenance
308     *  classes have the done method. Just allow it since we know what type of
309     *  maint class is being created
310     */
311    private function updateVersions() {
312        $child = $this->runChild( Metastore::class );
313        $child->done();
314        $child->loadParamsAndArgs(
315            null,
316            array_merge( $this->parameters->getOptions(), [
317                'index-version-basename' => $this->indexBaseName,
318                'update-index-version' => true,
319            ] ),
320            $this->parameters->getArgs()
321        );
322        $child->execute();
323        $child->done();
324    }
325
326    private function validateIndex() {
327        if ( $this->startOver ) {
328            $this->createIndex( true, "Blowing away index to start over...\n" );
329        } elseif ( !$this->getIndex()->exists() ) {
330            $this->createIndex( false, "Creating index...\n" );
331        }
332
333        $this->validateIndexSettings();
334    }
335
336    /**
337     * @param bool $rebuild
338     * @param string $msg
339     */
340    private function createIndex( $rebuild, $msg ) {
341        $this->canCleanupCreatedIndex = true;
342        $index = $this->getIndex();
343        $indexCreator = new \CirrusSearch\Maintenance\IndexCreator(
344            $index,
345            new ConfigUtils( $index->getClient(), $this ),
346            $this->analysisConfig,
347            $this->mapping,
348            $this->similarityConfig,
349        );
350
351        $this->outputIndented( $msg );
352
353        $this->unwrap( $indexCreator->createIndex(
354            $rebuild,
355            $this->getMaxShardsPerNode(),
356            $this->getShardCount(),
357            $this->getReplicaCount(),
358            $this->refreshInterval,
359            $this->getMergeSettings(),
360            $this->getSearchConfig()->get( "CirrusSearchExtraIndexSettings" )
361        ) );
362
363        $this->outputIndented( "Index created.\n" );
364    }
365
366    /**
367     * @return \CirrusSearch\Maintenance\Validators\Validator[]
368     */
369    private function getIndexSettingsValidators() {
370        $validators = [];
371        $validators[] = new \CirrusSearch\Maintenance\Validators\NumberOfShardsValidator(
372            $this->getIndex(), $this->getShardCount(), $this );
373        $validators[] = new \CirrusSearch\Maintenance\Validators\ReplicaRangeValidator(
374            $this->getIndex(), $this->getReplicaCount(), $this );
375        $validators[] = $this->getShardAllocationValidator();
376        $validators[] = new \CirrusSearch\Maintenance\Validators\MaxShardsPerNodeValidator(
377            $this->getIndex(), $this->getMaxShardsPerNode(), $this );
378        return $validators;
379    }
380
381    private function validateIndexSettings() {
382        $validators = $this->getIndexSettingsValidators();
383        foreach ( $validators as $validator ) {
384            $this->unwrap( $validator->validate() );
385        }
386    }
387
388    private function validateAnalyzers() {
389        $validator = new \CirrusSearch\Maintenance\Validators\AnalyzersValidator(
390            $this->getIndex(), $this->analysisConfig, $this );
391        $validator->printDebugCheckConfig( $this->printDebugCheckConfig );
392        $this->unwrap( $validator->validate() );
393    }
394
395    private function validateMapping() {
396        $validator = new MappingValidator(
397            $this->getIndex(),
398            $this->masterTimeout,
399            $this->optimizeIndexForExperimentalHighlighter,
400            $this->availablePlugins,
401            $this->mapping,
402            $this
403        );
404        $validator->printDebugCheckConfig( $this->printDebugCheckConfig );
405        $this->unwrap( $validator->validate() );
406    }
407
408    private function validateAlias() {
409        $this->outputIndented( "Validating aliases...\n" );
410        // Since validate the specific alias first as that can cause reindexing
411        // and we want the all index to stay with the old index during reindexing
412        $this->validateSpecificAlias();
413        // At this point the index is live and under no circumstances should it be
414        // automatically deleted.
415        $this->canCleanupCreatedIndex = false;
416
417        if ( $this->indexSuffix !== Connection::ARCHIVE_INDEX_SUFFIX ) {
418            // Do not add the archive index to the global alias
419            $this->validateAllAlias();
420        }
421    }
422
423    /**
424     * Validate the alias that is just for this index's type.
425     */
426    private function validateSpecificAlias() {
427        $connection = $this->getConnection();
428
429        $fieldsToCleanup = array_filter( explode( ',', $this->getOption( 'fieldsToDelete', '' ) ) );
430        $fieldsToCleanup += $this->getSearchConfig()->get( "CirrusSearchIndexFieldsToCleanup" );
431        $reindexer = new Reindexer(
432            $this->getSearchConfig(),
433            $connection,
434            $connection,
435            $this->getIndex(),
436            $this->getOldIndex(),
437            $this,
438            $fieldsToCleanup
439        );
440
441        $validator = new \CirrusSearch\Maintenance\Validators\SpecificAliasValidator(
442            $this->getConnection()->getClient(),
443            $this->getIndexAliasName(),
444            $this->getSpecificIndexName(),
445            $this->startOver,
446            $reindexer,
447            [
448                $this->reindexSlices,
449                $this->reindexChunkSize,
450                $this->reindexAcceptableCountDeviation
451            ],
452            $this->getIndexSettingsValidators(),
453            $this->reindexAndRemoveOk,
454            $this
455        );
456        $this->unwrap( $validator->validate() );
457    }
458
459    public function validateAllAlias() {
460        $validator = new \CirrusSearch\Maintenance\Validators\IndexAllAliasValidator(
461            $this->getConnection()->getClient(),
462            $this->getIndexName(),
463            $this->getSpecificIndexName(),
464            $this->startOver,
465            $this->getIndexAliasName(),
466            $this
467        );
468        $this->unwrap( $validator->validate() );
469    }
470
471    public function validateIndexHasChanged(): bool {
472        if ( $this->getOption( 'ignoreIndexChanged' ) ) {
473            return true;
474        }
475        $validator = new \CirrusSearch\Maintenance\Validators\IndexHasChangedValidator(
476            $this->getConnection()->getClient(),
477            $this->getOldIndex(),
478            $this->getIndex(),
479            $this,
480        );
481        return $this->unwrap( $validator->validate() );
482    }
483
484    /**
485     * @return \CirrusSearch\Maintenance\Validators\Validator
486     */
487    private function getShardAllocationValidator() {
488        return new \CirrusSearch\Maintenance\Validators\ShardAllocationValidator(
489            $this->getIndex(), $this->getSearchConfig()->get( "CirrusSearchIndexAllocation" ), $this );
490    }
491
492    protected function validateShardAllocation() {
493        $this->unwrap( $this->getShardAllocationValidator()->validate() );
494    }
495
496    /**
497     * @param string $langCode
498     * @param array $availablePlugins
499     * @return AnalysisConfigBuilder
500     */
501    private function pickAnalyzer( $langCode, array $availablePlugins = [] ) {
502        $analysisConfigBuilder = new \CirrusSearch\Maintenance\AnalysisConfigBuilder(
503            $langCode, $availablePlugins );
504        $this->outputIndented( 'Picking analyzer...' .
505                                $analysisConfigBuilder->getDefaultTextAnalyzerType( $langCode ) .
506                                "\n" );
507        return $analysisConfigBuilder;
508    }
509
510    /**
511     * @throws ConfigException
512     */
513    protected function initMappingConfigBuilder() {
514        $configFlags = 0;
515        if ( $this->prefixSearchStartsWithAny ) {
516            $configFlags |= MappingConfigBuilder::PREFIX_START_WITH_ANY;
517        }
518        if ( $this->phraseSuggestUseText ) {
519            $configFlags |= MappingConfigBuilder::PHRASE_SUGGEST_USE_TEXT;
520        }
521        switch ( $this->indexSuffix ) {
522            case Connection::ARCHIVE_DOC_TYPE:
523                $mappingConfigBuilder = new ArchiveMappingConfigBuilder( $this->optimizeIndexForExperimentalHighlighter, $configFlags );
524                break;
525            default:
526                $mappingConfigBuilder = new MappingConfigBuilder( $this->optimizeIndexForExperimentalHighlighter, $configFlags );
527        }
528        $this->mapping = $mappingConfigBuilder->buildConfig();
529        $this->safeToOptimizeAnalysisConfig = $mappingConfigBuilder->canOptimizeAnalysisConfig();
530    }
531
532    /**
533     * @return \Elastica\Index being updated
534     */
535    public function getIndex() {
536        return $this->getConnection()->getIndex(
537            $this->indexBaseName, $this->indexSuffix, $this->indexIdentifier );
538    }
539
540    /**
541     * @return string name of the index being updated
542     */
543    protected function getSpecificIndexName() {
544        return $this->getConnection()->getIndexName(
545            $this->indexBaseName, $this->indexSuffix, $this->indexIdentifier );
546    }
547
548    /**
549     * @return string name of the index type being updated
550     */
551    protected function getIndexAliasName() {
552        return $this->getConnection()->getIndexName( $this->indexBaseName, $this->indexSuffix );
553    }
554
555    /**
556     * @return string
557     */
558    protected function getIndexName() {
559        return $this->getConnection()->getIndexName( $this->indexBaseName );
560    }
561
562    /**
563     * @return \Elastica\Index
564     */
565    protected function getOldIndex() {
566        return $this->getConnection()->getIndex( $this->indexBaseName, $this->indexSuffix );
567    }
568
569    /**
570     * Get the merge settings for this index.
571     * @return array
572     */
573    private function getMergeSettings() {
574        $mergeSettings = $this->getSearchConfig()->get( "CirrusSearchMergeSettings" );
575
576        return $mergeSettings[$this->indexSuffix]
577            // If there aren't configured merge settings for this index type
578            // default to the content type.
579            ?? $mergeSettings['content']
580            // It's also fine to not specify merge settings.
581            ?? [];
582    }
583
584    /**
585     * @return int Number of shards this index should have
586     */
587    private function getShardCount() {
588        return $this->getConnection()->getSettings()->getShardCount( $this->indexSuffix );
589    }
590
591    /**
592     * @return string Number of replicas this index should have. May be a range such as '0-2'
593     */
594    private function getReplicaCount() {
595        return $this->getConnection()->getSettings()->getReplicaCount( $this->indexSuffix );
596    }
597
598    /**
599     * @return int Maximum number of shards that can be allocated on a single elasticsearch
600     *  node. -1 for unlimited.
601     */
602    private function getMaxShardsPerNode() {
603        return $this->getConnection()->getSettings()->getMaxShardsPerNode( $this->indexSuffix );
604    }
605
606    private function initAnalysisConfig() {
607        $analysisConfigBuilder = $this->pickAnalyzer( $this->langCode, $this->availablePlugins );
608
609        $this->analysisConfig = $analysisConfigBuilder->buildConfig();
610        if ( $this->safeToOptimizeAnalysisConfig ) {
611            $filter = new AnalysisFilter();
612            $deduplicate = $this->getSearchConfig()->get( 'CirrusSearchDeduplicateAnalysis' );
613            // A bit adhoc, this is the list of analyzers that should not be renamed, because
614            // they are referenced at query time.
615            $protected = [ 'token_reverse' ];
616            [ $this->analysisConfig, $this->mapping ] = $filter
617                ->filterAnalysis( $this->analysisConfig, $this->mapping, $deduplicate, $protected );
618        }
619        $this->similarityConfig = $analysisConfigBuilder->buildSimilarityConfig();
620    }
621
622    private function cleanupCreatedIndex( $msg ) {
623        if ( $this->canCleanupCreatedIndex && $this->getIndex()->exists() ) {
624            $utils = new ConfigUtils( $this->getConnection()->getClient(), $this );
625            $indexName = $this->getSpecificIndexName();
626            $status = $utils->isIndexLive( $indexName );
627            if ( !$status->isGood() ) {
628                $this->output( (string)$status );
629            } elseif ( $status->getValue() === false ) {
630                $this->output( "$msg $indexName\n" );
631                $this->getIndex()->delete();
632            }
633        }
634    }
635
636    /**
637     * Output a message and terminate the current script.
638     *
639     * @param string $msg Error Message
640     * @param int $exitCode PHP exit status. Should be in range 1-254
641     * @return never
642     */
643    protected function fatalError( $msg, $exitCode = 1 ) {
644        try {
645            $this->cleanupCreatedIndex( "Cleaning up incomplete index" );
646        } catch ( \Elastica\Exception\ExceptionInterface $e ) {
647            $this->output( "Exception thrown while cleaning up created index: $e\n" );
648        } finally {
649            parent::fatalError( $msg, $exitCode );
650        }
651    }
652}
653
654$maintClass = UpdateOneSearchIndexConfig::class;
655require_once RUN_MAINTENANCE_IF_MAIN;