Desenvolvendo o backend da nova versão do And After resolvi aprimorar o sistema de recomendações de posts relacionados do site e torná-lo mais relevante para os leitores.
Para acompanhar este post é necessário entender sobre relacionamento no banco de dados, saber um pouco como funciona o Data Mapper, a ferramenta ORM e também ter uma noção de relacionamento de tabelas com o Data Mapper.
Posts relacionados – Como funciona hoje?
Atualmente o sistema pega as tags de um post e, uma a uma, busca outros posts que utilizem a mesma tag. Estes posts são exibidos (sem repetição) e com um limite estipulado.
Isso significa que as últimas tags do post que o leitor está lendo podem ser ignoradas – pois possivelmente com as primeiras tags o sistema de relacionamento já tem posts suficientes para uma lista de recomendação.
Como deveria funcionar?
Para "afinar" o sistema de recomendação o ideal é levar em consideração todas as tags de dois posts que podem ser similares e ver quantas tags eles tem em comum. Quanto mais tags em comum, mais relacionado está o texto – porém não podemos esquecer de levar em consideração a data de publicação do mesmo, para evitar ficar exibindo apenas posts muito antigos.
Tecnologias
Estou utilizando PHP com o framework Code Igniter (tudo bonitinho com MVC) e banco de dados MySQL, a ferramenta ORM utilizada é o Data Mapper (leia também: como instalar e configurar o Data Mapper no Code Igniter).
Para atingir o objetivo criei a seguinte regra de negócio:
- Percorrer cada tag que o post atual tem e buscar os posts recentes que contém esta mesma tag.
- Atribuir um "ponto" para cada vez que um post é encontrado
- Ordenar a lista de posts relacionados pelos "pontos" (número de vezes que um post retornou como relacionado do atual)
Vamos ao código! A função abaixo recebe duas variáveis: $post (o id do post que o usuário está lendo) e $limit (quantos posts relacionados devem retornar no array de resultado).
function getRelatedPost($post, $limit) {
//Pega o post atual e suas tags
$p = new Post();
$p->select("id");
$p->where("id", $post);
$p->get(1);
$tags = $p->tag->get();
$arr = array();
//Percorre as tags do post atual
foreach($tags as $t){
//pega os posts relacionados pela tag
$related = new Post();
$related->where_related_tag("id", $t->id);
$related->where("id <>", $post);
$related->order_by("creationDate DESC");
$related->get($limit);
//Se existirem posts, percorre cada post relacionado para esta tag
if(!empty($related->id)){
foreach($related as $pr){
//Se o post já existir no array soma mais um ponto para ele
//Se ele não existir cria ele no array com valor 1
$id = $pr->id;
if(!empty($arr[$id])){
$arr[$id] = ($arr[$id]+1);
}else{
$arr[$id] = 1;
}
}
}
}
//Ordena o array pelos pontos
//e pega cada post relacionado para retornar como resultado
arsort($arr, SORT_REGULAR);
$relArr = array();
foreach ($arr as $i => $value) {
$relArr[] = $this->getPostById($i);
}
return($relArr);
}
Lembrando que estou utilizando a versão
OverZealous do Data Mapper, que foi desenvolvida como um upgrade da versão padrão desta ferramenta.
E agora o que rola por trás deste código todo, que o Code Igniter retorna no benchmark para os curiosos de plantão:
SELECT `tags`.*
FROM (`tags`)
LEFT OUTER JOIN `posts_tags` as posts_tags ON `tags`.`id` = `posts_tags`.`tag_id`
LEFT OUTER JOIN `posts` as posts ON `posts`.`id` = `posts_tags`.`post_id`
WHERE `posts`.`id` = 8
SELECT `posts`.*
FROM (`posts`)
LEFT OUTER JOIN `posts_tags` as posts_tags ON `posts`.`id` = `posts_tags`.`post_id`
LEFT OUTER JOIN `tags` as tags ON `tags`.`id` = `posts_tags`.`tag_id`
WHERE `tags`.`id` = 14
AND `posts`.`id` <> 8
ORDER BY `posts`.`creationDate` DESC
LIMIT 5
Qualquer dúvida, correção ou melhoria no código comente! 🙂