Dans cet article, nous verronsr comment faire pour intégrer un moteur de recherche dans votre application web.
Nous n'aborderons pas dans cet article, le moteur de recherche google.
Context de l'étude
Dans le cadre d'un site de gestion de voyage, j'ai besoin de proposer une recherche d'article par nom de ville et pays.
L'application est écrite en java avec les frameworks suivant: JPA, JTA, EJB3.
Afin de rester je plus libre des infrastructures de déploiement, la persistance utilise uniquement le standard.
Le projet ne doit donc pas être rattaché à Hibernate ou Eclipse.
Le serveur de production est à ce jour un Glassfish 3.1.1 avec comme implémentation, par défaut de JPA, le framework EclipseLink.
La base de donnée est une MySql 5.1.5 accédé via une trasaction distribué (XAConnection).
Solutions - Tour d'horizon
Voici quelques solutions que j'ai pu trouver lors de mes recherches:
- Hibernae Search : Framework d'indexation des données, basé sur le standard JPA. Inconvénient, il impose l'utilisation du framework de même nom (Hibernate) comme solution d'ORM (Object-relation-mapping).
- Lucene : Le plus connu des framework d'indexation et de moteur de recherche. Il est hébergé par la communauté apache fondation. Inconvénient, il ne supporte pas de manière native les normes JPA et JTA.
- Compass : Surcouche au framework Lucene. Il a été conçu pour simplifier l'intégration avec différent standard tels que JPA, JTA et la plus part des ORM (Object relation mapping) du monde JAVA.
- ElasticSearch : application type serveur, basé sur le framework Lucene. Son fondateur est le même que pour Compass. Il offre une plus grande facilité d'intégration et d'extension dans les environnements clouds. Inconvénient, il ne supporte pas encore les standard JPA et JTA.
La solution que j'ai donc choisi d'utiliser dans un premier temps est donc le framework Compass.
Présentation de Compass
Site du projet: http://www.compass-project.org
Version utilisé: 2.2.0
Les sources de mon proto: lien sur KENAI
Intégration avec maven
La dépendance à mettre dans votre projet:
<dependency>
<groupId>org.compass-project</groupId>
<artifactId>compass</artifactId>
<version>2.2.0</version>
<type>jar</type>
</dependency>
Le repository à mettre dans votre projet:
<repository>
<id>compass-project.org</id>
<name>Compass</name>
<url>http://repo.compass-project.org</url>
</repository>
Présenation du proto
L'objectif de ce proto était de tester le framework dans un environnement proche de celui de la production.
Pour répondre à cette problématique, j'ai décider le créer un EJB Singleton pour représenter mon service d'indexation et de recherche.
Pour la partie test, j'ai utilisé Glassfish 3.1.1 en version embedded afin d'être le plus proche possible de l'environnement cible.
La partie base de donnée a été configuré au sein du domaine glassfish. Un premier test a été réalisé avec la base Derby Embedded en mode datasource XA (Transaction distribué). Puis avec une base Mysql en version cible.
Couche service
package com.evasion.poc.compass;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import org.compass.annotations.config.CompassAnnotationsConfiguration;
import org.compass.core.*;
import org.compass.core.config.CompassConfiguration;
import org.compass.gps.CompassGps;
import org.compass.gps.device.jpa.JpaGpsDevice;
import org.compass.gps.impl.SingleCompassGps;
/**
*
* @author sebastien
*/
@Singleton
@Startup
@TransactionManagement(TransactionManagementType.BEAN)
public class MySingleton {
private Compass compass = null;
private CompassGps gps=null;
@PostConstruct
public void initialiser() {
CompassConfiguration conf = new CompassAnnotationsConfiguration();
conf.configure(getClass().getClassLoader().getResource("compass.cfg.xml"));
conf.addClass(TestEntity.class);
compass = conf.buildCompass();
gps = new SingleCompassGps(compass);
// Création du composant JPA
EntityManagerFactory emf = Persistence.createEntityManagerFactory("CompassPU");
JpaGpsDevice jpaDevice = new JpaGpsDevice("jpa", emf);
jpaDevice.setInjectEntityLifecycleListener(true);
jpaDevice.setNativeExtractor(new GlassfishV3NativeJpaExtractor());
gps.addGpsDevice(jpaDevice);
gps.start();
gps.index();
System.out.println("Start OK");
}
@PreDestroy
public void preDestroy() {
compass.close();
}
public Collection search(String searchTerms) {
CompassSession session = compass.openSession();
Collection result = null;
CompassTransaction ctx = null;
CompassHits hits;
try {
ctx = session.beginTransaction();
hits = session.find(searchTerms);
result = new ArrayList(hits.getLength());
for (Iterator<CompassHit> it = hits.iterator(); it.hasNext();) {
result.add(it.next().getData());
}
ctx.commit();
} catch (CompassException ce) {
if (ctx != null) {
ctx.rollback();
}
} finally {
session.close();
}
return result;
}
}
Comme vous pouvez le voir, je n'utilise pas le mode de transaction managé par le conteneur. Les actions de démarrage de Compass et d'indexation ne supporte pas le mode de transaction global.
De même je n'utilise pas l'annotation @PersistenceUnit pour initialiser la propriété EntinyManagerFactory.
Couche test
Le test autométisé va être réalisé avec Junit4, GlassFish Embedded 3.1.1 et les bases Derby Embedded et Mysql 5.
Voici le code source du test:
import com.evasion.poc.compass.MySingleton;
import com.evasion.poc.compass.TestEntity;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import javax.ejb.embeddable.EJBContainer;
import javax.naming.Context;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.PersistenceContext;
import javax.transaction.UserTransaction;
import org.junit.*;
import static org.junit.Assert.*;
/**
*
* @author sebastien
*/
public class MySingletonTest {
private static UserTransaction utx;
private static EntityManagerFactory emf;
private static EntityManager em;
private static EJBContainer ejbContainer;
private static MySingleton singleton;
public MySingletonTest() {
}
@BeforeClass
public static void setUpClass() throws Exception {
//Map<String, Object> properties = new HashMap<String, Object>();
//properties.put(EJBContainer.MODULES, new File("target/classes"));
ejbContainer = EJBContainer.createEJBContainer();
Context ctx = ejbContainer.getContext();
// le nom JNDI d'un EJB dépend du serveur d'applications utilisé :
// jboss : "HelloWorldService/local"
// glassfish : "java:global/classes.ext/HelloWorldService"
String serviceName = "java:global/classes/" + MySingleton.class.getSimpleName();
singleton = (MySingleton) ctx.lookup(serviceName);
utx = (UserTransaction) ctx.lookup("java:comp/UserTransaction");
em = Persistence.createEntityManagerFactory("CompassPU").createEntityManager();
}
@AfterClass
public static void tearDownClass() throws Exception {
ejbContainer.close();
}
TestEntity bean1, bean2;
@Before
public void setUp() throws Exception{
utx.begin();
em.getTransaction().begin();
bean1 = new TestEntity();
bean1.setName("bean1");
em.persist(bean1);
bean2 = new TestEntity();
bean2.setName("bean2");
em.persist(bean2);
em.flush();
em.getTransaction().commit();
utx.commit();
}
@After
public void tearDown() throws Exception{
utx.begin();
em.remove(bean1);
em.remove(bean2);
utx.commit();
}
/**
* Test of search method, of class MySingleton.
*/
@Test
public void testSearch() throws Exception {
System.out.println("search");
String searchTerms = "bean";
Collection expResult = new ArrayList(2);
expResult.add(bean1);
expResult.add(bean2);
Collection result = singleton.search(searchTerms);
assertEquals(expResult, result);
}
}
Problématique rencontré
Durant mes tests, j'ai eu quelque problème de TimeOut de lock lors de recherche. Pour contourné ceci j'ai rallongé le timeout.
En revnache bien plus embettant, lors que l'on utilise Mysql en mode XA, alors le framework ne parviens pas à créer les tables d'indexation et à réalisé certaines opération de mise à jour. L'explication de cela réside du fait que le driver Mysql lève une expcetion si l'on tente un create, delete de table ou un commit sur une connection XA.
En revanche cela marche plutôt bien sans le XA.
Ecrire un commentaire - Voir les 0 commentaires
