Introdução à pesquisa do Hibernate

1. Visão geral

Neste artigo, discutiremos os fundamentos do Hibernate Search, como configurá-lo, e implementaremos algumas consultas simples.

Sempre que temos que implementar a funcionalidade de pesquisa de texto completo, usar ferramentas com as quais já estamos familiarizados é sempre uma vantagem.

Caso já estejamos usando Hibernate e JPA para ORM, estamos a apenas um passo de Hibernate Search.

Hibernate Search integrates Apache Lucene, a high-performance and extensible full-text search-engine library written in Java. Isso combina o poder do Lucene com a simplicidade do Hibernate e JPA.

Simplificando, só temos que adicionar algumas anotações adicionais às nossas classes de domínio, ethe tool will take care of the things like database/index synchronization.

O Hibernate Search também fornece uma integração com o Elasticsearch; no entanto, como ainda está em um estágio experimental, vamos nos concentrar no Lucene aqui.

3. Configurações

3.1. Dependências do Maven

Antes de começar, primeiro precisamos adicionar odependencies necessário ao nossopom.xml:


    org.hibernate
    hibernate-search-orm
    5.8.2.Final

Por uma questão de simplicidade, usaremosH2 como nosso banco de dados:


    com.h2database
    h2
    1.4.196

3.2. Configurações

Também temos que especificar onde Lucene deve armazenar o índice.

Isso pode ser feito por meio da propriedadehibernate.search.default.directory_provider.

Escolheremosfilesystem, que é a opção mais direta para nosso caso de uso. Mais opções estão listadas emofficial documentation. Filesystem-master/filesystem-slave and infinispan are noteworthy for clustered applications, where the index has to be synchronized between nodes.

Também temos que definir um diretório base padrão onde os índices serão armazenados:

hibernate.search.default.directory_provider = filesystem
hibernate.search.default.indexBase = /data/index/default

4. As classes modelo

Após a configuração, agora estamos prontos para especificar nosso modelo.

On top of the JPA annotations @Entity and @Table, we have to add an @Indexed annotation. Diz ao Hibernate Search que a entidadeProduct deve ser indexada.

After that, we have to define the required attributes as searchable by adding a @Field annotation:

@Entity
@Indexed
@Table(name = "product")
public class Product {

    @Id
    private int id;

    @Field(termVector = TermVector.YES)
    private String productName;

    @Field(termVector = TermVector.YES)
    private String description;

    @Field
    private int memory;

    // getters, setters, and constructors
}

O atributotermVector = TermVector.YES será necessário para a consulta “Mais como isto” posteriormente.

5. Construindo o Índice Lucene

Antes de iniciar as consultas reais,we have to trigger Lucene to build the index initially:

FullTextEntityManager fullTextEntityManager
  = Search.getFullTextEntityManager(entityManager);
fullTextEntityManager.createIndexer().startAndWait();

After this initial build, Hibernate Search will take care of keeping the index up to date. I. e. podemos criar, manipular e excluir entidades por meio deEntityManager como de costume.

Nota:we have to make sure that entities are fully committed to the database before they can be discovered and indexed by Lucene (a propósito, esta também é a razão pela qual a importação de dados de teste inicial em nossoexample code test cases vem em um caso de teste JUnit dedicado, anotado com@Commit).

6. Criação e execução de consultas

Agora, estamos prontos para criar nossa primeira consulta.

Na seção seguinte,we’ll show the general workflow for preparing and executing a query.

Depois disso, criaremos algumas consultas de exemplo para os tipos de consulta mais importantes.

6.1. Fluxo de trabalho geral para criar e executar uma consulta

Preparing and executing a query in general consists of four steps:

Na etapa 1, temos que obter um JPAFullTextEntityManagere disso umQueryBuilder:

FullTextEntityManager fullTextEntityManager
  = Search.getFullTextEntityManager(entityManager);

QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory()
  .buildQueryBuilder()
  .forEntity(Product.class)
  .get();

Na etapa 2, criaremos uma consulta Lucene por meio da consulta DSL do Hibernate:

org.apache.lucene.search.Query query = queryBuilder
  .keyword()
  .onField("productName")
  .matching("iphone")
  .createQuery();

Na etapa 3, envolveremos a consulta Lucene em uma consulta Hibernate:

org.hibernate.search.jpa.FullTextQuery jpaQuery
  = fullTextEntityManager.createFullTextQuery(query, Product.class);

Finalmente, na etapa 4, executaremos a consulta:

List results = jpaQuery.getResultList();

Note: por padrão, Lucene classifica os resultados por relevância.

As etapas 1, 3 e 4 são iguais para todos os tipos de consulta.

A seguir, focaremos na etapa 2, i. e. como criar diferentes tipos de consultas.

6.2. Consultas de palavras-chave

O caso de uso mais básico ésearching for a specific word.

Isso é o que já fizemos na seção anterior:

Query keywordQuery = queryBuilder
  .keyword()
  .onField("productName")
  .matching("iphone")
  .createQuery();

Aqui,keyword() especifica que estamos procurando por uma palavra específica,onField() diz à Lucene onde procurar ematching() o que procurar.

6.3. Fuzzy Queries

As consultas difusas estão funcionando como consultas de palavras-chave, exceto quewe can define a limit of “fuzziness”, acima das quais Lucene deve aceitar os dois termos como correspondentes.

PorwithEditDistanceUpTo(),we can define how much a term may deviate from the other. Pode ser definido como 0, 1 e 2, em que o valor padrão é 2 (note: esta limitação é proveniente da implementação do Lucene).

PorwithPrefixLength(), podemos definir o comprimento do prefixo que deve ser ignorado pela imprecisão:

Query fuzzyQuery = queryBuilder
  .keyword()
  .fuzzy()
  .withEditDistanceUpTo(2)
  .withPrefixLength(0)
  .onField("productName")
  .matching("iPhaen")
  .createQuery();

6.4. Consultas curinga

A Pesquisa do Hibernate também nos permite executar consultas curinga, i. e. consultas para as quais uma parte de uma palavra é desconhecida.

Para isso, podemos usar “?” para um único caractere e“*” para qualquer sequência de caracteres:

Query wildcardQuery = queryBuilder
  .keyword()
  .wildcard()
  .onField("productName")
  .matching("Z*")
  .createQuery();

6.5. Frase de consultas

Se quisermos procurar mais de uma palavra, podemos usar consultas de frase. Podemos olharfor exact or for approximate sentences, usandophrase()ewithSlop(), se necessário. O fator slop define o número de outras palavras permitidas na frase:

Query phraseQuery = queryBuilder
  .phrase()
  .withSlop(1)
  .onField("description")
  .sentence("with wireless charging")
  .createQuery();

6.6. Consultas de string de consulta simples

Com os tipos de consulta anteriores, tivemos que especificar o tipo de consulta explicitamente.

Se quisermos dar mais poder ao usuário, podemos usar consultas de string de consulta simples:by that, he can define his own queries at runtime.

Os seguintes tipos de consulta são suportados:

  • booleano (E usando "+", OU usando "|", NÃO usando "-")

  • prefixo (prefixo *)

  • frase ("alguma frase")

  • precedência (entre parênteses)

  • difuso (difuso ~ 2)

  • operador near para consultas de frase ("alguma frase" ~ 3)

O exemplo a seguir combinaria consultas difusas, de frase e booleanas:

Query simpleQueryStringQuery = queryBuilder
  .simpleQueryString()
  .onFields("productName", "description")
  .matching("Aple~2 + \"iPhone X\" + (256 | 128)")
  .createQuery();

6.7. Consultas de intervalo

Range queries search for avalue in between given boundaries. Isso pode ser aplicado a números, datas, timestamps e strings:

Query rangeQuery = queryBuilder
  .range()
  .onField("memory")
  .from(64).to(256)
  .createQuery();

6.8. Mais consultas semelhantes a esta

Nosso último tipo de consulta é “More Like This” - consulta. Para isso, fornecemos uma entidade eHibernate Search returns a list with similar entities, cada uma com uma pontuação de similaridade.

Conforme mencionado antes, o atributotermVector = TermVector.YES em nossa classe de modelo é necessário para este caso: ele diz à Lucene para armazenar a frequência de cada termo durante a indexação.

Com base nisso, a similaridade será calculada no tempo de execução da consulta:

Query moreLikeThisQuery = queryBuilder
  .moreLikeThis()
  .comparingField("productName").boostedTo(10f)
  .andField("description").boostedTo(1f)
  .toEntity(entity)
  .createQuery();
List results = (List) fullTextEntityManager
  .createFullTextQuery(moreLikeThisQuery, Product.class)
  .setProjection(ProjectionConstants.THIS, ProjectionConstants.SCORE)
  .getResultList();

6.9. Pesquisando mais de um campo

Até agora, criamos apenas consultas para pesquisar um atributo, usandoonField().

Dependendo do caso de uso,we can also search two or more attributes:

Query luceneQuery = queryBuilder
  .keyword()
  .onFields("productName", "description")
  .matching(text)
  .createQuery();

Além disso,we can specify each attribute to be searched separately, e. g. se quisermos definir um impulso para um atributo:

Query moreLikeThisQuery = queryBuilder
  .moreLikeThis()
  .comparingField("productName").boostedTo(10f)
  .andField("description").boostedTo(1f)
  .toEntity(entity)
  .createQuery();

6.10. Combinando consultas

Por fim, o Hibernate Search também suporta a combinação de consultas usando várias estratégias:

  • SHOULD: a consulta deve conter os elementos correspondentes da subconsulta

  • MUST: a consulta deve conter os elementos correspondentes da subconsulta

  • MUST NOT: a consulta não deve conter os elementos correspondentes da subconsulta

As agregações sãosimilar to the boolean ones AND, OR and NOT. No entanto, os nomes são diferentes para enfatizar que também têm impacto na relevância.

Por exemplo, umSHOULD entre duas consultas é semelhante ao booleanoOR: se uma das duas consultas tiver uma correspondência, essa correspondência será retornada.

No entanto, se ambas as consultas corresponderem, a correspondência terá uma relevância mais alta em comparação com apenas uma consulta:

Query combinedQuery = queryBuilder
  .bool()
  .must(queryBuilder.keyword()
    .onField("productName").matching("apple")
    .createQuery())
  .must(queryBuilder.range()
    .onField("memory").from(64).to(256)
    .createQuery())
  .should(queryBuilder.phrase()
    .onField("description").sentence("face id")
    .createQuery())
  .must(queryBuilder.keyword()
    .onField("productName").matching("samsung")
    .createQuery())
  .not()
  .createQuery();

7. Conclusão

Neste artigo, discutimos os conceitos básicos da pesquisa do Hibernate e mostramos como implementar os tipos de consulta mais importantes. Tópicos mais avançados podem ser encontrados emofficial documentation.

Como sempre, o código-fonte completo dos exemplos está disponívelover on GitHub.