This commit is contained in:
Andrey Kuvshinov
2025-09-04 22:59:09 +03:00
commit 938518f22c

982
ak-opensearch.php Normal file
View File

@@ -0,0 +1,982 @@
<?php
/**
* Plugin Name: AK OpenSearch
* Description: Индексация постов WordPress в OpenSearch с поддержкой пакетной обработки и WP-CLI.
* Version: 0.1.0
* Author: Andrey Delfin
*/
/**
* wp opensearch status - проверяем статус
* wp opensearch test_index - тест
* wp opensearch delete_index - удаляем индекс
* wp opensearch create_index --force - создаем индекс
* wp opensearch test_index - тестируем индекс (10 штук отправляем)
* wp opensearch test_search тестируем поиск
*
* wp opensearch index --post-type=anew --batch-size=50 --offset=0
*
* # Полный маппинг в JSON
* wp opensearch get_mapping
*
* # Маппинг в виде таблицы
* wp opensearch get_mapping --format=table
*
* # Информация о конкретном поле
* wp opensearch get_mapping --field=title
* # Pretty JSON вывод
* wp opensearch get_mapping --pretty
* # Маппинг в CSV формате
* wp opensearch get_mapping --format=csv
*/
if ( ! defined('ABSPATH')) {
exit;
}
/**
* ----------------------------
* БАЗОВЫЙ КЛАСС КЛИЕНТА OPENSEARCH
* ----------------------------
*/
class AK_OpenSearch_Base_Client {
protected $host;
protected $user;
protected $pass;
public $index;
public function __construct($host, $user, $pass, $index = 'wordpress') {
$this->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');
}