PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
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 5.3.5, at includes/classes/Elasticsearch.php

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