Sistema de busca Full-Text no Rails usando MySQL

Quando desenvolvemos sistemas web, é bem comum que já no levantamento de históriasda primeira iteração, alguém grite lá no fundo:

"Não esqueçam que o sistema deve ter um mecanismo de busca"

Não importa se estamos fazendo um grande indexador de blogs ou um pequeno sistema de locadora, a funcionalidade de busca é extremamente comum. Mecanismos de buscas trazem segurança ao usuário, é uma questão de usabilidade ter sempre uma caixa com essa finalidade no canto superior direito do site.

Como estamos desenvolvendo em RubyOnRails, teríamos algumas possibilidades de implementação um tanto quando sofisticadas, com criação de arquivos de índices e resultados ordenados por relevância, para citar algumas funções. Tudo isso com a simplicidade de instalar uma gem e um plugin. Ferret e Sphinx são bons exemplos desse tipo de gem. Ambas têm atrativos a performance e a escalabilidade.

Decidi pelo Ferret em meu projeto e comecei a vivenciar alguns problemas, como a falta suporte a UTF-8 no Windows, problemas na busca por palavras no plural, acentuadas, ou com case diferente do indexado e principalmente, a necessidade de um processo exclusivo para a atualização dos índices, pois ocorrem erros se vários processos tentarem escrever no índice, o que atrapalha muito quando se usa uma hospedagem compartilhada. Encontrei desenvolvedores na internet dizendo para deixar o Ferret e usar o Sphinx, devido a sua instabilidade em ambiente de produção. Diante da inevitável mudança e retrabalho, me fiz a seguinte pergunta:

Eu realmente preciso de todas essas funcionalidades e de toda essa performance?

Como sabemos quanto mais funcionalidades, mais complicado fica o sistema de manter e de alterar (vide a lei "Menos Massa" do livro Caindo na Real). Ainda seguindo as práticas do "caindo na real", fiquei convencido de que o melhor a fazer era deixar meu sistema começar pequeno, em uma hospedagem compatilhada e rodando em fastcgi ou em mod_rails. Este é um conceito que gosto bastante, escalabilidade só deve ser um problema quando a aplicação tiver número de usuários suficiente para tal.

Passei a procurar uma solução mais simples que me atendesse e encontrei o plugin acts_as_fulltextable, que não usa nenhuma gem extra.

Mas apenas um plugin para realizar buscas? Sem gems? Deve ficar extremamente lento...

Para minha surpresa a solução se mostrou extremamente eficaz. O plugin faz uso de uma funcionalidade do MySQL chamada Full-Text Search. Trata-se de um tipo especial de índice de banco que pode ser aplicado a tabelas MyISAM em campos CHAR,VARCHAR e TEXT. Assim, as buscas passam a ser feitas diretamente pelo MySQL, através de instrução MATCH() ... AGAINST, como pode ser entendido lendo-se a documentação. O que o plugin faz é criar uma tabela extra, com os campos escolhidos em cada Model e aplicar esse tipo de índice. Vamos ao passos para usá-lo.

Instalando o acts_as_fulltextable:

script/plugin install http://wonsys.googlecode.com/svn/plugins/acts_as_fulltextable/ 

Para adicionar atributos dos models ao índice, é necessário que a linha abaixo esteja no código da classe Model (arquivo Model.rb):

acts_as_fulltextable :atributo1, :atributo2, :atributo3

Depois, cria-se uma migration através do gerador que acompanha o plugin, passando os models com campos indexados:

script/generate fulltext_rows model1 model2 model3 ...

Atualizamos a estrutura do banco:

rake db:migrate

E pronto, magicamente podemos chamar os métodos em nossos controllers

Model.find_fulltext('string de busca', :limit => 10, :offset => 0)

Para buscar em um Model específico, limitando o número de respostas em 10, ou ainda

FulltextRow.search('string de busca', :only => [:model1, :model2, :model3], :limit => 10, :offset => 0)

Para procurar em todos os models indexados, ou naqueles indicados em :only.

Acabei de criar um novo Model, como adiciono na tabela de índice?

Simples, basta colocar a seguinte linha na sua migration, substituindo NewModel pelo seu Model:

NewModel.find(:all).each {|i| i.create_fulltext_record}

Quero paginar o resultado da busca com will_paginate. Tem como?

A versão que está no repositório dos desenvolvedores já suporta will_paginate. Existe um código que verifica se o will_paginate está instalado e mostra o resultado através do seu método paginate. Logo, pode-se trocar os parâmetros :limit e :offset por :page normalmente:

FulltextRow.search('string de busca', :only => [:model1, :model2, :model3], :page => params[:page])[/sourcecode]

Se você está com problemas em implementar uma busca em seus site que seja case-insensitive e que ignore acentuação automaticamente, aceitando caracteres UFT-8, além de ser fácil de instalar e configurar, o acts_as_fulltextable foi feito pra você.

Published on in Blog