Aplicações Sênior: Engine de Recomendação com Algoritmo de Pesos

Reter a atenção do usuário em um blog ou loja virtual é o Santo Graal do engajamento digital.
O método tradicional do WordPress para sugerir “Posts Relacionados” baseia-se em buscas simples por categorias em comum utilizando a classe WP_Query.
O problema dessa abordagem é que ela gera junções (JOINs) pesadas no banco de dados a cada carregamento de página e, pior, não possui inteligência analítica real.
Um post que compartilha apenas uma tag genérica com outro pode acabar sendo recomendado, frustrando o leitor.
Nesta masterclass, vamos abandonar soluções superficiais e construir uma Engine de Recomendação Baseada em Conteúdo (Content-Based Filtering).
Calcularemos a similaridade matemática entre todos os posts do site usando um sistema de pesos (Scoring) e armazenaremos esses resultados pré-computados em uma tabela dedicada.
Esta é a arquitetura que permite sugerir conteúdo de forma cirúrgica com custo zero de processamento no momento da visita.

O Conceito de Matriz de Similaridade

Para o nosso algoritmo, um relacionamento não é binário (existe ou não existe), ele possui uma “Força”.
Definiremos regras claras: se dois posts compartilham a mesma Categoria, eles ganham 5 pontos de similaridade.
Se compartilham uma Tag, ganham 2 pontos cada.
Se compartilham o mesmo Autor, ganham 1 ponto extra.
O objetivo é cruzar cada postagem do banco de dados contra todas as outras, somar essas pontuações e extrair apenas os 5 vizinhos mais próximos (Nearest Neighbors) com as maiores notas.
Fazer essa matemática complexa em tempo real no carregamento da página do artigo destruiria o seu servidor MySQL.
Portanto, a genialidade desta arquitetura reside na Pré-Computação Offline.

CREATE TABLE wp_post_recomendacoes (
    post_id BIGINT UNSIGNED NOT NULL,
    relacionado_id BIGINT UNSIGNED NOT NULL,
    score DECIMAL(8,2) NOT NULL,
    processado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (post_id, relacionado_id),
    INDEX idx_busca_rapida (post_id, score)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

Desenvolvendo o Algoritmo de Pontuação em PHP

O coração do sistema é uma rotina de Batch Processing (Processamento em Lote) que rodará durante a madrugada via Cron.
Ela iterará sobre o catálogo e calculará os pesos matemáticos.
No código abaixo, utilizaremos a estrutura de dados nativa do WordPress de forma inteligente, extraindo arrays de IDs de termos para realizar interseções rápidas em memória RAM usando a função array_intersect() nativa do PHP.
Esta operação computacional baseada em Teoria dos Conjuntos é imensamente mais veloz do que tentar resolver tudo com uma única e monstruosa consulta SQL.

class RecomendacaoEngine {
    public static function calcular_todas_matrizes() {
        global $wpdb;
        
        // Limpeza drástica para recálculo completo (Truncate é atômico e rápido)
        $wpdb->query("TRUNCATE TABLE wp_post_recomendacoes");
        
        // Busca todos os posts publicados
        $posts = $wpdb->get_results("SELECT ID, post_author FROM wp_posts WHERE post_status = 'publish' AND post_type = 'post'");
        
        // Pré-carregamento de termos em memória para evitar N+1 Queries
        $termos_map = [];
        foreach ($posts as $p) {
            $termos_map[$p->ID] = [
                'categorias' => wp_get_post_categories($p->ID),
                'tags' => wp_get_post_tags($p->ID, ['fields' => 'ids'])
            ];
        }
        
        $insercoes = [];
        
        // O famoso Loop O(N²) otimizado
        foreach ($posts as $alvo) {
            $scores = [];
            
            foreach ($posts as $candidato) {
                if ($alvo->ID === $candidato->ID) continue; // Não recomenda a si mesmo
                
                $pontos = 0;
                
                // Regra 1: Categorias (Peso 5)
                $cat_comuns = count(array_intersect($termos_map[$alvo->ID]['categorias'], $termos_map[$candidato->ID]['categorias']));
                $pontos += ($cat_comuns * 5);
                
                // Regra 2: Tags (Peso 2)
                $tag_comuns = count(array_intersect($termos_map[$alvo->ID]['tags'], $termos_map[$candidato->ID]['tags']));
                $pontos += ($tag_comuns * 2);
                
                // Regra 3: Mesmo Autor (Peso 1)
                if ($alvo->post_author === $candidato->post_author) {
                    $pontos += 1;
                }
                
                if ($pontos > 0) {
                    $scores[$candidato->ID] = $pontos;
                }
            }
            
            // Ordena mantendo os IDs e extrai apenas os Top 5
            arsort($scores);
            $top_5 = array_slice($scores, 0, 5, true);
            
            foreach ($top_5 as $relacionado_id => $score_final) {
                $insercoes[] = $wpdb->prepare(
                    "(%d, %d, %f)",
                    $alvo->ID, $relacionado_id, $score_final
                );
            }
        }
        
        // Inserção em massa super otimizada (Bulk Insert)
        if (!empty($insercoes)) {
            $lotes = array_chunk($insercoes, 500);
            foreach ($lotes as $lote) {
                $query = "INSERT INTO wp_post_recomendacoes (post_id, relacionado_id, score) VALUES " . implode(',', $lote);
                $wpdb->query($query);
            }
        }
    }
}

A Importância do Bulk Insert

Observe atentamente a fase final do algoritmo.
Em vez de executar um comando INSERT individual para cada relação descoberta, nós aglomeramos (chunk) os dados em lotes de 500 registros e enviamos tudo em uma única instrução massiva para o MySQL.
Isso é conhecido como Bulk Insert.
A sobrecarga de rede (Network Overhead) de abrir e fechar milhares de transações individuais faria esse script demorar horas.
Ao empacotar as queries, reduzimos o tempo de processamento em até 95%.
Esta técnica demonstra profunda empatia com a infraestrutura de banco de dados.

Retorno Instantâneo no Front-End

Com a matriz computada de madrugada, o trabalho do front-end torna-se maravilhosamente simples e performático.
Quando um visitante entra no artigo com ID 150, nosso tema WordPress faz uma única e levíssima consulta indexada.
A velocidade de retorno dessa leitura baterá a casa dos 2 milissegundos, pois o banco de dados não precisa calcular nada, apenas ler uma tabela já ordenada pelo índice de score.
Você entrega recomendações inteligentíssimas sem sacrificar a métrica do Time to First Byte (TTFB).

// Exibição na view (single.php)
function exibir_recomendacoes_inteligentes($post_id) {
    global $wpdb;
    
    // Consulta direta ao nosso índice computado (Custo zero de processamento)
    $recomendados_ids = $wpdb->get_col($wpdb->prepare(
        "SELECT relacionado_id FROM wp_post_recomendacoes 
         WHERE post_id = %d ORDER BY score DESC LIMIT 5",
        $post_id
    ));
    
    if (empty($recomendados_ids)) return;
    
    // Busca os posts reais com apenas 1 query limpa
    $query_recomendados = new WP_Query([
        'post__in' => $recomendados_ids,
        'orderby' => 'post__in',
        'ignore_sticky_posts' => true
    ]);
    
    if ($query_recomendados->have_posts()) {
        echo '<div class="recomendados-box"><h3>Você também vai dominar:</h3><ul>';
        while ($query_recomendados->have_posts()) {
            $query_recomendados->the_post();
            echo '<li><a href="' . get_permalink() . '">' . get_the_title() . '</a></li>';
        }
        echo '</ul></div>';
        wp_reset_postdata();
    }
}

Evolução para o Algoritmo TF-IDF

Para desenvolvedores que desejam escalar essa engine para níveis de Machine Learning, o próximo passo lógico é substituir os pesos fixos pelo cálculo estatístico TF-IDF (Term Frequency – Inverse Document Frequency).
Nesse modelo matemático, uma Tag extremamente comum (como “PHP”) teria um peso baixo, enquanto uma Tag rara e específica (como “Opcache Tuning”) teria um peso astronômico na geração de afinidade entre dois artigos.
Essa evolução pode ser feita isoladamente dentro da classe de cálculo sem nunca tocar no código de front-end.
Isso ratifica o poder do desacoplamento arquitetural ensinado nesta aula.

Conclusão da Arquitetura Analítica

O conceito vital que absorvemos aqui é a separação entre Leitura e Escrita/Processamento.
Transferir o fardo computacional para rotinas off-peak (fora dos horários de pico) é o que permite a sistemas mastodônticos como a Netflix ou o Spotify servirem conteúdo complexo instantaneamente.
A aplicação dessas heurísticas no ambiente familiar do WordPress e MySQL quebra os limites impostos pelos tutoriais básicos e eleva seu software a um patamar corporativo.
A verdadeira inovação começa onde os tutoriais comuns terminam.

Rolar para cima