PluginProbe
ElasticPress / 4.4.0
ElasticPress v4.4.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.4.0, at includes/classes/Elasticsearch.php

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