Ruby on Rails Projetos

O que aprendi desenvolvendo um motor de pesquisa (e estatística) com Ruby on Rails

23 Jan 2023

Me foi dada a seguinte tarefa: desenvolver um motor de pesquisa e que fosse possível guardar estatísticas sobre o que os usuários estavam buscando. "Hmm, talvez eu pudesse replicar a busca de filmes da Netflix", pensei. E assim nasceu o Movie Search. Foi um projeto de 48 horas que me ajudou a melhorar muitos conceitos e aprender novas tecnologias. Nessa postagem, vou compartilhar os desafios e soluções que encontrei:

Link para o repositório no GitHUb: https://github.com/devaniljr/movie_search
Link para o site demo: https://moviesearch.devanil.dev/

Design

Eu gosto de desenhar um protótipo de todos os projetos que faço:

It's good to imagine the desired results.


Midjourney foi usado para gerar esse logo bonitinho. E para CSS eu vou sempre defender o Tailwind. Você pode começar um projeto novo já configurado com Tailwind usando rails new <app-name> --css tailwind. E sempre lembre de rodar o servidor com bin/dev em vez do rails server.

A caixa de pesquisa em tempo real

Esse foi o período perfeito para experimentar Hotwire com Ruby on Rails. A promessa do Hotwire é fornecer interativade para a aplicação usando HTML em vez de JavaScript ou JSON. Eu segui esse tutorial e os resultados foram ótimos. Eu usei um turbo_frame_tag e fiz alguns truques no formulário de pesquisa, e toda vez que eu apertava enter os resultados aparceriam sem atualizar toda a página. Foi muito legal ver em ação sem nenhuma dor de cabeça.  

Resultados instantâneos com Hotwire.


Mas eu precisava de algo a mais: enquanto o usuário digita, os resultados apareciam em tempo real. Isso é algo que o turbo frame não faz. Eu precisei incluir um pouco de JavaScript que diz para "apertar enter a cada x milisegundos" e então retornar os resultados. Tudo que precisei foi um pequeno código em um controller Stimulus e voila, minha funcionalidade de pesquisa em tempo real estava pronta.

Foi muito divertido brincar com o Hotwire pela primeira vez, mas acho que agora é necessário estudar mais profundamente como tudo funciona. Eu tentei colocar atualizações em tempo real ali em baixo do site e não descobri como. Espero poder descobrir como no futuro próximo depois de assistir ao curso do Pragmatic Studio's.

Eu comecei com uma simples query SQL usando ILIKE para a pesquisa, mas logo percebi que iria precisar de resultados melhores para fazer as estatísticas funcionarem. É aqui que o Elasticsearch entra. Eu nunca tinha usado antes, mas foi bem fácil de configurar usando a gem Searchkick e esse tutorial. O que o Elasticsearch faz é tentar achar o que é que o usuário está procurando e retornar os melhores resultados. Eu gostei da forma que ele funciona e definitivamente vou aprender mais sobre.  

O algoritmo para as estatísticas

Eu ainda tinha um problema: eu precisava salvar em algum lugar o que a pessoa estava pesquisando a mostrar estatísticas sobre isso. Me deram o requisito de ser escalável e dar contar de um grande número de pesquisas por minuto. Então eu decidi guardar alguns dados no Redis, tratar esses dados com um cron job e Sidekiq, a aí sim guardar esses dados no banco de dados com fins de estatística.

Essa foi a lógica desenvolvida:

  1. 1. A pessoa digita a busca. A query e o primeiro resultado são guardados no Redis. Se a pessoa apenas digitar "Inter" e se satisfazer porque já encontrou "Interstellar", então ela vai salvar "Inter" como a query e "Interstellar" como o resultado.

  2. 2. A cada 5 minutos, um job é rodado no Sidekiq para tratar esses dados guardados no Redis e descobrir qual filme foi pesquisado. Os passos abaixo não foram executados diretamente no paso 1 porque, como é uma pesquisa em tempo real, cada tecla a apertada iria engatilhar toda essa lógica, o que iria exigir bastante de consumo.

    1. 2.1. Se o Elasticsearch retornar um resultado claro, então o algoritmo considera esse como o resultado pretendido da busca.

    2. 2.2. Se não existir nenhum resultado, então é feita uma busca na API do Movie Database e busca algum filme por lá.

    1. 2.2.1. Além disso, se a API achar algum filme, ele guarda no próprio banco de dados do site e vai começar a aparecer nas buscas.

    1. 2.3 Se nenhum resultado é encontrado, então a própria query é salva, mas sem nenhuma conexão com algum filme do banco de dados.

  3. 3. Depois de determinar qual foi o filme pesquisado, novas chaves são chamadas no Redis. Uma para o total de buscas, outra para total de buscas no ano e outra para total de buscas no mês.

  4. 4. O resultado é salvo no banco de dados do Postgres.

  5. 5. Finalmente, a chave temporária associada a essa busca específica é deletada no Redis.

O algoritmo de busca.


Claro, ainda há muito espaço para melhorar tudo. Aqui estão alguns casos extremos que precisam ser considerados no futuro:

  1. 1. Se a pessoa digita "Star" e fica satisfeita com a busca, mas não clica necessariamente no primeiro resultado, e acaba clicando no segundo, terceiro... No momento o algoritmo só salva o primeiro resultado, ignorando o clique da pessoa. Uma forma de resolver isso seria salvar a página para qual a pessoa redirecionada depois da busca. Estou pensando em adicionar outra camada para o algoritmo na action show. Como cada busca tem sua própria chava aleatória guardada na sessão do usuário, é fácil conferir se a pessoa entrou na página de algum filme específico e priorizar essa ação em vez do primeiro resultado (mas manter ele também).

  2. 2. Toda vez que a caixa de pesquisas está vazia, uma nova chave aleatória é fornecida e salva na sessão do usuário. Apesar disso funcionar na maioria dos casos, caso a pessoa selecione todo o texto dentro da caixa e comece a digitar a nova busca, o algoritmo não considera uma nova busca (porque o formulário nunca esteve vazio). Estou considerando uma solução, e uma possibilidade é adicionar javascript para limitar algumas teclas de serem pressionadas.

Depois do algoritmo descobrir o que a pessoa estava pesquisando, basta mostrar o número de pesquisas salvas no banco de dados, assim como o número de vezes que aquela pesquisa foi feita.

O deploy

Eu achava que o deploy seria mais difícil porque exigiria colocar no ar não apenas o Rails e o Postgres como usual, mas também o Redis e o Elasticsearch. Mas tudo rodou sem muito problema no meu servidor Dokku. A parte mais difícil foi configurar o Sidekiq. Então aqui estão alguns problemas que encontrei:

  1. 1. Os controllers do Stimulus não estavam passando pelo bundle. Demorou um tempo para perceber que mesmo que eu estivesse usando importmap para o javascritp, o meu arquivo de configuração não estava configurado para isso. Eu precisei seguir a instalação manual do Stimulus descrita na página do Github deles.

  2. 2. Sidekiq precisou de muita configuração para funcionar no servidor Dokku. A boa notícia é que é só seguir esse tutorial passo a passo.

  3. 3. Como esse seria meu primeiro projeto hospedado em um subdomínio do devanil.dev, tive alguns problemas configurado o DNS para descobrir esse projeto. A solução foi migrar as configurações do DNS para o Digital Ocean e seguir seguir esse tutorial.

Esse foi um dos projetos mais desafiadores que trabalhei, e fiquei muito satisfeito com os resultados. Mas se você tiver qualquer consideração sobre minha lógica ou código, por favor, me contate. Eu realmente preciso desse feedback para crescer como desenvolvedor. E vamos ir melhorando linha de código por linha de código.

Receba atualizações por e-mail

Sempre que tiver novidades por aqui vou enviar aos inscritos.

Entre na conversa

Criei uma postagem lá no LinkedIn para receber comentários:

Comente