Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
25.84% covered (danger)
25.84%
23 / 89
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
PhraseSuggesterProfileRepoWrapper
25.84% covered (danger)
25.84%
23 / 89
87.50% covered (warning)
87.50%
7 / 8
1111.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fromFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 repositoryType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 repositoryName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProfile
18.52% covered (danger)
18.52%
15 / 81
0.00% covered (danger)
0.00%
0 / 1
1091.33
 hasProfile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 listExposedProfiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace CirrusSearch\Profile;
4
5use CirrusSearch\Util;
6use MediaWiki\Config\Config;
7use Wikimedia\ObjectCache\BagOStuff;
8
9/**
10 * Wrapper to augment the phrase suggester profile settings
11 * with customization on-wiki using system messages.
12 */
13class PhraseSuggesterProfileRepoWrapper implements SearchProfileRepository {
14
15    private const MAX_ERRORS_HARD_LIMIT = 2;
16    private const MAX_TERM_FREQ_HARD_LIMIT = 0.6;
17    private const PREFIX_LENGTH_HARD_LIMIT = 2;
18    public const CIRRUSSEARCH_DIDYOUMEAN_SETTINGS = 'cirrussearch-didyoumean-settings';
19
20    /**
21     * @var string[]
22     */
23    private static $ALLOWED_MODE = [ 'missing', 'popular', 'always' ];
24
25    /**
26     * @var SearchProfileRepository
27     */
28    private $wrapped;
29
30    /**
31     * @var BagOStuff
32     */
33    private $bagOStuff;
34
35    public function __construct( SearchProfileRepository $wrapped, BagOStuff $bagOStuff ) {
36        $this->wrapped = $wrapped;
37        $this->bagOStuff = $bagOStuff;
38    }
39
40    /**
41     * @param string $type
42     * @param string $name
43     * @param string $phpFile
44     * @param BagOStuff $cache
45     * @return SearchProfileRepository
46     */
47    public static function fromFile( $type, $name, $phpFile, BagOStuff $cache ) {
48        return new self( ArrayProfileRepository::fromFile( $type, $name, $phpFile ), $cache );
49    }
50
51    /**
52     * @param string $type
53     * @param string $name
54     * @param string $configEntry
55     * @param Config $config
56     * @param BagOStuff $cache
57     * @return self
58     */
59    public static function fromConfig( $type, $name, $configEntry, Config $config, BagOStuff $cache ): self {
60        return new self( new ConfigProfileRepository( $type, $name, $configEntry, $config ), $cache );
61    }
62
63    /**
64     * The repository type
65     * @return string
66     */
67    public function repositoryType() {
68        return $this->wrapped->repositoryType();
69    }
70
71    /**
72     * The repository name
73     * @return string
74     */
75    public function repositoryName() {
76        return $this->wrapped->repositoryName();
77    }
78
79    /**
80     * Load a profile named $name
81     * @param string $name
82     * @return array|null the profile data or null if not found
83     */
84    public function getProfile( $name ) {
85        $settings = $this->wrapped->getProfile( $name );
86        if ( $settings === null ) {
87            return null;
88        }
89        $lines = $this->bagOStuff->getWithSetCallback(
90            $this->bagOStuff->makeKey( self::CIRRUSSEARCH_DIDYOUMEAN_SETTINGS ),
91            600,
92            static function () {
93                $source = wfMessage( 'cirrussearch-didyoumean-settings' )->inContentLanguage();
94                if ( $source->isDisabled() ) {
95                    return [];
96                }
97                return Util::parseSettingsInMessage( $source->plain() );
98            }
99        );
100
101        $laplaceAlpha = null;
102        $stupidBackoffDiscount = null;
103        foreach ( $lines as $line ) {
104            $linePieces = explode( ':', $line, 2 );
105            if ( count( $linePieces ) !== 2 ) {
106                // Skip improperly formatted lines without a key:value
107                continue;
108            }
109            [ $k, $v ] = $linePieces;
110
111            switch ( $k ) {
112                case 'max_errors':
113                    if ( is_numeric( $v ) && $v >= 1 && $v <= self::MAX_ERRORS_HARD_LIMIT ) {
114                        $settings['max_errors'] = floatval( $v );
115                    }
116                    break;
117                case 'confidence':
118                    if ( is_numeric( $v ) && $v >= 0 ) {
119                        $settings['confidence'] = floatval( $v );
120                    }
121                    break;
122                case 'max_term_freq':
123                    if ( is_numeric( $v ) && $v >= 0 && $v <= self::MAX_TERM_FREQ_HARD_LIMIT ) {
124                        $settings['max_term_freq'] = floatval( $v );
125                    }
126                    break;
127                case 'min_doc_freq':
128                    if ( is_numeric( $v ) && $v >= 0 && $v < 1 ) {
129                        $settings['min_doc_freq'] = floatval( $v );
130                    }
131                    break;
132                case 'prefix_length':
133                    if ( is_numeric( $v ) && $v >= 0 && $v <= self::PREFIX_LENGTH_HARD_LIMIT ) {
134                        $settings['prefix_length'] = intval( $v );
135                    }
136                    break;
137                case 'suggest_mode':
138                    if ( in_array( $v, self::$ALLOWED_MODE ) ) {
139                        $settings['mode'] = $v;
140                    }
141                    break;
142                case 'collate':
143                    if ( $v === 'true' ) {
144                        $settings['collate'] = true;
145                    } elseif ( $v === 'false' ) {
146                        $settings['collate'] = false;
147                    }
148                    break;
149                case 'smoothing':
150                    if ( $v === 'laplace' ) {
151                        $settings['smoothing_model'] = [
152                            'laplace' => [
153                                'alpha' => 0.5
154                            ]
155                        ];
156                    } elseif ( $v === 'stupid_backoff' ) {
157                        $settings['smoothing_model'] = [
158                            'stupid_backoff' => [
159                                'discount' => 0.4
160                            ]
161                        ];
162                    }
163                    break;
164                case 'laplace_alpha':
165                    if ( is_numeric( $v ) && $v >= 0 && $v <= 1 ) {
166                        $laplaceAlpha = floatval( $v );
167                    }
168                    break;
169                case 'stupid_backoff_discount':
170                    if ( is_numeric( $v ) && $v >= 0 && $v <= 1 ) {
171                        $stupidBackoffDiscount = floatval( $v );
172                    }
173                    break;
174            }
175        }
176
177        // Apply smoothing model options, if none provided we'll use elasticsearch defaults
178        if ( isset( $settings['smoothing_model']['laplace'] ) && $laplaceAlpha !== null ) {
179            $settings['smoothing_model']['laplace'] = [
180                'alpha' => $laplaceAlpha
181            ];
182        }
183        if ( isset( $settings['smoothing_model']['stupid_backoff'] ) && $stupidBackoffDiscount !== null ) {
184            $settings['smoothing_model']['stupid_backoff'] = [
185                'discount' => $stupidBackoffDiscount
186            ];
187        }
188        return $settings;
189    }
190
191    /**
192     * Check if a profile named $name exists in this repository
193     * @param string $name
194     * @return bool
195     */
196    public function hasProfile( $name ) {
197        return $this->wrapped->hasProfile( $name );
198    }
199
200    /**
201     * Get the list of profiles that we want to expose to the user.
202     *
203     * @return array[] list of profiles index by name
204     */
205    public function listExposedProfiles() {
206        return $this->wrapped->listExposedProfiles();
207    }
208}