Introduzione a Hibernate Search

1. Panoramica

In questo articolo, discuteremo le basi di Hibernate Search, come configurarlo e implementeremo alcune semplici query.

2. Nozioni di base su Hibernate Search

Ogni volta che dobbiamo implementare la funzionalità di ricerca full-text, l'utilizzo di strumenti con cui siamo già esperti è sempre un vantaggio.

Nel caso in cui stiamo già utilizzando Hibernate e JPA per ORM, siamo solo a un passo da Hibernate Search.

Hibernate Search integra Apache Lucene, una libreria del motore di ricerca full-text estensibile e ad alte prestazioni scritta in Java . Questo combina la potenza di Lucene con la semplicità di Hibernate e JPA.

In poche parole, dobbiamo solo aggiungere alcune annotazioni aggiuntive alle nostre classi di dominio e lo strumento si occuperà di cose come la sincronizzazione del database / indice.

Hibernate Search fornisce anche un'integrazione con Elasticsearch; tuttavia, poiché è ancora in una fase sperimentale, qui ci concentreremo su Lucene.

3. Configurazioni

3.1. Dipendenze di Maven

Prima di iniziare, dobbiamo prima aggiungere le dipendenze necessarie al nostro pom.xml :

 org.hibernate hibernate-search-orm 5.8.2.Final 

Per semplicità, useremo H2 come nostro database:

 com.h2database h2 1.4.196 

3.2. Configurazioni

Dobbiamo anche specificare dove Lucene dovrebbe memorizzare l'indice.

Questo può essere fatto tramite la proprietà hibernate.search.default.directory_provider .

Sceglieremo il filesystem , che è l'opzione più semplice per il nostro caso d'uso. Altre opzioni sono elencate nella documentazione ufficiale. Filesystem-master / filesystem-slave e infinispan sono degni di nota per le applicazioni cluster, dove l'indice deve essere sincronizzato tra i nodi.

Dobbiamo anche definire una directory di base predefinita in cui verranno archiviati gli indici:

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

4. Le classi del modello

Dopo la configurazione, siamo ora pronti per specificare il nostro modello.

Oltre alle annotazioni JPA @Entity e @Table , dobbiamo aggiungere un'annotazione @Indexed . Indica a Hibernate Search che l'entità Product deve essere indicizzata.

Dopodiché, dobbiamo definire gli attributi richiesti come ricercabili aggiungendo un'annotazione @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 }

L' attributo termVector = TermVector.YES sarà richiesto in seguito per la query "More Like This".

5. Costruire il Lucene Index

Prima di iniziare le query effettive, dobbiamo attivare Lucene per creare inizialmente l'indice :

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

Dopo questa build iniziale, Hibernate Search si occuperà di mantenere aggiornato l'indice . I. e. possiamo creare, manipolare ed eliminare entità tramite EntityManager come al solito.

Nota: dobbiamo assicurarci che le entità siano completamente impegnate nel database prima che possano essere scoperte e indicizzate da Lucene (a proposito, questo è anche il motivo per cui l'importazione dei dati di test iniziale nei nostri casi di test del codice di esempio arriva in una JUnit dedicata caso di test, annotato con @Commit ).

6. Creazione ed esecuzione di query

Ora siamo pronti per creare la nostra prima query.

Nella sezione seguente, mostreremo il flusso di lavoro generale per la preparazione e l'esecuzione di una query.

Successivamente, creeremo alcune query di esempio per i tipi di query più importanti.

6.1. Flusso di lavoro generale per la creazione e l'esecuzione di una query

La preparazione e l'esecuzione di una query in generale consiste in quattro passaggi :

Nel passaggio 1, dobbiamo ottenere un JPA FullTextEntityManager e da questo un QueryBuilder :

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

Nel passaggio 2, creeremo una query Lucene tramite la query di ibernazione DSL:

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

Nel passaggio 3, avvolgeremo la query Lucene in una query Hibernate:

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

Infine, nel passaggio 4 eseguiremo la query:

List results = jpaQuery.getResultList();

Nota : per impostazione predefinita, Lucene ordina i risultati in base alla pertinenza.

I passaggi 1, 3 e 4 sono gli stessi per tutti i tipi di query.

Di seguito, ci concentreremo sul passaggio 2, ovvero come creare diversi tipi di query.

6.2. Query di parole chiave

Il caso d'uso più elementare è la ricerca di una parola specifica .

Questo è quello che abbiamo già fatto nella sezione precedente:

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.

Ad esempio, un DOVREBBE tra due query è simile a booleano OR: se una delle due query ha una corrispondenza, verrà restituita questa corrispondenza.

Tuttavia, se entrambe le query corrispondono, la corrispondenza avrà una rilevanza maggiore rispetto a se una sola query corrisponde:

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. Conclusione

In questo articolo, abbiamo discusso le basi di Hibernate Search e mostrato come implementare i tipi di query più importanti. Argomenti più avanzati possono essere trovati nella documentazione ufficiale.

Come sempre, il codice sorgente completo degli esempi è disponibile su GitHub.