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. * * 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. */ 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)); if ($term === '') return ''; global $wpdb; $words = preg_split('/\s+/', $term); // 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)); } /** * 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);