Making WordPress.org

source: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-search.php @ 13652

Last change on this file since 13652 was 13652, checked in by dd32, 2 months ago

Revert [13648].

  • Property svn:eol-style set to native
File size: 12.5 KB
Line 
1<?php
2namespace WordPressdotorg\Plugin_Directory;
3
4// Hmm
5add_filter( 'option_has_jetpack_search_product', '__return_true' );
6
7/**
8 * Override Jetpack Search class with special features for the Plugin Directory
9 *
10 * @package WordPressdotorg\Plugin_Directory
11 */
12class Plugin_Search {
13
14        // Set this to true to disable the new class and use the old jetpack-search.php code.
15        const USE_OLD_SEARCH = false;
16
17        // Internal state - These are all overridden below, but here for reference purposes for a non-block english search.
18        protected $locale          = 'en_US';
19        protected $is_block_search = false;
20        protected $is_english      = true;
21        protected $en_boost        = 0.00001;
22        protected $desc_boost      = 1;
23        protected $desc_en_boost   = 0.00001;
24
25        /**
26         * Fetch the instance of the Plugin_Search class.
27         *
28         * @static
29         */
30        public static function instance() {
31                static $instance = null;
32
33                if ( ! $instance ) {
34                        $instance = new self();
35                }
36
37                return $instance;
38        }
39
40        /**
41         * Plugin_Search constructor.
42         *
43         * @access private
44         */
45        private function __construct() {
46                if ( isset( $_GET['s'] ) )
47                        return false;
48
49                add_action( 'init', array( $this, 'init' ) );
50
51                return false;
52        }
53
54        public function init() {
55
56                if ( self::USE_OLD_SEARCH ) {
57                        // Instantiate our copy of the Jetpack_Search class.
58                        if ( class_exists( 'Jetpack' ) && ! class_exists( 'Jetpack_Search' )
59                                && ! isset( $_GET['s'] ) ) { // Don't run the ES query if we're going to redirect to the pretty search URL
60                                        require_once __DIR__ . '/libs/site-search/jetpack-search.php';
61                                        \Jetpack_Search::instance();
62                        }
63                } else {
64                        add_filter( 'jetpack_get_module', array( $this, 'jetpack_get_module' ), 10, 2 );
65                        add_filter( 'option_jetpack_active_modules', array( $this, 'option_jetpack_active_modules' ), 10, 1 );
66                        add_filter( 'pre_option_has_jetpack_search_product', array( $this, 'option_has_jetpack_search_product' ), 10, 1 );
67
68                        // add_filter( 'jetpack_search_abort', array( $this, 'log_jetpack_search_abort' ) );
69
70                        // $es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
71
72                        add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'jetpack_search_es_wp_query_args' ), 10, 2 );
73                        add_filter( 'jetpack_search_es_query_args', array( $this, 'jetpack_search_es_query_args' ), 10, 2 );
74                        add_filter( 'posts_pre_query', array( $this, 'set_max_num_pages' ), 15, 2 ); // After `Classic_Search::filter__posts_pre_query()`
75
76                        // Load Jetpack Search.
77                        include_once WP_PLUGIN_DIR . '/jetpack/vendor/autoload_packages.php';
78
79                        if ( class_exists( '\Automattic\Jetpack\Search\Classic_Search' ) ) {
80                                // New Jetpack
81                                \Automattic\Jetpack\Search\Classic_Search::instance();
82
83                        } else {
84                                // Old(er) Jetpack, load the classic search module, Temporarily.
85
86                                include_once WP_PLUGIN_DIR . '/jetpack/modules/search/class.jetpack-search.php';
87                                include_once WP_PLUGIN_DIR . '/jetpack/modules/search/class.jetpack-search-helpers.php';
88
89                                \Jetpack_Search::instance()->setup();
90                        }
91
92                }
93
94        }
95
96        function var_export($expression, $return=FALSE) {
97                $export = var_export($expression, TRUE);
98                $patterns = [
99                        "/array \(/" => '[',
100                        "/^([ ]*)\)(,?)$/m" => '$1]$2',
101                        "/=>[ ]?\n[ ]+\[/" => '=> [',
102                        "/([ ]*)(\'[^\']+\') => ([\[\'])/" => '$1$2 => $3',
103                ];
104                $export = preg_replace(array_keys($patterns), array_values($patterns), $export);
105                if ((bool)$return) return $export; else echo $export;
106        }
107
108        public function option_jetpack_active_modules( $modules ) {
109                if ( self::USE_OLD_SEARCH ) {
110                        if ( $i = array_search( 'search', $modules ) )
111                                unset( $modules[$i] );
112                } else {
113                        $modules[] = 'search';
114                }
115
116                return array_unique( $modules );
117        }
118
119        public function option_has_jetpack_search_product( $option ) {
120                if ( !self::USE_OLD_SEARCH ) {
121                        return true;
122                }
123                return $option;
124        }
125
126        /* Make sure the search module is available regardless of Jetpack plan.
127         * This works because search indexes were manually created for w.org.
128         */
129        public function jetpack_get_module( $module, $slug ) {
130                if ( !self::USE_OLD_SEARCH ) {
131                        if ( 'search' === $slug && isset( $module[ 'plan_classes' ] ) && !in_array( 'free', $module[ 'plan_classes' ] ) ) {
132                                $module[ 'plan_classes' ][] = 'free';
133                        }
134                }
135
136                return $module;
137        }
138
139
140        /**
141         * Localise the ES fields searched for localised queries.
142         */
143        public function localise_es_fields( $fields ) {
144                $localised_prefixes = [
145                        'all_content',
146                        'title',
147                        'excerpt',
148                        'description',
149                ];
150
151                $localised_fields = array();
152
153                foreach ( (array) $fields as $field ) {
154                        // title.ngram
155                        list( $field, $type ) = explode( '.', $field . '.' );
156                        if ( $type ) {
157                                $type = ".{$type}";
158                        }
159
160                        if ( ! in_array( $field, $localised_prefixes ) ) {
161                                $localised_fields[] = $field . $type;
162                                continue;
163                        }
164
165                        if ( $this->is_english ) {
166                                $localised_fields[] = $field . '_en' . $type;
167                                continue;
168                        }
169
170                        $boost    = '';
171                        $en_boost = '^' . $this->en_boost;
172                        if ( 'description' === $field ) {
173                                $boost = '^' . $this->desc_boost;
174                                $en_boost = '^' . $this->desc_en_boost;
175                        }
176
177                        $localised_fields[] = $field . '_' . $this->locale . $type . $boost;
178                        $localised_fields[] = $field . '_en' . $type . $en_boost;
179                }
180
181                return $localised_fields;
182        }
183
184        public function jetpack_search_es_wp_query_args( $args, $query ) {
185
186                // Block Search.
187                $this->is_block_search = !empty( $query->query['block_search'] );
188                if ( $this->is_block_search ) {
189                        $args['block_search'] = $query->query['block_search'];
190                }
191
192                // How much weighting to put on the Description field.
193                // Blocks get a much lower value here, as it's more title/excerpt (short description) based.
194                $this->desc_boost = $this->is_block_search ? 0.05 : 1;
195
196                // Because most plugins don't have any translations we need to
197                // correct for the very low scores that locale-specific fields.
198                // end up getting. This is caused by the average field length being
199                // very close to zero and thus the BM25 alg discounts fields that are
200                // significantly longer.
201                //
202                // As of 2017-01-23 it looked like we were off by about 10,000x,
203                // so rather than 0.1 we use a much smaller multiplier of en content
204                $this->en_boost             = 0.00001;
205                $this->desc_en_boost        = $this->desc_boost * $this->en_boost;
206
207                // We need to be locale aware for this
208                $this->locale     = get_locale();
209                $this->is_english = ( ! $this->locale || str_starts_with( $this->locale, 'en_' ) );
210
211                $args['query_fields'] = $this->localise_es_fields( 'all_content' );
212
213                return $args;
214        }
215
216        public function jetpack_search_es_query_args( $es_query_args, $query ) {
217                // These are the things that jetpack_search_es_wp_query_args doesn't let us change, so we need to filter the es_query_args late in the code path to add more custom stuff.
218
219                $es_query_args['filter']['and'] ??= [];
220
221                // Exclude disabled plugins.
222                $es_query_args['filter']['and'][] = [
223                        'term' => [
224                                'disabled' => [
225                                        'value' => false,
226                                ],
227                        ]
228                ];
229
230                if ( $this->is_block_search ) {
231                        // Limit to the Block Tax.
232                        $es_query_args['filter']['and'][] = [
233                                'term' => [
234                                        'taxonomy.plugin_section.name' => [
235                                                'value' => 'block'
236                                        ]
237                                ]
238                        ];
239                }
240
241                // The should match is where we add the fields to be searched in, and the weighting of them (boost).
242                $should_match = [];
243                if ( isset( $es_query_args[ 'query' ][ 'function_score' ][ 'query' ][ 'bool' ][ 'should' ] ) ) {
244                        $should_match = & $es_query_args[ 'query' ][ 'function_score' ][ 'query' ][ 'bool' ][ 'should' ];
245                }
246
247                $search_phrase = $should_match[0][ 'multi_match' ][ 'query' ] ?? '';
248
249                // The function score is where calculations on fields occur.
250                $function_score = [];
251                if ( isset( $es_query_args[ 'query' ][ 'function_score' ][ 'functions' ] ) ) {
252                        $function_score = & $es_query_args[ 'query' ][ 'function_score' ][ 'functions' ];
253                }
254
255                // Set boost on the match query, from jetpack_search_es_wp_query_args.
256                if ( isset( $es_query_args[ 'query' ][ 'function_score' ][ 'query' ][ 'bool' ][ 'must' ][0][ 'multi_match' ] ) ) {
257                        $es_query_args[ 'query' ][ 'function_score' ][ 'query' ][ 'bool' ][ 'must' ][0][ 'multi_match' ][ 'boost' ] = 0.1;
258                }
259
260                // This extends the search to additionally search in the title, excerpt, description and plugin_tags.
261                if ( isset( $should_match[0][ 'multi_match' ] ) ) {
262                        $should_match[0][ 'multi_match' ][ 'boost' ]  = 2;
263                        $should_match[0][ 'multi_match' ][ 'fields' ] = $this->localise_es_fields( [
264                                'title',
265                                'excerpt',
266                                'description',
267                                'taxonomy.plugin_tags.name',
268                        ] );
269                }
270
271                // Setup the boosting for various fields.
272                $should_match[] = [
273                        'multi_match' => [
274                                'query'  => $search_phrase,
275                                'fields' => $this->localise_es_fields( [ 'title.ngram' ] ),
276                                'type'   => 'phrase',
277                                'boost'  => 2,
278                        ],
279                ];
280
281                // A direct slug match
282                $should_match[] = [
283                        'multi_match' => [
284                                'query'  => $search_phrase,
285                                'fields' => $this->localise_es_fields( 'title', 'slug_text' ),
286                                'type'   => 'most_fields',
287                                'boost'  => 5,
288                        ],
289                ];
290
291                $should_match[] = [
292                        'multi_match' => [
293                                'query'  => $search_phrase,
294                                'fields' => $this->localise_es_fields( [
295                                        'excerpt',
296                                        'description',
297                                        'taxonomy.plugin_tags.name',
298                                ] ),
299                                'type'   => 'best_fields',
300                                'boost'  => 2,
301                        ],
302                ];
303
304                $should_match[] = [
305                        'multi_match' => [
306                                'query'  => $search_phrase,
307                                'fields' => $this->localise_es_fields( [
308      ��                                 'author',
309                                        'contributor'
310                                ] ),
311                                'type'   => 'best_fields',
312                                'boost'  => 3,
313                        ],
314                ];
315
316                // We'll overwrite the default Jetpack Search function scoring with our own.
317                $function_score = [
318                        [
319                                // The more recent a plugin was updated, the more relevant it is.
320                                'exp' => [
321                                        'plugin_modified' => [
322                                                'origin' => date('Y-m-d'),
323                                                'offset' => '180d',
324                                                'scale'  => '360d',
325                                                'decay'  => 0.5,
326                                        ],
327                                ]
328                        ],
329                        [
330                                // The older a plugins tested-up-to is, the less likely it's relevant.
331                                'exp' => [
332                                        'tested' => [
333                                                'origin' => sprintf( '%0.1f', defined( 'WP_CORE_STABLE_BRANCH' ) ? WP_CORE_STABLE_BRANCH : $GLOBALS['wp_version'] ),
334                                                'offset' => 0.1,
335                                                'scale'  => 0.4,
336                                                'decay'  => 0.6,
337                                        ],
338                                ],
339                        ],
340                        [
341                                // A higher install base is a sign that the plugin will be relevant to the searcher.
342                                'field_value_factor' => [
343                                        'field'    => 'active_installs',
344                                        'factor'   => 0.375,
345                                        'modifier' => 'log2p',
346                                        'missing'  => 1,
347                                ],
348                        ],
349                        [
350                                // For plugins with less than 1 million installs, we need to adjust their scores a bit more.
351                                'filter' => [
352                                        'range' => [
353                                                        'active_installs' => [
354                                                                'lte' => 1000000,
355                                                        ],
356                                                ],
357                                        ],
358                                'exp' => [
359                                        'active_installs' => [
360                                                'origin' => 1000000,
361                                                'offset' => 0,
362                                                'scale'  => 900000,
363                                                'decay'  => 0.75,
364                                        ],
365                                ],
366                        ],
367                        [
368                                // The more resolved support threads (as a percentage) a plugin has, the more responsive the developer is, and the better experience the end-user will have.
369                                'field_value_factor' => [
370                                        'field'    => 'support_threads_resolved',
371                                        'factor'   => 0.25,
372                                        'modifier' => 'log2p',
373                                        'missing'  => 0.5,
374                                ],
375                        ],
376                        [
377                                // A higher rated plugin is more likely to be preferred.
378                                'field_value_factor' => [
379                                        'field'    => 'rating',
380                                        'factor'   => 0.25,
381                                        'modifier' => 'sqrt',
382                                        'missing'  => 2.5,
383                                ],
384                        ],
385                ];
386
387                unset( $es_query_args[ 'query' ][ 'function_score' ][ 'max_boost' ] );
388                unset( $es_query_args[ 'query' ][ 'function_score' ][ 'score_mode' ] );
389
390                // Couple of extra fields wanted in the response, mainly for debugging
391                $es_query_args[ 'fields' ] = [
392                        'slug',
393                        'post_id',
394                ];
395
396                return $es_query_args;
397        }
398
399        /**
400         * Limit the number of pagination links to 50.
401         *
402         * Jetpack ignores the `max_num_pages` that's set in `WP_Query` args and overrides it in
403         * `Classic_Search::filter__posts_pre_query()`. When there are more than 1,000 matches, their value causes
404         * Core's `paginate_links()` to generated 51 links, even though we redirect the user to the homepage when
405         * page 51+ is requested.
406         */
407        function set_max_num_pages( $posts, $query ) {
408                $post_type = (array) $query->query_vars['post_type'] ?? '';
409
410                if ( is_admin() || ! is_search() || ! in_array( 'plugin', $post_type ) ) {
411                        return $posts;
412                }
413
414                if ( $query->max_num_pages > 50 ) {
415                        $query->max_num_pages = 50;
416                }
417
418                return $posts;
419        }
420
421        public function log_search_es_wp_query_args( $es_wp_query_args, $query ) {
422                error_log( '--- ' . __FUNCTION__ . ' ---' );
423                error_log( $this->var_export( $es_wp_query_args, true ) );
424
425                return $es_wp_query_args;
426        }
427
428        public function log_jetpack_search_abort( $reason ) {
429                error_log( "--- jetpack_search_abort $reason ---" );
430        }
431
432        public function log_did_jetpack_search_query( $query ) {
433                error_log( '--- did_jetpack_search_query ---' );
434                error_log( $this->var_export( $query, true ) );
435        }
436}
Note: See TracBrowser for help on using the repository browser.