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 =
792694656532097709
L;
/**
* 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"
, 3
f);
boostPerField.put
(
"category.name"
, 2
f);
boostPerField.put
(
"content"
, 1
f);
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.