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 🙂 |
