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

1,774 lines 48.5 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 '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( $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( $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( $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 // If WP_Error or not 200, return false or error message depends on attribute.
855 if ( is_wp_error( $request ) || 200 !== $response_code ) {
856 if ( 'bool' === $return_type ) {
857 return false;
858 }
859
860 if ( is_wp_error( $request ) ) {
861 return $request;
862 }
863
864 $response_body = wp_remote_retrieve_body( $request );
865 $parsed_response = json_decode( $response_body, true );
866 if ( is_array( $parsed_response ) ) {
867 $status = $parsed_response['status'] ?? 'status-not-set';
868 $error = $parsed_response['error'] ?? 'error-not-set';
869 } else {
870 $status = $response_code;
871 $error = $response_body;
872 }
873 return new \WP_Error( $status, $error );
874 }
875
876 return true;
877 }
878
879 /**
880 * Get current index mapping from Elasticsearch.
881 *
882 * @param string $index The index name.
883 * @since 3.5
884 * @return array
885 */
886 public function get_mapping( $index ) {
887 $request_args = [
888 'method' => 'GET',
889 'timeout' => 30,
890 ];
891
892 $request = $this->remote_request( $index, $request_args, [], 'get_mapping' );
893
894 if ( is_wp_error( $request ) || 200 !== wp_remote_retrieve_response_code( $request ) ) {
895 return [];
896 }
897
898 $body = wp_remote_retrieve_body( $request );
899
900 if ( ! $body ) {
901 return [];
902 }
903
904 $mapping = json_decode( $body, true );
905
906 return is_array( $mapping ) ? $mapping : [];
907 }
908
909 /**
910 * Close an open index.
911 *
912 * @param string $index Index name.
913 * @since 3.5
914 * @return boolean
915 */
916 public function close_index( $index ) {
917 $request_args = [
918 'method' => 'POST',
919 'timeout' => 30,
920 ];
921
922 $close = trailingslashit( $index ) . '_close';
923 $request = $this->remote_request( $close, $request_args, [], 'close_index' );
924
925 return ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) );
926 }
927
928 /**
929 * Open a closed index.
930 *
931 * @param string $index Index name.
932 * @since 3.5
933 * @return boolean
934 */
935 public function open_index( $index ) {
936 $request_args = [
937 'method' => 'POST',
938 'timeout' => 30,
939 ];
940
941 $open = trailingslashit( $index ) . '_open';
942 $request = $this->remote_request( $open, $request_args, [], 'open_index' );
943
944 return ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) );
945 }
946
947 /**
948 * Get index settings.
949 *
950 * @param string $index Index name.
951 * @since 4.4.0
952 * @return array|WP_Error Raw ES response from the $index/_settings?flat_settings=true endpoint
953 */
954 public function get_index_settings( string $index ) {
955 $endpoint = trailingslashit( $index ) . '_settings?flat_settings=true';
956 $request = $this->remote_request( $endpoint, [], [], 'get_index_settings' );
957
958 if ( is_wp_error( $request ) ) {
959 return $request;
960 }
961
962 $response_body = wp_remote_retrieve_body( $request );
963
964 $settings = json_decode( $response_body, true );
965
966 return $settings;
967 }
968
969 /**
970 * Update index settings.
971 *
972 * @param string $index Index name.
973 * @param array $settings Setting update array.
974 * @param boolean $close_first Optional. True if index must be closed prior to update.
975 * Dynamic settings can be updated on open indices. Static
976 * settings must be closed. Default false.
977 * @since 3.5
978 * @return boolean
979 */
980 public function update_index_settings( $index, $settings, $close_first = false ) {
981 $request_args = [
982 'body' => wp_json_encode( $settings ),
983 'method' => 'PUT',
984 'timeout' => 30,
985 ];
986
987 if ( $close_first ) {
988 $this->close_index( $index );
989 }
990
991 $settings_url = trailingslashit( $index ) . '_settings';
992 $request = $this->remote_request( $settings_url, $request_args, [], 'update_index_settings' );
993
994 $updated = ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) );
995
996 /**
997 * Fires after updating an index settings
998 *
999 * @hook ep_update_index_settings
1000 * @since 4.4.0
1001 * @param {string} $index Index name
1002 * @param {array} $settings Setting update array
1003 */
1004 do_action( 'ep_update_index_settings', $index, $settings );
1005
1006 if ( $close_first ) {
1007 $opened = $this->open_index( $index );
1008 return ( $updated && $opened );
1009 }
1010
1011 return $updated;
1012 }
1013
1014 /**
1015 * Delete an Elasticsearch index
1016 *
1017 * @param string $index Index name.
1018 * @since 3.0
1019 * @return boolean
1020 */
1021 public function delete_index( $index ) {
1022
1023 $request_args = [
1024 'method' => 'DELETE',
1025 'timeout' => 30,
1026 ];
1027
1028 $request = $this->remote_request( $index, $request_args, [], 'delete_index' );
1029
1030 // 200 means the delete was successful
1031 // 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
1032 if ( ! is_wp_error( $request ) && ( 200 === wp_remote_retrieve_response_code( $request ) || 404 === wp_remote_retrieve_response_code( $request ) ) ) {
1033 $response_body = wp_remote_retrieve_body( $request );
1034
1035 return json_decode( $response_body );
1036 }
1037
1038 return false;
1039 }
1040
1041 /**
1042 * Delete all indices
1043 *
1044 * @since 3.0
1045 * @return boolean
1046 */
1047 public function delete_all_indices() {
1048 return $this->delete_index( '*' );
1049 }
1050
1051 /**
1052 * Check if an ES index exists
1053 *
1054 * @param string $index Index name.
1055 * @since 3.0
1056 * @return boolean
1057 */
1058 public function index_exists( $index ) {
1059
1060 $request_args = [
1061 'method' => 'HEAD',
1062 ];
1063
1064 $request = $this->remote_request( $index, $request_args, [], 'index_exists' );
1065
1066 // 200 means the index exists.
1067 // 404 means the index was non-existent.
1068 if ( ! is_wp_error( $request ) && ( 200 === wp_remote_retrieve_response_code( $request ) || 404 === wp_remote_retrieve_response_code( $request ) ) ) {
1069
1070 if ( 404 === wp_remote_retrieve_response_code( $request ) ) {
1071 return false;
1072 }
1073
1074 if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
1075 return true;
1076 }
1077 }
1078
1079 return false;
1080 }
1081
1082 /**
1083 * Bulk index Elasticsearch documents
1084 *
1085 * @param string $index Index name.
1086 * @param string $type Index type. Previously this was used for index type. Now it's just passed to hooks for legacy reasons.
1087 * @param string $body Encoded JSON.
1088 * @since 3.0
1089 * @return WP_Error|array
1090 */
1091 public function bulk_index( $index, $type, $body ) {
1092 /**
1093 * Filter Elasticsearch bulk index request path
1094 *
1095 * @hook ep_bulk_index_request_path
1096 * @param {string} Request path
1097 * @param {string} $body Bulk index request body
1098 * @param {string} $type Index type
1099 * @return {string} New path
1100 */
1101 if ( version_compare( $this->get_elasticsearch_version(), '7.0', '<' ) ) {
1102 $path = apply_filters( 'ep_bulk_index_request_path', $index . '/' . $type . '/_bulk', $body, $type );
1103 } else {
1104 $path = apply_filters( 'ep_bulk_index_request_path', $index . '/_bulk', $body, $type );
1105 }
1106
1107 $request_args = array(
1108 'method' => 'POST',
1109 'body' => $body,
1110 'timeout' => apply_filters( 'ep_bulk_index_timeout', 30 ),
1111 );
1112
1113 $request = $this->remote_request( $path, $request_args, [], 'bulk_index' );
1114
1115 if ( is_wp_error( $request ) ) {
1116 return $request;
1117 }
1118
1119 $response = wp_remote_retrieve_response_code( $request );
1120
1121 if ( 200 !== $response ) {
1122 return new WP_Error( $response, wp_remote_retrieve_response_message( $request ), $request );
1123 }
1124
1125 return json_decode( wp_remote_retrieve_body( $request ), true );
1126 }
1127
1128 /**
1129 * Return queries for debugging
1130 *
1131 * @since 1.8
1132 * @return array
1133 */
1134 public function get_query_log() {
1135 return $this->queries;
1136 }
1137
1138 /**
1139 * Wrapper for wp_remote_request
1140 *
1141 * This is a wrapper function for wp_remote_request to account for request failures.
1142 *
1143 * @since 1.6
1144 *
1145 * @param string $path Site URL to retrieve.
1146 * @param array $args Optional. Request arguments. Default empty array.
1147 * @param array $query_args Optional. The query args originally passed to WP_Query.
1148 * @param string $type Type of request, used for debugging.
1149 *
1150 * @return WP_Error|array The response or WP_Error on failure.
1151 */
1152 public function remote_request( $path, $args = [], $query_args = [], $type = null ) {
1153
1154 if ( empty( $args['method'] ) ) {
1155 $args['method'] = 'GET';
1156 }
1157
1158 // Checks for any previously set headers
1159 $existing_headers = isset( $args['headers'] ) ? (array) $args['headers'] : [];
1160
1161 // Add the API Header.
1162 // Note that the "User Agent" header will be changed via WordPress's `http_headers_useragent` filter later.
1163 $new_headers = $this->format_request_headers();
1164
1165 $args['headers'] = array_merge( $existing_headers, $new_headers );
1166
1167 /**
1168 * Filter Elasticsearch args prior to remote request
1169 *
1170 * @hook ep_pre_request_args
1171 * @since 3.6.4
1172 * @param {array} $args Request args
1173 * @param {string} $path Site URL to retrieve
1174 * @param {array} $query_args The query args originally passed to WP_Query.
1175 * @param {string|null} $type Type of request, used for debugging.
1176 * @return {array} New request args
1177 */
1178 $args = apply_filters( 'ep_pre_request_args', $args, $path, $query_args, $type );
1179
1180 $query = array(
1181 'time_start' => microtime( true ),
1182 'time_finish' => false,
1183 'args' => $args,
1184 'blocking' => true,
1185 'failed_hosts' => [],
1186 'request' => false,
1187 'host' => Utils\get_host(),
1188 'query_args' => $query_args,
1189 );
1190
1191 $request = false;
1192 $failures = 0;
1193
1194 add_filter( 'http_headers_useragent', [ $this, 'add_elasticpress_version_to_user_agent' ] );
1195
1196 // Optionally let us try back up hosts and account for failures.
1197 while ( true ) {
1198 /**
1199 * Filter Elasticsearch host prior to remote request
1200 *
1201 * @hook ep_pre_request_host
1202 * @param {string} Request host
1203 * @param {int} $failures Number of current failures
1204 * @param {string} $path Request path
1205 * @param {array} $args Request arguments
1206 * @return {string} New host
1207 */
1208 $query['host'] = apply_filters( 'ep_pre_request_host', $query['host'], $failures, $path, $args );
1209
1210 /**
1211 * Filter Elasticsearch url prior to remote request
1212 *
1213 * @hook ep_pre_request_url
1214 * @param {string} Request url
1215 * @param {int} $failures Number of current failures
1216 * @param {string} $host Request host
1217 * @param {string} $path Request path
1218 * @param {array} $args Request arguments
1219 * @return {string} New url
1220 */
1221 $query['url'] = apply_filters( 'ep_pre_request_url', esc_url( trailingslashit( $query['host'] ) . $path ), $failures, $query['host'], $path, $args );
1222
1223 /**
1224 * Filter whether remote request should be intercepted
1225 *
1226 * @hook ep_intercept_remote_request
1227 * @param {boolean} $intercept True to intercept
1228 * @return {boolean} New value
1229 */
1230 if ( true === apply_filters( 'ep_intercept_remote_request', false ) ) {
1231 /**
1232 * Filter intercepted request
1233 *
1234 * @hook ep_do_intercept_request
1235 * @since 3.2.2
1236 * @since 3.6.5 added $type
1237 * @param {array} $request New remote request response
1238 * @param {array} $query Remote request arguments
1239 * @param {args} $args Request arguments
1240 * @param {int} $failures Number of failures
1241 * @param {string} $type Type of request
1242 * @return {array} New request
1243 */
1244 $request = apply_filters( 'ep_do_intercept_request', new WP_Error( 400, 'No Request defined' ), $query, $args, $failures, $type );
1245 } else {
1246 $request = wp_remote_request( $query['url'], $args ); // try the existing host to avoid unnecessary calls.
1247 }
1248
1249 $request_response_code = (int) wp_remote_retrieve_response_code( $request );
1250
1251 $is_valid_res = ( $request_response_code >= 200 && $request_response_code <= 299 );
1252 $is_non_blocking_request = ( 0 === $request_response_code );
1253
1254 if ( false === $request || is_wp_error( $request ) || ( ! $is_valid_res && ! $is_non_blocking_request ) ) {
1255 $failures++;
1256
1257 /**
1258 * Filter max number of times to attempt remote requests
1259 *
1260 * @hook ep_max_remote_request_tries
1261 * @param {int} $tries Number of times to try
1262 * @param {path} $path Request path
1263 * @param {args} $args Request arguments
1264 * @return {int} New number of tries
1265 */
1266 if ( $failures >= apply_filters( 'ep_max_remote_request_tries', 1, $path, $args ) ) {
1267 break;
1268 }
1269 } else {
1270 break;
1271 }
1272 }
1273
1274 remove_filter( 'http_headers_useragent', [ $this, 'add_elasticpress_version_to_user_agent' ] );
1275
1276 // Return now if we're not blocking, since we won't have a response yet.
1277 if ( isset( $args['blocking'] ) && false === $args['blocking'] ) {
1278 $query['blocking'] = true;
1279 $query['request'] = $request;
1280 $this->add_query_log( $query );
1281
1282 return $request;
1283 }
1284
1285 $query['time_finish'] = microtime( true );
1286 $query['request'] = $request;
1287 $this->add_query_log( $query );
1288
1289 /**
1290 * Fires after Elasticsearch remote request
1291 *
1292 * @hook ep_remote_request
1293 * @param {array} $query Remote request arguments
1294 * @param {string} $type Request type
1295 */
1296 do_action( 'ep_remote_request', $query, $type );
1297
1298 return $request;
1299
1300 }
1301
1302 /**
1303 * Parse response from Elasticsearch
1304 *
1305 * Determines if there is an issue or if the response is valid.
1306 *
1307 * @since 1.9
1308 * @param object $response JSON decoded response from Elasticsearch.
1309 * @return array Contains the status message or the returned statistics.
1310 */
1311 public function parse_api_response( $response ) {
1312
1313 if ( null === $response ) {
1314
1315 return array(
1316 'status' => false,
1317 'msg' => esc_html__( 'Invalid response from ElasticPress server. Please contact your administrator.' ),
1318 );
1319
1320 } elseif (
1321 isset( $response->error ) &&
1322 (
1323 ( is_string( $response->error ) && stristr( $response->error, 'IndexMissingException' ) ) ||
1324 ( isset( $response->error->reason ) && stristr( $response->error->reason, 'no such index' ) )
1325 )
1326 ) {
1327
1328 if ( is_multisite() ) {
1329
1330 $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' );
1331
1332 } else {
1333
1334 $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' );
1335
1336 }
1337
1338 return array(
1339 'status' => false,
1340 'msg' => $error,
1341 );
1342
1343 }
1344
1345 return array(
1346 'status' => true,
1347 'data' => $response->_all->primaries->indexing,
1348 );
1349
1350 }
1351
1352 /**
1353 * Set ES plugins and version, detect server type, and cache everything
1354 *
1355 * @since 4.2.1
1356 * @param bool $force Bust cache or not.
1357 * @return array
1358 */
1359 public function set_elasticsearch_info( $force = false ) {
1360 if ( empty( Utils\get_host() ) ) {
1361 return;
1362 }
1363
1364 if ( ! $force && null !== $this->elasticsearch_version && null !== $this->elasticsearch_plugins ) {
1365 return;
1366 }
1367
1368 // Get ES info from cache if available. If we are forcing, then skip cache check.
1369 if ( ! $force ) {
1370 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1371 $es_info = get_site_transient( 'ep_es_info' );
1372 } else {
1373 $es_info = get_transient( 'ep_es_info' );
1374 }
1375 if ( ! empty( $es_info ) ) {
1376 $this->elasticsearch_version = $es_info['version'];
1377 $this->elasticsearch_plugins = $es_info['plugins'];
1378 $this->server_type = $es_info['server_type'];
1379 return;
1380 }
1381 }
1382
1383 $path = '_nodes/plugins';
1384
1385 $request = $this->remote_request( $path, array( 'method' => 'GET' ) );
1386
1387 if ( is_wp_error( $request ) || 200 !== wp_remote_retrieve_response_code( $request ) ) {
1388 $this->elasticsearch_version = false;
1389 $this->elasticsearch_plugins = false;
1390
1391 /**
1392 * Try a different endpoint in case the plugins url is restricted
1393 *
1394 * @since 2.2.1
1395 */
1396
1397 $request = $this->remote_request( '', array( 'method' => 'GET' ) );
1398
1399 if ( ! is_wp_error( $request ) && 200 === wp_remote_retrieve_response_code( $request ) ) {
1400 $response_body = wp_remote_retrieve_body( $request );
1401 $response = json_decode( $response_body, true );
1402
1403 try {
1404 $this->elasticsearch_version = $response['version']['number'];
1405 if ( ! empty( $response['version']['distribution'] ) ) {
1406 $this->server_type = $response['version']['distribution'];
1407 }
1408 } catch ( \Exception $e ) {
1409 // Do nothing.
1410 }
1411 }
1412 return;
1413 }
1414
1415 $response = json_decode( wp_remote_retrieve_body( $request ), true );
1416
1417 $this->elasticsearch_plugins = [];
1418 $this->elasticsearch_version = false;
1419
1420 if ( isset( $response['nodes'] ) ) {
1421 $node = end( $response['nodes'] );
1422 // Save version of last node. We assume all nodes are same version.
1423 $this->elasticsearch_version = $node['version'];
1424
1425 if ( isset( $node['plugins'] ) && is_array( $node['plugins'] ) ) {
1426 foreach ( $node['plugins'] as $plugin ) {
1427 $this->elasticsearch_plugins[ $plugin['name'] ] = $plugin['version'];
1428 }
1429 }
1430 if ( isset( $node['modules'] )
1431 && is_array( $node['modules'] )
1432 && ! empty( $node['modules'] )
1433 && ! empty( $node['modules'][0]['opensearch_version'] )
1434 ) {
1435 $this->server_type = 'opensearch';
1436 }
1437 }
1438
1439 /**
1440 * Cache ES info
1441 *
1442 * @since 2.3.1
1443 */
1444 $this->cache_elasticsearch_info();
1445 }
1446
1447 /**
1448 * Return ES plugins, version and type.
1449 *
1450 * This function also sets those values in the object instance, getting it from cache
1451 * or not, according to `$force` value.
1452 *
1453 * @param bool $force Bust cache or not.
1454 * @since 2.2
1455 * @return array
1456 */
1457 public function get_elasticsearch_info( $force = false ) {
1458 $this->set_elasticsearch_info( $force );
1459 return [
1460 'plugins' => $this->elasticsearch_plugins,
1461 'version' => $this->elasticsearch_version,
1462 'server_type' => $this->server_type,
1463 ];
1464 }
1465
1466 /**
1467 * Cache the ES info.
1468 *
1469 * @since 4.2.1
1470 */
1471 protected function cache_elasticsearch_info() {
1472 /**
1473 * Filter elasticsearch info cache expiration
1474 *
1475 * @hook ep_es_info_cache_expiration
1476 * @param {int} $time Cache time in seconds
1477 * @return {int} New cache time
1478 */
1479 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1480 set_site_transient(
1481 'ep_es_info',
1482 array(
1483 'version' => $this->elasticsearch_version,
1484 'plugins' => $this->elasticsearch_plugins,
1485 'server_type' => $this->server_type,
1486 ),
1487 apply_filters( 'ep_es_info_cache_expiration', ( 5 * MINUTE_IN_SECONDS ) )
1488 );
1489 } else {
1490 set_transient(
1491 'ep_es_info',
1492 array(
1493 'version' => $this->elasticsearch_version,
1494 'plugins' => $this->elasticsearch_plugins,
1495 'server_type' => $this->server_type,
1496 ),
1497 apply_filters( 'ep_es_info_cache_expiration', ( 5 * MINUTE_IN_SECONDS ) )
1498 );
1499 }
1500 }
1501
1502 /**
1503 * Get cluster status
1504 *
1505 * Retrieves cluster stats from Elasticsearch.
1506 *
1507 * @since 1.9
1508 * @return array Contains the status message or the returned statistics.
1509 */
1510 public function get_cluster_status() {
1511
1512 if ( is_wp_error( Utils\get_host() ) ) {
1513
1514 return array(
1515 'status' => false,
1516 'msg' => esc_html__( 'Elasticsearch Host is not available.', 'elasticpress' ),
1517 );
1518
1519 } else {
1520
1521 $request = $this->remote_request( '_cluster/stats', array( 'method' => 'GET' ) );
1522
1523 if ( ! is_wp_error( $request ) ) {
1524
1525 $response = json_decode( wp_remote_retrieve_body( $request ) );
1526
1527 return $response;
1528
1529 }
1530
1531 return array(
1532 'status' => false,
1533 'msg' => $request->get_error_message(),
1534 );
1535
1536 }
1537 }
1538
1539 /**
1540 * Get an Elasticsearch pipeline
1541 *
1542 * @param string $id Id of pipeline.
1543 * @since 2.3
1544 * @return WP_Error|bool|array
1545 */
1546 public function get_pipeline( $id ) {
1547 $path = '_ingest/pipeline/' . $id;
1548
1549 $request_args = array(
1550 'method' => 'GET',
1551 );
1552
1553 /**
1554 * Filter get pipeline request arguments
1555 *
1556 * @hook ep_get_pipeline_args
1557 * @param {array} $request_args Request arguments
1558 * @return {array} New arguments
1559 */
1560 $request = $this->remote_request( $path, apply_filters( 'ep_get_pipeline_args', $request_args ), [], 'get_pipeline' );
1561
1562 if ( is_wp_error( $request ) ) {
1563 return $request;
1564 }
1565
1566 $response = wp_remote_retrieve_response_code( $request );
1567
1568 if ( 200 !== $response ) {
1569 return new WP_Error( $response, wp_remote_retrieve_response_message( $request ), $request );
1570 }
1571
1572 $body = json_decode( wp_remote_retrieve_body( $request ), true );
1573
1574 if ( empty( $body ) ) {
1575 return false;
1576 }
1577
1578 return $body;
1579 }
1580
1581 /**
1582 * Put an Elasticsearch pipeline
1583 *
1584 * @param string $id Pipeline id.
1585 * @param array $args Args to send to ES.
1586 * @since 2.3
1587 * @return WP_Error|bool
1588 */
1589 public function create_pipeline( $id, $args ) {
1590 $path = '_ingest/pipeline/' . $id;
1591
1592 $request_args = array(
1593 'body' => wp_json_encode( $args ),
1594 'method' => 'PUT',
1595 );
1596
1597 /**
1598 * Filter create pipeline request arguments
1599 *
1600 * @hook ep_create_pipeline_args
1601 * @param {array} $request_args Request arguments
1602 * @return {array} New arguments
1603 */
1604 $request = $this->remote_request( $path, apply_filters( 'ep_create_pipeline_args', $request_args ), [], 'create_pipeline' );
1605
1606 if ( is_wp_error( $request ) ) {
1607 return $request;
1608 }
1609
1610 $response = wp_remote_retrieve_response_code( $request );
1611
1612 if ( 200 > $response || 300 <= $response ) {
1613 return new WP_Error( $response, wp_remote_retrieve_response_message( $request ), $request );
1614 }
1615
1616 $body = json_decode( wp_remote_retrieve_body( $request ), true );
1617
1618 if ( empty( $body ) ) {
1619 return false;
1620 }
1621
1622 return true;
1623 }
1624
1625 /**
1626 * Conditionally add the ElasticPress version to the User Agent string.
1627 *
1628 * @since 3.6.1
1629 * @param string $user_agent Original User Agent.
1630 * @return string
1631 */
1632 public function add_elasticpress_version_to_user_agent( $user_agent ) {
1633 /**
1634 * Filter the User Agent header when submitting requests to Elasticsearch.
1635 *
1636 * @hook ep_remote_request_add_ep_user_agent
1637 * @param {bool} $should_add_ep_verion Whether the ElasticPress version should be added to the User Agent string.
1638 * @return {bool} New value
1639 * @since 3.6.1
1640 */
1641 if ( apply_filters( 'ep_remote_request_add_ep_user_agent', Utils\is_epio() ) ) {
1642 $end_part = '; ' . get_bloginfo( 'url' );
1643 $user_agent = str_replace(
1644 $end_part,
1645 ' (ElasticPress/' . EP_VERSION . ')' . $end_part,
1646 $user_agent
1647 );
1648 }
1649 return $user_agent;
1650 }
1651
1652 /**
1653 * Query logging. Don't log anything to the queries property when
1654 * WP_DEBUG is not enabled. Calls action 'ep_add_query_log' if you
1655 * want to access the query outside of the ElasticPress plugin. This
1656 * runs regardless of debufg settings.
1657 *
1658 * @param array $query Query to log.
1659 */
1660 protected function add_query_log( $query ) {
1661 if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || ( defined( 'WP_EP_DEBUG' ) && WP_EP_DEBUG ) ) {
1662 $this->queries[] = $query;
1663 }
1664
1665 /**
1666 * Fires after item is added to the query log
1667 *
1668 * @hook ep_add_query_log
1669 * @param {array} $query Query to log
1670 */
1671 do_action( 'ep_add_query_log', $query );
1672 }
1673
1674 /**
1675 * Get all index names.
1676 *
1677 * @param string $status Whether to return active indexables or all registered.
1678 * @since 4.4.0, 4.5.0 Added $status
1679 * @return array
1680 */
1681 public function get_index_names( $status = 'active' ) {
1682 $sites = ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) ? Utils\get_sites() : array( array( 'blog_id' => get_current_blog_id() ) );
1683
1684 $all_indexables = Indexables::factory()->get_all( null, false, $status );
1685
1686 $global_indexes = [];
1687 $non_global_indexes = [];
1688 foreach ( $all_indexables as $indexable ) {
1689 if ( $indexable->global ) {
1690 $global_indexes[] = $indexable->get_index_name();
1691 continue;
1692 }
1693
1694 foreach ( $sites as $site ) {
1695 if ( ! Utils\is_site_indexable( $site['blog_id'] ) ) {
1696 continue;
1697 }
1698 $non_global_indexes[] = $indexable->get_index_name( $site['blog_id'] );
1699 }
1700 }
1701
1702 return array_merge( $non_global_indexes, $global_indexes );
1703 }
1704
1705 /**
1706 * Return all indices from the cluster.
1707 *
1708 * @since 4.4.0
1709 * @return array Array of indices in Elasticsearch
1710 */
1711 public function get_cluster_indices() : array {
1712 $path = '_cat/indices?format=json';
1713
1714 $response = $this->remote_request( $path );
1715
1716 return (array) json_decode( wp_remote_retrieve_body( $response ), true );
1717 }
1718
1719 /**
1720 * Return a comparison between which indices should be and are present in the ES server.
1721 *
1722 * @since 4.6.0
1723 * @return array Array with `missing_indices` and `present_indices` keys.
1724 */
1725 public function get_indices_comparison() {
1726 $all_index_names = $this->get_index_names();
1727 $cluster_indices = $this->get_cluster_indices();
1728
1729 $cluster_index_names = wp_list_pluck( $cluster_indices, 'index' );
1730
1731 return [
1732 'missing_indices' => array_diff( $all_index_names, $cluster_index_names ),
1733 'present_indices' => array_intersect( $all_index_names, $cluster_index_names ),
1734 ];
1735 }
1736
1737 /**
1738 * Given an index return its total fields limit
1739 *
1740 * @since 4.4.0
1741 * @param string $index_name The index name
1742 * @return int|null
1743 */
1744 public function get_index_total_fields_limit( $index_name ) {
1745 $cache_key = 'ep_total_fields_limit_' . $index_name;
1746
1747 $is_network = defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK;
1748 if ( $is_network ) {
1749 $cached = get_site_transient( $cache_key );
1750 } else {
1751 $cached = get_transient( $cache_key );
1752 }
1753 if ( ! empty( $cached ) ) {
1754 return $cached;
1755 }
1756
1757 $index_settings = $this->get_index_settings( $index_name );
1758 if ( is_wp_error( $index_settings ) || empty( $index_settings[ $index_name ]['settings']['index.mapping.total_fields.limit'] ) ) {
1759 return null;
1760 }
1761
1762 $es_field_limit = $index_settings[ $index_name ]['settings']['index.mapping.total_fields.limit'];
1763
1764 if ( $is_network ) {
1765 set_site_transient( $cache_key, $es_field_limit, DAY_IN_SECONDS );
1766 } else {
1767 set_transient( $cache_key, $es_field_limit, DAY_IN_SECONDS );
1768 }
1769
1770 return (int) $es_field_limit;
1771 }
1772
1773 }
1774