Technologies

Search in the SI : Hibernate Search

Publié le : Auteur: Manuel EVENO Laisser un commentaire
Data overload

Hibernate Search est une surcouche qui facilite l’intégration du moteur d’indexation et de recherche Lucene. Il permet notamment la recherche fulltext dans des données structurées en SGBD (ou dans une base de données NoSQL). Son principe est simple, il propose de s’appuyer sur les entités JPA (déjà mappés sur la base de données) en définissant un mapping Object<->Document (au sens document Lucene). Ensuite Hibernate Search se charge de tout : Maintien des indexes à jour, récupération des entités JPA à partir d’une requête dans le moteur de recherche, … On s’affranchit ainsi de l’écriture de beaucoup de code (voir un tutoriel pour s’en rendre compte). Voyons dans le détail comment mettre en place Hibernate Search dans nos applications.

Fonctionnalités

Permet notamment les types de recherche suivant : Recherche exacte Recherche phonétique Recherche par approximation (n-gram ou Levenshtein) Recherche par synonyme, Recherche par mot de la même famille Recherche par intervalle (exemple avec des dates) Permet de donner une priorité à certains champs La création de l’index est personnalisable Découpage des mots (Radicalisation, mots entiers, etc) Exclusion de certains mots (articles, etc) Formatage (majuscules, accents, etc)

Configuration de Hibernate Search

Pour activer Hibernate Search s’active automatiquement quand il est détecté dans le classpath par Hibernate Core ou un provider JPA. Il est néanmoins psosible de l’empêcher en positionnant la propriété hibernate.search.autoregister_listeners à false dans votre configuration. Pour mettre en place Hibernate Search, il suffit d’ajouter un peu de configuration dans votre fichier persistence.xml (ou hibernate.cfg.xml) :

  • Le mode de stockage des indexes. 
  • L’endroit où seront stocker les indexes (si stockage fichier)

Les modes de stockages peuvent être choisi parmi :

  • directory : stockage dans une arborscence de répertoire (mode par défaut)
  • near-real-time : basé aussi sur des fichiers aussi mais active le mode Lucene NRT (Near Real Time)

Ce mode de stockage peut être configuré au global pour tous les indexes Lucene ou par index. Dans notre exemple, nous utiliserons une base de données mémoire (HSQL) et le mode de stockage « directory« . Voici le fichier persistence.xml avec la configuration associée : Exemple de fichier

<?xml version="1.0" encoding="UTF-8"?>
<!-- Persistence deployment descriptor for dev profile -->
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
             version="1.0">

    <persistence-unit name="hibernate-search-example">
        <properties>

            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
            <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/>
            <property name="hibernate.connection.username" value="sa"/>
            <property name="hibernate.connection.password" value=""/>
            <property name="hibernate.connection.url" value="jdbc:hsqldb:."/>

            <property name="hibernate.hbm2ddl.auto" value="create-drop"/>

            <property name="hibernate.search.default.directory_provider" value="filesystem"/>
            <property name="hibernate.search.default.indexBase" value="target/indexes"/>
        </properties>
    </persistence-unit>
</persistence>

Si vous désirez définir un mode de stockage différent par index, les deux propriétés Hibernate Search en remplacant default par le nom de l’index concerné. A partir de maintenance, Hibernate Search est configuré et il va gérer toute la tuyauterie avec Lucene. Il ne nous reste plus qu’à définir les entités à indexer !

Définition des entités à indexer

Il faut maintenant définir le mapping Objet<->Document. Ce mapping va permettre à l’EntityManager JPA de gérer l’indexation des documents qu’ils voient passé dans sa session. On s’affranchit ainsi de l’écriture de beaucoup de code servant à l’indexation des documents (voir un tutoriel pour s’en rendre compte). Prenons une classe JPA de base :

@Entity
public class Author {
    @Id @GeneratedValue
    private Integer id;

    private String name;

    @OneToMany
    private Set<Book> books;

    private Adress address;
}

A partir de cette entité, nous pouvons configurer les éléments à indexer  en ajoutant directement des annotations spécifiques Hibernate Search à l’entité. Il faut notamment définir les annotations suivantes :

  • @Indexed : Spécifie que cette entité est indexé. Il est possible de préciser le nom de l’index dans lequel seront stockés les indexes (peut être mutualisé pour plusieurs entités)
  • @DocumentId : L’id du document (correpond en général à l’id de l’entité)
  • @Field : A positionner sur chaque attribut de la classe qu’on souhaite indexer
  • @Bridge : Lucene ne sait traiter que des String. Il faut donc dans le cas de type particulier (exemple: Data) fournir un bridge capable de convertir en String et vice-versa.
  • @IndexEmbedded : Spécifie qu’une entité liée (par une relation JPA) est « connectée » à l’entité indexée (permettra de faire des simili-jointures). L’entité liée doit aussi être configurée.
  • @AnalyserDef ou @Analyzer : Permet de configurer des analyseurs particuliers pour certains champs à indexer.
  • @Boost : Permet de définir un poids plus important sur l’entité ou le champ concerné. Ceci aura une influence sur les résultats des requêtes de recherche.
  • @DynamicBoost : Permet de définir une classe permettant un calcul dynamique du « boost »

Exemple :

@Indexed(index=”authors”)
@AnalyzerDef(name=”combinedAnalyzers”,
	tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
	filters = { @TokenFilterDef(factory = LowerCaseFilterFactory.class) }
)
@Entity
public class Author {
	@Id
	@GeneratedValue
	@DocumentId
	private Integer id;

	@Field(index = Index.TOKENIZED)
	@Analyzer(definition = “combinedAnalyzers”)
	private String name;

	@OneToMany @IndexEmbedded
	private Set<Book> books;

	private Adress address;
}

Il est aussi possible de dissocier le mapping JPA du mapping Document Lucene. Dans ce cas, nous utiliserons l’API Hibernate Search qui démarre avec la classe SearchMapping. Exemple :

public class AuthorSearchMappingFactory {
	@Factory
	public SearchMapping getSearchMapping() {
		SearchMapping mapping = new SearchMapping();
		mapping
			.analyzerDef("customAnalyzer", StandardTokenizerFactory.class)
					.filter(LowerCaseFilterFactory.class)
					.filter(SnowballPorterFilterFactory.class)
						.param("language", "English")
			.entity(Author.class)
			.indexed()
			.property("id",ElementType.FIELD).documentId()
			.property("adress", ElementType.FIELD)
					.field().bridge(AdressBrigde.class).store(Store.YES)
			.property("books", ElementType.FIELD).indexEmbedded()
			.property("name", ElementType.METHOD).field().store(Store.YES)
			.entity(Book.class)
			.indexed()
			.property("id", ElementType.METHOD).documentId()
			.property("title", ElementType.METHOD)
					.field().analyzer("customAnalyzer");
	}
}

Cette classe doit ensuite être définie dans le fichier de configuration JPA (persistence.xml) pour être prise en compte :

<persistence ...>
	<persistence-unit name="users">
		...
		<properties>
		<property name="hibernate.search.model_mapping"
			value="com.acme.MyAppSearchMappingFactory"/>
		</properties>
	</persistence-unit>
</persistence>

Effectuer des recherches

Pour effectuer des recherches, il faut récupérer une instance de la classe FullTextEntityManager à partir de l’entityManager :

FullTextEntityManager fullTextEntityManager = org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

Ensuite on utilise soit le DSL fournit par Hibernate Search soit directement  la syntaxe un peu rebutante de Lucene (voir lien). Voici un exemple avec le DSL Hibernate Search :

EntityManager em = entityManagerFactory.createEntityManager();
FullTextEntityManager fullTextEntityManager = org.hibernate.search.jpa.Search.getFullTextEntityManager(em);
// Création d’une requête sur les Book
QueryBuilder qb = fullTextEntityManager.getSearchFactory()
	.buildQueryBuilder().forEntity( Book.class ).get(); 
// Requête de recherche
org.apache.lucene.search.Query query = qb.keyword()
	// Les champs sur lesquels portent la recherche
	.onFields("title", "subtitle", "authors.name")
	// Le texte recherché, saisi par l’utilisateur
	.matching("Java rocks!")
	.createQuery();
// wrap Lucene query in a javax.persistence.Query
javax.persistence.Query persistenceQuery = fullTextEntityManager
	.createFullTextQuery(query, Book.class);
// execute search
List result = persistenceQuery.getResultList();
em.close();

Comme vous pouvez le voir la requête Hibernate Search est ensuite convertie en requête (Query) JPA. Ainsi la recherche effectuée avec Hibernate Search nous retourne directement des entités JPA issus de notre modèle sans avoir à faire de mapping fastidieux ou de requêtes JPA supplémentaires. Pour plus de détails sur les possibilités de recherche voire la documentation très complète sur le sujet.

Affiner l’indexation

En fonction des des types de champs et des types de requêtes que nous souhaitons effectués sur nos entités, il faudra affiner la configuration des analyseurs. Ces derniers nous permettent de définir la façon dont ils sont indexés. Ce sont eux qui permettent de mettre en place les fonctionnalités citées plus haut dans cet article.

Configuration des analyseurs

Il est possible de configurer l’analyseur d’un point de vue global (dans le persistence.xml) en définissant la propriété hibernate.search.analyzer (défaut : org.apache.lucene.analysis.standard.StandardAnalyzer). Il est aussi possible de définir des analyseurs au niveau de chaque entité via l’annotation @Analyzer (ou son équivalent via la classe SearchMapping). La possibilité est aussi donnée de définir des groupes d’analyseur réutilisables via l’annotation @AnalyzerDef et d’y faire réfaire ensuite via l’annotation @Analyzer (definition= » »). Chaque analyseur peut être composé des éléments suivants :

  • name : un nom unique permettant de référencer l’analyseur
  • charFilters : une liste de char filter. Chaque CharFilter est responsable d’effectuer une pré-traitement sur les caractères avant la phase de « tokenisation » (découpage). Un usage commun consiste à normaliser les caractères.
  • tokenizer : Le tokenizer se charge de découper la donnée à indexer en ‘mot’. Il existe deux implémentations standards (StandardTokenizer ou ClassicTokenizer qui est bien adapté à du texte).
  • filters : une liste de filtres à appliquer. Ces filtres se chargent de filtrer les mots (modifier, supprimer voire ajouter des mots). Il est possible via ces filtres d’appliquer des logiques d’utilisation de synonymes ou de radicalisation des termes indexés.

La documentation d’Hibernate Search décrit succintement les différents analyseurs fournis par défaut. Remarque : Selon les analyseurs utilisés, vous devrez ajouter des dépendances à vos projets. Beaucoup sont fournis par le projet Solr. Ces analyseurs peuvent aussi être utilisés dans les recherches. Ainsi, les mots clés ou la phrase utilisés dans la recherche (fournis par l’utilisateur) peuvent aussi être analysés (et traités) avec les mêmes analyseurs qui ont servis à l’indexation.

Prise en charge du français

Un ensemble de classes dédiées sont disponibles pour la prise en charge du français :

  • FrenchAnalyzer (FrenchStemmer (dérivé du SnowballFilter), FrenchStemFilter ou ses déclinaisons)
  • Gestion des accents et caractères spécifiques francais (ex: ç)
    • ASCIIFoldingFilter : convertit dans leur équivalent les caractères qui ne sont pas dans le bloc “Latin Basic” (les 127 premiers caractères ASCII), typiquement toutes les lettres accentuées.
    • ICUFoldingFilter : Prend en charge les caractères unicodes

Conclusion

Hibernate Search est une très bonne solution pour ajouter des fonctionnalités de recherche dans vos applications de gestion existantes : elle est basé sur le moteur Lucene qui a fait ses preuves et s’intègre pleinement avec Hibernate ce qui nous facilité grandement la tâche.