Vitor Pamplona

Innovation on Vision: Imaging , Enhancement and Simulation

Ferret, o Lucene do Ruby

Ferret é uma engine de indexação e pesquisa semelhante ao Lucene, porém produzida para mundo Ruby. Obviamente, esta engine já possui seu plugin para o Rails, o acts_as_ferret, assunto deste post.

Para instalar o Ferret e seu plugin, faça:

sudo apt-get install ruby1.8-dev 
sudo gem install ferret
sudo gem install acts_as_ferret

Vá até a pasta do seu projeto e baixe o plugin:

script/plugin install svn://projects.jkraemer.net/acts_as_ferret/tags/stable/acts_as_ferret 

Pronto. Para utilizar, declare em cada classe do seu modelo, quais campos devem ser indexados, desta maneira:

class Member < ActiveRecord::Base
acts_as_ferret :fields => [:first_name, :last_name]
end

O Ferret indexará os campos first_name e last_name , incluindo ambos em suas consultas. Nesta parte pode ser configurado uma série de parâmetros para o Ferret, desde pesos para cada atributo (boost) até formas de indexação e algumas gambiarras.

Para efetuar uma pesquisa, basta fazer:

total_results, members = Member.find_id_by_contents("Gregg") 

O total_results terá o número de registros retornados e o members terá uma estrutura semelhante a essa:

members = [
{:model => "Member", :id => "4", :score => "1.0"},
{:model => "Member", :id => "21", :score => "0.93211"},
{:model => "Member", :id => "27", :score => "0.32212"}
]

O score é o índice de relacionamento entre as suas palavras-chave e o registro que retornou. Quanto maior o índice, aumenta a probabilidade de ser o registro procurado.

Se você deseja retornar os objetos ao invés dos ids , basta fazer:

@results = Member.find_by_contents("Gregg") 

Tanto o find_by_contents quanto o find_id_by_contents possibilitam o envio de opções, por exemplo, recursos de paginação para pesquisas mono-modelo.

Procurando em múltiplos modelos

Algumas vezes é necessário fazer uma única pesquisa em várias classes de modelo, nas qual seus relacionamentos são desconsiderados. A resposta da consulta é um array que pode conter diferentes classes de dados e, portanto, devem ser trabalhadas para uma vizualização correta.

Para tal, altere o environment.rb, adicionando as classes modelo que serão alvo da pesquisa:

FERRETABLE_MODELS = %w[Members Classe1 Classe2 Classe3 Classe4] 

Cada nova classe criada, se ela pertencer a pesquisa, deve ser adicionada neste array.

Após, crie um formulário para a pesquisa:

<h1>Pesquise a vontade!</h1>
<p>

<% form_tag :action => "search" do %>
<p>
<label>Pesquisa: </label>
<%= text_field_tag 'searching' %>
</p>
<p><%= submit_tag 'pesquisar' %></p>
<% end %>
</p>

e na action correspondente, adicione:

results = []
@total_hits = 0;
FERRETABLE_MODELS.each do |klass|
k = Kernel.const_get klass
k.find_by_contents(params[:searching]).each do |m|
results.push m
@total_hits += 1;
end
end

@members = results.sort{ |a,b| b.ferret_score <=> a.ferret_score }

Este método buscará, em cada classe listada no array FERRETABLE_MODELS , os registros que se encaixem com a pesquisa e os colocará dentro do mesmo array. Em seguida, o array é ordenado pelo score da pesquisa de maneira decrescente.

Agora basta criar uma view para o members .

<h1>A pesquisa retornou <%=h @total_hits %> registros</h1>

<table class="list">
<tr>
<th>Cadastro</th>
<th>Registro</th>
<th>Ranking</th>
</tr>

<% for result in @members %>
<tr>
<td><%= link_to result.class.to_s, {:controller => result.class.to_s.underscore.pluralize, :action => :index} %></td>
<td><%= link_to result.to_search_view, {:controller => result.class.to_s.underscore.pluralize, :action => :show, :id => result.id} %></td>
<td><%=h result.ferret_score %></td>
</tr>
<% end %>
</table>

Paginando resultados da consulta em múltiplos modelos

Primeiramente, instale o plugin paginating_find:

script/plugin install http://svn.cardboardrocket.com/paginating_find 

Após, altere a action search, descrita na última seção, para:

def search
@searching = params[:searching]
params[:page] = 1 if (params[:page].to_i < 1)

results = []
@total_hits = 0;
FERRETABLE_MODELS.each do |klass|
k = Kernel.const_get klass
k.find_by_contents(@searching).each do |m|
results.push m
@total_hits += 1;
end
end

@members = results.sort{ |a,b| b.ferret_score <=> a.ferret_score }
page_size = 20

@pageEnumerator = PagingEnumerator.new(page_size, @total_hits, false, params[:page], 1) do |page|
offset = (params[:page].to_i - 1) * page_size
limit = page_size
@members[offset..limit+offset-1]
end
end

Repare que agora temos:

  1. uma variável de classe para armazenar a query. Esta informação é necessária para manter a query nas trocas de página.
  2. um novo parâmetro chamado: page que conterá o número da página em exibição.
  3. e um PageEnumerator, estrutura que é responsável por mostrar apenas os dados referente a página desejada.

A view, deve ser alterada para:

<h1>A pesquisa retornou <%=h @total_hits %> registros</h1>

<table class="list">
<tr>
<th>Cadastro</th>
<th>Registro</th>
<th>Ranking</th>
</tr>

<% for result in @pageEnumerator %>
<tr>
<td><%= link_to result.class.to_s, {:controller => result.class.to_s.underscore.pluralize, :action => :index} %></td>
<td><%= link_to result.to_search_view, {:controller => result.class.to_s.underscore.pluralize, :action => :show, :id => result.id} %></td>
<td><%=h result.ferret_score %></td>
</tr>
<% end %>
</table>
<div class="pagination">Páginas: <%= paginating_links(@pageEnumerator, :params => {:searching => @searching}) %></div>

Repare agora que os dados são listados através do @ pageEnumerator e não mais do @ members e que a função paginating_links produzirá os links das páginas. Basta clicar num deles para que a página seja exibida.

Fontes: http://www.railsenvy.com/2007/2/19/acts-as-ferret-tutorial, http://infovore.org/tag/ferret e API documentation.

Posted in Feb 23, 2008 by Vitor Pamplona - Edit - History

Add New Comment

Your Name:


Write the code showed above on the text below.