Making WordPress.org

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

Last change on this file since 13648 was 13648, checked in by dd32, 3 months ago

Plugin Directory: Search: Properly limit block searches to the Block directory.

Likely broke via [13332].

  • Property svn:eol-style set to native
File size: 12.7 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                // Replace any existing filter with an AND for our custom filters.
220                $es_query_args['filter'] = [
221                        'and' => $es_query_args['filter'] ? [ $es_query_args['filter'] ] : [],
222                ];
223
224                // Exclude 'disabled' plugins. This is separate from the 'status' field, which is used for the plugin status.
225                $es_query_args['filter']['and'] = [
226                        'terms' => [
227                                'disabled' => false,
228                        ]
229                ];
230
231                // Limit to the Block Directory.
232                if ( $this->is_block_search ) {
233            ��           $es_query_args['filter']['and'][] = [
234                                'terms' => [
235                                        'taxonomy.plugin_section.name' => 'block',
236                                ]
237                        ];
238                }
239
240                // The should match is where we add the fields to be searched in, and the weighting of them (boost).
241                $should_match = [];
242                if ( isset( $es_query_args[ 'query' ][ 'function_score' ][ 'query' ][ 'bool' ][ 'should' ] ) ) {
243                        $should_match = & $es_query_args[ 'query' ][ 'function_score' ][ 'query' ][ 'bool' ][ 'should' ];
244                }
245
246                $search_phrase = $should_match[0][ 'multi_match' ][ 'query' ] ?? '';
247
248                // The function score is where calculations on fields occur.
249                $function_score = [];
250                if ( isset( $es_query_args[ 'query' ][ 'function_score' ][ 'functions' ] ) ) {
251                        $function_score = & $es_query_args[ 'query' ][ 'function_score' ][ 'functions' ];
252                }
253
254                // Set boost on the match query, from jetpack_search_es_wp_query_args.
255                if ( isset( $es_query_args[ 'query' ][ 'function_score' ][ 'query' ][ 'bool' ][ 'must' ][0][ 'multi_match' ] ) ) {
256                        $es_query_args[ 'query' ][ 'function_score' ][ 'query' ][ 'bool' ][ 'must' ][0][ 'multi_match' ][ 'boost' ] = 0.1;
257                }
258
259                // This extends the search to additionally search in the title, excerpt, description and plugin_tags.
260                if ( isset( $should_match[0][ 'multi_match' ] ) ) {
261                        $should_match[0][ 'multi_match' ][ 'boost' ]  = 2;
262                        $should_match[0][ 'multi_match' ][ 'fields' ] = $this->localise_es_fields( [
263                                'title',
264                                'excerpt',
265                                'description',
266                                'taxonomy.plugin_tags.name',
267                        ] );
268                }
269
270                // Setup the boosting for various fields.
271                $should_match[] = [
272                        'multi_match' => [
273                                'query'  => $search_phrase,
274                                'fields' => $this->localise_es_fields( [ 'title.ngram' ] ),
275                                'type'   => 'phrase',
276                                'boost'  => 2,
277                        ],
278                ];
279
280                // A direct slug match
281                $should_match[] = [
282                        'multi_match' => [
283                                'query'  => $search_phrase,
284                                'fields' => $this->localise_es_fields( 'title', 'slug_text' ),
285                                'type'   => 'most_fields',
286                                'boost'  => 5,
287                        ],
288                ];
289
290                $should_match[] = [
291                        'multi_match' => [
292                                'query'  => $search_phrase,
293                                'fields' => $this->localise_es_fields( [
294                                        'excerpt',
295                                        'description',
296                                        'taxonomy.plugin_tags.name',
297                                ] ),
298                                'type'   => 'best_fields',
299                                'boost'  => 2,
300                        ],
301                ];
302
303                $should_match[] = [
304                        'multi_match' => [
305                                'query'  => $search_phrase,
306                                'fields' => $this->localise_es_fields( [
307                                        'author',
308                                        'contributor'
309                                ] ),
310                                'type'   => 'best_fields',
311                                'boost'  => 3,
312                        ],
313                ];
314
315                // We'll overwrite the default Jetpack Search function scoring with our own.
316                $function_score = [
317                        [
318                                // The more recent a plugin was updated, the more relevant it is.
319                                'exp' => [
320                                        'plugin_modified' => [
321                                                'origin' => date('Y-m-d'),
322                                                'offset' => '180d',
323                                                'scale'  => '360d',
324                                                'decay'  => 0.5,
325                                        ],
326                                ]
327                        ],
328                        [
329                                // The older a plugins tested-up-to is, the less likely it's relevant.
330                                'exp' => [
331                                        'tested' => [
332                                                'origin' => sprintf( '%0.1f', defined( 'WP_CORE_STABLE_BRANCH' ) ? WP_CORE_STABLE_BRANCH : $GLOBALS['wp_version'] ),
333                                                'offset' => 0.1,
334                                                'scale'  => 0.4,
335                                                'decay'  => 0.6,
336                                        ],
337                                ],
338                        ],
339                        [
340                                // A higher install base is a sign that the plugin will be relevant to the searcher.
341                                'field_value_factor' => [
342                                        'field'    => 'active_installs',
343                                        'factor'   => 0.375,
344                                        'modifier' => 'log2p',
345                                        'missing'  => 1,
346                                ],
347                        ],
348                        [
349                                // For plugins with less than 1 million installs, we need to adjust their scores a bit more.
350                                'filter' => [
351                                        'range' => [
352                                                        'active_installs' => [
353                                                                'lte' => 1000000,
354                                                        ],
355                                                ],
356                                        ],
357                                'exp' => [
358                                        'active_installs' => [
359                                                'origin' => 1000000,
360                                                'offset' => 0,
361                                                'scale'  => 900000,
362                                                'decay'  => 0.75,
363                                        ],
364                                ],
365                        ],
366                        [
367                                // 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.
368                                'field_value_factor' => [
369                                        'field'    => 'support_threads_resolved',
370                                        'factor'   => 0.25,
371                                        'modifier' => 'log2p',
372                                        'missing'  => 0.5,
373                                ],
374                        ],
375                        [
376                                // A higher rated plugin is more likely to be preferred.
377                                'field_value_factor' => [
378                                        'field'    => 'rating',
379                                        'factor'   => 0.25,
380                                        'modifier' => 'sqrt',
381                                        'missing'  => 2.5,
382                                ],
383                        ],
384                ];
385
386                unset( $es_query_args[ 'query' ][ 'function_score' ][ 'max_boost' ] );
387                unset( $es_query_args[ 'query' ][ 'function_score' ][ 'score_mode' ] );
388
389                // Couple of extra fields wanted in the response, mainly for debugging
390                $es_query_args[ 'fields' ] = [
391                        'slug',
392                        'post_id',
393                ];
394
395                return $es_query_args;
396        }
397
398        /**
399         * Limit the number of pagination links to 50.
400         *
401         * Jetpack ignores the `max_num_pages` that's set in `WP_Query` args and overrides it in
402         * `Classic_Search::filter__posts_pre_query()`. When there are more than 1,000 matches, their value causes
403         * Core's `paginate_links()` to generated 51 links, even though we redirect the user to the homepage when
404         * page 51+ is requested.
405         */
406        function set_max_num_pages( $posts, $query ) {
407                $post_type = (array) $query->query_vars['post_type'] ?? '';
408
409                if ( is_admin() || ! is_search() || ! in_array( 'plugin', $post_type ) ) {
410                        return $posts;
411                }
412
413                if ( $query->max_num_pages > 50 ) {
414                        $query->max_num_pages = 50;
415                }
416
417                return $posts;
418        }
419
420        public function log_search_es_wp_query_args( $es_wp_query_args, $query ) {
421                error_log( '--- ' . __FUNCTION__ . ' ---' );
422                error_log( $this->var_export( $es_wp_query_args, true ) );
423
424                return $es_wp_query_args;
425        }
426
427        public function log_jetpack_search_abort( $reason ) {
428                error_log( "--- jetpack_search_abort $reason ---" );
429        }
430
431        public function log_did_jetpack_search_query( $query ) {
432                error_log( '--- did_jetpack_search_query ---' );
433                error_log( $this->var_export( $query, true ) );
434        }
435}
Note: See TracBrowser for help on using the repository browser.