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

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

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