Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Prochaines sessions inter-entreprises : 28-31 mars 2017 / 13-16 juin 2017
Sessions intra-entreprises sur demande.
Inscrivez-vous vite !

Paris JUG "Emmanuel Bernard"

Le Paris JUG se réunit de nouveau le mardi 9 mars à l'ISEP pour donner la parole à Emmanuel Bernard.
De 19h30 à 20h25 puis de 21h05 à 22h00, l'un dse principaux contributeurs d'Hibernate donnera les dernières nouvelles du célèbre framework de persistance, et présentera les différents algorithmes de recherche disponibles dans Hibernate Search.

Ceux qui ne pourront y assister en personne pourront toujours suivre la Wave en temps réel sur ce billet, ou depuis leur compte Wave à cette adresse : Wave Paris JUG "Emmanuel Bernard".
Elle sera retranscrite par la suite au format texte, pour ceux de mes lecteurs qui n'auraient pas de compte Wave.

Paris JUG "Emmanuel Bernard"

Cette wave est destinée au compte-rendu de la séance.

E. Bernard est le fondateur de Hibernate Search, Hibernate Validator, Hibernate Annotations...
Il travaille aujourd'hui pour RedHat/JBoss. Parmi les produits JBoss, la version Opensource est la vitrine technologique : bleeding edge, mais pas forcément stable ou sécurisé. Sinon, il faut prendre la version commerciale qui est stabilisée.

Deux sujets ce soir : Hibernate Search, et une réflexion plus générale sur les APIs.

Hibernate Search

Emmanuel a travaillé à la Fnac sur la partie recherche. Ses travaux permettent d'apporter à Hibernate des fonctionnalités avancées de recherches issues de véritables use-cases.

Qu'est-ce que la recherche ?

Certains utilisateurs n'aiment pas être trop guidés selon des catégories prédéfinies, et prefèrent une recherche transverse par mots-clés. Le problème, c'est qu'il faut alors pouvoir attaquer l'intégralité du modèle métier...

Problématiques :

  • le mot recherché peut être n'importe où dans le modèle métier
  • il faut faire des recherches "plain text" sur les colonnes, puisque une colonne de base contient rarement un seul mot.
  • il faut gérer les synonymes (analyse sémantique)
  • il faut aussi gérer les approximations (phonétique, fautes...). Voir l'algorithme de Levenshtein.

Et enfin, il faut présenter les résultats dans un ordre pertinent : comment déterminer cet ordre ?

La recherche "plain text" demande une certaine infrastructure : il faut découper le contenu de toutes les colonnes en mots, effectuer des analyses de fréquence, créer des indexes spécifiques... Il existe également certains produits full-text qui fonctionnent en mode "boîte noire" ; dans Hibernate Search, c'est Lucene.

Ces produits offrent des fonctionnalités qui n'existent pas en SQL (ou qui sont très lentes/contraignantes) :

  • déterminer le "meilleur" document pour le présenter en premier
  • gérer les fautes
  • etc.
Comment classer les documents ?

La notion de "relevance" est avant tout humaine, mais certains algos mathématiques peuvent aider. Par exemple, le titre d'un livre est plus intéressant que son quatrième de couverture, lui-même plus intéressant que le contenu du livre. Evidemment, le nombre d'occurrences est aussi très important...

Le calcul est dévolu à un module de similarité.

Extraction des données

La recherche étant effectuée par mots, il faut d'abord tokenizer les chaînes pour obtenir des mots. Ensuite, il faut appliquer des filtres pour supprimer les mots courants selon la langue (the, a, for... en anglais), et les normaliser (ex: les mettre en minuscule).

Au niveau des analyseurs, certains découpent également les mots en fragments de 3 lettres (trigrammes), de manière à être plus tolérant aux fautes courantes comme l'inversion de lettres.

Démonstration

Emmanuel fait maintenant une démonstration de Hibernate Search.
Il montre une classe normale annotée JPA (@Entity, etc.). La principale différence est l'annotation @Indexed qui indique à Hibernate d'indexer cette classe. Ensuite, certains champs sont annotés @Field.

Par contre, un moteur de recherche n'indexe que des chaînes de caractères. Il faut donc pouvoir convertir n'importe quelle donnée en chaîne (convertir une date en chaîne par exemple).
De même, on veut éviter que certains champs soient trigrammisés (découpés), par exemple la date : cela n'apporterait rien. On peut le spécifier au niveau de l'annotation @Field.

A noter qu'il est possible d'appliquer plusieurs modes d'indexation sur un même champ : par exemple, il serait intéressant de remonter les réponses exactes sur un titre avant les réponses approximatives.

On peut définir ses propres analyseurs en composant des tokenizers et filtres unitaires.

Sous le capot

Le moteur de recherche lui-même utilise les APIs JPA et Hibernate : il se branche dessus (sous-classement). Par exemple, FullTextEntityManager étend EntityManager.

Hibernate Search ne masque pas l'utilisation de Lucene : il expose au contraire son interface, pour que le développeur puisse en utiliser toute la puissance. La requête Lucene est ensuite wrappée dans une requête Hibernate, dont on peut alors utiliser toutes les fonctionnalités (pagination, etc.)

Aujourd'hui l'écriture des requêtes Lucene peut être très verbeuse. Dans une prochaine version, un DSL spécifique sera disponible.

Recherche phonétique

Au lieu d'indexer des mots, on peut indexer leur correspondance phonétique. La recherche est alors également elle-même traduite en phonétique, et les deux représentations sont comparées. Cela marche plutôt bien pour les langues latines, mais pour le Japonais par exemple, c'est beaucoup moins intéressant, voire impossible.

Recherche des synonymes

L'idée est d'indexer non pas chaque mot, mais des mots de référence possédant des listes de synonymes. Au moment de l'indexation, on remplace tous les synonymes par les mots de référence : on réduit ainsi le volume indexé.

On peut également indexer des mots de la même famille sous le même index. Par exemple: loving -> love. On peut appliquer des règles simples comme, supprimer les terminaisons en -ing... C'est l'algorithme de Porter.

Conclusion

Au final, Lucene est assez bas niveau et il est facile de mal l'utiliser.

Le modèle métier est également potentiellement très complexe, très structuré, alors que la recherche doit être faite "à plat" sur des mots. Hibernate Search permet donc de s'intégrer directement au modèle métier de manière transparente.

Hibernate Search possède également des fonctionnalités avancées, comme :

  • le filtrage des données remontées selon des profils / droits
  • le sharding (répartition des indexes)
  • clustering (synchrone ou asynchrone)

Designer des API

Pourquoi faire des API ?
C'est très long à mettre au point et surtout, il y a toujours des mécontents. En plus, une fois publiée et figée (v1.0), il est très difficile d'en changer. "L'API c'est pour la vie".
Mais, une bonne API fait gagner du temps à tous les utilisateurs.

Les API sont faites pour être utilisées par des humains :

  • support par des IDE : le compilateur doit trouver les erreurs avant qu'elles n'arrivent en prod. Auto-completion, etc.
  • robustesse, notamment type-safety (cf point précédent)
  • lisibilité, notamment un nombre de méthodes limité
  • doit être utilisable par des débutants et des experts

Mais avant tout, une API doit être intuitive car personne ne lit réellement la documentation associée - surtout si elle est obsolète...

Emmanuel a écrit beaucoup d'APIs, et a appris de ses erreurs.
Si on ne tombe pas, on n'apprend pas (devise du snowboard) : il faut faire plusieurs passes avant de trouver une bonne API.

Les fonctionnalités incertaines ou sujettes à changement doivent être marquées expérimentales, de façon à pouvoir les supprimer par la suite si nécessaire : l'utilisateur était prévenu.

La première version d'Hibernate Validator était la V3 pour garder une numérotation homogène avec les autres produits.
La première version était simple, mais est rapidement devenue trop compliquée, il a fallu refactorer.
Pour Hibernate Search, il a développé une API simple pour 80% des use-cases, mais qui fournit des points d'extension pour les cas avancés.

Beaucoup d'applications n'utiliseraient aucun moteur de recherche si Hibernate Search n'existait pas, car c'est la solution la moins compliquée.
Pour l'API Bean Validation, il y a une spécification. C'est donc un compromis, et on peut passer parfois des jours à trouver un nom simple pour une méthode.

Il faut également résister à la tentation de résoudre trop de problèmes avec la v1 : il faut laisser de la place pour les améliorations futures.

Emmanuel donne un exemple d'API qui a évolué : les annotations de mapping des relations dans Hibernate.
A l'origine, @JoinColumn était une annotation interne de @OneToMany, car c'est fait comme ça dans la version XML. Mais au final elles ont été séparées.

Enfin, il est difficile de prévoir une classes facilement extensible. Que mettre en privé, protected, package ? Il est tentant de prévoir des méthodes d'extension autour de tous les événements du cycle de vie de la classe (Template Method Pattern), mais cela complique beaucoup le design.

Bonnes pratiques :

  • utilisez vos propres API sur des vrais projets (les simples tests sont insuffisants)
  • pensez "sémantique" au-delà du code, pour que les API soient compréhensibles.
  • si possible, fournissez un DSL pour faciliter l'usage de l'API, c'est-à-dire un ensemble de méthodes lisibles qui s'enchaînent bien, pour former des phrases compréhensibles. L'utilisation du pattern Façade peut être intéressante.
  • rendez les cas d'utilisation principaux très simples à implémenter, par exemple en fournissant des valeurs par défaut intelligentes, valides dans 80% des cas.
  • lisez Effective Java de Joshua Bloch :)

Exemple de DSL :

mapping
    .fullTextFilterDef("security", SecurityFilterFactory.Class)
    .cache(FilterCacheModeType.INSTANCE_ONLY)

Attention, le contrat peut, et même VA évoluer.
Pour prévoir ces changements, on peut jouer avec les Generics pour utiliser des types "bounded" (cf. un article sur The Coder's Breakfast sur les "self-bounded generics").

Comment choisir entre des classes et des interfaces dans l'API ? L'avantage d'une classe abstraite, c'est qu'on peut fournir une implémentation par défaut, qu'on peut faire évoluer sans modifier l'interface de l'API.

De même, faut-il utiliser les Enums ? Les Enums ne peuvent pas être sous-classés, il est donc impossible pour l'utilisateur d'étendre une fonctionnalité représentée par un Enum.

En conclusion :

  • pensez à l'utilisateur
  • pensez au futur

Voir : blog.emmanuelbernard.com


Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.