Intégration d'Hibernate Search a une application Seam 2.1

De nombreuses applications Web nécessitent un moteur de recherche pour faciliter la recherche d'informations à vos utilisateurs.
Cependant, ce type d'outil peut vite être complexe à développer si l'on souhaite le rendre vraiment Full Text.
Qu'entends-t-on par Full Text ?
Il s'agit en fait d'une technique de recherche dans document ou une base de donnée, nécessitant d'examiner tous les mots du texte.
Néanmoins en raison des ambiguïtés du langage naturel une recherche Full Text aboutit souvent à des résultats peu pertinents.
Heureusement pour nous des solutions existent. Entre autre le bien connu Lucene. Apache LuceneApache Lucene.
Lucene est un moteur de recherche libre développé par Apache écrit en java qui permet d'indexer et de recherche dans le texte.
Les index vont alors être gérés sous formes de fichiers très optimisés qui vont grandement accellérer vos recherches.
Enfin Lucene gère nativement la syntaxe de recherche type Google, vous n'aurez pas donc pas à parser ce qui est entré par vos utilisateurs.
Hibernate Search est donc en quelque sorte la couche qui va vous permettre d'intégrer Lucene à votre application Seam, dont le contenu dynamique est géré par Hibernate (vers votre base de donée donc).
Je vous conseil de télécharger l'outil LukeLuke qui vas vous permettre de visualiser les index que nous allons créer plus tard dans ce tutoriel.
Ici nous allons donc voir comment intégrer Hibernate Search à votre application Seam, et comment l'utiliser pour développer un moteur de recherche simple.

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Prérequis

Pour réalisez ce tutoriel vous devez déjà bien connaitre :

  • Seam
  • Hibernate
  • JPA

II. Configuration de votre application

Comme vous vous en doutez il ne suffit pas d'ajouter un jar dans un coin de votre application pour faire fonctionner ce système.
Il faut donc commencer par configurer Hibernate
Vous devez donc ajouter quelques éléments à votre persistence.xml.
Ajouter les lignes suivantes à la persistence-unit déjà configurée de votre application, en prenant soin de modifier le repertoire ou seront stockés les index.

 
Sélectionnez

	  <!-- use a file system based index -->
         <property name="hibernate.search.default.directory_provider"   value="org.hibernate.search.store.FSDirectoryProvider"/>
        <!-- directory where the indexes will be stored -->
         <property name="hibernate.search.default.indexBase"  value="/Users/leakim/Documents/Projects/JBoss/hibernateSearchIndex"/>
         <property name="hibernate.ejb.event.post-insert" value="org.hibernate.search.event.FullTextIndexEventListener"/>
         <property name="hibernate.ejb.event.post-update" value="org.hibernate.search.event.FullTextIndexEventListener"/>
         <property name="hibernate.ejb.event.post-delete" value="org.hibernate.search.event.FullTextIndexEventListener"/>

III. Dépendances

Nous allons maintenant ajouter les dépendances nécessaires, attention je vous précise ici la version d'hibernate pour une raison simple : il faut que les versions d'Hibernate et Hibernate Search soient compatibles entres elles ou vous aurez des ennuis.
Voyons donc les dépendances avec Maven (je vous renvoie à mon article sur l'utilisation de Maven avec Seam, voir références en bas de cet article).
Dans votre pom.xml principal :

 
Sélectionnez

     <!-- Hibernate search -->
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-commons-annotations</artifactId>
       <version>3.0.0.ga</version>
     </dependency>
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-search</artifactId>
       <version>3.0.1.GA</version>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-core</artifactId>
       <version>2.3.0</version>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-analyzers</artifactId>
       <version>2.3.0</version>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-highlighter</artifactId>
       <version>2.3.0</version>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-snowball</artifactId>
       <version>2.3.0</version>
     </dependency>
      <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>3.3.2.GA</version>
        <exclusions>
          <exclusion>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
          </exclusion>
        </exclusions>
      </dependency>

Dans le projet qui va implémenter le moteur de recherche :

 
Sélectionnez

	  <!-- Hibernate Search dependencies -->
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-commons-annotations</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-search</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-core</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-analyzers</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-snowball</artifactId>
       <scope>provided</scope>
     </dependency>
      <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <scope>provided</scope>
      </dependency>

Ici vous avez des dépendances supplémentaires, je les ai précisés pour ceux d'entre vous qui connaitraient déjà et souhaite utiliser Lucene SnowBall ou les Analyzer personnalisés.
Nous ne rentrerons pas ici en détails sur ces éléments, je vous renvoi à la doc de Lucene pour cela.

IV. Déclaration d'entités pour l'indexation

Maintenant qu'Hibernate Search est installé, il va falloir indexer nos entités.
Vous allez voir avec l'exemple que c'est très simple grâce aux annotations disponibles

 
Sélectionnez

/**
 * Entity which represent a user text entry.
 * @author leakim
 *
 */
@Entity
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Indexed
@Name("textEntry")
public class TextEntry implements Serializable {

    /**
     * uid.
     */
    private static final long serialVersionUID = 792694656532097709L;

    /**
     * Unique Id.
     */
    @Id @GeneratedValue
    @DocumentId
    private Long id;

    /**
     * Text Entry title.
     */
    @NotNull @Length(max = 70)
    @Field(index = Index.TOKENIZED)
    private String title;

    /**
     * Text Content.
     */
    @NotNull
    @Field(index = Index.TOKENIZED)
    @Basic(fetch = FetchType.LAZY)
    @Length(max = 10000)
    private String content;


    /**
     *
     */
    @NotNull
    private Date date = new Date();


    /**
     * The category.
     */

    @ManyToOne
    @IndexedEmbedded
    private Category category;

    /**
     * The status.
     */
    private TextEntryStatus status;
    
    /** Code retiré par soucis de lisibilité*/
 
Sélectionnez

/**
 * Entity intended to represent a category.
 * @author leakim
 *
 */
@Entity
@Table(name = "Category", uniqueConstraints = {
        @UniqueConstraint(columnNames = "name") })
@Indexed
public class Category implements Serializable {

    /**
     * ID.
     */
    @Id @GeneratedValue
    @DocumentId
    private Long id;


    /**
     * The name of the category.
     */
    @Length(max = 25)
    @Field(index = Index.TOKENIZED)
    private String name;
     /** Code retiré par soucis de lisibilité*/

Explications

  • @Indexed Indique à Hibernate que l'entité est Indexée.
  • @DocumentId Indique à Hibernate l'ID utilisé comme Document ID, autant Indexed que DocumentId sont strictement nécessaire si vous voulez indexer, sans cela ça ne marchera pas.
  • @Field(index = Index.TOKENIZED) Indique à Hibernate Search qu'il s'agit d'un champ indexé de type Tokenized, ce qui signifie en quelque sorte un champ multi-éléments.(Pensez au StringTokenizer ...)
  • @IndexedEmbedded Ici l'annotation porte bien son nom, il s'agit d'embarquer l'index de l'entité annotée
  • Si vous souhaitez faire une recherche dans une liste ou un set, il faudra utiliser @ContainedIn

V. Indexation

Voilà vos entités sont prêtes à être indéxées, pour cela on peut créer une petite classe utilitaire.
Ici je le fais à chaque démarrage de l'application car celle-ci est en cours de développement, mais ce pas forcément nécessaire.
Par exemple :

 
Sélectionnez

/**
 * Index Blog entry at startup.
 *
 * @author Mikael Robert
 */
@Name("indexerService")
@Scope(ScopeType.APPLICATION)
@Startup
public class IndexerService {

    /**
     * The logger.
     */
    @Logger
    private Log logger;

    /**
     * Hibernate Search Entity Manager.
     */
    @In
    private FullTextEntityManager entityManager;

    /**
     * Creation method called on application startup.
     */
    @SuppressWarnings("unchecked")
    @Create
    public void indexTextEntry() {
        List blogEntries = entityManager.createQuery("select be from TextEntry be").getResultList();
        entityManager.purgeAll(TextEntry.class);
        logger.debug("Indexing textEntries ....");
        for (Object be : blogEntries) {
            entityManager.index(be);
        }

        entityManager.purgeAll(Category.class);
        List categoryList = entityManager.createQuery("select category From Category category").getResultList();
        logger.debug("Indexing Categories....");
        for (Object category : categoryList) {
            entityManager.index(category);
        }

    }
}

Explications
Vous voyez c'est relativement simple : on sélectionne tous les éléments de la table correspondant à l'entité à indexer, et on demander à Hibernate de les indexer.
Le purgeAll utilisé en début de chaque indexation indique de supprimer les indexs éxistants?

VI. Recherche

Et maintenant l'élément attendu, la partie moteur de recherche

 
Sélectionnez

/**
 * @author leakim
 *
 */
@Name("searchService")
@Scope(ScopeType.PAGE)
public class SearchService {

    /**
     * logger.
     */
    @Logger
    private Log logger;

    /**
     * Hibernate fullTextEntityManager.
     */
    @In
    private FullTextEntityManager entityManager;

    /**
     * The search pattern.
     */
    private String searchPattern;

    /**
     * The searchs results.
     */
    private List<TextEntry> searchResults;


    /**
     * Construct the search results.
     *
     */
    public void  search() {
        if (searchResults != null) {
            logger.debug("Old result list size : #0", searchResults.size());
        }
        if (searchPattern == null || "".equals(searchPattern)) {
            searchPattern = null;
            searchResults =  null;
        } else {
            Map<String, Float> boostPerField = new HashMap<String, Float>();
            boostPerField.put("title", 3f);
            boostPerField.put("category.name", 2f);
            boostPerField.put("content", 1f);
            String[] productFields =  {"title", "content", "category.name"};

            QueryParser parser = new MultiFieldQueryParser(productFields, new StandardAnalyzer(), boostPerField);
            parser.setAllowLeadingWildcard(true);
            org.apache.lucene.search.Query luceneQuery = null;
            try {
                luceneQuery = parser.parse("*" + searchPattern + "*");
            } catch (ParseException e) {
                searchResults =   null;
            }

            searchResults =   entityManager.createFullTextQuery(luceneQuery, TextEntry.class)
                .setMaxResults(100).getResultList();
            logger.debug("Search count #0", searchResults.size());
        }
    }
}

Explications

  • Le champ de texte de recherche de la page web est mappé au champ searchPattern.
  • La partie correspondant à la construction du map boostPerField permet, lors d'une recherche multi champs, d'indiquer un "booster" à Hibernate Search. Cela va agir sur la "pertinence" du champ. On attribut une "note" indiquant l'importance du champ dans la recherche.
  • Les productFields sont donc les champs dans les quels Hibernate Search devra chercher.
  • Ici vous voyez donc que quand la luceneQuery parse le champ on ajoute "*" de chaque coté du searchPattern, syntaxe bien connue :)
  • StandardAnalyzer : je ne vais pas entrer dans les détails sur les Analyzer dans ce tutorial, mais Lucene utilise des objets Analyzer pour analyser votre texte, cela vous permet avec les snowBall de grandement affiner la pertinence de vos résultats.
  • setAllowLeadingWildcard permet l'utilisation de * et ?.

La logique est donc :

  • Vérifier le pattern entré par l'utilisateur.
  • Instancier la recherche multi champs et les booster.
  • Parser le champ.
  • Lancer la recherche avec le fullTextEntityManager. Attention ! pas l'entityManager classique.
  • Récupérer la liste en retour de la fonction et l'afficher.

VII. Conclusion

Et voilà vous avez votre moteur de recherche. Vous pourrez rapidement le constater, celui-ci est déjà très pertinent.
Mais le gros apport est surtout au niveau de la performance, les index Lucene garantissent une rapidité de parcours et de recherche assez ahurissante.
De plus vos requêtes mettront systèmatiquement le même temps, quelque soit le nombre de lignes dans la ou les tables parcourues.

Liste de mes articles sur Seam :
Architecture Maven d'un projet Seam 2
Annotations Seam : Datamodel, Factory et Unwrap.
Création d'un composant facelet personnalisé avec Facelets JSF et Richfaces.
Intégration d'Hibernate Search à une application Seam 2.
Présentation Globale de Seam.
  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2009 Mikael Robert. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.