PluginProbe
ElasticPress / 4.5.2
ElasticPress v4.5.2
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.5.2, at includes/classes/Elasticsearch.php

1,748 lines 47.7 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( $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( $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 ],
472 $response,
473 $query,
474 $query_args,
475 $query_object
476 );
477 }
478
479 /**
480 * Fires after invalid Elasticsearch query
481 *
482 * @hook ep_invalid_response
483 * @param {array} $request Remote request response
484 * @param {array} $query Prepared Elasticsearch query
485 * @param {array} $query_args Current WP Query arguments
486 * @param {mixed} $query_object Could be WP_Query, WP_User_Query, etc.
487 */
488 do_action( 'ep_invalid_response', $request, $query, $query_args, $query_object );
489
490 return false;
491 }
492
493 /**
494 * Returns the number of total results that ElasticSearch found for the given query
495 *
496 * @param array $response Response to get total hits from.
497 * @since 2.5
498 * @return int
499 */
500 public function get_total_hits_from_query( $response ) {
501
502 if ( $this->is_empty_query( $response ) ) {
503 return 0;
504 }
505
506 return $response['hits']['total'];
507 }
508
509 /**
510 * Returns array containing hits returned from query, if such exist
511 *
512 * @param array $response Response to get hits from.
513 * @since 2.5
514 * @return array
515 */
516 public function get_hits_from_query( $response ) {
517
518 if ( $this->is_empty_query( $response ) ) {
519 return [];
520 }
521
522 /**
523 * Filter Elasticsearch allows to flatten hits, if searched hits are come within aggregations.
524 *
525 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-hits-aggregation.html
526 *
527 * @hook ep_get_hits_from_query
528 * @param {array} $hits from Elasticsearch
529 * @param {response} $response Raw response from Elasticsearch
530 * @return {array} hits
531 */
532 return apply_filters( 'ep_get_hits_from_query', $response['hits']['hits'], $response );
533 }
534
535 /**
536 * Check if a response array contains results or not
537 *
538 * @param array $response Response to check.
539 * @since 0.1.2
540 * @return bool
541 */
542 public function is_empty_query( $response ) {
543
544 if ( ! is_array( $response ) ) {
545 return true;
546 }
547
548 if ( isset( $response['error'] ) ) {
549 return true;
550 }
551
552 if ( empty( $response['hits'] ) ) {
553 return true;
554 }
555
556 if ( isset( $response['hits']['total'] ) && 0 === (int) $response['hits']['total'] ) {
557 return true;
558 }
559
560 return false;
561 }
562
563 /**
564 * Delete an Elasticsearch document
565 *
566 * @param string $index Index name.
567 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
568 * @param int $document_id Document id to delete.
569 * @param boolean $blocking Blocking HTTP request or not.
570 * @since 3.0
571 * @return boolean
572 */
573 public function delete_document( $index, $type, $document_id, $blocking = true ) {
574 if ( version_compare( $this->get_elasticsearch_version(), '7.0', '<' ) ) {
575 $path = $index . '/' . $type . '/' . $document_id;
576 } else {
577 $path = $index . '/_doc/' . $document_id;
578 }
579
580 $request_args = [
581 'method' => 'DELETE',
582 'timeout' => 15,
583 'blocking' => $blocking,
584 ];
585
586 $request = $this->remote_request( $path, $request_args, [], 'delete' );
587
588 if ( ! is_wp_error( $request ) ) {
589 $response_body = wp_remote_retrieve_body( $request );
590
591 $response = json_decode( $response_body, true );
592
593 if ( ! empty( $response['found'] ) ) {
594 return true;
595 }
596 }
597
598 return false;
599 }
600
601 /**
602 * Add appropriate headers to request
603 *
604 * @since 1.4
605 * @return array
606 */
607 public function format_request_headers() {
608 $headers = array(
609 'Content-Type' => 'application/json',
610 );
611
612 // Check for ElasticPress API key and add to header if needed.
613 if ( defined( 'EP_API_KEY' ) && EP_API_KEY ) {
614 $headers['X-ElasticPress-API-Key'] = EP_API_KEY;
615 }
616
617 /**
618 * ES Shield info
619 *
620 * @since 1.9
621 */
622 $shield = Utils\get_shield_credentials();
623
624 if ( ! empty( $shield ) ) {
625 // phpcs:disable
626 $headers['Authorization'] = 'Basic ' . base64_encode( $shield );
627 // phpcs:enable
628 }
629
630 $request_id = Utils\generate_request_id();
631 if ( ! empty( $request_id ) ) {
632 $headers['X-ElasticPress-Request-ID'] = $request_id;
633 }
634
635 /**
636 * Filter Elasticsearch request headers
637 *
638 * @hook ep_format_request_headers
639 * @param {array} $headers Current headers
640 * @return {array} New headers
641 */
642 $headers = apply_filters( 'ep_format_request_headers', $headers );
643
644 return $headers;
645 }
646
647 /**
648 * Get a document from Elasticsearch given an id
649 *
650 * @param string $index Index name.
651 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
652 * @param int $document_id Document id to get.
653 * @since 3.0
654 * @return boolean|array
655 */
656 public function get_document( $index, $type, $document_id ) {
657 if ( version_compare( $this->get_elasticsearch_version(), '7.0', '<' ) ) {
658 $path = $index . '/' . $type . '/' . $document_id;
659 } else {
660 $path = $index . '/_doc/' . $document_id;
661 }
662
663 $request_args = [ 'method' => 'GET' ];
664
665 $request = $this->remote_request( $path, $request_args, [], 'get' );
666
667 if ( ! is_wp_error( $request ) ) {
668 $response_body = wp_remote_retrieve_body( $request );
669
670 $response = json_decode( $response_body, true );
671
672 if ( ! empty( $response['exists'] ) || ! empty( $response['found'] ) ) {
673 return $response['_source'];
674 }
675 }
676
677 return false;
678 }
679
680 /**
681 * Delete the network alias.
682 *
683 * Network aliases are used to query documents across blogs in a network.
684 *
685 * @param string $alias Alias to use.
686 * @since 3.0
687 * @return array|boolean
688 */
689 public function delete_network_alias( $alias ) {
690 $path = '*/_alias/' . $alias;
691
692 $request_args = [ 'method' => 'DELETE' ];
693
694 $request = $this->remote_request( $path, $request_args, [], 'delete_network_alias' );
695
696 if ( ! is_wp_error( $request ) && ( 200 >= wp_remote_retrieve_response_code( $request ) && 300 > wp_remote_retrieve_response_code( $request ) ) ) {
697 $response_body = wp_remote_retrieve_body( $request );
698
699 return json_decode( $response_body );
700 }
701
702 return false;
703 }
704
705 /**
706 * Get multiple documents from Elasticsearch given an array of ids
707 *
708 * @param string $index Index name.
709 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
710 * @param array $document_ids Array of document ids to get.
711 * @since 3.6.0
712 * @return boolean|array
713 */
714 public function get_documents( $index, $type, $document_ids ) {
715 if ( version_compare( $this->get_elasticsearch_version(), '7.0', '<' ) ) {
716 $path = apply_filters( 'ep_index_' . $type . '_request_path', $index . '/' . $type . '/_mget', $document_ids, $type );
717 } else {
718 $path = apply_filters( 'ep_index_' . $type . '_request_path', $index . '/_mget', $document_ids, $type );
719 }
720
721 $request_args = [
722 'method' => 'POST',
723 'body' => wp_json_encode(
724 array(
725 'ids' => $document_ids,
726 )
727 ),
728 ];
729
730 $request = $this->remote_request( $path, $request_args, [], 'post' );
731
732 if ( is_wp_error( $request ) ) {
733 return false;
734 }
735
736 $response_body = wp_remote_retrieve_body( $request );
737
738 $response = json_decode( $response_body, true );
739
740 $docs = [];
741
742 if ( isset( $response['docs'] ) && is_array( $response['docs'] ) ) {
743 foreach ( $response['docs'] as $doc ) {
744 if ( ! empty( $doc['exists'] ) || ! empty( $doc['found'] ) ) {
745 $docs[ $doc['_id'] ] = $doc['_source'];
746 }
747 }
748 }
749
750 /**
751 * Filter documents found by Elasticsearch through the /_mget endpoint.
752 *
753 * @hook ep_get_documents
754 * @since 3.6.0
755 * @param {array} $docs Documents found indexed by ID
756 * @param {string} $index Index name
757 * @param {string} $type Index type
758 * @param {array} $document_ids Array of document ids
759 * @return {array} Documents to be returned
760 */
761 $docs = apply_filters( 'ep_get_documents', $docs, $index, $type, $document_ids );
762
763 return $docs;
764 }
765
766 /**
767 * Create the network alias.
768 *
769 * Network aliases are used to query documents across blogs in a network.
770 *
771 * @param array $indexes Indexes to group under alias.
772 * @param string $network_alias Name of network alias.
773 * @since 3.0
774 * @return boolean
775 */
776 public function create_network_alias( $indexes, $network_alias ) {
777
778 $path = '_aliases';
779
780 $args = array(
781 'actions' => [],
782 );
783
784 foreach ( $indexes as $index ) {
785 if ( empty( $index ) ) {
786 continue;
787 }
788
789 $args['actions'][] = array(
790 'add' => array(
791 'index' => $index,
792 'alias' => $network_alias,
793 ),
794 );
795 }
796
797 $request_args = array(
798 'body' => wp_json_encode( $args ),
799 'method' => 'POST',
800 'timeout' => 25,
801 );
802
803 $request = $this->remote_request( $path, $request_args, [], 'create_network_alias' );
804
805 if ( ! is_wp_error( $request ) && ( 200 >= wp_remote_retrieve_response_code( $request ) && 300 > wp_remote_retrieve_response_code( $request ) ) ) {
806 return true;
807 }
808
809 return false;
810 }
811
812 /**
813 * Put a mapping into Elasticsearch
814 *
815 * @param string $index Index name.
816 * @param array $mapping Mapping array.
817 * @param string $return_type Desired return type. Can be either 'bool' or 'raw'
818 * @since 3.0
819 * @return boolean|WP_Error
820 */
821 public function put_mapping( $index, $mapping, $return_type = 'bool' ) {
822 /**
823 * Filter Elasticsearch mapping before put mapping
824 *
825 * @hook ep_config_mapping
826 * @param {array} $mapping Elasticsearch mapping
827 * @param {string} $index Index name
828 * @return {array} New mapping
829 */
830 $mapping = apply_filters( 'ep_config_mapping', $mapping, $index );
831
832 $request_args = [
833 'body' => wp_json_encode( $mapping ),
834 'method' => 'PUT',
835 'timeout' => 30,
836 ];
837
838 $request = $this->remote_request( $index, $request_args, [], 'put_mapping' );
839
840 /**
841 * Filter Elasticsearch put mapping response
842 *
843 * @hook ep_config_mapping_request
844 * @param {array} $request Elasticsearch response
845 * @param {string} $index Elasticsearch index name
846 * @param {array} $mapping Mapping sent to Elasticsearch
847 * @return {array} New response
848 */
849 $request = apply_filters( 'ep_config_mapping_request', $request, $index, $mapping );
850
851 $response_code = wp_remote_retrieve_response_code( $request );
852
853 // If WP_Error or not 200, return false or error message depends on attribute.
854 if ( is_wp_error( $request ) || 200 !== $response_code ) {
855 if ( 'bool' === $return_type ) {
856 return false;
857 }
858
859 if ( is_wp_error( $request ) ) {
860 return $request;
861 }
862
863 $response_body = wp_remote_retrieve_body( $request );
864 $parsed_response = json_decode( $response_body, true );
865 return new \WP_Error( $parsed_response['status'], $parsed_response['error'] );
866 }
867
868 return true;
869 }
870
871 /**
872 * Get current index mapping from Elasticsearch.
873 *
874 * @param string $index The index name.
875 * @since 3.5
876 * @return array
877 */
878 public function get_mapping( $index ) {
879 $request_args = [
880 'method' => 'GET',
881 'timeout' => 30,
882 ];
883
884 $request = $this->remote_request( $index, $request_args, [], 'get_mapping' );
885
886 if ( is_wp_error( $request ) || 200 !== wp_remote_retrieve_response_code( $request ) ) {
887 return [];
888 }
889
890 $body = wp_remote_retrieve_body( $request );
891
892 if ( ! $body ) {
893 return [];
894 }
895
896 $mapping = json_decode( $body, true );
897
898 return is_array( $mapping ) ? $mapping : [];
899 }
900
901 /**
902 * Close an open index.
903 *
904 * @param string $index Index name.
905 * @since 3.5
906 * @return boolean
907 */
908 public function close_index( $index ) {
909 $request_args = [
910 'method' => 'POST',
911 'timeout' => 30,
912 ];
913
914 $close = trailingslashit( $index ) . '_close';
915 $request = $this->remote_request( $close, $request_args, [], 'close_index' );
916
917 return ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) );
918 }
919
920 /**
921 * Open a closed index.
922 *
923 * @param string $index Index name.
924 * @since 3.5
925 * @return boolean
926 */
927 public function open_index( $index ) {
928 $request_args = [
929 'method' => 'POST',
930 'timeout' => 30,
931 ];
932
933 $open = trailingslashit( $index ) . '_open';
934 $request = $this->remote_request( $open, $request_args, [], 'open_index' );
935
936 return ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) );
937 }
938
939 /**
940 * Get index settings.
941 *
942 * @param string $index Index name.
943 * @since 4.4.0
944 * @return array|WP_Error Raw ES response from the $index/_settings?flat_settings=true endpoint
945 */
946 public function get_index_settings( string $index ) {
947 $endpoint = trailingslashit( $index ) . '_settings?flat_settings=true';
948 $request = $this->remote_request( $endpoint, [], [], 'get_index_settings' );
949
950 if ( is_wp_error( $request ) ) {
951 return $request;
952 }
953
954 $response_body = wp_remote_retrieve_body( $request );
955
956 $settings = json_decode( $response_body, true );
957
958 return $settings;
959 }
960
961 /**
962 * Update index settings.
963 *
964 * @param string $index Index name.
965 * @param array $settings Setting update array.
966 * @param boolean $close_first Optional. True if index must be closed prior to update.
967 * Dynamic settings can be updated on open indices. Static
968 * settings must be closed. Default false.
969 * @since 3.5
970 * @return boolean
971 */
972 public function update_index_settings( $index, $settings, $close_first = false ) {
973 $request_args = [
974 'body' => wp_json_encode( $settings ),
975 'method' => 'PUT',
976 'timeout' => 30,
977 ];
978
979 if ( $close_first ) {
980 $this->close_index( $index );
981 }
982
983 $settings_url = trailingslashit( $index ) . '_settings';
984 $request = $this->remote_request( $settings_url, $request_args, [], 'update_index_settings' );
985
986 $updated = ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) );
987
988 /**
989 * Fires after updating an index settings
990 *
991 * @hook ep_update_index_settings
992 * @since 4.4.0
993 * @param {string} $index Index name
994 * @param {array} $settings Setting update array
995 */
996 do_action( 'ep_update_index_settings', $index, $settings );
997
998 if ( $close_first ) {
999 $opened = $this->open_index( $index );
1000 return ( $updated && $opened );
1001 }
1002
1003 return $updated;
1004 }
1005
1006 /**
1007 * Delete an Elasticsearch index
1008 *
1009 * @param string $index Index name.
1010 * @since 3.0
1011 * @return boolean
1012 */
1013 public function delete_index( $index ) {
1014
1015 $request_args = [
1016 'method' => 'DELETE',
1017 'timeout' => 30,
1018 ];
1019
1020 $request = $this->remote_request( $index, $request_args, [], 'delete_index' );
1021
1022 // 200 means the delete was successful
1023 // 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
1024 if ( ! is_wp_error( $request ) && ( 200 === wp_remote_retrieve_response_code( $request ) || 404 === wp_remote_retrieve_response_code( $request ) ) ) {
1025 $response_body = wp_remote_retrieve_body( $request );
1026
1027 return json_decode( $response_body );
1028 }
1029
1030 return false;
1031 }
1032
1033 /**
1034 * Delete all indices
1035 *
1036 * @since 3.0
1037 * @return boolean
1038 */
1039 public function delete_all_indices() {
1040 return $this->delete_index( '*' );
1041 }
1042
1043 /**
1044 * Check if an ES index exists
1045 *
1046 * @param string $index Index name.
1047 * @since 3.0
1048 * @return boolean
1049 */
1050 public function index_exists( $index ) {
1051
1052 $request_args = [
1053 'method' => 'HEAD',
1054 ];
1055
1056 $request = $this->remote_request( $index, $request_args, [], 'index_exists' );
1057
1058 // 200 means the index exists.
1059 // 404 means the index was non-existent.
1060 if ( ! is_wp_error( $request ) && ( 200 === wp_remote_retrieve_response_code( $request ) || 404 === wp_remote_retrieve_response_code( $request ) ) ) {
1061
1062 if ( 404 === wp_remote_retrieve_response_code( $request ) ) {
1063 return false;
1064 }
1065
1066 if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
1067 return true;
1068 }
1069 }
1070
1071 return false;
1072 }
1073
1074 /**
1075 * Bulk index Elasticsearch documents
1076 *
1077 * @param string $index Index name.
1078 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
1079 * @param string $body Encoded JSON.
1080 * @since 3.0
1081 * @return WP_Error|array
1082 */
1083 public function bulk_index( $index, $type, $body ) {
1084 /**
1085 * Filter Elasticsearch bulk index request path
1086 *
1087 * @hook ep_bulk_index_request_path
1088 * @param {string} Request path
1089 * @param {string} $body Bulk index request body
1090 * @param {string} $type Index type
1091 * @return {string} New path
1092 */
1093 if ( version_compare( $this->get_elasticsearch_version(), '7.0', '<' ) ) {
1094 $path = apply_filters( 'ep_bulk_index_request_path', $index . '/' . $type . '/_bulk', $body, $type );
1095 } else {
1096 $path = apply_filters( 'ep_bulk_index_request_path', $index . '/_bulk', $body, $type );
1097 }
1098
1099 $request_args = array(
1100 'method' => 'POST',
1101 'body' => $body,
1102 'timeout' => apply_filters( 'ep_bulk_index_timeout', 30 ),
1103 );
1104
1105 $request = $this->remote_request( $path, $request_args, [], 'bulk_index' );
1106
1107 if ( is_wp_error( $request ) ) {
1108 return $request;
1109 }
1110
1111 $response = wp_remote_retrieve_response_code( $request );
1112
1113 if ( 200 !== $response ) {
1114 return new WP_Error( $response, wp_remote_retrieve_response_message( $request ), $request );
1115 }
1116
1117 return json_decode( wp_remote_retrieve_body( $request ), true );
1118 }
1119
1120 /**
1121 * Return queries for debugging
1122 *
1123 * @since 1.8
1124 * @return array
1125 */
1126 public function get_query_log() {
1127 return $this->queries;
1128 }
1129
1130 /**
1131 * Wrapper for wp_remote_request
1132 *
1133 * This is a wrapper function for wp_remote_request to account for request failures.
1134 *
1135 * @since 1.6
1136 *
1137 * @param string $path Site URL to retrieve.
1138 * @param array $args Optional. Request arguments. Default empty array.
1139 * @param array $query_args Optional. The query args originally passed to WP_Query.
1140 * @param string $type Type of request, used for debugging.
1141 *
1142 * @return WP_Error|array The response or WP_Error on failure.
1143 */
1144 public function remote_request( $path, $args = [], $query_args = [], $type = null ) {
1145
1146 if ( empty( $args['method'] ) ) {
1147 $args['method'] = 'GET';
1148 }
1149
1150 // Checks for any previously set headers
1151 $existing_headers = isset( $args['headers'] ) ? (array) $args['headers'] : [];
1152
1153 // Add the API Header.
1154 // Note that the "User Agent" header will be changed via WordPress's `http_headers_useragent` filter later.
1155 $new_headers = $this->format_request_headers();
1156
1157 $args['headers'] = array_merge( $existing_headers, $new_headers );
1158
1159 /**
1160 * Filter Elasticsearch args prior to remote request
1161 *
1162 * @hook ep_pre_request_args
1163 * @since 3.6.4
1164 * @param {array} $args Request args
1165 * @param {string} $path Site URL to retrieve
1166 * @param {array} $query_args The query args originally passed to WP_Query.
1167 * @param {string|null} $type Type of request, used for debugging.
1168 * @return {array} New request args
1169 */
1170 $args = apply_filters( 'ep_pre_request_args', $args, $path, $query_args, $type );
1171
1172 $query = array(
1173 'time_start' => microtime( true ),
1174 'time_finish' => false,
1175 'args' => $args,
1176 'blocking' => true,
1177 'failed_hosts' => [],
1178 'request' => false,
1179 'host' => Utils\get_host(),
1180 'query_args' => $query_args,
1181 );
1182
1183 $request = false;
1184 $failures = 0;
1185
1186 add_filter( 'http_headers_useragent', [ $this, 'add_elasticpress_version_to_user_agent' ] );
1187
1188 // Optionally let us try back up hosts and account for failures.
1189 while ( true ) {
1190 /**
1191 * Filter Elasticsearch host prior to remote request
1192 *
1193 * @hook ep_pre_request_host
1194 * @param {string} Request host
1195 * @param {int} $failures Number of current failures
1196 * @param {string} $path Request path
1197 * @param {array} $args Request arguments
1198 * @return {string} New host
1199 */
1200 $query['host'] = apply_filters( 'ep_pre_request_host', $query['host'], $failures, $path, $args );
1201
1202 /**
1203 * Filter Elasticsearch url prior to remote request
1204 *
1205 * @hook ep_pre_request_url
1206 * @param {string} Request url
1207 * @param {int} $failures Number of current failures
1208 * @param {string} $host Request host
1209 * @param {string} $path Request path
1210 * @param {array} $args Request arguments
1211 * @return {string} New url
1212 */
1213 $query['url'] = apply_filters( 'ep_pre_request_url', esc_url( trailingslashit( $query['host'] ) . $path ), $failures, $query['host'], $path, $args );
1214
1215 /**
1216 * Filter whether remote request should be intercepted
1217 *
1218 * @hook ep_intercept_remote_request
1219 * @param {boolean} $intercept True to intercept
1220 * @return {boolean} New value
1221 */
1222 if ( true === apply_filters( 'ep_intercept_remote_request', false ) ) {
1223 /**
1224 * Filter intercepted request
1225 *
1226 * @hook ep_do_intercept_request
1227 * @since 3.2.2
1228 * @since 3.6.5 added $type
1229 * @param {array} $request New remote request response
1230 * @param {array} $query Remote request arguments
1231 * @param {args} $args Request arguments
1232 * @param {int} $failures Number of failures
1233 * @param {string} $type Type of request
1234 * @return {array} New request
1235 */
1236 $request = apply_filters( 'ep_do_intercept_request', new WP_Error( 400, 'No Request defined' ), $query, $args, $failures, $type );
1237 } else {
1238 $request = wp_remote_request( $query['url'], $args ); // try the existing host to avoid unnecessary calls.
1239 }
1240
1241 $request_response_code = (int) wp_remote_retrieve_response_code( $request );
1242
1243 $is_valid_res = ( $request_response_code >= 200 && $request_response_code <= 299 );
1244 $is_non_blocking_request = ( 0 === $request_response_code );
1245
1246 if ( false === $request || is_wp_error( $request ) || ( ! $is_valid_res && ! $is_non_blocking_request ) ) {
1247 $failures++;
1248
1249 /**
1250 * Filter max number of times to attempt remote requests
1251 *
1252 * @hook ep_max_remote_request_tries
1253 * @param {int} $tries Number of times to try
1254 * @param {path} $path Request path
1255 * @param {args} $args Request arguments
1256 * @return {int} New number of tries
1257 */
1258 if ( $failures >= apply_filters( 'ep_max_remote_request_tries', 1, $path, $args ) ) {
1259 break;
1260 }
1261 } else {
1262 break;
1263 }
1264 }
1265
1266 remove_filter( 'http_headers_useragent', [ $this, 'add_elasticpress_version_to_user_agent' ] );
1267
1268 // Return now if we're not blocking, since we won't have a response yet.
1269 if ( isset( $args['blocking'] ) && false === $args['blocking'] ) {
1270 $query['blocking'] = true;
1271 $query['request'] = $request;
1272 $this->add_query_log( $query );
1273
1274 return $request;
1275 }
1276
1277 $query['time_finish'] = microtime( true );
1278 $query['request'] = $request;
1279 $this->add_query_log( $query );
1280
1281 /**
1282 * Fires after Elasticsearch remote request
1283 *
1284 * @hook ep_remote_request
1285 * @param {array} $query Remote request arguments
1286 * @param {string} $type Request type
1287 */
1288 do_action( 'ep_remote_request', $query, $type );
1289
1290 return $request;
1291
1292 }
1293
1294 /**
1295 * Parse response from Elasticsearch
1296 *
1297 * Determines if there is an issue or if the response is valid.
1298 *
1299 * @since 1.9
1300 * @param object $response JSON decoded response from Elasticsearch.
1301 * @return array Contains the status message or the returned statistics.
1302 */
1303 public function parse_api_response( $response ) {
1304
1305 if ( null === $response ) {
1306
1307 return array(
1308 'status' => false,
1309 'msg' => esc_html__( 'Invalid response from ElasticPress server. Please contact your administrator.' ),
1310 );
1311
1312 } elseif (
1313 isset( $response->error ) &&
1314 (
1315 ( is_string( $response->error ) && stristr( $response->error, 'IndexMissingException' ) ) ||
1316 ( isset( $response->error->reason ) && stristr( $response->error->reason, 'no such index' ) )
1317 )
1318 ) {
1319
1320 if ( is_multisite() ) {
1321
1322 $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' );
1323
1324 } else {
1325
1326 $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' );
1327
1328 }
1329
1330 return array(
1331 'status' => false,
1332 'msg' => $error,
1333 );
1334
1335 }
1336
1337 return array(
1338 'status' => true,
1339 'data' => $response->_all->primaries->indexing,
1340 );
1341
1342 }
1343
1344 /**
1345 * Set ES plugins and version, detect server type, and cache everything
1346 *
1347 * @since 4.2.1
1348 * @param bool $force Bust cache or not.
1349 * @return array
1350 */
1351 public function set_elasticsearch_info( $force = false ) {
1352 if ( empty( Utils\get_host() ) ) {
1353 return;
1354 }
1355
1356 if ( ! $force && null !== $this->elasticsearch_version && null !== $this->elasticsearch_plugins ) {
1357 return;
1358 }
1359
1360 // Get ES info from cache if available. If we are forcing, then skip cache check.
1361 if ( ! $force ) {
1362 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1363 $es_info = get_site_transient( 'ep_es_info' );
1364 } else {
1365 $es_info = get_transient( 'ep_es_info' );
1366 }
1367 if ( ! empty( $es_info ) ) {
1368 $this->elasticsearch_version = $es_info['version'];
1369 $this->elasticsearch_plugins = $es_info['plugins'];
1370 $this->server_type = $es_info['server_type'];
1371 return;
1372 }
1373 }
1374
1375 $path = '_nodes/plugins';
1376
1377 $request = $this->remote_request( $path, array( 'method' => 'GET' ) );
1378
1379 if ( is_wp_error( $request ) || 200 !== wp_remote_retrieve_response_code( $request ) ) {
1380 $this->elasticsearch_version = false;
1381 $this->elasticsearch_plugins = false;
1382
1383 /**
1384 * Try a different endpoint in case the plugins url is restricted
1385 *
1386 * @since 2.2.1
1387 */
1388
1389 $request = $this->remote_request( '', array( 'method' => 'GET' ) );
1390
1391 if ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) ) {
1392 $response_body = wp_remote_retrieve_body( $request );
1393 $response = json_decode( $response_body, true );
1394
1395 try {
1396 $this->elasticsearch_version = $response['version']['number'];
1397 if ( ! empty( $response['version']['distribution'] ) ) {
1398 $this->server_type = $response['version']['distribution'];
1399 }
1400 } catch ( \Exception $e ) {
1401 // Do nothing.
1402 }
1403 }
1404 return;
1405 }
1406
1407 $response = json_decode( wp_remote_retrieve_body( $request ), true );
1408
1409 $this->elasticsearch_plugins = [];
1410 $this->elasticsearch_version = false;
1411
1412 if ( isset( $response['nodes'] ) ) {
1413 $node = end( $response['nodes'] );
1414 // Save version of last node. We assume all nodes are same version.
1415 $this->elasticsearch_version = $node['version'];
1416
1417 if ( isset( $node['plugins'] ) && is_array( $node['plugins'] ) ) {
1418 foreach ( $node['plugins'] as $plugin ) {
1419 $this->elasticsearch_plugins[ $plugin['name'] ] = $plugin['version'];
1420 }
1421 }
1422 if ( isset( $node['modules'] )
1423 && is_array( $node['modules'] )
1424 && ! empty( $node['modules'] )
1425 && ! empty( $node['modules'][0]['opensearch_version'] )
1426 ) {
1427 $this->server_type = 'opensearch';
1428 }
1429 }
1430
1431 /**
1432 * Cache ES info
1433 *
1434 * @since 2.3.1
1435 */
1436 $this->cache_elasticsearch_info();
1437 }
1438
1439 /**
1440 * Return ES plugins, version and type.
1441 *
1442 * This function also sets those values in the object instance, getting it from cache
1443 * or not, according to `$force` value.
1444 *
1445 * @param bool $force Bust cache or not.
1446 * @since 2.2
1447 * @return array
1448 */
1449 public function get_elasticsearch_info( $force = false ) {
1450 $this->set_elasticsearch_info( $force );
1451 return [
1452 'plugins' => $this->elasticsearch_plugins,
1453 'version' => $this->elasticsearch_version,
1454 'server_type' => $this->server_type,
1455 ];
1456 }
1457
1458 /**
1459 * Cache the ES info.
1460 *
1461 * @since 4.2.1
1462 */
1463 protected function cache_elasticsearch_info() {
1464 /**
1465 * Filter elasticsearch info cache expiration
1466 *
1467 * @hook ep_es_info_cache_expiration
1468 * @param {int} $time Cache time in seconds
1469 * @return {int} New cache time
1470 */
1471 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1472 set_site_transient(
1473 'ep_es_info',
1474 array(
1475 'version' => $this->elasticsearch_version,
1476 'plugins' => $this->elasticsearch_plugins,
1477 'server_type' => $this->server_type,
1478 ),
1479 apply_filters( 'ep_es_info_cache_expiration', ( 5 * MINUTE_IN_SECONDS ) )
1480 );
1481 } else {
1482 set_transient(
1483 'ep_es_info',
1484 array(
1485 'version' => $this->elasticsearch_version,
1486 'plugins' => $this->elasticsearch_plugins,
1487 'server_type' => $this->server_type,
1488 ),
1489 apply_filters( 'ep_es_info_cache_expiration', ( 5 * MINUTE_IN_SECONDS ) )
1490 );
1491 }
1492 }
1493
1494 /**
1495 * Get cluster status
1496 *
1497 * Retrieves cluster stats from Elasticsearch.
1498 *
1499 * @since 1.9
1500 * @return array Contains the status message or the returned statistics.
1501 */
1502 public function get_cluster_status() {
1503
1504 if ( is_wp_error( Utils\get_host() ) ) {
1505
1506 return array(
1507 'status' => false,
1508 'msg' => esc_html__( 'Elasticsearch Host is not available.', 'elasticpress' ),
1509 );
1510
1511 } else {
1512
1513 $request = $this->remote_request( '_cluster/stats', array( 'method' => 'GET' ) );
1514
1515 if ( ! is_wp_error( $request ) ) {
1516
1517 $response = json_decode( wp_remote_retrieve_body( $request ) );
1518
1519 return $response;
1520
1521 }
1522
1523 return array(
1524 'status' => false,
1525 'msg' => $request->get_error_message(),
1526 );
1527
1528 }
1529 }
1530
1531 /**
1532 * Get an Elasticsearch pipeline
1533 *
1534 * @param string $id Id of pipeline.
1535 * @since 2.3
1536 * @return WP_Error|bool|array
1537 */
1538 public function get_pipeline( $id ) {
1539 $path = '_ingest/pipeline/' . $id;
1540
1541 $request_args = array(
1542 'method' => 'GET',
1543 );
1544
1545 /**
1546 * Filter get pipeline request arguments
1547 *
1548 * @hook ep_get_pipeline_args
1549 * @param {array} $request_args Request arguments
1550 * @return {array} New arguments
1551 */
1552 $request = $this->remote_request( $path, apply_filters( 'ep_get_pipeline_args', $request_args ), [], 'get_pipeline' );
1553
1554 if ( is_wp_error( $request ) ) {
1555 return $request;
1556 }
1557
1558 $response = wp_remote_retrieve_response_code( $request );
1559
1560 if ( 200 !== $response ) {
1561 return new WP_Error( $response, wp_remote_retrieve_response_message( $request ), $request );
1562 }
1563
1564 $body = json_decode( wp_remote_retrieve_body( $request ), true );
1565
1566 if ( empty( $body ) ) {
1567 return false;
1568 }
1569
1570 return $body;
1571 }
1572
1573 /**
1574 * Put an Elasticsearch pipeline
1575 *
1576 * @param string $id Pipeline id.
1577 * @param array $args Args to send to ES.
1578 * @since 2.3
1579 * @return WP_Error|bool
1580 */
1581 public function create_pipeline( $id, $args ) {
1582 $path = '_ingest/pipeline/' . $id;
1583
1584 $request_args = array(
1585 'body' => wp_json_encode( $args ),
1586 'method' => 'PUT',
1587 );
1588
1589 /**
1590 * Filter create pipeline request arguments
1591 *
1592 * @hook ep_create_pipeline_args
1593 * @param {array} $request_args Request arguments
1594 * @return {array} New arguments
1595 */
1596 $request = $this->remote_request( $path, apply_filters( 'ep_create_pipeline_args', $request_args ), [], 'create_pipeline' );
1597
1598 if ( is_wp_error( $request ) ) {
1599 return $request;
1600 }
1601
1602 $response = wp_remote_retrieve_response_code( $request );
1603
1604 if ( 200 > $response || 300 <= $response ) {
1605 return new WP_Error( $response, wp_remote_retrieve_response_message( $request ), $request );
1606 }
1607
1608 $body = json_decode( wp_remote_retrieve_body( $request ), true );
1609
1610 if ( empty( $body ) ) {
1611 return false;
1612 }
1613
1614 return true;
1615 }
1616
1617 /**
1618 * Conditionally add the ElasticPress version to the User Agent string.
1619 *
1620 * @since 3.6.1
1621 * @param string $user_agent Original User Agent.
1622 * @return string
1623 */
1624 public function add_elasticpress_version_to_user_agent( $user_agent ) {
1625 /**
1626 * Filter the User Agent header when submitting requests to Elasticsearch.
1627 *
1628 * @hook ep_remote_request_add_ep_user_agent
1629 * @param {bool} $should_add_ep_verion Whether the ElasticPress version should be added to the User Agent string.
1630 * @return {bool} New value
1631 * @since 3.6.1
1632 */
1633 if ( apply_filters( 'ep_remote_request_add_ep_user_agent', Utils\is_epio() ) ) {
1634 $end_part = '; ' . get_bloginfo( 'url' );
1635 $user_agent = str_replace(
1636 $end_part,
1637 ' (ElasticPress/' . EP_VERSION . ')' . $end_part,
1638 $user_agent
1639 );
1640 }
1641 return $user_agent;
1642 }
1643
1644 /**
1645 * Query logging. Don't log anything to the queries property when
1646 * WP_DEBUG is not enabled. Calls action 'ep_add_query_log' if you
1647 * want to access the query outside of the ElasticPress plugin. This
1648 * runs regardless of debufg settings.
1649 *
1650 * @param array $query Query to log.
1651 */
1652 protected function add_query_log( $query ) {
1653 if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || ( defined( 'WP_EP_DEBUG' ) && WP_EP_DEBUG ) ) {
1654 $this->queries[] = $query;
1655 }
1656
1657 /**
1658 * Fires after item is added to the query log
1659 *
1660 * @hook ep_add_query_log
1661 * @param {array} $query Query to log
1662 */
1663 do_action( 'ep_add_query_log', $query );
1664 }
1665
1666 /**
1667 * Get all index names.
1668 *
1669 * @param string $status Whether to return active indexables or all registered.
1670 * @since 4.4.0, 4.5.0 Added $status
1671 * @return array
1672 */
1673 public function get_index_names( $status = 'active' ) {
1674 $sites = ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) ? Utils\get_sites() : array( array( 'blog_id' => get_current_blog_id() ) );
1675
1676 $all_indexables = Indexables::factory()->get_all( null, false, $status );
1677
1678 $global_indexes = [];
1679 $non_global_indexes = [];
1680 foreach ( $all_indexables as $indexable ) {
1681 if ( $indexable->global ) {
1682 $global_indexes[] = $indexable->get_index_name();
1683 continue;
1684 }
1685
1686 foreach ( $sites as $site ) {
1687 if ( ! Utils\is_site_indexable( $site['blog_id'] ) ) {
1688 continue;
1689 }
1690 $non_global_indexes[] = $indexable->get_index_name( $site['blog_id'] );
1691 }
1692 }
1693
1694 return array_merge( $non_global_indexes, $global_indexes );
1695 }
1696
1697 /**
1698 * Return all indices from the cluster.
1699 *
1700 * @since 4.4.0
1701 * @return array Array of indices in Elasticsearch
1702 */
1703 public function get_cluster_indices() : array {
1704 $path = '_cat/indices?format=json';
1705
1706 $response = $this->remote_request( $path );
1707
1708 return (array) json_decode( wp_remote_retrieve_body( $response ), true );
1709 }
1710
1711 /**
1712 * Given an index return its total fields limit
1713 *
1714 * @since 4.4.0
1715 * @param string $index_name The index name
1716 * @return int|null
1717 */
1718 public function get_index_total_fields_limit( $index_name ) {
1719 $cache_key = 'ep_total_fields_limit_' . $index_name;
1720
1721 $is_network = defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK;
1722 if ( $is_network ) {
1723 $cached = get_site_transient( $cache_key );
1724 } else {
1725 $cached = get_transient( $cache_key );
1726 }
1727 if ( ! empty( $cached ) ) {
1728 return $cached;
1729 }
1730
1731 $index_settings = $this->get_index_settings( $index_name );
1732 if ( is_wp_error( $index_settings ) || empty( $index_settings[ $index_name ]['settings']['index.mapping.total_fields.limit'] ) ) {
1733 return null;
1734 }
1735
1736 $es_field_limit = $index_settings[ $index_name ]['settings']['index.mapping.total_fields.limit'];
1737
1738 if ( $is_network ) {
1739 set_site_transient( $cache_key, $es_field_limit, DAY_IN_SECONDS );
1740 } else {
1741 set_transient( $cache_key, $es_field_limit, DAY_IN_SECONDS );
1742 }
1743
1744 return (int) $es_field_limit;
1745 }
1746
1747 }
1748