PluginProbe
ElasticPress / 4.7.1
ElasticPress v4.7.1
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Elasticsearch.php

Elasticsearch.php in ElasticPress 4.7.1, at includes/classes/Elasticsearch.php

1,797 lines 49.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ElasticPress-Elasticsearch API functions
4 *
5 * @since 3.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress;
10
11 use ElasticPress\Utils as Utils;
12 use ElasticPress\Indexables;
13 use \WP_Error as WP_Error;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Elasticsearch API class
21 */
22 class Elasticsearch {
23
24 /**
25 * Logged queries for debugging
26 *
27 * @since 1.8
28 * @var array
29 */
30 private $queries = [];
31
32 /**
33 * ES plugins
34 *
35 * @var array
36 * @since 2.2
37 */
38 public $elasticsearch_plugins = null;
39
40 /**
41 * ES version number
42 *
43 * @var string
44 * @since 2.2
45 */
46 public $elasticsearch_version = null;
47
48 /**
49 * Server type (elasticsearch, opensearch, etc.)
50 *
51 * @var string
52 */
53 public $server_type = 'elasticsearch';
54
55 /**
56 * Return singleton instance of class
57 *
58 * @return object
59 * @since 0.1.0
60 */
61 public static function factory() {
62 static $instance = false;
63
64 if ( ! $instance ) {
65 $instance = new self();
66 }
67
68 return $instance;
69 }
70
71 /**
72 * Index a document in Elasticsearch.
73 *
74 * We require $document to have ID set
75 *
76 * @param string $index Index name.
77 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
78 * @param array $document Formatted Elasticsearch document.
79 * @param boolean $blocking Blocking HTTP request or not.
80 * @since 3.0
81 * @return boolean|array
82 */
83 public function index_document( $index, $type, $document, $blocking = true ) {
84 /**
85 * Filter Elasticsearch index document request path
86 *
87 * @hook ep_index_{document_type}_request_path
88 * @param {string} $path Path to index document
89 * @param {int} $document_id Document ID
90 * @param {array} $document Document to index
91 * @param {string} $type Type of document
92 * @return {string} New path
93 * @since 3.0
94 */
95 if ( version_compare( (string) $this->get_elasticsearch_version(), '7.0', '<' ) ) {
96 $path = apply_filters( 'ep_index_' . $type . '_request_path', $index . '/' . $type . '/' . $document['ID'], $document, $type );
97 } else {
98 $path = apply_filters( 'ep_index_' . $type . '_request_path', $index . '/_doc/' . $document['ID'], $document, $type );
99 }
100
101 $path = apply_filters( 'ep_index_request_path', $path, $document, $type );
102
103 if ( function_exists( 'wp_json_encode' ) ) {
104 $encoded_document = wp_json_encode( $document );
105 } else {
106 // phpcs:disable
107 $encoded_document = json_encode( $document );
108 // phpcs:enable
109 }
110
111 $request_args = array(
112 'body' => $encoded_document,
113 'method' => 'POST',
114 'timeout' => apply_filters( 'ep_index_document_timeout', 15 ),
115 'blocking' => $blocking,
116 );
117
118 $request = $this->remote_request( $path, $request_args, [], 'index' );
119
120 /**
121 * Backwards compat for pre-3.0
122 */
123
124 /**
125 * Fires after indexing document
126 *
127 * @hook ep_index_post_retrieve_raw_response
128 * @param {array} $request Remote request response
129 * @param {array} $document Current document
130 * @param {string} $path Elasticsearch request path
131 */
132 do_action( 'ep_index_post_retrieve_raw_response', $request, $document, $path );
133
134 /**
135 * Fires after indexing document
136 *
137 * @hook ep_index_retrieve_raw_response
138 * @param {array} $request Remote request response
139 * @param {array} $document Current document
140 * @param {string} $path Elasticsearch request path
141 */
142 do_action( 'ep_index_retrieve_raw_response', $request, $document, $path );
143
144 if ( ! is_wp_error( $request ) ) {
145 $response_body = wp_remote_retrieve_body( $request );
146
147 $return = json_decode( $response_body );
148 } else {
149 $return = false;
150 }
151
152 /**
153 * Backwards compat for pre-3.0
154 */
155
156 /**
157 * Fires after indexing document and body decoding
158 *
159 * @hook ep_index_index_post
160 * @param {array} $document Current document
161 * @param {array|boolean} $return Elasticsearch response. False on error.
162 */
163 do_action( 'ep_after_index_post', $document, $return );
164
165 /**
166 * Fires after indexing document and body decoding
167 *
168 * @hook ep_index_index
169 * @param {array} $document Current document
170 * @param {array|boolean} $return Elasticsearch response. False on error.
171 */
172 do_action( 'ep_after_index', $document, $return );
173
174 return $return;
175 }
176
177 /**
178 * Pull the site id from the index name
179 *
180 * @param string $index_name Index name.
181 * @since 0.9.0
182 * @return int
183 */
184 public function parse_site_id( $index_name ) {
185 return (int) preg_replace( '#^.*\-([0-9]+)$#', '$1', $index_name );
186 }
187
188 /**
189 * Refresh all index. Sometimes useful if you need changes to show up instantly.
190 *
191 * @since 3.0
192 * @return bool
193 */
194 public function refresh_indices() {
195
196 $request_args = array( 'method' => 'POST' );
197
198 $request = $this->remote_request( '_refresh', $request_args, [], 'refresh_indices' );
199
200 if ( ! is_wp_error( $request ) ) {
201 if ( isset( $request['response']['code'] ) && 200 === $request['response']['code'] ) {
202 return true;
203 }
204 }
205
206 return false;
207 }
208
209 /**
210 * Get Elasticsearch version. We cache this so we don't have to do it every time.
211 *
212 * @param bool $force Bust cache or not.
213 * @since 2.1.2
214 * @return string|bool
215 */
216 public function get_elasticsearch_version( $force = false ) {
217
218 $info = $this->get_elasticsearch_info( $force );
219
220 /**
221 * Filter Elasticsearch version
222 *
223 * @hook ep_elasticsearch_version
224 * @param {string} $version Version
225 * @return {string} New version
226 * @since 2.1.2
227 */
228 return apply_filters( 'ep_elasticsearch_version', $info['version'] );
229 }
230
231 /**
232 * Get server type. We cache this so we don't have to do it every time.
233 *
234 * @param bool $force Bust cache or not.
235 * @since 4.2.1
236 * @return string|bool
237 */
238 public function get_server_type( $force = false ) {
239
240 $info = $this->get_elasticsearch_info( $force );
241
242 /**
243 * Filter server type
244 *
245 * @hook ep_server_type
246 * @param {string} $type Type (elasticsearch, opensearch, others)
247 * @return {string} New type
248 * @since 4.2.1
249 */
250 return apply_filters( 'ep_server_type', $info['server_type'] );
251 }
252
253 /**
254 * Get Elasticsearch plugins. We cache this so we don't have to do it every time.
255 *
256 * @param bool $force Force cache refresh or not.
257 * @since 2.2
258 * @return string|bool
259 */
260 public function get_elasticsearch_plugins( $force = false ) {
261
262 $info = $this->get_elasticsearch_info( $force );
263
264 /**
265 * Filter Elasticsearch plugins
266 *
267 * @hook ep_elasticsearch_plugins
268 * @param {array} $plugins Elasticsearch plugins
269 * @return {array} New plugins
270 * @since 2.2
271 */
272 return apply_filters( 'ep_elasticsearch_plugins', $info['plugins'] );
273 }
274
275 /**
276 * Run a query on Elasticsearch
277 *
278 * @param string $index Index name.
279 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
280 * @param array $query Prepared ES query.
281 * @param array $query_args WP query args.
282 * @param mixed $query_object Could be WP_Query, WP_User_Query, etc.
283 * @since 3.0
284 * @return bool|array
285 */
286 public function query( $index, $type, $query, $query_args, $query_object = null ) {
287 if ( version_compare( (string) $this->get_elasticsearch_version(), '7.0', '<' ) ) {
288 $path = $index . '/' . $type . '/_search';
289 } else {
290 $path = $index . '/_search';
291 }
292
293 // For backwards compat
294 /**
295 * Filter Elasticsearch query request path
296 *
297 * @hook ep_search_request_path
298 * @param {string} $path Request path
299 * @param {string} $index Index name
300 * @param {string} $type Index type
301 * @param {array} $query Prepared Elasticsearch query
302 * @param {array} $query_args Query arguments
303 * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
304 * @return {string} New path
305 */
306 $path = apply_filters( 'ep_search_request_path', $path, $index, $type, $query, $query_args, $query_object );
307
308 /**
309 * Filter Elasticsearch query request path
310 *
311 * @hook ep_query_request_path
312 * @param {string} $path Request path
313 * @param {string} $index Index name
314 * @param {string} $type Index type
315 * @param {array} $query Prepared Elasticsearch query
316 * @param {array} $query_args Query arguments
317 * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
318 * @return {string} New path
319 */
320 $path = apply_filters( 'ep_query_request_path', $path, $index, $type, $query, $query_args, $query_object );
321
322 $request_args = array(
323 'body' => wp_json_encode( $query ),
324 'method' => 'POST',
325 'headers' => array(
326 'Content-Type' => 'application/json',
327 ),
328 );
329
330 /**
331 * Filter whether to send the EP-Search-Term header or not.
332 *
333 * @todo Evaluate if we should remove tests for is_admin() and empty post types.
334 *
335 * @since 3.5.2
336 * @hook ep_query_send_ep_search_term_header
337 * @param {bool} $send_header True means send the EP-Search-Term header
338 * @param {array} $query_args WP query args
339 * @return {bool} New $send_header value
340 */
341 $send_ep_search_term_header = apply_filters(
342 'ep_query_send_ep_search_term_header',
343 (
344 Utils\is_epio() &&
345 ! empty( $query_args['s'] ) &&
346 Utils\is_integrated_request( 'search' ) &&
347 ! isset( $_GET['post_type'] ) // phpcs:ignore WordPress.Security.NonceVerification
348 ),
349 $query_args
350 );
351
352 // If needed, send the search term as a header to ES so the backend understands what a normal query looks like
353 if ( $send_ep_search_term_header ) {
354 $request_args['headers']['EP-Search-Term'] = rawurlencode( $query_args['s'] );
355 }
356
357 /**
358 * Filter Elasticsearch query request arguments
359 *
360 * @hook ep_query_request_args
361 * @since 3.6.4
362 * @param {array} $request_args Request arguments
363 * @param {string} $path Request path
364 * @param {string} $index Index name
365 * @param {string} $type Index type
366 * @param {array} $query Prepared Elasticsearch query
367 * @param {array} $query_args Query arguments
368 * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
369 * @return {array} New request arguments
370 */
371 $request_args = apply_filters( 'ep_query_request_args', $request_args, $path, $index, $type, $query, $query_args, $query_object );
372
373 $request = $this->remote_request( $path, $request_args, $query_args, 'query' );
374
375 $remote_req_res_code = absint( wp_remote_retrieve_response_code( $request ) );
376
377 $is_valid_res = ( $remote_req_res_code >= 200 && $remote_req_res_code <= 299 );
378
379 /**
380 * Filter whether Elasticsearch remote request response code is valid
381 *
382 * @hook ep_remote_request_is_valid_res
383 * @param {boolean} $is_valid_res Whether response code is valid or not
384 * @param {array} $request Remote request response
385 * @return {string} New value
386 */
387 if ( ! is_wp_error( $request ) && apply_filters( 'ep_remote_request_is_valid_res', $is_valid_res, $request ) ) {
388
389 $response_body = wp_remote_retrieve_body( $request );
390
391 $response = json_decode( $response_body, true );
392
393 $hits = $this->get_hits_from_query( $response );
394 $total_hits = $this->get_total_hits_from_query( $response );
395
396 if ( ! empty( $response['aggregations'] ) ) {
397 /**
398 * Deprecated way to retrieve aggregations.
399 *
400 * @hook ep_retrieve_aggregations
401 * @param {array} $aggregations Elasticsearch aggregations
402 * @param {array} $query Prepared Elasticsearch query
403 * @param {string} $scope Backwards compat for scope parameter.
404 * @param {array} $query_args Current WP Query arguments
405 */
406 do_action( 'ep_retrieve_aggregations', $response['aggregations'], $query, '', $query_args );
407 }
408
409 /**
410 * Fires after valid Elasticsearch query
411 *
412 * @hook ep_valid_response
413 * @param {array} $response Elasticsearch decoded response
414 * @param {array} $query Prepared Elasticsearch query
415 * @param {array} $query_args Current WP Query arguments
416 * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
417 */
418 do_action( 'ep_valid_response', $response, $query, $query_args, $query_object );
419
420 // Backwards compat
421 /**
422 * Fires after valid Elasticsearch query
423 *
424 * @hook ep_retrieve_raw_response
425 * @param {array} $response Elasticsearch request
426 * @param {array} $query Prepared Elasticsearch query
427 * @param {array} $query_args Current WP Query arguments
428 * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
429 */
430 do_action( 'ep_retrieve_raw_response', $request, $query, $query_args, $query_object );
431
432 $documents = [];
433
434 foreach ( $hits as $hit ) {
435 $document = isset( $hit['_source'] ) ? $hit['_source'] : array();
436 $document['site_id'] = $this->parse_site_id( $hit['_index'] );
437
438 if ( ! empty( $hit['highlight'] ) ) {
439 $document['highlight'] = $hit['highlight'];
440 }
441
442 /**
443 * Filter Elasticsearch retrieved document
444 *
445 * @hook ep_retrieve_the_{index_type}
446 * @param {array} $document Document retrieved from Elasticsearch
447 * @param {array} $hit Raw Elasticsearch hit
448 * @param {string} $index Index name
449 * @return {array} New document
450 */
451 $documents[] = apply_filters( 'ep_retrieve_the_' . $type, $document, $hit, $index );
452 }
453
454 /**
455 * Filter Elasticsearch query results
456 *
457 * @hook ep_es_query_results
458 * @param {array} $results Results from Elasticsearch
459 * @param {response} $response Raw response from Elasticsearch
460 * @param {array} $query Raw Elasticsearch query
461 * @param {array} $query_args Query arguments
462 * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
463 * @return {array} New results
464 */
465 return apply_filters(
466 'ep_es_query_results',
467 [
468 'found_documents' => $total_hits,
469 'documents' => $documents,
470 'aggregations' => $response['aggregations'] ?? [],
471 'suggest' => $response['suggest'] ?? [],
472 ],
473 $response,
474 $query,
475 $query_args,
476 $query_object
477 );
478 }
479
480 /**
481 * Fires after invalid Elasticsearch query
482 *
483 * @hook ep_invalid_response
484 * @param {array} $request Remote request response
485 * @param {array} $query Prepared Elasticsearch query
486 * @param {array} $query_args Current WP Query arguments
487 * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
488 */
489 do_action( 'ep_invalid_response', $request, $query, $query_args, $query_object );
490
491 return false;
492 }
493
494 /**
495 * Returns the number of total results that ElasticSearch found for the given query
496 *
497 * @param array $response Response to get total hits from.
498 * @since 2.5
499 * @return int
500 */
501 public function get_total_hits_from_query( $response ) {
502
503 if ( $this->is_empty_query( $response ) ) {
504 return 0;
505 }
506
507 return $response['hits']['total'];
508 }
509
510 /**
511 * Returns array containing hits returned from query, if such exist
512 *
513 * @param array $response Response to get hits from.
514 * @since 2.5
515 * @return array
516 */
517 public function get_hits_from_query( $response ) {
518
519 if ( $this->is_empty_query( $response ) ) {
520 return [];
521 }
522
523 /**
524 * Filter Elasticsearch allows to flatten hits, if searched hits are come within aggregations.
525 *
526 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-hits-aggregation.html
527 *
528 * @hook ep_get_hits_from_query
529 * @param {array} $hits from Elasticsearch
530 * @param {response} $response Raw response from Elasticsearch
531 * @return {array} hits
532 */
533 return apply_filters( 'ep_get_hits_from_query', $response['hits']['hits'], $response );
534 }
535
536 /**
537 * Check if a response array contains results or not
538 *
539 * @param array $response Response to check.
540 * @since 0.1.2
541 * @return bool
542 */
543 public function is_empty_query( $response ) {
544
545 if ( ! is_array( $response ) ) {
546 return true;
547 }
548
549 if ( isset( $response['error'] ) ) {
550 return true;
551 }
552
553 if ( empty( $response['hits'] ) ) {
554 return true;
555 }
556
557 if ( isset( $response['hits']['total'] ) && 0 === (int) $response['hits']['total'] ) {
558 return true;
559 }
560
561 return false;
562 }
563
564 /**
565 * Delete an Elasticsearch document
566 *
567 * @param string $index Index name.
568 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
569 * @param int $document_id Document id to delete.
570 * @param boolean $blocking Blocking HTTP request or not.
571 * @since 3.0
572 * @return boolean
573 */
574 public function delete_document( $index, $type, $document_id, $blocking = true ) {
575 if ( version_compare( (string) $this->get_elasticsearch_version(), '7.0', '<' ) ) {
576 $path = $index . '/' . $type . '/' . $document_id;
577 } else {
578 $path = $index . '/_doc/' . $document_id;
579 }
580
581 $request_args = [
582 'method' => 'DELETE',
583 'timeout' => 15,
584 'blocking' => $blocking,
585 ];
586
587 $request = $this->remote_request( $path, $request_args, [], 'delete' );
588
589 if ( ! is_wp_error( $request ) ) {
590 $response_body = wp_remote_retrieve_body( $request );
591
592 $response = json_decode( $response_body, true );
593
594 if ( ! empty( $response['found'] ) ) {
595 return true;
596 }
597 }
598
599 return false;
600 }
601
602 /**
603 * Add appropriate headers to request
604 *
605 * @since 1.4
606 * @return array
607 */
608 public function format_request_headers() {
609 $headers = array(
610 'Content-Type' => 'application/json',
611 );
612
613 // Check for ElasticPress API key and add to header if needed.
614 if ( defined( 'EP_API_KEY' ) && EP_API_KEY ) {
615 $headers['X-ElasticPress-API-Key'] = EP_API_KEY;
616 }
617
618 /**
619 * ES Shield info
620 *
621 * @since 1.9
622 */
623 $shield = Utils\get_shield_credentials();
624
625 if ( ! empty( $shield ) ) {
626 // phpcs:disable
627 $headers['Authorization'] = 'Basic ' . base64_encode( $shield );
628 // phpcs:enable
629 }
630
631 $request_id = Utils\generate_request_id();
632 if ( ! empty( $request_id ) ) {
633 $headers['X-ElasticPress-Request-ID'] = $request_id;
634 }
635
636 /**
637 * Filter Elasticsearch request headers
638 *
639 * @hook ep_format_request_headers
640 * @param {array} $headers Current headers
641 * @return {array} New headers
642 */
643 $headers = apply_filters( 'ep_format_request_headers', $headers );
644
645 return $headers;
646 }
647
648 /**
649 * Get a document from Elasticsearch given an id
650 *
651 * @param string $index Index name.
652 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
653 * @param int $document_id Document id to get.
654 * @since 3.0
655 * @return boolean|array
656 */
657 public function get_document( $index, $type, $document_id ) {
658 if ( version_compare( (string) $this->get_elasticsearch_version(), '7.0', '<' ) ) {
659 $path = $index . '/' . $type . '/' . $document_id;
660 } else {
661 $path = $index . '/_doc/' . $document_id;
662 }
663
664 $request_args = [ 'method' => 'GET' ];
665
666 $request = $this->remote_request( $path, $request_args, [], 'get' );
667
668 if ( ! is_wp_error( $request ) ) {
669 $response_body = wp_remote_retrieve_body( $request );
670
671 $response = json_decode( $response_body, true );
672
673 if ( ! empty( $response['exists'] ) || ! empty( $response['found'] ) ) {
674 return $response['_source'];
675 }
676 }
677
678 return false;
679 }
680
681 /**
682 * Delete the network alias.
683 *
684 * Network aliases are used to query documents across blogs in a network.
685 *
686 * @param string $alias Alias to use.
687 * @since 3.0
688 * @return array|boolean
689 */
690 public function delete_network_alias( $alias ) {
691 $path = '*/_alias/' . $alias;
692
693 $request_args = [ 'method' => 'DELETE' ];
694
695 $request = $this->remote_request( $path, $request_args, [], 'delete_network_alias' );
696
697 if ( ! is_wp_error( $request ) && ( 200 >= wp_remote_retrieve_response_code( $request ) && 300 > wp_remote_retrieve_response_code( $request ) ) ) {
698 $response_body = wp_remote_retrieve_body( $request );
699
700 return json_decode( $response_body );
701 }
702
703 return false;
704 }
705
706 /**
707 * Get multiple documents from Elasticsearch given an array of ids
708 *
709 * @param string $index Index name.
710 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
711 * @param array $document_ids Array of document ids to get.
712 * @since 3.6.0
713 * @return boolean|array
714 */
715 public function get_documents( $index, $type, $document_ids ) {
716 if ( version_compare( (string) $this->get_elasticsearch_version(), '7.0', '<' ) ) {
717 $path = apply_filters( 'ep_index_' . $type . '_request_path', $index . '/' . $type . '/_mget', $document_ids, $type );
718 } else {
719 $path = apply_filters( 'ep_index_' . $type . '_request_path', $index . '/_mget', $document_ids, $type );
720 }
721
722 $request_args = [
723 'method' => 'POST',
724 'body' => wp_json_encode(
725 array(
726 'ids' => $document_ids,
727 )
728 ),
729 ];
730
731 $request = $this->remote_request( $path, $request_args, [], 'post' );
732
733 if ( is_wp_error( $request ) ) {
734 return false;
735 }
736
737 $response_body = wp_remote_retrieve_body( $request );
738
739 $response = json_decode( $response_body, true );
740
741 $docs = [];
742
743 if ( isset( $response['docs'] ) && is_array( $response['docs'] ) ) {
744 foreach ( $response['docs'] as $doc ) {
745 if ( ! empty( $doc['exists'] ) || ! empty( $doc['found'] ) ) {
746 $docs[ $doc['_id'] ] = $doc['_source'];
747 }
748 }
749 }
750
751 /**
752 * Filter documents found by Elasticsearch through the /_mget endpoint.
753 *
754 * @hook ep_get_documents
755 * @since 3.6.0
756 * @param {array} $docs Documents found indexed by ID
757 * @param {string} $index Index name
758 * @param {string} $type Index type
759 * @param {array} $document_ids Array of document ids
760 * @return {array} Documents to be returned
761 */
762 $docs = apply_filters( 'ep_get_documents', $docs, $index, $type, $document_ids );
763
764 return $docs;
765 }
766
767 /**
768 * Create the network alias.
769 *
770 * Network aliases are used to query documents across blogs in a network.
771 *
772 * @param array $indexes Indexes to group under alias.
773 * @param string $network_alias Name of network alias.
774 * @since 3.0
775 * @return boolean
776 */
777 public function create_network_alias( $indexes, $network_alias ) {
778
779 $path = '_aliases';
780
781 $args = array(
782 'actions' => [],
783 );
784
785 foreach ( $indexes as $index ) {
786 if ( empty( $index ) ) {
787 continue;
788 }
789
790 $args['actions'][] = array(
791 'add' => array(
792 'index' => $index,
793 'alias' => $network_alias,
794 ),
795 );
796 }
797
798 $request_args = array(
799 'body' => wp_json_encode( $args ),
800 'method' => 'POST',
801 'timeout' => 25,
802 );
803
804 $request = $this->remote_request( $path, $request_args, [], 'create_network_alias' );
805
806 if ( ! is_wp_error( $request ) && ( 200 >= wp_remote_retrieve_response_code( $request ) && 300 > wp_remote_retrieve_response_code( $request ) ) ) {
807 return true;
808 }
809
810 return false;
811 }
812
813 /**
814 * Put a mapping into Elasticsearch
815 *
816 * @param string $index Index name.
817 * @param array $mapping Mapping array.
818 * @param string $return_type Desired return type. Can be either 'bool' or 'raw'
819 * @since 3.0
820 * @return boolean|WP_Error
821 */
822 public function put_mapping( $index, $mapping, $return_type = 'bool' ) {
823 /**
824 * Filter Elasticsearch mapping before put mapping
825 *
826 * @hook ep_config_mapping
827 * @param {array} $mapping Elasticsearch mapping
828 * @param {string} $index Index name
829 * @return {array} New mapping
830 */
831 $mapping = apply_filters( 'ep_config_mapping', $mapping, $index );
832
833 $request_args = [
834 'body' => wp_json_encode( $mapping ),
835 'method' => 'PUT',
836 'timeout' => 30,
837 ];
838
839 $request = $this->remote_request( $index, $request_args, [], 'put_mapping' );
840
841 /**
842 * Filter Elasticsearch put mapping response
843 *
844 * @hook ep_config_mapping_request
845 * @param {array} $request Elasticsearch response
846 * @param {string} $index Elasticsearch index name
847 * @param {array} $mapping Mapping sent to Elasticsearch
848 * @return {array} New response
849 */
850 $request = apply_filters( 'ep_config_mapping_request', $request, $index, $mapping );
851
852 $response_code = wp_remote_retrieve_response_code( $request );
853
854 /**
855 * Fires after sending a put mapping request
856 *
857 * @hook ep_after_put_mapping
858 * @since 4.7.0
859 * @param {string} $index Index name
860 * @param {WP_Error|array} $request The response or WP_Error on failure.
861 */
862 do_action( 'ep_after_put_mapping', $index, $request );
863
864 // If WP_Error or not 200, return false or error message depends on attribute.
865 if ( is_wp_error( $request ) || 200 !== $response_code ) {
866 if ( 'bool' === $return_type ) {
867 return false;
868 }
869
870 if ( is_wp_error( $request ) ) {
871 return $request;
872 }
873
874 $response_body = wp_remote_retrieve_body( $request );
875 $parsed_response = json_decode( $response_body, true );
876 if ( is_array( $parsed_response ) ) {
877 $status = $parsed_response['status'] ?? 'status-not-set';
878 $error = $parsed_response['error'] ?? 'error-not-set';
879 } else {
880 $status = $response_code;
881 $error = $response_body;
882 }
883 return new \WP_Error( $status, $error );
884 }
885
886 return true;
887 }
888
889 /**
890 * Get current index mapping from Elasticsearch.
891 *
892 * @param string $index The index name.
893 * @since 3.5
894 * @return array
895 */
896 public function get_mapping( $index ) {
897 $request_args = [
898 'method' => 'GET',
899 'timeout' => 30,
900 ];
901
902 $request = $this->remote_request( $index, $request_args, [], 'get_mapping' );
903
904 if ( is_wp_error( $request ) || 200 !== wp_remote_retrieve_response_code( $request ) ) {
905 return [];
906 }
907
908 $body = wp_remote_retrieve_body( $request );
909
910 if ( ! $body ) {
911 return [];
912 }
913
914 $mapping = json_decode( $body, true );
915
916 return is_array( $mapping ) ? $mapping : [];
917 }
918
919 /**
920 * Close an open index.
921 *
922 * @param string $index Index name.
923 * @since 3.5
924 * @return boolean
925 */
926 public function close_index( $index ) {
927 $request_args = [
928 'method' => 'POST',
929 'timeout' => 30,
930 ];
931
932 $close = trailingslashit( $index ) . '_close';
933 $request = $this->remote_request( $close, $request_args, [], 'close_index' );
934
935 return ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) );
936 }
937
938 /**
939 * Open a closed index.
940 *
941 * @param string $index Index name.
942 * @since 3.5
943 * @return boolean
944 */
945 public function open_index( $index ) {
946 $request_args = [
947 'method' => 'POST',
948 'timeout' => 30,
949 ];
950
951 $open = trailingslashit( $index ) . '_open';
952 $request = $this->remote_request( $open, $request_args, [], 'open_index' );
953
954 return ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) );
955 }
956
957 /**
958 * Get index settings
959 *
960 * @param string $index Index name
961 * @param bool $force_refresh Whether to use or not a cached value. Default false, use cached.
962 * @since 4.4.0, 4.7.0 added the $force_refresh parameter
963 * @return array|WP_Error Raw ES response from the $index/_settings?flat_settings=true endpoint
964 */
965 public function get_index_settings( string $index, bool $force_refresh = false ) {
966 $transient_key = "ep_index_settings_{$index}";
967
968 if ( ! $force_refresh ) {
969 $cache = Utils\get_transient( $transient_key );
970 if ( false !== $cache ) {
971 return $cache;
972 }
973 }
974
975 $endpoint = trailingslashit( $index ) . '_settings?flat_settings=true';
976 $request = $this->remote_request( $endpoint, [], [], 'get_index_settings' );
977
978 if ( is_wp_error( $request ) ) {
979 Utils\set_transient( $transient_key, $request, MINUTE_IN_SECONDS );
980 return $request;
981 }
982
983 if ( wp_remote_retrieve_response_code( $request ) !== 200 ) {
984 Utils\set_transient( $transient_key, $request, MINUTE_IN_SECONDS );
985 return new \WP_Error(
986 'ep_get_index_settings_failed',
987 esc_html__( 'Error while getting the index settings.', 'elasticpress' ),
988 $request
989 );
990 }
991
992 $response_body = wp_remote_retrieve_body( $request );
993
994 $settings = json_decode( $response_body, true );
995
996 Utils\set_transient( $transient_key, $settings, DAY_IN_SECONDS );
997
998 return $settings;
999 }
1000
1001 /**
1002 * Get a particular index setting
1003 *
1004 * @param string $index Index name
1005 * @param string $setting Setting name
1006 * @param bool $force_refresh Whether to use or not a cached value. Default false, use cached.
1007 * @return mixed
1008 */
1009 public function get_index_setting( string $index, string $setting, bool $force_refresh = false ) {
1010 $settings = $this->get_index_settings( $index, $force_refresh );
1011
1012 if ( is_wp_error( $settings ) || empty( $settings[ $index ]['settings'][ $setting ] ) ) {
1013 return null;
1014 }
1015
1016 return $settings[ $index ]['settings'][ $setting ];
1017 }
1018
1019 /**
1020 * Given an index return its total fields limit
1021 *
1022 * @since 4.4.0, 4.7.0 wrapper of get_index_setting()
1023 * @param string $index_name The index name
1024 * @return int|null
1025 */
1026 public function get_index_total_fields_limit( $index_name ) {
1027 return $this->get_index_setting( $index_name, 'index.mapping.total_fields.limit' );
1028 }
1029
1030 /**
1031 * Update index settings.
1032 *
1033 * @param string $index Index name.
1034 * @param array $settings Setting update array.
1035 * @param boolean $close_first Optional. True if index must be closed prior to update.
1036 * Dynamic settings can be updated on open indices. Static
1037 * settings must be closed. Default false.
1038 * @since 3.5
1039 * @return boolean
1040 */
1041 public function update_index_settings( $index, $settings, $close_first = false ) {
1042 $request_args = [
1043 'body' => wp_json_encode( $settings ),
1044 'method' => 'PUT',
1045 'timeout' => 30,
1046 ];
1047
1048 if ( $close_first ) {
1049 $this->close_index( $index );
1050 }
1051
1052 $settings_url = trailingslashit( $index ) . '_settings';
1053 $request = $this->remote_request( $settings_url, $request_args, [], 'update_index_settings' );
1054
1055 $updated = ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) );
1056
1057 /**
1058 * Fires after updating an index settings
1059 *
1060 * @hook ep_update_index_settings
1061 * @since 4.4.0
1062 * @param {string} $index Index name
1063 * @param {array} $settings Setting update array
1064 */
1065 do_action( 'ep_update_index_settings', $index, $settings );
1066
1067 if ( $close_first ) {
1068 $opened = $this->open_index( $index );
1069 return ( $updated && $opened );
1070 }
1071
1072 return $updated;
1073 }
1074
1075 /**
1076 * Delete an Elasticsearch index
1077 *
1078 * @param string $index Index name.
1079 * @since 3.0
1080 * @return boolean
1081 */
1082 public function delete_index( $index ) {
1083
1084 $request_args = [
1085 'method' => 'DELETE',
1086 'timeout' => 30,
1087 ];
1088
1089 $request = $this->remote_request( $index, $request_args, [], 'delete_index' );
1090
1091 // 200 means the delete was successful
1092 // 404 means the index was non-existent, but we should still pass this through as we will occasionally want to delete an already deleted index
1093 if ( ! is_wp_error( $request ) && ( 200 === wp_remote_retrieve_response_code( $request ) || 404 === wp_remote_retrieve_response_code( $request ) ) ) {
1094 $response_body = wp_remote_retrieve_body( $request );
1095
1096 return json_decode( $response_body );
1097 }
1098
1099 return false;
1100 }
1101
1102 /**
1103 * Delete all indices
1104 *
1105 * @since 3.0
1106 * @return boolean
1107 */
1108 public function delete_all_indices() {
1109 return $this->delete_index( '*' );
1110 }
1111
1112 /**
1113 * Check if an ES index exists
1114 *
1115 * @param string $index Index name.
1116 * @since 3.0
1117 * @return boolean
1118 */
1119 public function index_exists( $index ) {
1120
1121 $request_args = [
1122 'method' => 'HEAD',
1123 ];
1124
1125 $request = $this->remote_request( $index, $request_args, [], 'index_exists' );
1126
1127 // 200 means the index exists.
1128 // 404 means the index was non-existent.
1129 if ( ! is_wp_error( $request ) && ( 200 === wp_remote_retrieve_response_code( $request ) || 404 === wp_remote_retrieve_response_code( $request ) ) ) {
1130
1131 if ( 404 === wp_remote_retrieve_response_code( $request ) ) {
1132 return false;
1133 }
1134
1135 if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
1136 return true;
1137 }
1138 }
1139
1140 return false;
1141 }
1142
1143 /**
1144 * Bulk index Elasticsearch documents
1145 *
1146 * @param string $index Index name.
1147 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
1148 * @param string $body Encoded JSON.
1149 * @since 3.0
1150 * @return WP_Error|array
1151 */
1152 public function bulk_index( $index, $type, $body ) {
1153 /**
1154 * Filter Elasticsearch bulk index request path
1155 *
1156 * @hook ep_bulk_index_request_path
1157 * @param {string} Request path
1158 * @param {string} $body Bulk index request body
1159 * @param {string} $type Index type
1160 * @return {string} New path
1161 */
1162 if ( version_compare( (string) $this->get_elasticsearch_version(), '7.0', '<' ) ) {
1163 $path = apply_filters( 'ep_bulk_index_request_path', $index . '/' . $type . '/_bulk', $body, $type );
1164 } else {
1165 $path = apply_filters( 'ep_bulk_index_request_path', $index . '/_bulk', $body, $type );
1166 }
1167
1168 $request_args = array(
1169 'method' => 'POST',
1170 'body' => $body,
1171 'timeout' => apply_filters( 'ep_bulk_index_timeout', 30 ),
1172 );
1173
1174 $request = $this->remote_request( $path, $request_args, [], 'bulk_index' );
1175
1176 if ( is_wp_error( $request ) ) {
1177 return $request;
1178 }
1179
1180 $response = wp_remote_retrieve_response_code( $request );
1181
1182 if ( 200 !== $response ) {
1183 return new WP_Error( $response, wp_remote_retrieve_response_message( $request ), $request );
1184 }
1185
1186 return json_decode( wp_remote_retrieve_body( $request ), true );
1187 }
1188
1189 /**
1190 * Return queries for debugging
1191 *
1192 * @since 1.8
1193 * @return array
1194 */
1195 public function get_query_log() {
1196 return $this->queries;
1197 }
1198
1199 /**
1200 * Wrapper for wp_remote_request
1201 *
1202 * This is a wrapper function for wp_remote_request to account for request failures.
1203 *
1204 * @since 1.6
1205 *
1206 * @param string $path Site URL to retrieve.
1207 * @param array $args Optional. Request arguments. Default empty array.
1208 * @param array $query_args Optional. The query args originally passed to WP_Query.
1209 * @param string $type Type of request, used for debugging.
1210 *
1211 * @return WP_Error|array The response or WP_Error on failure.
1212 */
1213 public function remote_request( $path, $args = [], $query_args = [], $type = null ) {
1214
1215 if ( empty( $args['method'] ) ) {
1216 $args['method'] = 'GET';
1217 }
1218
1219 // Checks for any previously set headers
1220 $existing_headers = isset( $args['headers'] ) ? (array) $args['headers'] : [];
1221
1222 // Add the API Header.
1223 // Note that the "User Agent" header will be changed via WordPress's `http_headers_useragent` filter later.
1224 $new_headers = $this->format_request_headers();
1225
1226 $args['headers'] = array_merge( $existing_headers, $new_headers );
1227
1228 /**
1229 * Filter Elasticsearch args prior to remote request
1230 *
1231 * @hook ep_pre_request_args
1232 * @since 3.6.4
1233 * @param {array} $args Request args
1234 * @param {string} $path Site URL to retrieve
1235 * @param {array} $query_args The query args originally passed to WP_Query.
1236 * @param {string|null} $type Type of request, used for debugging.
1237 * @return {array} New request args
1238 */
1239 $args = apply_filters( 'ep_pre_request_args', $args, $path, $query_args, $type );
1240
1241 $query = array(
1242 'time_start' => microtime( true ),
1243 'time_finish' => false,
1244 'args' => $args,
1245 'blocking' => true,
1246 'failed_hosts' => [],
1247 'request' => false,
1248 'host' => Utils\get_host(),
1249 'query_args' => $query_args,
1250 );
1251
1252 $request = false;
1253 $failures = 0;
1254
1255 add_filter( 'http_headers_useragent', [ $this, 'add_elasticpress_version_to_user_agent' ] );
1256
1257 // Optionally let us try back up hosts and account for failures.
1258 while ( true ) {
1259 /**
1260 * Filter Elasticsearch host prior to remote request
1261 *
1262 * @hook ep_pre_request_host
1263 * @param {string} Request host
1264 * @param {int} $failures Number of current failures
1265 * @param {string} $path Request path
1266 * @param {array} $args Request arguments
1267 * @return {string} New host
1268 */
1269 $query['host'] = apply_filters( 'ep_pre_request_host', $query['host'], $failures, $path, $args );
1270
1271 /**
1272 * Filter Elasticsearch url prior to remote request
1273 *
1274 * @hook ep_pre_request_url
1275 * @param {string} Request url
1276 * @param {int} $failures Number of current failures
1277 * @param {string} $host Request host
1278 * @param {string} $path Request path
1279 * @param {array} $args Request arguments
1280 * @return {string} New url
1281 */
1282 $query['url'] = apply_filters( 'ep_pre_request_url', esc_url( trailingslashit( $query['host'] ) . $path ), $failures, $query['host'], $path, $args );
1283
1284 /**
1285 * Filter whether remote request should be intercepted
1286 *
1287 * @hook ep_intercept_remote_request
1288 * @param {boolean} $intercept True to intercept
1289 * @return {boolean} New value
1290 */
1291 if ( true === apply_filters( 'ep_intercept_remote_request', false ) ) {
1292 /**
1293 * Filter intercepted request
1294 *
1295 * @hook ep_do_intercept_request
1296 * @since 3.2.2
1297 * @since 3.6.5 added $type
1298 * @param {array} $request New remote request response
1299 * @param {array} $query Remote request arguments
1300 * @param {args} $args Request arguments
1301 * @param {int} $failures Number of failures
1302 * @param {string} $type Type of request
1303 * @return {array} New request
1304 */
1305 $request = apply_filters( 'ep_do_intercept_request', new WP_Error( 400, 'No Request defined' ), $query, $args, $failures, $type );
1306 } else {
1307 $request = wp_remote_request( $query['url'], $args ); // try the existing host to avoid unnecessary calls.
1308 }
1309
1310 $request_response_code = (int) wp_remote_retrieve_response_code( $request );
1311
1312 $is_valid_res = ( $request_response_code >= 200 && $request_response_code <= 299 );
1313 $is_non_blocking_request = ( 0 === $request_response_code );
1314
1315 if ( false === $request || is_wp_error( $request ) || ( ! $is_valid_res && ! $is_non_blocking_request ) ) {
1316 $failures++;
1317
1318 /**
1319 * Filter max number of times to attempt remote requests
1320 *
1321 * @hook ep_max_remote_request_tries
1322 * @param {int} $tries Number of times to try
1323 * @param {path} $path Request path
1324 * @param {args} $args Request arguments
1325 * @return {int} New number of tries
1326 */
1327 if ( $failures >= apply_filters( 'ep_max_remote_request_tries', 1, $path, $args ) ) {
1328 break;
1329 }
1330 } else {
1331 break;
1332 }
1333 }
1334
1335 remove_filter( 'http_headers_useragent', [ $this, 'add_elasticpress_version_to_user_agent' ] );
1336
1337 // Return now if we're not blocking, since we won't have a response yet.
1338 if ( isset( $args['blocking'] ) && false === $args['blocking'] ) {
1339 $query['blocking'] = true;
1340 $query['request'] = $request;
1341 $this->add_query_log( $query );
1342
1343 return $request;
1344 }
1345
1346 $query['time_finish'] = microtime( true );
1347 $query['request'] = $request;
1348 $this->add_query_log( $query );
1349
1350 /**
1351 * Fires after Elasticsearch remote request
1352 *
1353 * @hook ep_remote_request
1354 * @param {array} $query Remote request arguments
1355 * @param {string} $type Request type
1356 */
1357 do_action( 'ep_remote_request', $query, $type );
1358
1359 return $request;
1360
1361 }
1362
1363 /**
1364 * Parse response from Elasticsearch
1365 *
1366 * Determines if there is an issue or if the response is valid.
1367 *
1368 * @since 1.9
1369 * @param object $response JSON decoded response from Elasticsearch.
1370 * @return array Contains the status message or the returned statistics.
1371 */
1372 public function parse_api_response( $response ) {
1373
1374 if ( null === $response ) {
1375
1376 return array(
1377 'status' => false,
1378 'msg' => esc_html__( 'Invalid response from ElasticPress server. Please contact your administrator.' ),
1379 );
1380
1381 } elseif (
1382 isset( $response->error ) &&
1383 (
1384 ( is_string( $response->error ) && stristr( $response->error, 'IndexMissingException' ) ) ||
1385 ( isset( $response->error->reason ) && stristr( $response->error->reason, 'no such index' ) )
1386 )
1387 ) {
1388
1389 if ( is_multisite() ) {
1390
1391 $error = __( 'Site not indexed. <p>Please run: <code>wp elasticpress index --setup --network-wide</code> using WP-CLI. Or use the index button on the left of this screen.</p>', 'elasticpress' );
1392
1393 } else {
1394
1395 $error = __( 'Site not indexed. <p>Please run: <code>wp elasticpress index --setup</code> using WP-CLI. Or use the index button on the left of this screen.</p>', 'elasticpress' );
1396
1397 }
1398
1399 return array(
1400 'status' => false,
1401 'msg' => $error,
1402 );
1403
1404 }
1405
1406 return array(
1407 'status' => true,
1408 'data' => $response->_all->primaries->indexing,
1409 );
1410
1411 }
1412
1413 /**
1414 * Set ES plugins and version, detect server type, and cache everything
1415 *
1416 * @since 4.2.1
1417 * @param bool $force Bust cache or not.
1418 * @return array
1419 */
1420 public function set_elasticsearch_info( $force = false ) {
1421 if ( empty( Utils\get_host() ) ) {
1422 return;
1423 }
1424
1425 if ( ! $force && null !== $this->elasticsearch_version && null !== $this->elasticsearch_plugins ) {
1426 return;
1427 }
1428
1429 // Get ES info from cache if available. If we are forcing, then skip cache check.
1430 if ( ! $force ) {
1431 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1432 $es_info = get_site_transient( 'ep_es_info' );
1433 } else {
1434 $es_info = get_transient( 'ep_es_info' );
1435 }
1436 if ( ! empty( $es_info ) ) {
1437 $this->elasticsearch_version = $es_info['version'];
1438 $this->elasticsearch_plugins = $es_info['plugins'];
1439 $this->server_type = $es_info['server_type'];
1440 return;
1441 }
1442 }
1443
1444 $path = '_nodes/plugins';
1445
1446 $request = $this->remote_request( $path, array( 'method' => 'GET' ) );
1447
1448 if ( is_wp_error( $request ) || 200 !== wp_remote_retrieve_response_code( $request ) ) {
1449 $this->elasticsearch_version = false;
1450 $this->elasticsearch_plugins = false;
1451
1452 /**
1453 * Try a different endpoint in case the plugins url is restricted
1454 *
1455 * @since 2.2.1
1456 */
1457
1458 $request = $this->remote_request( '', array( 'method' => 'GET' ) );
1459
1460 if ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) ) {
1461 $response_body = wp_remote_retrieve_body( $request );
1462 $response = json_decode( $response_body, true );
1463
1464 try {
1465 $this->elasticsearch_version = $response['version']['number'];
1466 if ( ! empty( $response['version']['distribution'] ) ) {
1467 $this->server_type = $response['version']['distribution'];
1468 }
1469 } catch ( \Exception $e ) {
1470 // Do nothing.
1471 }
1472 }
1473 return;
1474 }
1475
1476 $response = json_decode( wp_remote_retrieve_body( $request ), true );
1477
1478 $this->elasticsearch_plugins = [];
1479 $this->elasticsearch_version = false;
1480
1481 if ( isset( $response['nodes'] ) ) {
1482 $node = end( $response['nodes'] );
1483 // Save version of last node. We assume all nodes are same version.
1484 $this->elasticsearch_version = $node['version'];
1485
1486 if ( isset( $node['plugins'] ) && is_array( $node['plugins'] ) ) {
1487 foreach ( $node['plugins'] as $plugin ) {
1488 $this->elasticsearch_plugins[ $plugin['name'] ] = $plugin['version'];
1489 }
1490 }
1491 if ( isset( $node['modules'] )
1492 && is_array( $node['modules'] )
1493 && ! empty( $node['modules'] )
1494 && ! empty( $node['modules'][0]['opensearch_version'] )
1495 ) {
1496 $this->server_type = 'opensearch';
1497 }
1498 }
1499
1500 /**
1501 * Cache ES info
1502 *
1503 * @since 2.3.1
1504 */
1505 $this->cache_elasticsearch_info();
1506 }
1507
1508 /**
1509 * Return ES plugins, version and type.
1510 *
1511 * This function also sets those values in the object instance, getting it from cache
1512 * or not, according to `$force` value.
1513 *
1514 * @param bool $force Bust cache or not.
1515 * @since 2.2
1516 * @return array
1517 */
1518 public function get_elasticsearch_info( $force = false ) {
1519 $this->set_elasticsearch_info( $force );
1520 return [
1521 'plugins' => $this->elasticsearch_plugins,
1522 'version' => $this->elasticsearch_version,
1523 'server_type' => $this->server_type,
1524 ];
1525 }
1526
1527 /**
1528 * Cache the ES info.
1529 *
1530 * @since 4.2.1
1531 */
1532 protected function cache_elasticsearch_info() {
1533 /**
1534 * Filter elasticsearch info cache expiration
1535 *
1536 * @hook ep_es_info_cache_expiration
1537 * @param {int} $time Cache time in seconds
1538 * @return {int} New cache time
1539 */
1540 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1541 set_site_transient(
1542 'ep_es_info',
1543 array(
1544 'version' => $this->elasticsearch_version,
1545 'plugins' => $this->elasticsearch_plugins,
1546 'server_type' => $this->server_type,
1547 ),
1548 apply_filters( 'ep_es_info_cache_expiration', ( 5 * MINUTE_IN_SECONDS ) )
1549 );
1550 } else {
1551 set_transient(
1552 'ep_es_info',
1553 array(
1554 'version' => $this->elasticsearch_version,
1555 'plugins' => $this->elasticsearch_plugins,
1556 'server_type' => $this->server_type,
1557 ),
1558 apply_filters( 'ep_es_info_cache_expiration', ( 5 * MINUTE_IN_SECONDS ) )
1559 );
1560 }
1561 }
1562
1563 /**
1564 * Get cluster status
1565 *
1566 * Retrieves cluster stats from Elasticsearch.
1567 *
1568 * @since 1.9
1569 * @return array Contains the status message or the returned statistics.
1570 */
1571 public function get_cluster_status() {
1572
1573 if ( is_wp_error( Utils\get_host() ) ) {
1574
1575 return array(
1576 'status' => false,
1577 'msg' => esc_html__( 'Elasticsearch Host is not available.', 'elasticpress' ),
1578 );
1579
1580 } else {
1581
1582 $request = $this->remote_request( '_cluster/stats', array( 'method' => 'GET' ) );
1583
1584 if ( ! is_wp_error( $request ) ) {
1585
1586 $response = json_decode( wp_remote_retrieve_body( $request ) );
1587
1588 return $response;
1589
1590 }
1591
1592 return array(
1593 'status' => false,
1594 'msg' => $request->get_error_message(),
1595 );
1596
1597 }
1598 }
1599
1600 /**
1601 * Get an Elasticsearch pipeline
1602 *
1603 * @param string $id Id of pipeline.
1604 * @since 2.3
1605 * @return WP_Error|bool|array
1606 */
1607 public function get_pipeline( $id ) {
1608 $path = '_ingest/pipeline/' . $id;
1609
1610 $request_args = array(
1611 'method' => 'GET',
1612 );
1613
1614 /**
1615 * Filter get pipeline request arguments
1616 *
1617 * @hook ep_get_pipeline_args
1618 * @param {array} $request_args Request arguments
1619 * @return {array} New arguments
1620 */
1621 $request = $this->remote_request( $path, apply_filters( 'ep_get_pipeline_args', $request_args ), [], 'get_pipeline' );
1622
1623 if ( is_wp_error( $request ) ) {
1624 return $request;
1625 }
1626
1627 $response = wp_remote_retrieve_response_code( $request );
1628
1629 if ( 200 !== $response ) {
1630 return new WP_Error( $response, wp_remote_retrieve_response_message( $request ), $request );
1631 }
1632
1633 $body = json_decode( wp_remote_retrieve_body( $request ), true );
1634
1635 if ( empty( $body ) ) {
1636 return false;
1637 }
1638
1639 return $body;
1640 }
1641
1642 /**
1643 * Put an Elasticsearch pipeline
1644 *
1645 * @param string $id Pipeline id.
1646 * @param array $args Args to send to ES.
1647 * @since 2.3
1648 * @return WP_Error|bool
1649 */
1650 public function create_pipeline( $id, $args ) {
1651 $path = '_ingest/pipeline/' . $id;
1652
1653 $request_args = array(
1654 'body' => wp_json_encode( $args ),
1655 'method' => 'PUT',
1656 );
1657
1658 /**
1659 * Filter create pipeline request arguments
1660 *
1661 * @hook ep_create_pipeline_args
1662 * @param {array} $request_args Request arguments
1663 * @return {array} New arguments
1664 */
1665 $request = $this->remote_request( $path, apply_filters( 'ep_create_pipeline_args', $request_args ), [], 'create_pipeline' );
1666
1667 if ( is_wp_error( $request ) ) {
1668 return $request;
1669 }
1670
1671 $response = wp_remote_retrieve_response_code( $request );
1672
1673 if ( 200 > $response || 300 <= $response ) {
1674 return new WP_Error( $response, wp_remote_retrieve_response_message( $request ), $request );
1675 }
1676
1677 $body = json_decode( wp_remote_retrieve_body( $request ), true );
1678
1679 if ( empty( $body ) ) {
1680 return false;
1681 }
1682
1683 return true;
1684 }
1685
1686 /**
1687 * Conditionally add the ElasticPress version to the User Agent string.
1688 *
1689 * @since 3.6.1
1690 * @param string $user_agent Original User Agent.
1691 * @return string
1692 */
1693 public function add_elasticpress_version_to_user_agent( $user_agent ) {
1694 /**
1695 * Filter the User Agent header when submitting requests to Elasticsearch.
1696 *
1697 * @hook ep_remote_request_add_ep_user_agent
1698 * @param {bool} $should_add_ep_verion Whether the ElasticPress version should be added to the User Agent string.
1699 * @return {bool} New value
1700 * @since 3.6.1
1701 */
1702 if ( apply_filters( 'ep_remote_request_add_ep_user_agent', Utils\is_epio() ) ) {
1703 $end_part = '; ' . get_bloginfo( 'url' );
1704 $user_agent = str_replace(
1705 $end_part,
1706 ' (ElasticPress/' . EP_VERSION . ')' . $end_part,
1707 $user_agent
1708 );
1709 }
1710 return $user_agent;
1711 }
1712
1713 /**
1714 * Query logging. Don't log anything to the queries property when
1715 * WP_DEBUG is not enabled. Calls action 'ep_add_query_log' if you
1716 * want to access the query outside of the ElasticPress plugin. This
1717 * runs regardless of debufg settings.
1718 *
1719 * @param array $query Query to log.
1720 */
1721 protected function add_query_log( $query ) {
1722 if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || ( defined( 'WP_EP_DEBUG' ) && WP_EP_DEBUG ) ) {
1723 $this->queries[] = $query;
1724 }
1725
1726 /**
1727 * Fires after item is added to the query log
1728 *
1729 * @hook ep_add_query_log
1730 * @param {array} $query Query to log
1731 */
1732 do_action( 'ep_add_query_log', $query );
1733 }
1734
1735 /**
1736 * Get all index names.
1737 *
1738 * @param string $status Whether to return active indexables or all registered.
1739 * @since 4.4.0, 4.5.0 Added $status
1740 * @return array
1741 */
1742 public function get_index_names( $status = 'active' ) {
1743 $sites = ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) ?
1744 Utils\get_sites( 0, true ) :
1745 array( array( 'blog_id' => get_current_blog_id() ) );
1746
1747 $all_indexables = Indexables::factory()->get_all( null, false, $status );
1748
1749 $global_indexes = [];
1750 $non_global_indexes = [];
1751 foreach ( $all_indexables as $indexable ) {
1752 if ( $indexable->global ) {
1753 $global_indexes[] = $indexable->get_index_name();
1754 continue;
1755 }
1756
1757 foreach ( $sites as $site ) {
1758 $non_global_indexes[] = $indexable->get_index_name( $site['blog_id'] );
1759 }
1760 }
1761
1762 return array_merge( $non_global_indexes, $global_indexes );
1763 }
1764
1765 /**
1766 * Return all indices from the cluster.
1767 *
1768 * @since 4.4.0
1769 * @return array Array of indices in Elasticsearch
1770 */
1771 public function get_cluster_indices() : array {
1772 $path = '_cat/indices?format=json';
1773
1774 $response = $this->remote_request( $path );
1775
1776 return (array) json_decode( wp_remote_retrieve_body( $response ), true );
1777 }
1778
1779 /**
1780 * Return a comparison between which indices should be and are present in the ES server.
1781 *
1782 * @since 4.6.0
1783 * @return array Array with `missing_indices` and `present_indices` keys.
1784 */
1785 public function get_indices_comparison() {
1786 $all_index_names = $this->get_index_names();
1787 $cluster_indices = $this->get_cluster_indices();
1788
1789 $cluster_index_names = wp_list_pluck( $cluster_indices, 'index' );
1790
1791 return [
1792 'missing_indices' => array_diff( $all_index_names, $cluster_index_names ),
1793 'present_indices' => array_intersect( $all_index_names, $cluster_index_names ),
1794 ];
1795 }
1796 }
1797