Files
ak-opensearch/ak-opensearch.php
Andrey Kuvshinov 938518f22c add file
2025-09-04 22:59:09 +03:00

982 lines
38 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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');
}