Como ordenar resultados por relevancia personalizada con PHP en WordPress

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

  1. Cachea resultados: las consultas de relevancia pueden ser caras usa transients para resultados frecuentes.
  2. Limita columnas y joins: cuantos más JOIN, peor rendimiento. Evita sumar meta innecesarias en la consulta.
  3. Evita aplicar filtros globalmente: controla con parámetros en WP_Query para no alterar otras consultas (admin, widgets, etc.).
  4. Paginar correctamente: WP_Query seguirá funcionando si no rompes SELECT/WHERE. Comprueba FOUND_ROWS si alteras el COUNT.
  5. Precalcular para resultados masivos: si la relevancia puede calcularse offline (cron), precálcular y almacenar meta puede ser lo más eficaz.
  6. 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 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *