1 | <?php |
---|
2 | namespace WordPressdotorg\Plugin_Directory; |
---|
3 | |
---|
4 | // Hmm |
---|
5 | add_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 | */ |
---|
12 | class 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 | } |
---|