Introduction
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 a développer si l’on souhaite le rendre vraiment Full Text.
Qu’entends-t-on par Full Text?
Ils ‘agit en fait d’une technique de recherche dans document ou une base de donnée, nécessitant d’éxaminer 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.
Heuresement pour nous des solutions existent. Entre autre le bien connlu Lucene. http://lucene.apache.org/java/docs/.
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 indexs 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 Luke (http://www.getopt.org/luke/) qui vas vous permettre de visualiser les indexs que nous allons créer plus tard dans ce tutoriel.
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 Indexs.
<!-- 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"/>
Dependances
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.
Dans votre pom 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é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
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 soucis 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 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’annotaions 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
Indexation
Voilà vos entités sont prêtes à être indéxés, 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émment 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 vouyez 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?
Recherche
Et maintenat 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 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 ?.
- 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.
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 indexs 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 ligne dans la ou les tables parcourues.