I. Prérequis▲
Pour réaliser 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 répertoire où seront stockés les index.
<!-- 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 :
<!-- 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 :
<!-- 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ées pour ceux d'entre vous qui connaitraient déjà et souhaitent utiliser Lucene SnowBall ou les Analyzer personnalisés.
Nous ne rentrerons pas ici en détail sur ces éléments, je vous renvoie à 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.
/**
* 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 souci de lisibilité*//**
* 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 souci 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écessaires 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ément. (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 indexé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 :
/**
* 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 demande à Hibernate de les indexer.
Le purgeAll utilisé en début de chaque indexation indique de supprimer les index existants.
VI. Recherche▲
Et maintenant l'élément attendu, la partie moteur de recherche
/**
* @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 multichamp, d'indiquer un « booster » à Hibernate Search. Cela va agir sur la « pertinence » du champ. On attribue une « note » indiquant l'importance du champ dans la recherche.
- Les productFields sont donc les champs dans lesquels Hibernate Search devra chercher.
- Ici vous voyez donc que quand la luceneQuery parse le champ on ajoute « * » de chaque côté du searchPattern, syntaxe bien connue :)
- StandardAnalyzer : je ne vais pas entrer dans les détails sur les Analyzer dans ce tutoriel, 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 multichamp 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, quel que soit le nombre de lignes dans la ou les tables parcourues.





