Introducción a la búsqueda de Hibernate

1. Información general

En este artículo, discutiremos los conceptos básicos de Hibernate Search, cómo configurarlo e implementaremos algunas consultas simples.

2. Conceptos básicos de la búsqueda de Hibernate

Siempre que tengamos que implementar la función de búsqueda de texto completo, utilizar herramientas con las que ya estamos familiarizados es siempre una ventaja.

En caso de que ya estemos usando Hibernate y JPA para ORM, estamos a solo un paso de Hibernate Search.

Hibernate Search integra Apache Lucene, una biblioteca de motor de búsqueda de texto completo extensible y de alto rendimiento escrita en Java . Esto combina el poder de Lucene con la simplicidad de Hibernate y JPA.

En pocas palabras, solo tenemos que agregar algunas anotaciones adicionales a nuestras clases de dominio, y la herramienta se encargará de cosas como la sincronización de la base de datos / índice.

Hibernate Search también proporciona una integración Elasticsearch; sin embargo, como todavía se encuentra en una etapa experimental, aquí nos centraremos en Lucene.

3. Configuraciones

3.1. Dependencias de Maven

Antes de comenzar, primero debemos agregar las dependencias necesarias a nuestro pom.xml :

 org.hibernate hibernate-search-orm 5.8.2.Final 

En aras de la simplicidad, usaremos H2 como nuestra base de datos:

 com.h2database h2 1.4.196 

3.2. Configuraciones

También tenemos que especificar dónde debe almacenar Lucene el índice.

Esto se puede hacer a través de la propiedad hibernate.search.default.directory_provider .

Elegiremos el sistema de archivos , que es la opción más sencilla para nuestro caso de uso. Más opciones se enumeran en la documentación oficial. Filesystem-master / filesystem-slave e infinispan son dignos de mención para aplicaciones agrupadas, donde el índice tiene que sincronizarse entre nodos.

También tenemos que definir un directorio base predeterminado donde se almacenarán los índices:

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

4. Las clases modelo

Después de la configuración, ahora estamos listos para especificar nuestro modelo.

Además de las anotaciones de JPA @Entity y @Table , tenemos que agregar una anotación @Indexed . Le dice a Hibernate Search que la entidad Producto se indexará.

Después de eso, tenemos que definir los atributos requeridos como buscables agregando una anotación @Field :

@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 }

El atributo termVector = TermVector.YES será necesario para la consulta "Más como este" más adelante.

5. Creación del índice de Lucene

Antes de comenzar las consultas reales, tenemos que activar Lucene para construir el índice inicialmente :

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

Después de esta compilación inicial, Hibernate Search se encargará de mantener el índice actualizado . I. e. podemos crear, manipular y eliminar entidades a través del EntityManager como de costumbre.

Nota: tenemos que asegurarnos de que las entidades estén completamente comprometidas con la base de datos antes de que puedan ser descubiertas e indexadas por Lucene (por cierto, esta también es la razón por la que la importación de datos de prueba inicial en nuestros casos de prueba de código de ejemplo viene en un JUnit dedicado caso de prueba, anotado con @Commit ).

6. Generación y ejecución de consultas

Ahora, estamos listos para crear nuestra primera consulta.

En la siguiente sección, mostraremos el flujo de trabajo general para preparar y ejecutar una consulta.

Después de eso, crearemos algunas consultas de ejemplo para los tipos de consultas más importantes.

6.1. Flujo de trabajo general para crear y ejecutar una consulta

La preparación y ejecución de una consulta en general consta de cuatro pasos :

In step 1, we have to get a JPA FullTextEntityManager and from that a QueryBuilder:

FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory() .buildQueryBuilder() .forEntity(Product.class) .get();

In step 2, we will create a Lucene query via the Hibernate query DSL:

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

In step 3, we'll wrap the Lucene query into a Hibernate query:

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

Finally, in step 4 we'll execute the query:

List results = jpaQuery.getResultList();

Note: by default, Lucene sorts the results by relevance.

Steps 1, 3 and 4 are the same for all query types.

In the following, we will focus on step 2, i. e. how to create different types of queries.

6.2. Keyword Queries

The most basic use-case is searching for a specific word.

This is what we actually did already in the previous section:

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

Here, keyword() specifies that we are looking for one specific word, onField() tells Lucene where to look and matching() what to look for.

6.3. Fuzzy Queries

Fuzzy queries are working like keyword queries, except that we can define a limit of “fuzziness”, above which Lucene shall accept the two terms as matching.

By withEditDistanceUpTo(), we can define how much a term may deviate from the other. It can be set to 0, 1, and 2, whereby the default value is 2 (note: this limitation is coming from the Lucene's implementation).

By withPrefixLength(), we can define the length of the prefix which shall be ignored by the fuzziness:

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

6.4. Wildcard Queries

Hibernate Search also enables us to execute wildcard queries, i. e. queries for which a part of a word is unknown.

For this, we can use “?” for a single character, and “*” for any character sequence:

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

6.5. Phrase Queries

If we want to search for more than one word, we can use phrase queries. We can either look for exact or for approximate sentences, using phrase() and withSlop(), if necessary. The slop factor defines the number of other words permitted in the sentence:

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

6.6. Simple Query String Queries

With the previous query types, we had to specify the query type explicitly.

If we want to give some more power to the user, we can use simple query string queries: by that, he can define his own queries at runtime.

The following query types are supported:

  • boolean (AND using “+”, OR using “|”, NOT using “-“)
  • prefix (prefix*)
  • phrase (“some phrase”)
  • precedence (using parentheses)
  • fuzzy (fuzy~2)
  • near operator for phrase queries (“some phrase”~3)

The following example would combine fuzzy, phrase and boolean queries:

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

6.7. Range Queries

Range queries search for avalue in between given boundaries. This can be applied to numbers, dates, timestamps, and strings:

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

6.8. More Like This Queries

Our last query type is the “More Like This” – query. For this, we provide an entity, and Hibernate Search returns a list with similar entities, each with a similarity score.

As mentioned before, the termVector = TermVector.YES attribute in our model class is required for this case: it tells Lucene to store the frequency for each term during indexing.

Based on this, the similarity will be calculated at query execution time:

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. Searching More Than One Field

Until now, we only created queries for searching one attribute, using onField().

Depending on the use case, we can also search two or more attributes:

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

Moreover, we can specify each attribute to be searched separately, e. g. if we want to define a boost for one attribute:

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

6.10. Combining Queries

Finally, Hibernate Search also supports combining queries using various strategies:

  • SHOULD: the query should contain the matching elements of the subquery
  • MUST: the query must contain the matching elements of the subquery
  • MUST NOT: the query must not contain the matching elements of the subquery

The aggregations are similar to the boolean ones AND, OR and NOT. However, the names are different to emphasize that they also have an impact on the relevance.

Por ejemplo, un DEBERÍA entre dos consultas es similar a un booleano O: si una de las dos consultas tiene una coincidencia, se devolverá esta coincidencia.

Sin embargo, si ambas consultas coinciden, la coincidencia tendrá una mayor relevancia en comparación con si solo una consulta coincide:

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. Conclusión

En este artículo, discutimos los conceptos básicos de Hibernate Search y mostramos cómo implementar los tipos de consultas más importantes. Se pueden encontrar temas más avanzados en la documentación oficial.

Como siempre, el código fuente completo de los ejemplos está disponible en GitHub.