quarta-feira, 28 de abril de 2010

Lucene Search

Para quem precisa disponibilizar um mecanismo de busca para um site, que permita indexação de conteúdos em diversos formatos (PDF, ODT, DOC, registros de banco de dados, etc.), a solução Lucene Search, da Apache Foundation (http://lucene.apache.org/) supre eficientemente essa demanda.

Nesse post, pretendo mostrar, de forma sucinta, como o recurso de indexação e busca pode ser implementado com Lucene Search, usando um framework JEE, no caso, o JBoss Seam (http://seamframework.org), ou outro qualquer de interesse do desenvolvedor.
Primeiro, é preciso permitir que as suas entidades (JPA ou Hibernate) tenham a capacidade de gerar informações de índice (metadados), que serão gravados em disco, a cada vez que um dado for alterado no banco. Haverá, é lógico, uma preocupação sobre quais campos da entidade (digamos, o tipo Pessoa será indexado unicamente pelo campo str_nome da tabela pessoa) serão indexados. Para configurar os eventos do Lucene Search, de forma que eventos de escrita no banco atualizem os "listenes" do Lucene, é preciso configurar o arquivo "ejb-jar.xml" ou o "hibernate.cfg.xml", com os seguintes itens de configuração:

<property name="hibernate.search.default.indexBase"> value="./lucene_indexes/"/>
<property name="hibernate.ejb.event.post-insert" value="org.hibernate.search.event.FullTextIndexEventListener"/>
<property name="hibernate.ejb.event.post-update" value="org.hibernate.search.event.FullTextIndexEventListener"/>
<property name="hibernate.ejb.event.post-delete" value="org.hibernate.search.event.FullTextIndexEventListener"/>

No caso, a propriedade "hibernate.search.default.indexBase" configura o diretório onde serão gravados os índices. É permitido usar diretórios relativos, como no caso acima, que configura o diretório de índices para o caminho lucene_indexes, que será referenciado dentro do path atual configurado para esse contexto. Dependendo do servidor de aplicação, o diretório "." pode referenciar o diretório server, do diretório padrão de instalação do JBoss, ou o server/default/deploy. Caso não exista o diretório acima, será automaticamente criado (criará o diretório server/default/deploy/lucene_indexes).
Outro passo importante é copiar as bibliotecas do Lucene e do Hibernate Search (que serão adicionados no arquivo MANIFEST.MF, no diretório META-INF):
lucene-core.jar
hibernate-search.jar
hibernate-commons-annotations.jar
hibernate-annotations.jar
lucene-highlighter-2.1.0.jar

Após isso, é necessário inserir as anotações específicas do Lucene e do Hibernate Search. A primeira indicará, na declaração da sua classe POJO (JPA/Hibernate) que ela será indexada:

@Entity
@Indexed
@Table(name = "relator", schema = "juris")
public class Relator implements java.io.Serializable {
...
É preciso marcar o campo da entidade que representa o índice. Na verdade, qualquer campo único pode ser usado. A anotação DocumentId apenas ajudará o Lucene a garantir a unicidade dos registros:
@Id
@DocumentId
@Column(name = "id_relator", unique = true, nullable = false)
public short getIdRelator() {
return this.idRelator;
}

Dentro da definição dessa entidade, agora é necessário especificar quais campos são indexados, e como eles serão indexados:
@Column(name = "str_nome_relator", length = 500)
@Length(max = 500)
@Field(index=Index.TOKENIZED,store=Store.YES,
analyzer= @Analyzer(impl = BrazilianAnalyzer.class))
public String getStrNomeRelator() {
return this.strNomeRelator;
}
Nesse ponto, cabem algumas explicações: o parâmetro index permite dizer se o campo será quebrado em tokens (trechos de caracteres), ou será tratado como uma sequência única. No caso, por ser um campo String composto por nome e sobrenome do relator, é aconselhável usar a constante Index.TOKENIZED. O parâmetro store diz se o campo será armazenado em arquivo, e pode ser visualizado através da ferramenta Luke (http://code.google.com/p/luke/). Essa ferramenta, por sinal, é muito útil para estudar a forma com que o Lucene gera os índices em arquivo, permitindo simular buscas usando diferentes analizadores semânticos de linguagens.

O outro parâmetro (analyzer) permite definir um analizador semântico diferente do Inglês. No caso, existe um BrazilianAnalyzer, e isso é muito importante, pois o tratamento sobre caracteres acentuados, por exemplo, é viabilizado através dos analizadores. Caso um analizador adequado não seja usado, internamente o Lucene pode ignorar caracteres acentuados, por exemplo, como se fossem símbolos inválidos. Ademais, o analizador específico para a linguagem que você utiliza para armazenar dados permite que uma busca por "Abraão" ou "Abraao" retorne o mesmo resultado. O recurso de internacionalização dos Analyzers permite ignorar detalhes de acentuação, melhorando a qualidade da busca.

Agora, já é possível fazer uma busca, usando o trecho a seguir:
try
{
QueryParser parser =
new MultiFieldQueryParser( new String[] {
"strNomeRelator"
},
new BrazilianAnalyzer() );
Query luceneQuery = parser.parse(nome_relator);

FullTextQuery ftq = entityManager.createFullTextQuery(luceneQuery
,Relator.class);
} catch (ParseException exc) {
}

Na chamado a classe MultiFieldQueryParser, nós passaremos como parâmetro: o campo que foi indexado, e que será buscado agora; e o analizador utilizado (BrazilianAnalyzer). Normalmente, pode-se passar null para o analyzer, pois ele já foi configurado na entidade Relator. Dessa forma, nós ainda não criamos a Query Lucene, apenas criamos um "criador de Queries Lucene". A seguir, nós chamamos o método parse, herdado da classe ancestral QueryParser, passando como parâmetro o valor que se quer buscar (por exemplo, a variável String nome_relator poderia conter "José", pois queríamos buscar por José na base). Com a query em mão, criaremos a FullTextQuery, essa sim que fará a busca propriamente dita na base de índices.

Para retornar a lista de registros encontrados, devemos chamar:

List<relator> resultPrimary = ftq.getResultList();

No próximo post, veremos como executar outras funções, como: marcar os hits encontrados, procurar em múltiplas tabelas, etc.

Marcadores: , , , , , , ,