Buscador: AND obligatorio en FULLTEXT, excluir paginas-indice K2 y soportar frase exacta entre comillas

Tres bugs corregidos (issue gitea.feadulta.com#178):
- OR implicito en Boolean Mode devolvia casi todo el sitio (~24.700/24.780 posts)
  para cualquier busqueda de varias palabras; ahora cada palabra es AND obligatorio (+).
- Paginas-indice residuales K2 (categorias 37/38, listados por autor) puntuaban
  altisimo en relevancia por su tamano y tapaban el articulo real; excluidas de
  resultados de busqueda.
- Palabras <3 caracteres forzadas como termino obligatorio rompian el calculo de
  relevancia (score 0 en filas que si coincidian); se descartan antes de construir
  la query.
- Anadido soporte real de frase exacta: termino entre comillas dobles se envia a
  MySQL Boolean Mode como frase, en vez de descartarse silenciosamente.
This commit is contained in:
2026-07-15 19:14:43 -04:00
parent f747ec30c7
commit 250466696b
2 changed files with 49 additions and 3 deletions
@@ -32,6 +32,16 @@ defined('FEA_AUTORES_EXCLUIR') or define('FEA_AUTORES_EXCLUIR', [
*/ */
defined('FEA_CATS_CARTA_EXCLUIR') or define('FEA_CATS_CARTA_EXCLUIR', [6, 21, 22]); defined('FEA_CATS_CARTA_EXCLUIR') or define('FEA_CATS_CARTA_EXCLUIR', [6, 21, 22]);
/**
* Categorías RESIDUALES de la migración K2: páginas-índice por autor (listado de enlaces
* a sus artículos), no son artículos. 37 = "Lista de autores habituales", 38 = "Lista
* completa de autores por orden alfabético" (~1.169 posts). Por su gran tamaño (varios KB
* de títulos enlazados) el ranking FULLTEXT las puntúa altísimo y tapan el resultado real
* (bug reportado en feedback: "Tu verdadero ser" de Fray Marcos sacaba la ficha-índice del
* autor antes que el artículo). Se excluyen de los resultados de búsqueda.
*/
defined('FEA_CATS_RESIDUAL_EXCLUIR') or define('FEA_CATS_RESIDUAL_EXCLUIR', [37, 38]);
// ───────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────
// i18n mínimo (es / en / fr / it / pt) // i18n mínimo (es / en / fr / it / pt)
@@ -139,6 +149,12 @@ add_action('pre_get_posts', function (WP_Query $q): void {
// Categoría (tema) // Categoría (tema)
if ($fea_cat > 0) $q->set('cat', $fea_cat); if ($fea_cat > 0) $q->set('cat', $fea_cat);
// Excluir páginas-índice residuales K2 (ver FEA_CATS_RESIDUAL_EXCLUIR) de los
// resultados de búsqueda. No aplica si el usuario ya filtró por una categoría concreta.
if ($is_search && $fea_cat <= 0) {
$q->set('category__not_in', FEA_CATS_RESIDUAL_EXCLUIR);
}
// Cita bíblica: coincidencia por PREFIJO (el valor empieza por el término, ej. "Jn"). // Cita bíblica: coincidencia por PREFIJO (el valor empieza por el término, ej. "Jn").
// Usamos REGEXP '^<term>' con el término escapado para evitar metacaracteres. // Usamos REGEXP '^<term>' con el término escapado para evitar metacaracteres.
if ($fea_cita !== '') { if ($fea_cita !== '') {
@@ -6,7 +6,7 @@
* relevancia FULLTEXT si no se pide otro criterio de orden. Degradación elegante: * 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. * 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). * Convive con fea-search-advanced.php (filtros pre_get_posts de autor/cat/cita/fecha).
* Version: 1.1 * Version: 1.3
*/ */
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
@@ -38,16 +38,46 @@ function fea_ft_index_exists(): bool {
} }
/** /**
* Calcula el término FULLTEXT en Boolean Mode (cada palabra con prefijo *). * Calcula el término FULLTEXT en Boolean Mode.
*
* Caso normal (sin comillas): cada palabra requerida con prefijo + y sufijo *. El '+'
* fuerza AND entre palabras — sin él, Boolean Mode las une con OR implícito y cualquier
* post que contenga UNA sola palabra común (p.ej. "ser") entra en el resultado, lo que en
* la práctica devolvía casi todo el sitio (~24.700 de ~24.780 posts, bug #8 reportado en
* feedback: "Tu verdadero ser" devolvía cientos de artículos irrelevantes).
*
* Caso frase exacta (entrecomillado, ej. `"tu verdadero ser"`): Boolean Mode soporta
* nativamente búsqueda de frase entre comillas dobles (coincidencia de adyacencia, no
* solo de palabras sueltas). Reenviamos el contenido saneado tal cual entre comillas, sin
* partirlo en palabras con + ni *, para que el usuario que escribe entre comillas obtenga
* de verdad una búsqueda de cadena exacta.
*
* Devuelve '' si el término sanitizado queda vacío. * Devuelve '' si el término sanitizado queda vacío.
*/ */
function fea_ft_boolean_term(string $raw): string { function fea_ft_boolean_term(string $raw): string {
// Frase exacta: todo el término entre comillas dobles.
if (preg_match('/^"(.*)"$/us', $raw, $m)) {
$phrase = trim(substr(preg_replace('/[^\p{L}\p{N}\s\'\-]/u', '', $m[1]), 0, 200));
if ($phrase === '') return '';
return '"' . $phrase . '"';
}
$term = trim(substr(preg_replace('/[^\p{L}\p{N}\s\'\-]/u', '', $raw), 0, 200)); $term = trim(substr(preg_replace('/[^\p{L}\p{N}\s\'\-]/u', '', $raw), 0, 200));
if ($term === '') return ''; if ($term === '') return '';
global $wpdb; global $wpdb;
$words = preg_split('/\s+/', $term); $words = preg_split('/\s+/', $term);
return implode('* ', array_map(fn($w) => $wpdb->esc_like($w), $words)) . '*';
// Descartamos palabras por debajo de innodb_ft_min_token_size (3 en este servidor):
// MySQL las excluye del índice, y forzarlas igualmente como término obligatorio ('+')
// no aporta nada al filtrado (WP igual las ignora) pero SÍ rompe el cálculo de
// relevancia: si un término '+' no existe en el índice, MATCH()...AGAINST() en modo
// boolean puede devolver 0 aunque la fila cumpla el resto de términos, dejando el
// ORDER BY sin poder distinguir resultados relevantes de irrelevantes.
$words = array_values(array_filter($words, fn($w) => mb_strlen($w) >= 3));
if (empty($words)) return '';
return implode(' ', array_map(fn($w) => '+' . $wpdb->esc_like($w) . '*', $words));
} }
/** /**