Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 118
0.00% covered (danger)
0.00%
0 / 1
CRAP
n/a
0 / 0
Wikibase\MediaInfo\Search\closureToAnonymousClass
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Wikibase\MediaInfo\Search;
4
5use CirrusSearch\Parser\FullTextKeywordRegistry;
6use CirrusSearch\SearchConfig;
7use MediaWiki\Context\RequestContext;
8use MediaWiki\MainConfigNames;
9use MediaWiki\MediaWikiServices;
10use UnexpectedValueException;
11use Wikibase\Repo\WikibaseRepo;
12
13if ( !function_exists( 'Wikibase\MediaInfo\Search\closureToAnonymousClass' ) ) {
14    /**
15     * This is a bit of a hack, turning a closure (or any callable) into
16     * an invokable anonymous function.
17     * It'll execute the callable just the same, but it also has a
18     * __toString, which will allow ApiResult::validateValue (used in
19     * ConfigDump to format this) to process the closure (which it is
20     * otherwise unable to do)
21     *
22     * @param callable $callable
23     * @return callable
24     */
25    function closureToAnonymousClass( callable $callable ) {
26        return new class ( $callable ) {
27            /** @var callable $callable */
28            private $callable;
29
30            public function __construct( callable $callable ) {
31                $this->callable = $callable;
32            }
33
34            public function __invoke( ...$args ) {
35                return ( $this->callable )( ...$args );
36            }
37
38            public function __toString() {
39                return self::class;
40            }
41        };
42    }
43}
44
45$mwServices = MediaWikiServices::getInstance();
46$config = $mwServices->getMainConfig();
47$profiles = $config->get( 'MediaInfoMediaSearchProfiles' ) ?: [];
48
49return array_map( static function ( array $settings ) use ( $config ) {
50    // parse default settings into profile settings, to ensure all expected
51    // settings have some (default) value if not explicitly specified
52    $defaultSettings = [
53        'boost' => [
54            'statement' => array_fill_keys( array_values( $config->get( 'MediaInfoProperties' ) ), 1 ),
55            'weighted_tags' => [],
56            'descriptions.$language' => 1,
57            'descriptions.$language.plain' => 1,
58            'title' => 1,
59            'title.plain' => 1,
60            'category' => 1,
61            'category.plain' => 1,
62            'heading' => 1,
63            'heading.plain' => 1,
64            'auxiliary_text' => 1,
65            'auxiliary_text.plain' => 1,
66            'file_text' => 1,
67            'file_text.plain' => 1,
68            'redirect.title' => 1,
69            'redirect.title.plain' => 1,
70            'text' => 1,
71            'text.plain' => 1,
72            'suggest' => 1,
73        ],
74        'decay' => [
75            'descriptions.$language' => 0.9,
76            'descriptions.$language.plain' => 0.9,
77            // below is not actually a field
78            'synonyms' => 0.5,
79        ],
80        'entitiesVariableBoost' => true,
81        'normalizeMultiClauseScores' => false,
82        'applyLogisticFunction' => false,
83        'useSynonyms' => false,
84        'logisticRegressionIntercept' => 0,
85        'entitySearchBaseUri' => $config->get( 'MediaInfoExternalEntitySearchBaseUri' ),
86        'titleMatchBaseUri' => $config->get( 'MediaInfoMediaSearchTitleMatchBaseUri' ),
87        'synonymsMaxAmount' => 5,
88        'synonymsMinScoreThreshold' => 0.5,
89        'synonymsMinByteLength' => 2,
90        'synonymsMinSimilarityToCanonicalForm' => 0.75,
91        'synonymsMinDifferenceFromOthers' => 0.25,
92        'weightedTagsMinScoreThreshold' => 0.5,
93        'nearMatchBoost' => 3.0,
94    ];
95    $settings = array_replace_recursive( $defaultSettings, $settings );
96
97    // work around '.' being replaced by '_' in query keys
98    $fixUnderscores = static function ( $underscored, $original ) use ( &$fixUnderscores ) {
99        $result = [];
100        foreach ( $underscored as $key => $value ) {
101            // build a regex where all underscores match either dot or underscore; rest has to be
102            // an exact match
103            $regex = '/^' . str_replace( '_', '[\._]', preg_quote( $key, '/' ) ) . '$/';
104            // then find a match in the expected keys
105            $matches = preg_grep( $regex, array_keys( $original ) );
106            if ( $matches ) {
107                $key = array_pop( $matches );
108            }
109            $result[ $key ] = is_array( $value ) ? $fixUnderscores( $value, $original[ $key ] ?? [] ) : $value;
110        }
111        return $result;
112    };
113
114    // allow settings (boost etc.) to be customized from URL query params
115    foreach ( RequestContext::getMain()->getRequest()->getQueryValues() as $key => $value ) {
116        // convert [ 'one:two' => 'three' ] into ['one']['two'] = 'three'
117        $flat = array_merge( explode( ':', urldecode( $key ) ), [ floatval( $value ) ] );
118        $result = array_reduce(
119            array_reverse( $flat ),
120            static function ( $previous, $key ) {
121                return $previous !== null ? [ $key => $previous ] : $key;
122            },
123            null
124        );
125
126        $settings = array_replace_recursive( $settings, $fixUnderscores( $result, $settings ) );
127    }
128
129    return [
130        'builder_factory' => closureToAnonymousClass( static function ( array $settings ) {
131            $languageCode = RequestContext::getMain()->getLanguage()->getCode();
132            $languageFallbackChain = WikibaseRepo::getLanguageFallbackChainFactory()
133                ->newFromLanguageCode( $languageCode );
134
135            $mwServices = MediaWikiServices::getInstance();
136            $config = $mwServices->getMainConfig();
137            $configFactory = $mwServices->getConfigFactory();
138            $searchConfig = $configFactory->makeConfig( 'CirrusSearch' );
139            if ( !$searchConfig instanceof SearchConfig ) {
140                throw new UnexpectedValueException( 'CirrusSearch config must be instanceof SearchConfig' );
141            }
142            $features = ( new FullTextKeywordRegistry( $searchConfig ) )->getKeywords();
143
144            $languages = array_merge( [ $languageCode ], $languageFallbackChain->getFetchLanguageCodes() );
145            $languages = array_unique( $languages );
146            $entitySearchBaseUri = sprintf( $settings[ 'entitySearchBaseUri' ], $languageCode );
147            $titleMatchBaseUri = sprintf( $settings[ 'titleMatchBaseUri' ], $languageCode );
148
149            $entitiesFetcher = new MediaSearchMemoryEntitiesFetcher(
150                new MediaSearchCachingEntitiesFetcher(
151                    new MediaSearchEntitiesFetcher(
152                        $mwServices->getHttpRequestFactory()->createMultiClient(),
153                        $entitySearchBaseUri,
154                        $titleMatchBaseUri,
155                        $languageCode,
156                        $config->get( MainConfigNames::LanguageCode )
157                    ),
158                    $mwServices->getMainWANObjectCache(),
159                    $languageCode,
160                    $config->get( MainConfigNames::LanguageCode ),
161                    $entitySearchBaseUri . '-' . $titleMatchBaseUri
162                )
163            );
164
165            return new MediaSearchQueryBuilder(
166                $features,
167                new MediaSearchASTQueryBuilder(
168                    new MediaSearchASTEntitiesExtractor( $entitiesFetcher ),
169                    $configFactory->makeConfig( 'WikibaseCirrusSearch' )->get( 'UseStemming' ),
170                    $languages,
171                    $config->get( MainConfigNames::LanguageCode ),
172                    $settings
173                )
174            );
175        } ),
176        'settings' => $settings,
177    ];
178}, $profiles );