Article écrit par Wassim R.
Introduction
L’utilisation de la recherche en full text dans les applications devient de plus en plus fréquente.
Il est essentiel que la capacité de recherche soit la plus performante possible en raison du volume considérable de données des applications.
Hibernate Search, en combinaison avec Spring Boot, offre une solution performante pour ajouter des capacités de recherche en full text, rapides et à grande échelle.
Dans cet article, nous allons explorer comment fonctionne et comment intégrer Hibernate Search avec Spring Boot en suivant un exemple concret.
Fonctionnement général d’elasticsearch
Voici les composants clés de l’architecture d’ElasticSearch :
- Cluster : Un ensemble de nœuds travaillant ensemble. Un cluster contient un ou plusieurs nœuds et est identifié par un nom unique.
- Nœud (Node) : Une instance d’Elasticsearch qui fait partie d’un cluster. Chaque nœud stocke les données et participe aux opérations d’indexation et de recherche.
- Index : Une collection de documents ayant des caractéristiques similaires. Un index est divisé en shards pour permettre le traitement parallèle des données.
- Document : L’unité de base d’informations stockée dans Elasticsearch sous la forme de JSON. Chaque document appartient à un index et a un type défini.
- Shard et Réplica : Chaque index est divisé en plusieurs shards, qui sont des sous-ensembles de l’index. Pour la redondance, chaque shard peut avoir des répliques.
- Réplica : les index et les partitions sont répliqués à travers les nœuds du cluster pour accroître la haute disponibilité des traitements en cas de panne. De plus, les répliques permettent de paralléliser les traitements de recherche de contenu dans le cluster, ce qui accroît la performance d’ElasticSearch.
Elasticsearch et Hibernate search
Hibernate Search permet aux applications Java de bénéficier de ces capacités de recherche avancées tout en restant dans l’écosystème Hibernate. On utilise des annotations sur les entités JPA pour marquer les champs qui doivent être indexés. Lorsqu’une entité est persistée ou mise à jour, les index correspondants sont automatiquement mis à jour. Des analyseurs personnalisés peuvent être configurés selon les besoins de recherche spécifiques. Une API est disponible pour effectuer ces recherches en utilisant une syntaxe basée sur la DSL (Domain Specific Language) de Lucene ou Elasticsearch.
Configuration de base Spring Boot
Pour commencer, nous devons configurer notre projet Spring Boot pour utiliser Hibernate Search. Voici les étapes à suivre :
1. Dépendances Maven
Dans un premier temps, il faut mettre en place les dépendances nécessaires dans notre fichier pom.xml
:
<dependency>
<groupId>org.hibernate.search</groupId>
<artifactId>hibernate-search-bom</artifactId>
<version>7.0.0.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.hibernate.search</groupId>
<artifactId>hibernate-search-mapper-orm</artifactId>
<!-- La version est gérée par le BOM ci-dessus-->
</dependency>
<dependency>
<groupId>org.hibernate.search</groupId>
<artifactId>hibernate-search-backend-lucene</artifactId>
<!-- La version est gérée par le BOM ci-dessus-->
</dependency>
Code 1 : dépendances maven pour hibernate search
- BOM (Bill of Materials): permet de centraliser et de gérer les versions des différentes dépendances Hibernate Search.
- Hibernate Search ORM Mapper : permet d’ajouter des capacités de recherche full text aux entités JPA.
- Hibernate Search Backend Lucene : Cette dépendance ajoute le support pour Lucene en tant que backend de recherche. Hibernate Search peut utiliser plusieurs backends différents pour stocker les index de recherche, et Lucene ou Elasticsearch sont des choix communs en sachant qu’Elasticsearch est basé sur Lucene qui est simplement la couche bas niveau d’ElasticSearch.
En ajoutant ces dépendances, le projet Spring Boot est prêt à utiliser Hibernate Search pour effectuer des recherches sur les entités JPA tout en assurant une gestion cohérente des versions des différentes bibliothèques impliquées.
2. Configuration des Propriétés
Dans un second temps, il faut mettre en place les propriétés nécessaires au bon fonctionnement :
spring.jpa.properties.hibernate.search.backend.type=lucene
spring.jpa.properties.hibernate.search.backend.directory.type=local-filesystem
spring.jpa.properties.hibernate.search.backend.directory.root=${STORAGE_INDEXES_DIRECTORY:/data/indexes}
spring.jpa.properties.hibernate.search.backend.analysis.configurer=fr.mss.sn.core.globalsearch.search.MyLuceneAnalysisConfigurer
spring.main.allow-bean-definition-overriding=true
Code 2 : application.properties
On spécifie que Lucene est utilisé comme backend de recherche. Puis, on définit le type de stockage des index de Lucene comme étant le système de fichiers local en indiquant le chemin racine où les index Lucene seront stockés. On peut également spécifié une classe de configuration personnalisée pour l’analyseur Lucene.
public class MyLuceneAnalysisConfigurer implements LuceneAnalysisConfigurer {
private static final String LOWERCASE = "lowercase";
@Override
public void configure(LuceneAnalysisConfigurationContext context) {
context.analyzer( "myCustomAnalyzer" ).custom()
.tokenizer( "standard" )
.charFilter( "htmlStrip" )
.tokenFilter(LOWERCASE)
.tokenFilter( "snowballPorter" )
.param( "language", "French" )
.tokenFilter( "asciiFolding" );
context.normalizer(LOWERCASE).custom()
.tokenFilter(LOWERCASE)
.tokenFilter( "asciiFolding" );
}
}
Code 3 : Analyseur personnalisé
3. Indexation des entités JPA
Il est maintenant temps d’indexer les entités que nous souhaitons rechercher dans notre application, comme l’exemple de Commission :
@Entity
@Table(name = "commission")
@Indexed
public class CommissionEntity extends AbstractEntity {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_commission")
@SequenceGenerator(name = "seq_commission", sequenceName = "seq_commission", allocationSize = 1)
private Long id;
@ManyToOne
@JoinColumn(name = "reference_id", nullable = false, foreignKey = @ForeignKey(name = "fk_referentiel_commission"))
private ReferenceEntity reference;
@Column(name = "intitule", nullable = false)
@FullTextField(analyzer = "myCustomAnalyzer")
private String intitule;
@Column(name = "intituleCourt", nullable = false)
@FullTextField(analyzer = "myCustomAnalyzer")
private String intituleCourt;
@Column(name = "pcm")
@FullTextField(analyzer = "myCustomAnalyzer")
private String pcm;
}
Code 4 : Entités JPA
Les annotations @FullTextField(analyzer = "myCustomAnalyzer")
et @Indexed
permettent d’indexer le champ pour une recherche en texte intégral avec l’analyseur personnalisé mentionné ci-dessus, rendant ainsi l’entité CommissionEntity utilisable dans la recherche.
4. Indexation des Données
Une fois les annotations mises en place, il faut lancer l’indexation au démarrage de l’application :
public class IndexingService {
private final EntityManager entityManager;
@Transactional
public void initiateIndexing() throws InterruptedException {
SearchSession searchSession = Search.session(entityManager);
MassIndexer indexer = searchSession.massIndexer(CommissionEntity.class);
indexer.startAndWait();
}
}
Code 5 : indéxation des entités
5. Exemple
On veut maintenant pouvoir, par exemple, rechercher dans un formulaire une commission particulière, soit par son intitulé, son numéro de convention collective ou son président de commission :
@Override
public List<CommissionDTO> globalSearchByCommission(String searchTerm) {
SearchSession searchSession = Search.session(entityManager);
String searchTermWithoutSpace = searchTerm.replaceAll("\\W+", "").toLowerCase();
SearchResult<CommissionEntity> result = searchSession.search(CommissionEntity.class)
.where(f -> f.bool()
.should(f.wildcard()
.fields("intitule")
.matching("*" + searchTermWithoutSpace + "*")
)
.should(f.wildcard()
.fields("commissionConventionCollectives.conventioncollective.idcc")
.matching("*" + searchTerm + "*")
)
.should(f.wildcard()
.fields("pcm")
.matching("*" + searchTermWithoutSpace + "*")
))
.fetch(1000);
return result.hits().stream()
.map(entity -> commissionMapper.convertToDTO(entity))
.toList();
}
Code 6 : Exemple de code
On utilise ici, un modèle de recherche flexible où chaque champ de la commission est exploré à l’aide de filtres booléens et de recherches par motif. Cette approche est particulièrement efficace lorsqu’on ne dispose que d’informations partielles ou incomplètes concernant la commission.
Avantages de l’Intégration
- Performance : Elasticsearch est conçu pour effectuer des recherches full text très rapidement, même sur de grandes quantités de données. En utilisant Elasticsearch, Hibernate Search peut offrir des performances de recherche optimisées.
- Scalabilité: Elasticsearch est un système distribué, ce qui signifie qu’il peut être étendu horizontalement pour gérer des quantités croissantes de données et de charges de travail de recherche.
- Richesse Fonctionnelle : Elasticsearch offre une large gamme de fonctionnalités, y compris des agrégations pour l’analyse de données, des recherches géospatiales, et des suggestions de texte.
Meilleures Pratiques
- Indexation Incrémentale : Configurez une indexation incrémentale pour garantir que vos index sont toujours à jour sans affecter les performances de l’application.
- Gestion des Shards et Réplicas : Configurez correctement les shards et les réplicas dans Elasticsearch pour optimiser la performance et la résilience.
- Analyseurs Personnalisés : Utilisez des analyseurs personnalisés pour améliorer la précision des recherches full text en fonction de la langue et du contexte spécifique de vos données.
Conclusion
L’intégration de Hibernate Search avec Spring Boot est une méthode puissante pour ajouter des capacités de recherche full text à vos applications. En suivant les étapes et les meilleures pratiques décrites dans cet article, vous pouvez améliorer l’efficacité et la réactivité de vos recherches, offrant ainsi une meilleure expérience utilisateur.