Files
feadulta/mu-plugins/fea-search-fulltext.php

102 lines
3.9 KiB
PHP

<?php
/**
* Plugin Name: Fe Adulta — Motor FULLTEXT (#8)
* Description: Sustituye el LIKE nativo de WP por MATCH AGAINST (MySQL FULLTEXT, InnoDB,
* Boolean Mode) cuando se hace una búsqueda por texto (/?s=…). Ordena por
* relevancia FULLTEXT si no se pide otro criterio de orden. Degradación elegante:
* si no hay término o el índice FULLTEXT no existe, usa el comportamiento nativo.
* Convive con fea-search-advanced.php (filtros pre_get_posts de autor/cat/cita/fecha).
* Version: 1.1
*/
if (!defined('ABSPATH')) exit;
/**
* Comprueba (cacheado) que existe el índice FULLTEXT 'fea_ft' en wp_posts.
* Si no existe, el motor se degrada al comportamiento nativo para no romper la búsqueda
* con un error SQL (MATCH AGAINST requiere el índice).
*/
function fea_ft_index_exists(): bool {
static $cached = null;
if ($cached !== null) return $cached;
// Cache persistente 12h vía transient para evitar el SHOW INDEX en cada request.
$t = get_transient('fea_ft_index_exists');
if ($t !== false) {
$cached = ($t === '1');
return $cached;
}
global $wpdb;
$found = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND INDEX_NAME = 'fea_ft'",
$wpdb->posts
));
$cached = $found > 0;
set_transient('fea_ft_index_exists', $cached ? '1' : '0', 12 * HOUR_IN_SECONDS);
return $cached;
}
/**
* Calcula el término FULLTEXT en Boolean Mode (cada palabra con prefijo *).
* Devuelve '' si el término sanitizado queda vacío.
*/
function fea_ft_boolean_term(string $raw): string {
$term = trim(substr(preg_replace('/[^\p{L}\p{N}\s\'\-]/u', '', $raw), 0, 200));
if ($term === '') return '';
global $wpdb;
$words = preg_split('/\s+/', $term);
return implode('* ', array_map(fn($w) => $wpdb->esc_like($w), $words)) . '*';
}
/**
* Reemplaza la cláusula WHERE de búsqueda por MATCH AGAINST.
* Hook: posts_search (filtra el WHERE que WP construye para /?s=).
*/
add_filter('posts_search', function (string $search, WP_Query $q): string {
if (is_admin() || !$q->is_main_query() || !$q->is_search()) return $search;
if (!fea_ft_index_exists()) return $search; // degradación elegante
$raw = trim((string) $q->get('s'));
if ($raw === '') return $search;
$ft_term = fea_ft_boolean_term($raw);
if ($ft_term === '') return $search;
global $wpdb;
$ft_esc = esc_sql($ft_term);
// WP construye " AND (...) " para la búsqueda; devolvemos un bloque AND compatible.
return " AND (MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content) AGAINST ('{$ft_esc}' IN BOOLEAN MODE)) ";
}, 10, 2);
/**
* Ordena por relevancia FULLTEXT cuando no se especifica otro orden.
* Hook: posts_clauses (permite modificar SELECT y ORDER BY juntos).
*/
add_filter('posts_clauses', function (array $clauses, WP_Query $q): array {
if (is_admin() || !$q->is_main_query() || !$q->is_search()) return $clauses;
if (!fea_ft_index_exists()) return $clauses; // degradación elegante
$raw = trim((string) $q->get('s'));
if ($raw === '') return $clauses;
// Sólo reordenamos por relevancia si el orderby es el nativo de búsqueda.
$ob = $q->get('orderby');
if (!in_array($ob, ['relevance', 'date', ''], true)) return $clauses;
$ft_term = fea_ft_boolean_term($raw);
if ($ft_term === '') return $clauses;
global $wpdb;
$ft_esc = esc_sql($ft_term);
// Añadimos la columna de relevancia al SELECT y la usamos en ORDER BY.
$score_col = "MATCH({$wpdb->posts}.post_title, {$wpdb->posts}.post_content) AGAINST ('{$ft_esc}' IN BOOLEAN MODE)";
$clauses['fields'] .= ", ({$score_col}) AS fea_ft_score";
$clauses['orderby'] = "fea_ft_score DESC, {$wpdb->posts}.post_date DESC";
return $clauses;
}, 10, 2);