Introduction à la recherche Hibernate

1. Vue d'ensemble

Dans cet article, nous aborderons les bases d'Hibernate Search, comment le configurer, et nous mettrons en œuvre quelques requêtes simples.

Chaque fois que nous devons mettre en œuvre une fonctionnalité de recherche en texte intégral, l'utilisation d'outils que nous connaissons déjà bien est toujours un plus.

Si nous utilisons déjà Hibernate et JPA pour ORM, nous ne sommes qu'à une étape d'Hibernate Search.

Hibernate Search integrates Apache Lucene, a high-performance and extensible full-text search-engine library written in Java. Cela allie la puissance de Lucene à la simplicité d'Hibernate et de JPA.

En termes simples, nous devons juste ajouter des annotations supplémentaires à nos classes de domaine, etthe tool will take care of the things like database/index synchronization.

Hibernate Search fournit également une intégration Elasticsearch; cependant, comme il est encore à un stade expérimental, nous allons nous concentrer sur Lucene ici.

3. Les configurations

3.1. Dépendances Maven

Avant de commencer, nous devons d'abord ajouter lesdependencies nécessaires à nospom.xml:


    org.hibernate
    hibernate-search-orm
    5.8.2.Final

Par souci de simplicité, nous utiliseronsH2 comme base de données:


    com.h2database
    h2
    1.4.196

3.2. Les configurations

Nous devons également spécifier où Lucene doit stocker l'index.

Cela peut être fait via la propriétéhibernate.search.default.directory_provider.

Nous choisironsfilesystem, qui est l'option la plus simple pour notre cas d'utilisation. Plus d'options sont répertoriées dans lesofficial documentation. Filesystem-master/filesystem-slave and infinispan are noteworthy for clustered applications, where the index has to be synchronized between nodes.

Nous devons également définir un répertoire de base par défaut où les index seront stockés:

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

4. Les classes modèles

Après la configuration, nous sommes maintenant prêts à spécifier notre modèle.

On top of the JPA annotations @Entity and @Table, we have to add an @Indexed annotation. Il indique à Hibernate Search que l'entitéProduct doit être indexée.

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
}

L'attributtermVector = TermVector.YES sera requis pour la requête «Plus comme ça» plus tard.

5. Création de l'indice Lucene

Avant de démarrer les requêtes réelles,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. nous pouvons créer, manipuler et supprimer des entités via lesEntityManager comme d'habitude.

Remarque:we have to make sure that entities are fully committed to the database before they can be discovered and indexed by Lucene (d'ailleurs, c'est aussi la raison pour laquelle l'importation initiale des données de test dans notreexample code test cases vient dans un cas de test JUnit dédié, annoté avec@Commit).

6. Création et exécution de requêtes

Nous sommes maintenant prêts à créer notre première requête.

Dans la section suivante,we’ll show the general workflow for preparing and executing a query.

Ensuite, nous créerons des exemples de requêtes pour les types de requêtes les plus importants.

6.1. Flux de travail général pour la création et l'exécution d'une requête

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

À l'étape 1, nous devons obtenir un JPAFullTextEntityManager et à partir de cela unQueryBuilder:

FullTextEntityManager fullTextEntityManager
  = Search.getFullTextEntityManager(entityManager);

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

À l'étape 2, nous allons créer une requête Lucene via le DSL Hibernate:

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

À l'étape 3, nous allons intégrer la requête Lucene dans une requête Hibernate:

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

Enfin, à l'étape 4, nous exécuterons la requête:

List results = jpaQuery.getResultList();

Note: par défaut, Lucene trie les résultats par pertinence.

Les étapes 1, 3 et 4 sont identiques pour tous les types de requêtes.

Dans ce qui suit, nous nous concentrerons sur l'étape 2, i. e. comment créer différents types de requêtes.

6.2. Requêtes de mots clés

Le cas d'utilisation le plus basique estsearching for a specific word.

Voici ce que nous avons déjà fait dans la section précédente:

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

Ici,keyword() spécifie que nous recherchons un mot spécifique,onField() indique à Lucene où chercher etmatching() quoi chercher.

6.3. Requêtes floues

Les requêtes floues fonctionnent comme des requêtes par mot-clé, sauf quewe can define a limit of “fuzziness”, au-dessus duquel Lucene acceptera les deux termes comme correspondant.

ParwithEditDistanceUpTo(),we can define how much a term may deviate from the other. Il peut être défini sur 0, 1 et 2, la valeur par défaut étant 2 (note: cette limitation provient de l’implémentation de Lucene).

ParwithPrefixLength(), on peut définir la longueur du préfixe qui sera ignorée par le flou:

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

6.4. Requêtes génériques

Hibernate Search nous permet également d’exécuter des requêtes génériques, i. e. requêtes pour lesquelles une partie d'un mot est inconnue.

Pour cela, nous pouvons utiliser «?” pour un seul caractère, et«*” pour toute séquence de caractères:

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

6.5. Requêtes d'expressions

Si nous voulons rechercher plus d'un mot, nous pouvons utiliser des requêtes d'expression. Nous pouvons soit regarderfor exact or for approximate sentences, en utilisantphrase() etwithSlop(), si nécessaire. Le facteur slop définit le nombre d'autres mots autorisés dans la phrase:

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

6.6. Requêtes de chaîne de requête simple

Avec les types de requête précédents, nous devions spécifier explicitement le type de requête.

Si nous voulons donner plus de puissance à l'utilisateur, nous pouvons utiliser des requêtes de chaîne de requête simples:by that, he can define his own queries at runtime.

Les types de requête suivants sont pris en charge:

  • booléen (ET en utilisant “+”, OU en utilisant “|”, PAS en utilisant “-“)

  • préfixe (préfixe *)

  • phrase (“une phrase”)

  • priorité (entre parenthèses)

  • flou (flou ~ 2)

  • near opérateur pour les requêtes de phrase (“une phrase” ~ 3)

L'exemple suivant combine des requêtes fuzzy, phrase et booléennes:

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

6.7. Requêtes de plage

Range queries search for avalue in between given boundaries. Ceci peut être appliqué aux nombres, aux dates, aux horodatages et aux chaînes:

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

6.8. Plus de requêtes similaires

Notre dernier type de requête est la requête «More Like This». Pour cela, nous fournissons une entité, etHibernate Search returns a list with similar entities, chacun avec un score de similarité.

Comme mentionné précédemment, l'attributtermVector = TermVector.YES dans notre classe de modèle est requis dans ce cas: il indique à Lucene de stocker la fréquence pour chaque terme lors de l'indexation.

Sur cette base, la similarité sera calculée au moment de l'exécution de la requête:

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. Recherche de plus d'un champ

Jusqu'à présent, nous ne créions des requêtes que pour rechercher un attribut, en utilisantonField().

Selon le cas d'utilisation,we can also search two or more attributes:

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

De plus,we can specify each attribute to be searched separately, e. g. si nous voulons définir un boost pour un attribut:

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

6.10. Combinaison de requêtes

Enfin, Hibernate Search prend également en charge la combinaison de requêtes en utilisant différentes stratégies:

  • SHOULD: la requête doit contenir les éléments correspondants de la sous-requête

  • MUST: la requête doit contenir les éléments correspondants de la sous-requête

  • MUST NOT: la requête ne doit pas contenir les éléments correspondants de la sous-requête

Les agrégations sontsimilar to the boolean ones AND, OR and NOT. Cependant, les noms sont différents pour souligner qu'ils ont également un impact sur la pertinence.

Par exemple, unSHOULD entre deux requêtes est similaire à la valeur booléenneOR: si l'une des deux requêtes a une correspondance, cette correspondance sera renvoyée.

Cependant, si les deux requêtes correspondent, la correspondance aura une pertinence plus élevée que si une seule requête correspond:

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

Dans cet article, nous avons présenté les bases de la recherche Hibernate et montré comment implémenter les types de requête les plus importants. Des sujets plus avancés peuvent être trouvés dans lesofficial documentation.

Comme toujours, le code source complet des exemples est disponibleover on GitHub.