From 938518f22c071da3884be1780527ad0dff981285 Mon Sep 17 00:00:00 2001 From: Andrey Kuvshinov Date: Thu, 4 Sep 2025 22:59:09 +0300 Subject: [PATCH] add file --- ak-opensearch.php | 982 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 982 insertions(+) create mode 100644 ak-opensearch.php diff --git a/ak-opensearch.php b/ak-opensearch.php new file mode 100644 index 0000000..bdf8575 --- /dev/null +++ b/ak-opensearch.php @@ -0,0 +1,982 @@ +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'); +} \ No newline at end of file