Caelum - Ensino e Inovação | Explore o poder de Java e Scrum

Hibernate Search com Lucene

Por Nico Steppat em 13/06/07

Há alguns meses tem um novo projeto na página do Hibernate. O Hibernate Search é uma “full text search engine” que tem sua implementação escrita em cima do Apache Lucene.

O Lucene por si só já é bem famoso pelas funcionalidades oferecidas, como buscas aproximadas, índices em memória, e velocidade. A parte mais complicada é a configuração e preparação (indexação) dos dados que são necessários para executar a busca. Aqui entra o Hibernate Search: ele te poupa desse trabalho e integra o Lucene muito bem com o Hibernate, dada a arquitetura de listeners e callbacks deste projeto. Com ele podemos buscar textualmente pelas entidades, usando as funcionalidades do Lucene sem a dor de cabeça da criação de Documents, definição dos tipos de Fields, controle de acesso concorrente aos índices e outros pequenos detalhes que aparecem ao usar o projeto diretamente. Vamos ver um exemplo simples como configurar e usar o Hibernate Search.

Anotações

Tanto nas Hibernate Annotations quanto no Hibernate Search você passa a maioria das configurações usando anotações no seu modelo de domínio. Para poder busca uma entidade você deve anota-lá com @Indexed. Usando esta anotação o Lucene irá analisar os dados da entidade e indexa-los, pois um interceptador do Hibernate fará esse trabalho por de trás dos panos. Quais dados você pretende indexar e como deve ser especificado anotando o atributo com @Field. Se você não usar nenhuma anotação nenhum dado será indexado. Além disso, é preciso anotar a chave primária com @DocumentId, como segue:


@Entity
@Indexed
public class Dvd {

  @Id  @GeneratedValue
  @DocumentId
  private Long id;

  @Field(index=Index.TOKENIZED,store=Store.YES)
  private String title;

  @Field(index=Index.TOKENIZED)
  private String description; 

        // possivelmente alguns getters e setters
}

Index

Usamos no exemplo os atributos index e store na anotação @Field. Quando Lucene indexa um campo, ele salva os dados num formato especial para ele poder executar suas buscas. Por exemplo, se você usa o título “Rambo 2″, o Lucene indexa Rambo e 2 separado. Esse tipo de indexação se chama TOKENIZED. Outros tipos são Index.NO (não será indexado e não tem como buscar) e Index.UN_TOKENIZED (não será “separado”, útil para campos considerado chaves). Essa nomenclatura é bem familiar para quem já usou o Lucene.

Store

Indica que Lucene deve gravar os dados (o título do DVD, no caso) no índice, de tal forma que esse valor possa ser recuperado integralmente diretamente pelos indices. Quando Lucene procura o título, ele pode devolver diretamente o valor, porque usamos Store.YES. É mais rápido, pois o hibernate não precisará consultar o banco para buscar aquele atributo, mas precisa mais espaço no disco e de certo forma é uma redundância de informação.

Eventos e diretórios

Hibernate Search encapsula o trabalho de indexação. Somente precisamos avisar o Hibernate Search em que momento nós desejaremos indexar as entidades em questão. Para isso existem listeners que podem ser configurados no hibernate.cfg.xml:

<session-factory>
    ...
    <event type="post-update">
            <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
    </event>
    <event type="post-insert">
            <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
    </event>
        <event type="post-delete">
            <listener class="org.hibernate.search.event.FullTextIndexEventListener"/>
    </event>
    ...
</session-factory>

Definimos 3 listeners globais. Hibernate os ativa automaticamente quando o tipo de evento acontecer. Nesse caso qualquer alteração numa entidade faz com que o Lucene altere o índice.

Além disso temos que definir um provedor que cria os arquivos e diretórios que serão usados para os índices do Lucene:

<session-factory>
   ...
   <property name="hibernate.search.default.directory_provider">
       org.hibernate.search.store.FSDirectoryProvider
   </property>
   <property name="hibernate.search.default.indexBase">
       /home/nico/workspace/mydvds/webapp/indexes
   </property>
   ...
</session-factory>

Existem outros provedores como RAMDirectoryProvider que somente grava na memória.

A busca

Configurado o Hibernate Search, vamos criar a busca. Você também usa um objeto session/query para fazer suas buscas, na verdade a sessão do Hibernate Search encapsula uma do hibernate. Para criar uma sessão usamos o método estático createFullTextSession da classe org.hibernate.search.Search e passamos a sessão do Hibernate como argumento:

FullTextSession LuceneSession = Search.createFullTextSession(hibernateSession);

Com a sessão na mão podemos criar um org.hibernate.search.FullTextQuery do hibernate search:

FullTextQuery fullTextQuery = LuceneSession.createFullTextQuery(LuceneQuery);

FullTextQuery estende Query do hibernate. Podemos invocar fullTextQuery.list() para receber a lista de objetos (neste caso dvds) baseado na query do Lucene.

Para criar essa query, usamos a api do Lucene. Ela oferece uma sintaxe simples e muito poderosa para definir nosso critério de busca. Essa sintaxe é verificada pelo QueryParser do Lucene:

String[] stopWords = new String[]{"de","do","da","dos","das","a","o","na","no","em"};    
QueryParser parser = new QueryParser("title",new StopAnalyzer(stopWords));));

Queremos procurar no campo title e passamos um objeto Analyzer. Ele é responsável por filtrar palavras insignificante na busca que foram definidos no array stopWords. Por padrão o StopAnalyzer somente conhece “stop words” do idioma inglês. É claro que o array deveria ser bem maior. Existem outros analyzers, como o BrazilianAnalyzer, que podem te ajudar bastante na tarefa de separar o que deve e como deve ser indexado.

Com o parser na mão podemos finalmente criar a query:

Query LuceneQuery = parser.parse("Deus");

procura por “deus” ou “Deus” e encontra o título “Cidade de Deus” por exemplo. Uma busca por “de” encontra nada, porque definimos “de” como uma stopword. Você pode fazer buscas muito mais interessantes, como “+cidade -Deus”, que busca por todos os dvds que possuem “cidade” mas não possuem “Deus” como título.

Interessante também é a possibilidade de fazer uma busca aproximada, as tão faladas fuzzy searches. Se você buscar por “cidadi~”, a query também devolverá o mesmo dvd como resultado da query!

O código final ficaria:

  public void search(Dvd dvdthrows ParseException {

    Session session = this.factory.getSession();
    
    FullTextSession LuceneSession = Search.createFullTextSession(session);
    
    String[] stopWords = {"de","do","da","dos","das","a","o","na","no","em"};    
    QueryParser parser = new QueryParser("title",new StopAnalyzer(stopWords));

    Query LuceneQuery = parser.parse(dvd.getTitle());
    FullTextQuery fullTextQuery = LuceneSession.createFullTextQuery(LuceneQuery, Dvd.class);    
  
    this.dvds = fullTextQuery.list();
  }

Repare que mesmo a query sendo do Hibernate Search, ela devolve uma lista de objetos managed de DVDs. Qualquer alteração deles acarretará em uma mudança no banco de dados e reindexação dos campos!

Conclusão

Essa foi simente uma introdução do Hibernate Search e Lucene. Hibernate Search facilita ainda mais a configuração e indexação pelo Lucene: possibilita, por exemplo, usar JMS para guardar os índices de maneira clusterizada, além de indexar os documentos assíncronamente. Também é simples fazer ordenações e projeções, combinações com criteria, mapeamento de relacionamento, boost factors, otimização dos índices, e muitos mais. O Paulo Silveira vai dar uma palestra entitulada “Organizando conteúdos com tags e Lucene” no evento Falando em Java da Caelum, que aborda alguns desses temas.

Obs: O codigo (sem jars) está disponível como download. Alterei a busca AJAX de DVDs da aplicação mydvds do vraptor para Hibernate Search.

9 Comments »

  1. Bom post! Vou pesquisar mais detalhes a respeito, pois é bastante interessante…

    Comment by Daniel F. Martins — June 14, 2007 @ 2:05 pm

  2. Link (que eu ainda não li) para apreciaçao de V.Sas:
    http://jroller.com/page/sjivan?entry=hibernate_search_cool_but_is

    Comment by Luca Bastos — June 14, 2007 @ 3:20 pm

  3. Luca, o Tiago Silveira já falou bastate dessa abordagem aqui. Realmente essa abordagem por anotação pode poluir um pouco seu código, e algumas vezes queremos indexar o mesmo item de diversas formas diferentes, o que fica impraticavel com o Hibernate Search. Vantagens e desvantagens…

    Comment by Paulo Silveira — June 14, 2007 @ 9:40 pm

  4. Muito bom o artigo. Você pode ter os benefícios do mapeamento objeto-relacional provido pelo Hibernate e usar o mesmo mecanismo de anotação para anotar as entidades e seus atributos juntamente para serem indexadas pela engine do Lucene. Bom trabalho pessoal. Vou buscar mais detalhes dessa integração.

    Comment by Rafael Naufal — June 15, 2007 @ 12:19 pm

  5. Olá Nico,
    Baixei a aplicação mydvds (com as alterações que você fez na busca ajax), mas estou tento problemas para salvar no banco (estou usando o MySQL) com as anotações do Hibernate Search.
    Abri um tópico no GUJ com a dúvida.
    O que pode ser???

    []’s

    Comment by Gustavo Rodriges — July 24, 2007 @ 2:09 pm

  6. Olá,
    como ficam as definiçoes dos listeners qdo se usa persistence.xml e nao hibernate.cfg.xml?
    Obrigado,

    Comment by JUNIOR — August 17, 2007 @ 1:39 pm

  7. Estou com a mesma dúvida do Júnior. Como faço para configurar no persistence.xml e não no hibernate.cfg.xml?

    Comment by Davi — February 11, 2008 @ 4:41 pm

  8. Excelente artigo!!!
    Parabéns.

    Agora tenho uma dúvida.
    Para avisar ao hibernate que comece a trabalahr e salvar meus índices preciso fazer algo?Ou só o save,update,delete bastam para isso?
    Ele cria arquivos físicos de indice no diretório que settei?
    Porque não consegui ver nenhum arquivo na pasta após uma operação de save.

    Comment by Antonio Lazaro — May 29, 2008 @ 12:16 pm

  9. Outra coisa,

    não consegui configurar essas propriedades na minha sessionfactory.
    Estou utilizando spring, alguma idéia do que possa ser?

    org.hibernate.search.store.FSDirectoryProvider

    C:\desenvolvimento\xml

    Comment by Antonio Lazaro — May 29, 2008 @ 3:30 pm

RSS feed for comments on this post. TrackBack URL

Leave a comment



Caelum | Ensino e Inovação
São Paulo: Rua Vergueiro, 3185, cj. 87, próximo ao Metrô Vila Mariana   |   Tel. (11) 5571-2751
Rio de Janeiro: Rua Senador Dantas, 80, cj. 307/308 - Centro   |   Tel. (21) 2220-4156 ou 2297-0033