Contents
Introducción
En WordPress por defecto los resultados se ordenan por fecha, título o por relevancia básica del motor de búsqueda interno cuando se usa la búsqueda nativa. Sin embargo, muchas veces necesitamos ordenar los posts por una relevancia personalizada: por ejemplo, dar más peso a coincidencias en el título, menos peso al contenido, incluir campos meta, o combinar búsquedas por taxonomías. Este artículo explica en detalle varias técnicas en PHP para ordenar resultados por una relevancia personalizada en WordPress, con ejemplos listos para usar y consideraciones de seguridad y rendimiento.
Resumen de enfoques posibles
- Ordenar por meta numérico: sencillo cuando la relevancia ya está almacenada en un campo meta (meta_value_num).
- Modificar las cláusulas SQL de WP_Query (posts_clauses): añadir una columna calculada de relevancia y ordenar por ella.
- Usar índices FULLTEXT y MATCH … AGAINST: más rápido y mejor calidad de ranking cuando se crean índices FULLTEXT adecuados.
- Motor externo (ElasticSearch/Algolia): para proyectos con muchísimos datos o necesidades de búsqueda avanzada.
Método 1 — Si ya tienes la puntuación de relevancia en un meta
Si ya calculas la relevancia en el momento de guardar o actualizar el post (por ejemplo, un cron o hook que calcula un score), lo más simple es almacenar ese valor en un meta numérico y usar WP_Query con orderby meta_value_num.
Ejemplo: WP_Query por meta_value_num
args = array( post_type => post, posts_per_page => 10, meta_key => mi_relevancia, // campo meta que contiene la puntuación orderby => meta_value_num, order => DESC, ) query = new WP_Query( args )
Ventajas: simple y eficiente si el valor está precalculado. Inconveniente: necesitas mantener actualizado ese meta (cron, hooks, o al guardar post).
Método 2 — Añadir columna calculada de relevancia en SQL (posts_clauses)
Este método permite calcular dinámicamente una puntuación de relevancia en la consulta SQL. Es flexible: puedes ponderar matches en post_title, post_excerpt, post_content y meta. Se hace con el filtro posts_clauses o posts_search y comprobando un parámetro personalizado para que no afecte a otras consultas.
Concepto de ponderación
Ejemplo de ponderación que usaremos en los ejemplos:
- Coincidencia en post_title: peso 5
- Coincidencia en post_excerpt: peso 3
- Coincidencia en post_content: peso 1
- Coincidencia en un meta (ej. tags_personalizados): peso 2
Implementación segura y aislada
Claves para seguridad y correcto funcionamiento:
- Usar wpdb->prepare y wpdb->esc_like para evitar inyección SQL.
- Controlar que la modificación solo afecte a la consulta deseada (por ejemplo, comprobar query->get(mi_relevancia) ).
- Evitar romper la paginación y tener cuidado con SELECT y GROUP BY si se añade JOINs.
Ejemplo completo usando posts_clauses
add_filter( posts_clauses, mi_relevancia_posts_clauses, 10, 2 ) function mi_relevancia_posts_clauses( clauses, query ) { global wpdb // Solo aplicamos a la consulta que marque mi_relevancia => true if ( ! query->get( mi_relevancia ) ) { return clauses } term = query->get( mi_relevancia_term ) if ( empty( term ) ) { return clauses } // Sanitizar el término para LIKE like = % . wpdb->esc_like( sanitize_text_field( term ) ) . % // Evitar conflictos con SELECT existente: añadimos la puntuación // Pesos: title=5, excerpt=3, content=1, meta=2 relevance_sql = (CASE WHEN {wpdb->posts}.post_title LIKE %s THEN 5 ELSE 0 END) (CASE WHEN {wpdb->posts}.post_excerpt LIKE %s THEN 3 ELSE 0 END) (CASE WHEN {wpdb->posts}.post_content LIKE %s THEN 1 ELSE 0 END) // Si usamos meta, hacemos un LEFT JOIN con postmeta y sumamos coincidencias clauses[join] .= wpdb->prepare( LEFT JOIN {wpdb->postmeta} AS pm_rel ON ( {wpdb->posts}.ID = pm_rel.post_id AND pm_rel.meta_key = %s ) , tags_personalizados ) relevance_sql .= (CASE WHEN pm_rel.meta_value LIKE %s THEN 2 ELSE 0 END) // Añadimos la columna calculada al SELECT clauses[fields] .= wpdb->prepare( , ( relevance_sql ) AS relevance_score , like, like, like, like ) // Ordenamos por la columna de relevancia y después por fecha como fallback clauses[orderby] = relevance_score DESC, {wpdb->posts}.post_date DESC return clauses } // Ejemplo de uso: args = array( post_type => post, posts_per_page => 10, mi_relevancia => true, // flag para activar la lógica mi_relevancia_term=> mi búsqueda, ) query = new WP_Query( args )
Notas:
- Este ejemplo usa LIKE, suficientemente bueno para términos cortos pero no es ideal para relevancia compleja.
- Si el término aparece varias veces en un campo, este CASE solo detecta existencia. Para contar ocurrencias hay que usar funciones adicionales o expresiones REGEXP/REPEAT.
- Si la consulta es compleja (joins adicionales), asegúrate de no duplicar filas o usar DISTINCT/GROUP BY.
Método 3 — Usar FULLTEXT y MATCH … AGAINST para relevancia verdadera
MySQL ofrece FULLTEXT indexes y MATCH…AGAINST para búsquedas de texto con ranking. Requiere crear índices FULLTEXT sobre las columnas a buscar (post_title, post_content o en una tabla separada). Es más eficiente y proporciona puntuaciones de relevancia nativas.
Crear índice FULLTEXT (ejemplo)
-- Ejecutar una sola vez en la base de datos (PHPMyAdmin o wpdb->query) ALTER TABLE wp_posts ADD FULLTEXT ft_title_content (post_title, post_content)
Advertencias: en instalaciones compartidas puede no estar permitido modificar la table. Además, MySQL tiene palabras vacías y límites mínimos de longitud, y FULLTEXT en versiones antiguas no funciona en columnas grandes sin ajustes.
Ejemplo de consulta con MATCH … AGAINST integrada en WP_Query
add_filter( posts_clauses, mi_fulltext_relevance_posts_clauses, 10, 2 ) function mi_fulltext_relevance_posts_clauses( clauses, query ) { global wpdb if ( ! query->get( mi_fulltext ) ) { return clauses } term = query->get( mi_fulltext_term ) if ( empty( term ) ) { return clauses } // Para seguridad usamos esc_sql en el término para MATCH...AGAINST term_esc = esc_sql( sanitize_text_field( term ) ) // Usamos MATCH sobre título y contenido con distintos pesos // NOTA: Los pesos se aplican multiplicando las puntuaciones clauses[fields] .= , ( (MATCH({wpdb->posts}.post_title) AGAINST({term_esc} IN BOOLEAN MODE) 5) (MATCH({wpdb->posts}.post_content) AGAINST({term_esc} IN BOOLEAN MODE) 1) ) AS relevance_score clauses[orderby] = relevance_score DESC, {wpdb->posts}.post_date DESC return clauses } // Uso: args = array( post_type => post, mi_fulltext => true, mi_fulltext_term=> palabras clave, posts_per_page => 10, ) query = new WP_Query( args )
Comentarios:
- Con MATCH … AGAINST las puntuaciones son reales y permiten ordenar por relevancia más adecuadamente.
- En algunos entornos necesitas usar IN BOOLEAN MODE y modificar el término (añadir operadores o ) para afinar la búsqueda.
- Verifica la configuración de stopwords y el valor de ft_min_word_len.
Consideraciones prácticas y rendimiento
- Cachea resultados: las consultas de relevancia pueden ser caras usa transients para resultados frecuentes.
- Limita columnas y joins: cuantos más JOIN, peor rendimiento. Evita sumar meta innecesarias en la consulta.
- Evita aplicar filtros globalmente: controla con parámetros en WP_Query para no alterar otras consultas (admin, widgets, etc.).
- Paginar correctamente: WP_Query seguirá funcionando si no rompes SELECT/WHERE. Comprueba FOUND_ROWS si alteras el COUNT.
- Precalcular para resultados masivos: si la relevancia puede calcularse offline (cron), precálcular y almacenar meta puede ser lo más eficaz.
- Seguridad: usa prepare, esc_sql y esc_like nunca concatenes input sin limpieza.
Ejemplo avanzado: combinación FULLTEXT meta ponderación y cache
En este ejemplo combinamos MATCH…AGAINST para título y contenido, comprobación LIKE en meta, ponderación, y guardamos resultados en transient por 5 minutos para reducir carga.
function obtener_posts_por_relevancia( term, per_page = 10, paged = 1 ) { global wpdb transient_key = relevancia_ . md5( term . _ . per_page . _ . paged ) cached = get_transient( transient_key ) if ( cached !== false ) { return cached } args = array( post_type => post, posts_per_page => per_page, paged => paged, mi_fulltext => true, mi_fulltext_term=> term, ) add_filter( posts_clauses, mi_fulltext_relevance_posts_clauses, 10, 2 ) query = new WP_Query( args ) remove_filter( posts_clauses, mi_fulltext_relevance_posts_clauses, 10 ) // Guardar IDs y total para reconstruir o reaprovechar result = array( posts => query->posts, found => query->found_posts, ) set_transient( transient_key, result, 5 MINUTE_IN_SECONDS ) return result }
Problemas comunes y cómo solucionarlos
- Duplicación de posts: ocurre si la consulta añade JOINs que multiplican filas. Solución: usar DISTINCT o agrupar por ID y seleccionar MAX(relevance_score).
- Relevancia plana (todos igual): cuando el SQL no detecta diferencias. Asegúrate de que las condiciones CASE o MATCH realmente producen valores distintos y que estás seleccionando la columna en fields.
- Baja performance en tablas grandes: usar FULLTEXT, índices o externalizar búsqueda (ElasticSearch).
- Compatibilidad con plugins de caché: algunos caches o plugins pueden guardar resultados viejos invalidar caches cuando cambian posts importantes.
Tabla resumen de pros/cons
Método | Pros | Cons |
---|---|---|
Meta precalculada | Rápido, simple | Necesita mantenimiento del valor |
Posts_clauses LIKE/CASE | Muy flexible, sin cambio de estructura DB | Menos preciso, puede ser lento |
FULLTEXT / MATCH | Relevancia real, rápido con índices | Requiere índices y ajustes MySQL |
Motor externo | Potente y escalable | Más infra y complejidad |
Recomendaciones finales
- Si la base de datos y hosting lo permiten, privilegia FULLTEXT para búsquedas textuales con ranking adecuado.
- Para proyectos medianos sin control sobre la BD, calcular y almacenar la relevancia en meta es la opción más segura y escalable.
- Si necesitas la máxima flexibilidad, modifica posts_clauses con cuidado y limita su alcance con un parámetro en WP_Query.
- Mide impacto de rendimiento y añade caching (transients, object cache) para consultas frecuentes.
Referencias rápidas
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |