Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ElasticSearchTTMServer.php
Go to the documentation of this file.
1<?php
11use Elastica\Aggregation\Terms;
12use Elastica\Client;
13use Elastica\Document;
14use Elastica\Exception\ExceptionInterface;
15use Elastica\Query;
16use Elastica\Query\BoolQuery;
17use Elastica\Query\FunctionScore;
18use Elastica\Query\MatchQuery;
19use Elastica\Query\Term;
20use Elastica\ResultSet;
21use MediaWiki\Extension\Elastica\MWElasticUtils;
28use MediaWiki\Logger\LoggerFactory;
29use MediaWiki\MediaWikiServices;
30use MediaWiki\Title\Title;
31use MediaWiki\WikiMap\WikiMap;
32
39 extends TTMServer
41{
47 private const BULK_INDEX_RETRY_ATTEMPTS = 5;
48
54 private const WAIT_UNTIL_READY_TIMEOUT = 3600;
55
57 protected $client;
61 protected $logger;
65 protected $updateMapping = false;
66
67 public function isLocalSuggestion( array $suggestion ): bool {
68 return $suggestion['wiki'] === WikiMap::getCurrentWikiId();
69 }
70
71 public function expandLocation( array $suggestion ): string {
72 return $suggestion['uri'];
73 }
74
75 public function query( string $sourceLanguage, string $targetLanguage, string $text ): array {
76 try {
77 return $this->doQuery( $sourceLanguage, $targetLanguage, $text );
78 } catch ( Exception $e ) {
79 throw new TranslationHelperException( 'Elastica exception: ' . $e );
80 }
81 }
82
83 protected function doQuery( $sourceLanguage, $targetLanguage, $text ) {
84 if ( !$this->useWikimediaExtraPlugin() ) {
85 // ElasticTTM is currently not compatible with elasticsearch 2.x/5.x
86 // It needs FuzzyLikeThis ported via the wmf extra plugin
87 throw new RuntimeException( 'The wikimedia extra plugin is mandatory.' );
88 }
89 /* Two query system:
90 * 1) Find all strings in source language that match text
91 * 2) Do another query for translations for those strings
92 */
93 $connection = $this->getClient()->getConnection();
94 $oldTimeout = $connection->getTimeout();
95 $connection->setTimeout( 10 );
96
97 $fuzzyQuery = new FuzzyLikeThis();
98 $fuzzyQuery->setLikeText( $text );
99 $fuzzyQuery->addFields( [ 'content' ] );
100
101 $boostQuery = new FunctionScore();
102 $boostQuery->addFunction(
103 'levenshtein_distance_score',
104 [
105 'text' => $text,
106 'field' => 'content'
107 ]
108 );
109 $boostQuery->setBoostMode( FunctionScore::BOOST_MODE_REPLACE );
110
111 // Wrap the fuzzy query so it can be used as a filter.
112 // This is slightly faster, as ES can throw away the scores by this query.
113 $bool = new BoolQuery();
114 $bool->addFilter( $fuzzyQuery );
115 $bool->addMust( $boostQuery );
116
117 $languageFilter = new Term();
118 $languageFilter->setTerm( 'language', $sourceLanguage );
119 $bool->addFilter( $languageFilter );
120
121 // The whole query
122 $query = new Query();
123 $query->setQuery( $bool );
124
125 // The interface usually displays three best candidates. These might
126 // come from more than three source things, if the translations are
127 // the same. In other words suggestions are grouped by the suggested
128 // translation. This algorithm might not find all suggestions, if the
129 // top N best matching source texts don't have equivalent translations
130 // in the target language, but worse matches which we did not fetch do.
131 // This code tries to balance between doing too many or too big queries
132 // and not fetching enough results to show all possible suggestions.
133 $sizeFirst = 100;
134 $sizeSecond = $sizeFirst * 5;
135
136 $query->setFrom( 0 );
137 $query->setSize( $sizeFirst );
138 $query->setParam( '_source', [ 'content' ] );
139 $cutoff = $this->config['cutoff'] ?? 0.65;
140 $query->setParam( 'min_score', $cutoff );
141 $query->setSort( [ '_score', 'wiki', 'localid' ] );
142
143 /* This query is doing two unrelated things:
144 * 1) Collect the message contents and scores so that they can
145 * be accessed later for the translations we found.
146 * 2) Build the query string for the query that fetches the translations.
147 */
148 $contents = $scores = $terms = [];
149 do {
150 $resultset = $this->getIndex()->search( $query );
151
152 if ( count( $resultset ) === 0 ) {
153 break;
154 }
155
156 foreach ( $resultset->getResults() as $result ) {
157 $data = $result->getData();
158 $score = $result->getScore();
159
160 $sourceId = preg_replace( '~/[^/]+$~', '', $result->getId() );
161 $contents[$sourceId] = $data['content'];
162 $scores[$sourceId] = $score;
163 $terms[] = "$sourceId/$targetLanguage";
164 }
165
166 // Check if it looks like that we are hitting the long tail already.
167 // Otherwise, we'll do a query to fetch some more to reach a "sane"
168 // breaking point, i.e. include all suggestions with same content
169 // for reliable used X times statistics.
170 if ( count( array_unique( $scores ) ) > 5 ) {
171 break;
172 }
173
174 // Okay, We are now in second iteration of the loop. We already got
175 // lots of suggestions. We will give up for now even if it means we
176 // return in some sense incomplete results.
177 if ( count( $resultset ) === $sizeSecond ) {
178 break;
179 }
180
181 // After the first query, the smallest score is the new threshold.
182 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable
183 $query->setParam( 'min_score', $score );
184 $query->setFrom( $query->getParam( 'size' ) + $query->getParam( 'from' ) );
185 $query->setSize( $sizeSecond );
186
187 // Break if we already got all hits
188 } while ( $resultset->getTotalHits() > count( $contents ) );
189
190 $suggestions = [];
191
192 // Skip second query if first query found nothing. Keeping only one return
193 // statement in this method to avoid forgetting to reset connection timeout
194 if ( $terms !== [] ) {
195 $idQuery = new Query\Terms( '_id', $terms );
196
197 $query = new Query( $idQuery );
198 $query->setSize( 25 );
199 $query->setParam( '_source', [ 'wiki', 'uri', 'content', 'localid' ] );
200 $resultset = $this->getIndex()->search( $query );
201
202 foreach ( $resultset->getResults() as $result ) {
203 $data = $result->getData();
204
205 // Construct the matching source id
206 $sourceId = preg_replace( '~/[^/]+$~', '', $result->getId() );
207
208 $suggestions[] = [
209 'source' => $contents[$sourceId],
210 'target' => $data['content'],
211 'context' => $data['localid'],
212 'quality' => $scores[$sourceId],
213 'wiki' => $data['wiki'],
214 'location' => $data['localid'] . '/' . $targetLanguage,
215 'uri' => $data['uri'],
216 ];
217 }
218
219 // Ensure results are in quality order
220 uasort( $suggestions, static function ( $a, $b ) {
221 if ( $a['quality'] === $b['quality'] ) {
222 return 0;
223 }
224
225 return ( $a['quality'] < $b['quality'] ) ? 1 : -1;
226 } );
227 }
228
229 $connection->setTimeout( $oldTimeout );
230
231 return $suggestions;
232 }
233
234 /* Write functions */
235
236 public function update( MessageHandle $handle, ?string $targetText ): bool {
237 if ( !$handle->isValid() || $handle->getCode() === '' ) {
238 return false;
239 }
240
241 /* There are various different cases here:
242 * [new or updated] [fuzzy|non-fuzzy] [translation|definition]
243 * 1) We don't distinguish between new or updated here.
244 * 2) Delete old translation, but not definition
245 * 3) Insert new translation or definition, if non-fuzzy
246 * The definition should never be fuzzied anyway.
247 *
248 * These only apply to known messages.
249 */
250
251 $sourceLanguage = $handle->getGroup()->getSourceLanguage();
252
253 // Do not delete definitions, because the translations are attached to that
254 if ( $handle->getCode() !== $sourceLanguage ) {
255 $localid = $handle->getTitleForBase()->getPrefixedText();
256 $this->deleteByQuery( $this->getIndex(), Query::create(
257 ( new BoolQuery() )
258 ->addFilter( new Term( [ 'wiki' => WikiMap::getCurrentWikiId() ] ) )
259 ->addFilter( new Term( [ 'language' => $handle->getCode() ] ) )
260 ->addFilter( new Term( [ 'localid' => $localid ] ) ) ) );
261 }
262
263 // If translation was made fuzzy, we do not need to add anything
264 if ( $targetText === null ) {
265 return true;
266 }
267
268 // source language is null, skip doing rest of the stuff
269 if ( $sourceLanguage === null ) {
270 return true;
271 }
272
273 $revId = $handle->getTitleForLanguage( $sourceLanguage )->getLatestRevID();
274 $doc = $this->createDocument( $handle, $targetText, $revId );
275 $fname = __METHOD__;
276
277 MWElasticUtils::withRetry( self::BULK_INDEX_RETRY_ATTEMPTS,
278 function () use ( $doc ) {
279 $this->getIndex()->addDocuments( [ $doc ] );
280 },
281 static function ( $e, $errors ) use ( $fname ) {
282 $c = get_class( $e );
283 $msg = $e->getMessage();
284 error_log( $fname . ": update failed ($c: $msg); retrying." );
285 sleep( 10 );
286 }
287 );
288
289 return true;
290 }
291
298 protected function createDocument( MessageHandle $handle, $text, $revId ) {
299 $language = $handle->getCode();
300
301 $localid = $handle->getTitleForBase()->getPrefixedText();
302 $wiki = WikiMap::getCurrentWikiId();
303 $globalid = "$wiki-$localid-$revId/$language";
304
305 $data = [
306 'wiki' => $wiki,
307 'uri' => $handle->getTitle()->getCanonicalURL(),
308 'localid' => $localid,
309 'language' => $language,
310 'content' => $text,
311 'group' => $handle->getGroupIds(),
312 ];
313
314 return new Document( $globalid, $data, '_doc' );
315 }
316
321 public function createIndex( $rebuild ) {
322 $indexSettings = [
323 'settings' => [
324 'index' => [
325 'number_of_shards' => $this->getShardCount(),
326 'analysis' => [
327 'filter' => [
328 'prefix_filter' => [
329 'type' => 'edge_ngram',
330 'min_gram' => 2,
331 'max_gram' => 20
332 ]
333 ],
334 'analyzer' => [
335 'prefix' => [
336 'type' => 'custom',
337 'tokenizer' => 'standard',
338 'filter' => [ 'lowercase', 'prefix_filter' ]
339 ],
340 'casesensitive' => [
341 'tokenizer' => 'standard'
342 ]
343 ]
344 ]
345 ],
346 ],
347 ];
348 $replicas = $this->getReplicaCount();
349 $key = str_contains( $replicas, '-' ) ? 'auto_expand_replicas' : 'number_of_replicas';
350 $indexSettings['settings']['index'][$key] = $replicas;
351
352 $this->getIndex()->create( $indexSettings, $rebuild );
353 }
354
360 public function beginBootstrap(): void {
361 $this->checkElasticsearchVersion();
362 $index = $this->getIndex();
363 if ( $this->updateMapping ) {
364 $this->logOutput( 'Updating the index mappings...' );
365 $this->createIndex( true );
366 } elseif ( !$index->exists() ) {
367 $this->createIndex( false );
368 }
369
370 $settings = $index->getSettings();
371 $settings->setRefreshInterval( '-1' );
372
373 $this->deleteByQuery( $this->getIndex(), Query::create(
374 ( new Term() )->setTerm( 'wiki', WikiMap::getCurrentWikiId() ) ) );
375
376 $properties = [
377 'wiki' => [ 'type' => 'keyword' ],
378 'localid' => [ 'type' => 'keyword' ],
379 'uri' => [ 'type' => 'keyword' ],
380 'language' => [ 'type' => 'keyword' ],
381 'group' => [ 'type' => 'keyword' ],
382 'content' => [
383 'type' => 'text',
384 'fields' => [
385 'content' => [
386 'type' => 'text',
387 'term_vector' => 'yes'
388 ],
389 'prefix_complete' => [
390 'type' => 'text',
391 'analyzer' => 'prefix',
392 'search_analyzer' => 'standard',
393 'term_vector' => 'yes'
394 ],
395 'case_sensitive' => [
396 'type' => 'text',
397 'analyzer' => 'casesensitive',
398 'term_vector' => 'yes'
399 ]
400 ]
401 ],
402 ];
403 if ( $this->useElastica6() ) {
404 // Elastica 6 support
405 // @phan-suppress-next-line PhanUndeclaredClassMethod
406 $mapping = new \Elastica\Type\Mapping();
407 // @phan-suppress-next-line PhanUndeclaredMethod, PhanUndeclaredClassMethod
408 $mapping->setType( $index->getType( '_doc' ) );
409 // @phan-suppress-next-line PhanUndeclaredClassMethod
410 $mapping->setProperties( $properties );
411 // @phan-suppress-next-line PhanUndeclaredClassMethod
412 $mapping->send( [ 'include_type_name' => 'true' ] );
413 } else {
414 // Elastica 7
415 $mapping = new \Elastica\Mapping( $properties );
416 $mapping->send( $index, [ 'include_type_name' => 'false' ] );
417 }
418
419 $this->waitUntilReady();
420 }
421
422 public function beginBatch(): void {
423 }
424
429 public function batchInsertDefinitions( array $batch ): void {
430 $lb = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch();
431 foreach ( $batch as $data ) {
432 $lb->addObj( $data[0]->getTitle() );
433 }
434 $lb->execute();
435
436 $this->batchInsertTranslations( $batch );
437 }
438
439 public function batchInsertTranslations( array $batch ): void {
440 $docs = [];
441 foreach ( $batch as $data ) {
442 [ $handle, $sourceLanguage, $text ] = $data;
443 $revId = $handle->getTitleForLanguage( $sourceLanguage )->getLatestRevID();
444 $docs[] = $this->createDocument( $handle, $text, $revId );
445 }
446
447 MWElasticUtils::withRetry( self::BULK_INDEX_RETRY_ATTEMPTS,
448 function () use ( $docs ) {
449 $this->getIndex()->addDocuments( $docs );
450 },
451 function ( $e, $errors ) {
452 $c = get_class( $e );
453 $msg = $e->getMessage();
454 $this->logOutput( "Batch failed ($c: $msg), trying again in 10 seconds" );
455 sleep( 10 );
456 }
457 );
458 }
459
460 public function endBatch(): void {
461 }
462
463 public function endBootstrap(): void {
464 $index = $this->getIndex();
465 $index->refresh();
466 $index->forcemerge();
467 $index->getSettings()->setRefreshInterval( '5s' );
468 }
469
470 public function getClient() {
471 if ( !$this->client ) {
472 if ( isset( $this->config['config'] ) ) {
473 $this->client = new Client( $this->config['config'] );
474 } else {
475 $this->client = new Client();
476 }
477 }
478 return $this->client;
479 }
480
482 public function useWikimediaExtraPlugin() {
483 return isset( $this->config['use_wikimedia_extra'] ) && $this->config['use_wikimedia_extra'];
484 }
485
487 private function getIndexName() {
488 return $this->config['index'] ?? 'ttmserver';
489 }
490
491 public function getIndex() {
492 return $this->getClient()
493 ->getIndex( $this->getIndexName() );
494 }
495
496 protected function getShardCount() {
497 return $this->config['shards'] ?? 1;
498 }
499
500 protected function getReplicaCount() {
501 return $this->config['replicas'] ?? '0-2';
502 }
503
504 protected function waitUntilReady() {
505 $statuses = MWElasticUtils::waitForGreen(
506 $this->getClient(),
507 $this->getIndexName(),
508 self::WAIT_UNTIL_READY_TIMEOUT );
509 $this->logOutput( "Waiting for the index to go green..." );
510 foreach ( $statuses as $message ) {
511 $this->logOutput( $message );
512 }
513
514 if ( !$statuses->getReturn() ) {
515 die( "Timeout! Please check server logs for {$this->getIndexName()}." );
516 }
517 }
518
519 public function setLogger( $logger ) {
520 $this->logger = $logger;
521 }
522
523 // Can it get any uglier?
524 protected function logOutput( $text ) {
525 if ( $this->logger ) {
526 $this->logger->statusLine( "$text\n" );
527 }
528 }
529
530 public function setDoReIndex(): void {
531 $this->updateMapping = true;
532 }
533
540 protected function parseQueryString( $queryString, array $opts ) {
541 $fields = $highlights = [];
542 $terms = preg_split( '/\s+/', $queryString );
543 $match = $opts['match'];
544 $case = $opts['case'];
545
546 // Map each word in the query string with its corresponding field
547 foreach ( $terms as $term ) {
548 $prefix = strstr( $term, '*', true );
549 if ( $prefix ) {
550 // For wildcard search
551 $fields['content.prefix_complete'][] = $prefix;
552 } elseif ( $case === '1' ) {
553 // For case-sensitive search
554 $fields['content.case_sensitive'][] = $term;
555 } else {
556 $fields['content'][] = $term;
557 }
558 }
559
560 // Allow searching either by message content or message id (page name
561 // without language subpage) with exact match only.
562 $searchQuery = new BoolQuery();
563 foreach ( $fields as $analyzer => $words ) {
564 foreach ( $words as $word ) {
565 $boolQuery = new BoolQuery();
566 $contentQuery = new MatchQuery();
567 $contentQuery->setFieldQuery( $analyzer, $word );
568 $boolQuery->addShould( $contentQuery );
569 $messageQuery = new Term();
570 $messageQuery->setTerm( 'localid', $word );
571 $boolQuery->addShould( $messageQuery );
572
573 if ( $match === 'all' ) {
574 $searchQuery->addMust( $boolQuery );
575 } else {
576 $searchQuery->addShould( $boolQuery );
577 }
578
579 // Fields for highlighting
580 $highlights[$analyzer] = [
581 'number_of_fragments' => 0
582 ];
583
584 // Allow searching by exact message title (page name with
585 // language subpage).
586 $title = Title::newFromText( $word );
587 if ( !$title ) {
588 continue;
589 }
590 $handle = new MessageHandle( $title );
591 if ( $handle->isValid() && $handle->getCode() !== '' ) {
592 $localid = $handle->getTitleForBase()->getPrefixedText();
593 $boolQuery = new BoolQuery();
594 $messageId = new Term();
595 $messageId->setTerm( 'localid', $localid );
596 $boolQuery->addMust( $messageId );
597 $searchQuery->addShould( $boolQuery );
598 }
599 }
600 }
601
602 return [ $searchQuery, $highlights ];
603 }
604
612 public function createSearch( $queryString, $opts, $highlight ) {
613 $query = new Query();
614
615 [ $searchQuery, $highlights ] = $this->parseQueryString( $queryString, $opts );
616 $query->setQuery( $searchQuery );
617
618 $language = new Terms( 'language' );
619 $language->setField( 'language' );
620 $language->setSize( 500 );
621 $query->addAggregation( $language );
622
623 $group = new Terms( 'group' );
624 $group->setField( 'group' );
625 // Would like to prioritize the top level groups and not show subgroups
626 // if the top group has only few hits, but that doesn't seem to be possile.
627 $group->setSize( 500 );
628 $query->addAggregation( $group );
629
630 $query->setSize( $opts['limit'] );
631 $query->setFrom( $opts['offset'] );
632
633 // BoolAnd filters are executed in sequence per document. Bool filters with
634 // multiple must clauses are executed by converting each filter into a bit
635 // field then anding them together. The latter is normally faster if either
636 // of the subfilters are reused. May not make a difference in this context.
637 $filters = new BoolQuery();
638
639 $language = $opts['language'];
640 if ( $language !== '' ) {
641 $languageFilter = new Term();
642 $languageFilter->setTerm( 'language', $language );
643 $filters->addFilter( $languageFilter );
644 }
645
646 $group = $opts['group'];
647 if ( $group !== '' ) {
648 $groupFilter = new Term();
649 $groupFilter->setTerm( 'group', $group );
650 $filters->addFilter( $groupFilter );
651 }
652
653 // Check that we have at least one filter to avoid invalid query errors.
654 if ( $language !== '' || $group !== '' ) {
655 // TODO: This seems wrong, but perhaps for aggregation purposes?
656 // should make $search a must clause and use the bool query
657 // as main.
658 $query->setPostFilter( $filters );
659 }
660
661 [ $pre, $post ] = $highlight;
662 $query->setHighlight( [
663 // The value must be an object
664 'pre_tags' => [ $pre ],
665 'post_tags' => [ $post ],
666 'fields' => $highlights,
667 ] );
668
669 return $this->getIndex()->createSearch( $query );
670 }
671
680 public function search( $queryString, $opts, $highlight ) {
681 $search = $this->createSearch( $queryString, $opts, $highlight );
682
683 try {
684 return $search->search();
685 } catch ( ExceptionInterface $e ) {
686 throw new TtmServerException( $e->getMessage() );
687 }
688 }
689
691 public function getFacets( $resultset ): array {
692 $this->assertResultSetInstance( $resultset );
693 $aggs = $resultset->getAggregations();
694 '@phan-var array[][][] $aggs';
695
696 $ret = [
697 'language' => [],
698 'group' => []
699 ];
700
701 foreach ( $aggs as $type => $info ) {
702 foreach ( $info['buckets'] as $row ) {
703 $ret[$type][$row['key']] = $row['doc_count'];
704 }
705 }
706
707 return $ret;
708 }
709
711 public function getTotalHits( $resultset ): int {
712 $this->assertResultSetInstance( $resultset );
713 return $resultset->getTotalHits();
714 }
715
717 public function getDocuments( $resultset ): array {
718 $this->assertResultSetInstance( $resultset );
719 $ret = [];
720 foreach ( $resultset->getResults() as $document ) {
721 $data = $document->getData();
722 $hl = $document->getHighlights();
723 if ( isset( $hl['content.prefix_complete'][0] ) ) {
724 $data['content'] = $hl['content.prefix_complete'][0];
725 } elseif ( isset( $hl['content.case_sensitive'][0] ) ) {
726 $data['content'] = $hl['content.case_sensitive'][0];
727 } elseif ( isset( $hl['content'][0] ) ) {
728 $data['content'] = $hl['content'][0];
729 }
730 $ret[] = $data;
731 }
732
733 return $ret;
734 }
735
745 private function deleteByQuery( \Elastica\Index $index, Query $query ) {
746 try {
747 MWElasticUtils::deleteByQuery( $index, $query, /* $allowConflicts = */ true );
748 } catch ( Exception $e ) {
749 LoggerFactory::getInstance( 'ElasticSearchTTMServer' )->error(
750 'Problem encountered during deletion.',
751 [ 'exception' => $e ]
752 );
753
754 throw new RuntimeException( "Problem encountered during deletion.\n" . $e );
755 }
756 }
757
758 /* @throws RuntimeException */
759 private function getElasticsearchVersion(): string {
760 $response = $this->getClient()->request( '' );
761 if ( !$response->isOK() ) {
762 throw new \RuntimeException( "Cannot fetch elasticsearch version: " . $response->getError() );
763 }
764
765 $result = $response->getData();
766 if ( !isset( $result['version']['number'] ) ) {
767 throw new \RuntimeException( 'Unable to determine elasticsearch version, aborting.' );
768 }
769
770 return $result[ 'version' ][ 'number' ];
771 }
772
773 private function checkElasticsearchVersion() {
774 $version = $this->getElasticsearchVersion();
775 if ( !str_starts_with( $version, '6.8' ) && !str_starts_with( $version, '7.' ) ) {
776 throw new \RuntimeException( "Only Elasticsearch 6.8.x and 7.x are supported. Your version: $version." );
777 }
778 }
779
780 private function useElastica6(): bool {
781 return class_exists( '\Elastica\Type' );
782 }
783
784 private function assertResultSetInstance( $resultset ): void {
785 if ( $resultset instanceof ResultSet ) {
786 return;
787 }
788
789 throw new RuntimeException(
790 "Expected resultset to be an instance of " . ResultSet::class
791 );
792 }
793}
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager( $services->getTitleFactory());}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getDBLoadBalancer());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance( 'Translate.MessageGroupSubscription'), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):MessageGroupSubscriptionHookHandler { return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getDBLoadBalancerFactory());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance( 'Translate'), $services->getMainObjectStash(), $services->getDBLoadBalancerFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getDBLoadBalancer());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup(), $services->getNamespaceInfo(), $services->getTitleFactory());}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getDBLoadBalancerFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getDBLoadBalancer(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'));}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getDBLoadBalancerFactory(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getDBLoadBalancer(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
TTMServer backed based on ElasticSearch.
createDocument(MessageHandle $handle, $text, $revId)
batchInsertTranslations(array $batch)
Called multiple times per batch if necessary.
$logger
Reference to the maintenance script to relay logging output.
getFacets( $resultset)
@inheritDoc
endBatch()
Called after every batch (MessageGroup).
search( $queryString, $opts, $highlight)
Search interface.
update(MessageHandle $handle, ?string $targetText)
Shovels the new translation into translation memory.
beginBootstrap()
Begin the bootstrap process.
createIndex( $rebuild)
Create index.
getTotalHits( $resultset)
@inheritDoc
getDocuments( $resultset)
@inheritDoc
beginBatch()
Called before every batch (MessageGroup).
createSearch( $queryString, $opts, $highlight)
Search interface.
setDoReIndex()
Instruct the service to fully wipe the index and start from scratch.
query(string $sourceLanguage, string $targetLanguage, string $text)
Fetches all relevant suggestions for given text.
parseQueryString( $queryString, array $opts)
Parse query string and build the search query.
isLocalSuggestion(array $suggestion)
Determines if the suggestion returned by this TTMServer comes from this wiki or any other wiki.
expandLocation(array $suggestion)
Given suggestion returned by this TTMServer, constructs fully qualified URL to the location of the tr...
Fuzzy Like This query.
Class for pointing to messages, like Title class is for titles.
getGroupIds()
Returns all message group ids this message belongs to.
Translation helpers can throw this exception when they cannot do anything useful with the current mes...
Class to handle TTMServer specific exceptions.
Some general static methods for instantiating TTMServer and helpers.
Definition TTMServer.php:20
Interface for TTMServer that can be queried (=all of them).
Interface for TTMServer that can act as backend for translation search.
Interface for TTMServer that can be updated.