host = rtrim($host, '/'); $this->user = $user; $this->pass = $pass; $this->index = $index; } /** * Отправка запроса к OpenSearch */ protected function send_request($method, $endpoint, $body = null, $is_raw_body = false) { $url = $this->host . '/' . $this->index . '/' . ltrim($endpoint, '/'); $args = [ 'method' => $method, 'headers' => [ 'Authorization' => 'Basic ' . base64_encode($this->user . ':' . $this->pass), 'Content-Type' => 'application/json', ], 'sslverify' => apply_filters('wp_opensearch_sslverify', false), 'timeout' => 30, ]; // Для GET-запросов параметры передаются в URL, а не в теле if ($method === 'GET' && !is_null($body) && is_array($body)) { $url .= '?' . http_build_query($body); $body = null; } // Добавляем тело для не-GET запросов if (!is_null($body) && $method !== 'GET') { if ($is_raw_body && is_string($body)) { $args['body'] = $body; } else { $args['body'] = wp_json_encode($body); } } $response = wp_remote_request($url, $args); if (is_wp_error($response)) { return new WP_Error( 'opensearch_network_error', $response->get_error_message(), ['method' => $method, 'endpoint' => $endpoint] ); } $response_code = wp_remote_retrieve_response_code($response); $response_body = json_decode(wp_remote_retrieve_body($response), true); if ($response_code >= 400) { $error_message = isset($response_body['error']) ? (is_array($response_body['error']) ? json_encode($response_body['error']) : $response_body['error']) : (isset($response_body['message']) ? $response_body['message'] : 'Unknown OpenSearch error'); return new WP_Error( 'opensearch_api_error', $error_message, [ 'status' => $response_code, 'request' => ['method' => $method, 'endpoint' => $endpoint, 'body' => $body], 'response' => $response_body ] ); } // Обработка ошибок bulk операций if (strpos($endpoint, '_bulk') !== false && isset($response_body['items'])) { $failed_items = array_filter($response_body['items'], function($item) { return isset($item['index']['error']); }); if (!empty($failed_items)) { return new WP_Error( 'opensearch_bulk_partial_failure', 'Bulk operation completed with some failures', [ 'failed_count' => count($failed_items), 'successful_count'=> count($response_body['items']) - count($failed_items), 'failures' => array_values($failed_items) ] ); } } return $response_body; } /** * Проверка существования индекса */ public function index_exists() { $response = wp_remote_request($this->host . '/' . $this->index, [ 'method' => 'HEAD', 'headers' => [ 'Authorization' => 'Basic ' . base64_encode($this->user . ':' . $this->pass), ], 'sslverify' => apply_filters('wp_opensearch_sslverify', false), ]); return !is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200; } /** * Удаление индекса */ public function delete_index() { return $this->send_request('DELETE', ''); } /** * Получение статистики индекса */ public function get_stats() { return $this->send_request('GET', '_stats'); } /** * Получение маппинга индекса */ public function get_mapping() { return $this->send_request('GET', '_mapping'); } /** * Проверка существующих документов */ public function check_existing_posts($post_ids) { if (empty($post_ids)) { return []; } $body = [ 'query' => [ 'terms' => [ '_id' => $post_ids ] ], 'size' => count($post_ids), '_source' => false ]; $response = $this->send_request('POST', '_search', $body); if (is_wp_error($response)) { return []; } return array_map(function($hit) { return $hit['_id']; }, $response['hits']['hits'] ?? []); } } /** * ---------------------------- * КЛАСС ДЛЯ ИНДЕКСАЦИИ ДАННЫХ * ---------------------------- */ class AK_OpenSearch_Indexer extends AK_OpenSearch_Base_Client { /** * Создание индекса */ public function create_index() { $settings = [ 'settings' => [ 'analysis' => [ 'filter' => [ 'russian_stop' => ['type' => 'stop','stopwords' => '_russian_'], 'russian_stemmer' => ['type' => 'stemmer','language' => 'russian'], 'russian_keywords' => ['type' => 'keyword_marker','keywords' => ['пример']] ], 'analyzer' => [ 'russian_analyzer' => [ 'tokenizer' => 'standard', 'filter' => ['lowercase','russian_stop','russian_keywords','russian_stemmer'] ] ] ], 'index' => ['number_of_shards' => 1,'number_of_replicas' => 1] ], 'mappings' => [ 'properties' => [ 'id' => ['type' => 'integer'], 'post_type' => ['type' => 'keyword'], 'title' => ['type' => 'text','analyzer' => 'russian_analyzer'], 'excerpt' => ['type' => 'text','analyzer' => 'russian_analyzer','fielddata' => true], 'content' => ['type' => 'text','analyzer' => 'russian_analyzer'], 'meta' => ['type' => 'object'], 'author' => [ 'properties' => [ 'id' => ['type' => 'integer'], 'name' => ['type' => 'text'], 'slug' => ['type' => 'keyword'] ] ], 'date' => [ 'type' => 'date', 'format' => 'yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis' ], 'modified' => [ 'type' => 'date', 'format' => 'yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis' ], 'url' => ['type' => 'keyword'] ] ] ]; return $this->send_request('PUT', '', $settings); } /** * Получение мета-данных поста */ private function get_post_meta($post_id) { $meta = get_post_meta($post_id); $result = []; foreach ($meta as $key => $values) { if (strpos($key, '_') === 0) continue; $result[$key] = count($values) === 1 ? maybe_unserialize($values[0]) : array_map('maybe_unserialize', $values); } return $result; } /** * Индексация документов пачками */ public function bulk_index_documents($post_type = 'post', $limit = 100, $offset = 0) { global $wpdb; $posts = $wpdb->get_results($wpdb->prepare( "SELECT ID, post_title, post_content, post_excerpt, post_type, post_author, post_date_gmt, post_modified_gmt FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_type = %s ORDER BY post_date DESC LIMIT %d OFFSET %d", $post_type, $limit, $offset )); if (empty($posts)) { return new WP_Error('empty_posts', 'No posts found for indexing'); } $bulk_body = ''; $post_ids = []; foreach ($posts as $post) { $meta = $this->get_post_meta($post->ID); // Очищаем мета-данные от несериализуемых значений $meta = array_filter($meta, function($item) { return !is_resource($item); }); $document = [ 'id' => $post->ID, 'title' => $post->post_title, 'post_type' => $post->post_type, 'excerpt' => wp_strip_all_tags($post->post_excerpt), 'content' => wp_strip_all_tags($post->post_content), 'meta' => $meta, 'author' => [ 'id' => $post->post_author, 'name' => get_the_author_meta('display_name', $post->post_author), 'slug' => get_the_author_meta('user_nicename', $post->post_author), ], 'date' => $post->post_date_gmt, 'modified' => $post->post_modified_gmt, ]; $bulk_body .= wp_json_encode(['index' => ['_index' => $this->index, '_id' => $post->ID]]) . "\n"; $bulk_body .= wp_json_encode($document, JSON_UNESCAPED_UNICODE) . "\n"; $post_ids[] = $post->ID; } $response = $this->send_request('POST', '_bulk', $bulk_body, true); if (is_wp_error($response)) { return $response; } // Проверка ошибок в bulk операции if (isset($response['items'])) { $errors = []; foreach ($response['items'] as $item) { if (isset($item['index']['error'])) { $errors[] = [ 'post_id' => $item['index']['_id'], 'reason' => $item['index']['error']['reason'] ?? 'Unknown error', 'type' => $item['index']['error']['type'] ?? 'unknown' ]; } } if (!empty($errors)) { $error_message = sprintf( "Bulk operation completed with %d failures out of %d items.", count($errors), count($response['items']) ); return new WP_Error('bulk_partial_failure', $error_message, [ 'total_items' => count($response['items']), 'failed_items' => count($errors), 'errors' => $errors ]); } } return $response; } /** * Индексация конкретных постов */ public function bulk_index($post_ids) { if (empty($post_ids)) { return new WP_Error('empty_posts', 'No post IDs provided'); } $bulk_body = ''; foreach ($post_ids as $post_id) { $post = get_post($post_id); if (!$post || $post->post_status !== 'publish') { continue; } $meta = $this->get_post_meta($post_id); $meta = array_filter($meta, function($item) { return !is_resource($item); }); $document = [ 'id' => $post->ID, 'title' => $post->post_title, 'post_type' => $post->post_type, 'excerpt' => wp_strip_all_tags($post->post_excerpt), 'content' => wp_strip_all_tags($post->post_content), 'meta' => $meta, 'author' => [ 'id' => $post->post_author, 'name' => get_the_author_meta('display_name', $post->post_author), 'slug' => get_the_author_meta('user_nicename', $post->post_author), ], 'date' => $post->post_date_gmt, 'modified' => $post->post_modified_gmt, ]; $bulk_body .= wp_json_encode(['index' => ['_index' => $this->index, '_id' => $post->ID]]) . "\n"; $bulk_body .= wp_json_encode($document, JSON_UNESCAPED_UNICODE) . "\n"; } return $this->send_request('POST', '_bulk', $bulk_body, true); } /** * Индексация одного поста */ public function index_post($post_id) { $post = get_post($post_id); if (!$post || $post->post_status !== 'publish') { return new WP_Error('invalid_post', 'Post not found or not published'); } $meta = $this->get_post_meta($post_id); $meta = array_filter($meta, function($item) { return !is_resource($item); }); $document = [ 'id' => $post->ID, 'title' => $post->post_title, 'post_type' => $post->post_type, 'excerpt' => wp_strip_all_tags($post->post_excerpt), 'content' => wp_strip_all_tags($post->post_content), 'meta' => $meta, 'author' => [ 'id' => $post->post_author, 'name' => get_the_author_meta('display_name', $post->post_author), 'slug' => get_the_author_meta('user_nicename', $post->post_author), ], 'date' => $post->post_date_gmt, 'modified' => $post->post_modified_gmt, ]; return $this->send_request('PUT', '_doc/' . $post_id, $document); } /** * Удаление поста из индекса */ public function delete_post($post_id) { return $this->send_request('DELETE', '_doc/' . $post_id); } } /** * ---------------------------- * КЛАСС ДЛЯ ПОИСКА * ---------------------------- */ class AK_OpenSearch_Searcher extends AK_OpenSearch_Base_Client { /** * Поиск документов */ public function search($query, $page = 1, $per_page = 10, $params = []) { $from = ($page - 1) * $per_page; $defaults = [ 'from' => $from, 'size' => $per_page, '_source' => ['id', 'title', 'excerpt', 'date', 'url', 'author'], 'sort' => [['date' => 'desc']] ]; $params = wp_parse_args($params, $defaults); $search_body = [ 'query' => [ 'multi_match' => [ 'query' => $query, 'fields' => ['title^3', 'content^2', 'excerpt'], 'fuzziness' => 'AUTO' ] ], 'highlight' => [ 'fields' => [ 'content' => new stdClass(), 'title' => new stdClass() ] ] ]; // Удаляем из параметров то, что уже есть в search_body unset($params['query'], $params['highlight']); // Объединяем параметры $search_body = array_merge($search_body, $params); return $this->send_request('POST', '_search', $search_body); } /** * Полнотекстовый поиск с дополнительными параметрами */ public function advanced_search($args = []) { $defaults = [ 'query' => '', 'page' => 1, 'per_page' => 10, 'post_type' => [], 'date_range' => [], 'sort' => [['date' => 'desc']] ]; $args = wp_parse_args($args, $defaults); $from = ($args['page'] - 1) * $args['per_page']; $query = []; // Базовый запрос if (!empty($args['query'])) { $query['bool']['must'][] = [ 'multi_match' => [ 'query' => $args['query'], 'fields' => ['title^3', 'content^2', 'excerpt'], 'fuzziness' => 'AUTO' ] ]; } // Фильтр по типу поста if (!empty($args['post_type'])) { $query['bool']['filter'][] = [ 'terms' => ['post_type' => (array)$args['post_type']] ]; } // Фильтр по дате if (!empty($args['date_range'])) { $date_filter = []; if (!empty($args['date_range']['from'])) { $date_filter['gte'] = $args['date_range']['from']; } if (!empty($args['date_range']['to'])) { $date_filter['lte'] = $args['date_range']['to']; } if (!empty($date_filter)) { $query['bool']['filter'][] = [ 'range' => ['date' => $date_filter] ]; } } // Если нет условий запроса, ищем все документы if (empty($query)) { $query = ['match_all' => new stdClass()]; } $search_body = [ 'query' => $query, 'from' => $from, 'size' => $args['per_page'], 'sort' => $args['sort'], '_source' => ['id', 'title', 'excerpt', 'date', 'url', 'author'], 'highlight' => [ 'fields' => [ 'content' => new stdClass(), 'title' => new stdClass() ] ] ]; return $this->send_request('POST', '_search', $search_body); } /** * Получение документа по ID */ public function get_document($post_id) { return $this->send_request('GET', '_doc/' . $post_id); } } /** * ---------------------------- * WP-CLI КОМАНДЫ * ---------------------------- */ if (defined('WP_CLI') && WP_CLI) { class WP_OpenSearch_CLI_Command extends WP_CLI_Command { private $indexer; private $searcher; public function __construct() { $host = defined('OPENSEARCH_HOST') ? OPENSEARCH_HOST : 'https://localhost:9200'; $user = defined('OPENSEARCH_USER') ? OPENSEARCH_USER : 'admin'; $pass = defined('OPENSEARCH_PASS') ? OPENSEARCH_PASS : 'password'; $index = defined('OPENSEARCH_INDEX') ? OPENSEARCH_INDEX : 'wordpress'; $this->indexer = new AK_OpenSearch_Indexer($host, $user, $pass, $index); $this->searcher = new AK_OpenSearch_Searcher($host, $user, $pass, $index); } /** wp opensearch create_index */ public function create_index($args, $assoc_args) { $force = isset($assoc_args['force']); if ($this->indexer->index_exists()) { if ($force) { WP_CLI::log("Deleting existing index..."); $res = $this->indexer->delete_index(); if (is_wp_error($res)) WP_CLI::error("Failed: ".$res->get_error_message()); WP_CLI::success("Index deleted"); } else { WP_CLI::error("Index exists. Use --force to recreate."); } } WP_CLI::log("Creating index..."); $res = $this->indexer->create_index(); if (is_wp_error($res)) WP_CLI::error("Failed: ".$res->get_error_message()); WP_CLI::success("Index created"); } /** wp opensearch delete_index */ public function delete_index($args, $assoc_args) { if (!$this->indexer->index_exists()) WP_CLI::error("Index does not exist"); WP_CLI::confirm("Delete index?", $assoc_args); $res = $this->indexer->delete_index(); if (is_wp_error($res)) WP_CLI::error("Failed: ".$res->get_error_message()); WP_CLI::success("Index deleted"); } /** wp opensearch status */ public function status($args, $assoc_args) { if (!$this->indexer->index_exists()) WP_CLI::error("Index does not exist"); $stats = $this->indexer->get_stats(); if (is_wp_error($stats)) WP_CLI::error("Failed: ".$stats->get_error_message()); $s = $stats['indices'][$this->indexer->index]; WP_CLI::line("Index: ".$this->indexer->index); WP_CLI::line("Docs: ".$s['total']['docs']['count']); WP_CLI::line("Size: ".size_format($s['total']['store']['size_in_bytes'])); } /** wp opensearch index_posts */ public function index_posts($args, $assoc_args) { $batch_size = isset($assoc_args['batch']) ? (int)$assoc_args['batch'] : 100; $post_types = isset($assoc_args['post-types']) ? explode(',', $assoc_args['post-types']) : ['post','page']; $skip_existing= isset($assoc_args['skip-existing']); if (!$this->indexer->index_exists()) { WP_CLI::error("Index does not exist. Run: wp opensearch create_index"); } $total = 0; foreach ($post_types as $type) { $total += wp_count_posts($type)->publish; } if ($total === 0) WP_CLI::error("No posts found"); $progress = \WP_CLI\Utils\make_progress_bar("Indexing", $total); $indexed = $skipped = $failed = 0; foreach ($post_types as $post_type) { $paged = 1; while ($posts = get_posts([ 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => $batch_size, 'paged' => $paged++, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', 'no_found_rows' => true, ])) { $posts_to_index = $posts; if ($skip_existing) { $existing = $this->indexer->check_existing_posts($posts); $posts_to_index = array_diff($posts, $existing); $skipped += count($existing); } if (!empty($posts_to_index)) { $res = $this->indexer->bulk_index($posts_to_index); if (is_wp_error($res)) { $failed += count($posts_to_index); WP_CLI::warning("Batch failed: ".$res->get_error_message()); } else { $indexed += count($posts_to_index); } } $progress->tick(count($posts)); usleep(300000); // 0.3s пауза } } $progress->finish(); WP_CLI::success("Done. Total: $total, Indexed: $indexed, Skipped: $skipped, Failed: $failed"); } /** * Index posts to OpenSearch */ public function index($args, $assoc_args) { $post_type = $assoc_args['post-type'] ?? 'post'; $batch_size = (int) ($assoc_args['batch-size'] ?? 100); $offset = (int) ($assoc_args['offset'] ?? 0); $all = isset($assoc_args['all']); if ($all) { WP_CLI::log("Starting full index of '$post_type' posts..."); $this->index_all_posts($post_type, $batch_size); } else { WP_CLI::log("Indexing batch of $batch_size '$post_type' posts starting from offset $offset..."); $result = $this->indexer->bulk_index_documents($post_type, $batch_size, $offset); if (is_wp_error($result)) { WP_CLI::error($result->get_error_message()); } else { WP_CLI::success(sprintf( "Successfully indexed %d posts (offset: %d)", $batch_size, $offset )); } } } /** * Index all posts of a given type in batches */ private function index_all_posts($post_type, $batch_size) { global $wpdb; $total = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_type = %s", $post_type )); if ($total === 0) { WP_CLI::warning("No published posts of type '$post_type' found."); return; } $progress = WP_CLI\Utils\make_progress_bar("Indexing $total posts", $total); $processed = 0; while ($processed < $total) { $result = $this->indexer->bulk_index_documents($post_type, $batch_size, $processed); if (is_wp_error($result)) { $progress->finish(); WP_CLI::error($result->get_error_message()); return; } $processed += $batch_size; $progress->tick($batch_size); usleep(500000); // 0.5 second } $progress->finish(); WP_CLI::success("Successfully indexed all $total posts of type '$post_type'"); } /** * Get index mapping information */ public function get_mapping($args, $assoc_args) { // Получаем параметры из командной строки $field = !empty($assoc_args['field']) ? $assoc_args['field'] : ''; $format = !empty($assoc_args['format']) ? $assoc_args['format'] : 'json'; $pretty = isset($assoc_args['pretty']); // Используем публичный метод для получения маппинга $result = $this->indexer->get_mapping(); if (is_wp_error($result)) { WP_CLI::error("Failed to get mapping: " . $result->get_error_message()); return; } // Если указано конкретное поле if (!empty($field)) { if (!empty($result) && is_array($result)) { $index_name = array_key_first($result); if ($index_name && isset($result[$index_name]['mappings']['properties'][$field])) { $field_data = $result[$index_name]['mappings']['properties'][$field]; // Вывод в запрошенном формате switch ($format) { case 'table': $table_data = [ ['Field' => $field, 'Type' => $field_data['type'] ?? 'unknown'] ]; // Добавляем дополнительные свойства поля if (isset($field_data['analyzer'])) { $table_data[0]['Analyzer'] = $field_data['analyzer']; } if (isset($field_data['search_analyzer'])) { $table_data[0]['Search Analyzer'] = $field_data['search_analyzer']; } if (isset($field_data['index'])) { $table_data[0]['Index'] = $field_data['index'] ? 'yes' : 'no'; } WP_CLI\Utils\format_items('table', $table_data, array_keys($table_data[0])); break; case 'csv': $csv_data = [ ['Field', 'Type', 'Analyzer', 'Search Analyzer', 'Index'] ]; $row = [ $field, $field_data['type'] ?? 'unknown', $field_data['analyzer'] ?? '', $field_data['search_analyzer'] ?? '', isset($field_data['index']) ? ($field_data['index'] ? 'yes' : 'no') : '' ]; $csv_data[] = $row; WP_CLI\Utils\format_items('csv', $csv_data, array_keys($csv_data[0])); break; case 'yaml': WP_CLI::line(yaml_emit($field_data)); break; default: // json if ($pretty) { WP_CLI::line(json_encode($field_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } else { WP_CLI::line(json_encode($field_data)); } break; } } else { WP_CLI::warning("Field '$field' not found in mapping"); // Показываем доступные поля $available_fields = []; if (!empty($result) && is_array($result)) { $index_name = array_key_first($result); if ($index_name && isset($result[$index_name]['mappings']['properties'])) { $available_fields = array_keys($result[$index_name]['mappings']['properties']); } } if (!empty($available_fields)) { WP_CLI::line("\nAvailable fields: " . implode(', ', $available_fields)); } } } } else { // Полный маппинг или список всех полей switch ($format) { case 'table': $table_data = []; if (!empty($result) && is_array($result)) { $index_name = array_key_first($result); if ($index_name && isset($result[$index_name]['mappings']['properties'])) { $properties = $result[$index_name]['mappings']['properties']; foreach ($properties as $field_name => $field_config) { $row = [ 'Field' => $field_name, 'Type' => $field_config['type'] ?? 'unknown', 'Analyzer' => $field_config['analyzer'] ?? '-', 'Search Analyzer' => $field_config['search_analyzer'] ?? '-', 'Index' => isset($field_config['index']) ? ($field_config['index'] ? 'yes' : 'no') : 'yes' ]; $table_data[] = $row; } } } WP_CLI\Utils\format_items('table', $table_data, ['Field', 'Type', 'Analyzer', 'Search Analyzer', 'Index']); break; case 'csv': $csv_data = [['Field', 'Type', 'Analyzer', 'Search Analyzer', 'Index']]; if (!empty($result) && is_array($result)) { $index_name = array_key_first($result); if ($index_name && isset($result[$index_name]['mappings']['properties'])) { $properties = $result[$index_name]['mappings']['properties']; foreach ($properties as $field_name => $field_config) { $row = [ $field_name, $field_config['type'] ?? 'unknown', $field_config['analyzer'] ?? '', $field_config['search_analyzer'] ?? '', isset($field_config['index']) ? ($field_config['index'] ? 'yes' : 'no') : 'yes' ]; $csv_data[] = $row; } } } WP_CLI\Utils\format_items('csv', $csv_data, array_keys($csv_data[0])); break; case 'yaml': WP_CLI::line(yaml_emit($result)); break; default: // json if ($pretty) { WP_CLI::line(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } else { WP_CLI::line(json_encode($result)); } break; } } } /** * Test command to index 10 latest 'anew' posts */ public function test_index($args, $assoc_args) { WP_CLI::log("Starting test index of 10 latest 'anew' posts..."); $result = $this->indexer->bulk_index_documents('anew', 10, 0); if (is_wp_error($result)) { WP_CLI::error("Error during indexing: " . $result->get_error_message()); return; } if (is_array($result) && isset($result['errors'])) { if ($result['errors']) { $error_count = 0; foreach ($result['items'] as $item) { if (isset($item['index']['error'])) { $error_count++; WP_CLI::warning(sprintf( "Failed to index post ID %d: %s", $item['index']['_id'], $item['index']['error']['reason'] ?? 'Unknown error' )); } } WP_CLI::warning(sprintf( "Test index completed with %d errors out of %d items", $error_count, count($result['items']) )); } else { WP_CLI::success("Successfully indexed 10 'anew' posts without errors"); } if (WP_CLI::get_config('verbose')) { WP_CLI::log("\nIndexing stats:"); WP_CLI::log("Took: " . ($result['took'] ?? 'N/A') . "ms"); WP_CLI::log("Errors: " . ($result['errors'] ? 'Yes' : 'No')); } } else { WP_CLI::success("Test index request completed. Response: " . json_encode($result)); } } public function test_search($args, $assoc_args) { $result = $this->searcher->search('Набиуллина'); if (is_wp_error($result)) { WP_CLI::error("Error during search: " . $result->get_error_message()); return; } var_dump( $result ); } } WP_CLI::add_command('opensearch', 'WP_OpenSearch_CLI_Command'); }