Making WordPress.org

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

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

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

The root-cause here is that the plugin_section needs to be limited by slug, rather than name.
Otherwise, { name:'Block-Enabled plugins', slug:'blocks'} matches a name = block query.

See [13648].
See https://wordpress.slack.com/archives/C1LBM36LC/p1714627935853749
See https://wordpress.slack.com/archives/C02QB8GMM/p1714634228857259

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