Hibernate Search, Bridges, Analizadores y más
En este tutorial vamos a tratar de comentar algunos detalles un poco más avanzados cuando trabajamos con Hibernate Search.
Trataré de explicar qué es un analizador, qué es un filtro, cómo podemos anotar las entidades del modelo con respecto a
Hibernate Search para poder buscar en entidades relacionadas, etc…
1. Introducción.
Si estás leyendo este tutorial, probablemente ya tengas nociones acerca de lo que es una base de datos inversa como Lucene y las posibilidades que ofrece.
Básicamente podríamos describir una base de datos de este tipo como palabra-céntrica o token-céntrica (terminos que no existen).
Es decir, aunque este tipo de Bases de Datos almacenan lo que denominan Documentos, estos son organizados en referencia a lo que se denominan tokens o términos.
Cualquier cosa puede ser considerada un documento siempre que se pueda convertir su información a texto (única cosa que entienden estas bases de datos).
Alguno dirá ¿ Y si no se puede convertir a texto ? Y yo le respondo, ¿ Hay algo que no se pueda describir con palabras ?.
Por lo tanto, lo primero que ha de hacer una base de datos de este tipo es analizar la información que se quiere almacenar, convertirla a texto, tokenizarla o separararla en términos que luego podrán ser usados para las búsquedas y relacionar
los documentos con esos términos o tokens. En este proceso de conversión a texto y tokenización o separación en términos de búsqueda es donde entran los bridges, los analizadores y los filtros.
2. Presentación del ejemplo.
Antes de empezar os dejo un zip con los fuentes del tutorial aquí
Dispongo de una base de datos relacional que contiene básicamente Noticias y Autores de esas noticias. Estoy usando actualmente Hibernate
trabajar con esta información. A continuación os muestro las entidades que describen este modelo de datos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
package com.autentia.adictos.hs.model; import javax.persistence.Entity; import javax.persistence.Id; @Entity public class Autor { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String nombre; private String apellidos; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getNombre() { return nombre; } public void setNombre(String nombre) { this.nombre = nombre; } public String getApellidos() { return apellidos; } public void setApellidos(String apellidos) { this.apellidos = apellidos; } @Override public String toString() { return "Autor [nombre=" + nombre + ", apellidos=" + apellidos + "]"; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
package com.autentia.adictos.hs.model; import java.util.Date; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.ManyToOne; import javax.persistence.Temporal; import javax.persistence.TemporalType; @Entity public class Noticia { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Temporal(TemporalType.TIMESTAMP) private Date fechaPublicacion; private String titular; private String entradilla; /** * El cuerpo de la noticia vendrá en HTML */ private String cuerpo; @ManyToOne @JoinColumn(name = "id_autor") private Autor autor; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Date getFechaPublicacion() { return fechaPublicacion; } public void setFechaPublicacion(Date fechaPublicacion) { this.fechaPublicacion = fechaPublicacion; } public String getTitular() { return titular; } public void setTitular(String titular) { this.titular = titular; } public String getEntradilla() { return entradilla; } public void setEntradilla(String entradilla) { this.entradilla = entradilla; } public String getCuerpo() { return cuerpo; } public void setCuerpo(String cuerpo) { this.cuerpo = cuerpo; } public Autor getAutor() { return autor; } public void setAutor(Autor autor) { this.autor = autor; } @Override public String toString() { return "Noticia [titular=" + titular + ", entradilla=" + entradilla + ", fechaPublicacion=" + fechaPublicacion + ", autor=" + autor + "]"; } } |
Hasta este momento, yo he sido feliz creando, modificando y eliminando noticias y autores.
Pero el cliente me ha pedido que si el podría buscar noticias como cuando busca en google.
Es decir el pone una palabrita en una cajita de texto y el sistema le devuelve aquellas noticias que contengan
esa palabrita en cualquier lugar (cuerpo, entradilla, autor, fecha …)
Evidentemente, tratar de realizar esto a través de HQL (o SQL) puede ser muy lento y probablemente no dé los resultados que espero.
¿ Puedo seguir siendo feliz ?. Espero que si.
3. Maven y sus cosicas
Vamos ahora configurar el proyecto con ayuda de Maven para poder obtener las herramientas que necesitamos. Os muestro el pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>HibernateSearchTest</groupId> <artifactId>HibernateSearchTest</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>HibernateSearchTest</name> <url>http://maven.apache.org</url> <build> <plugins> <plugin> <inherited>true</inherited> <!-- Configuramos la compilación para JDK 1.6 --> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.6</source> <target>1.6</target> <encoding>UTF-8</encoding> </configuration> </plugin> <!-- Configuramos los ficheros de recursos a UTF-8 --> <plugin> <artifactId>maven-resources-plugin</artifactId> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> <dependencies> <!-- JUNIT PARA LOS TESTS --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.4</version> <scope>test</scope> </dependency> <!-- BASE DE DATOS PARA LOS TESTS --> <dependency> <groupId>hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>1.8.0.7</version> <scope>test</scope> </dependency> <!-- DEPENDENCIAS DE HIBERNATE --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>3.3.1.GA</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-annotations</artifactId> <version>3.4.0.GA</version> </dependency> <!-- DEPENDENCIAS DE HIBERNATE SEARCH --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search</artifactId> <version>3.1.0.GA</version> </dependency> <!-- DEPENDENCIAS DE HIBERNATE MARCADAS COMO OPCIONALES --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.4.3</version> <exclusions> <exclusion> <groupId>com.sun.jdmk</groupId> <artifactId>jmxtools</artifactId> </exclusion> <exclusion> <groupId>com.sun.jmx</groupId> <artifactId>jmxri</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> <version>3.8.1.GA</version> <scope>runtime</scope> </dependency> <!-- DEPENDENCIAS DE APACHE SOLR --> <dependency> <groupId>org.apache.solr</groupId> <artifactId>solr-common</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>org.apache.solr</groupId> <artifactId>solr-core</artifactId> <version>1.3.0</version> <!-- Excluimos esta dependencia para evitar un problema de duplicidad de clases con diferentes versiones. --> <exclusions> <exclusion> <groupId>org.apache.solr</groupId> <artifactId>solr-lucene-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-snowball</artifactId> <version>2.4.1</version> </dependency> </dependencies> </project> |
4. Configuración de Hibernate, el Dao y preparación de las pruebas.
A continuación configuraremos Hibernate (hibernate.cfg.xml)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<hibernate-configuration> <session-factory> <!-- PARA LAS TRAZAS --> <property name="hibernate.show_sql">true</property> <property name="hibernate.format_sql">true</property> <!-- PARA QUE CREE LA BASE DE DATOS Y LA DESTRUYA AL FINAL --> <property name="hibernate.hbm2ddl.auto">create-drop</property> <!-- DIALECTO DE LA BASE DE DATOS --> <property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property> <!-- ACCESO A LA BASE DE DATOS. MODO PRUEBAS--> <property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property> <property name="hibernate.connection.url">jdbc:hsqldb:file:/tmp/appName/db/hsqldb/noticias;shutdown=true</property> <property name="hibernate.connection.username">sa</property> <property name="hibernate.connection.password"></property> <property name="hibernate.transaction.factory_class">org.hibernate.transaction.JDBCTransactionFactory</property> <property name="hibernate.current_session_context_class">thread</property> <!-- Para usar Hibernate Search. USAREMOS MODO PRUEBAS CON INDICE EN MEMORIA--> <property name="hibernate.search.default.directory_provider">org.hibernate.search.store.RAMDirectoryProvider</property> <property name="hibernate.cache.provider_class">org.hibernate.cache.HashtableCacheProvider</property> <!-- MAPEO DE LAS CLASES --> <mapping class="com.autentia.adictos.hs.model.Autor" /> <mapping class="com.autentia.adictos.hs.model.Noticia" /> </session-factory> </hibernate-configuration> |
Vamos ahora a crear una clase de utilidades para cargar los datos al inicio de las pruebas. He creado tres ficheros html que serán el cuerpo de las noticias y que leeré también durante la carga de datos y guardaré en las noticias.
Esta clase se apoya en un conjunto de clases que forman el Dao.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
package com.autentia.adictos.hs.test.utils; import java.io.FileReader; import java.io.IOException; import java.util.Date; import com.autentia.adictos.hs.dao.DaoFactory; import com.autentia.adictos.hs.dao.HibernateDaoImpl; import com.autentia.adictos.hs.model.Autor; import com.autentia.adictos.hs.model.Noticia; /** * Clase para creación de datos de pruebas * * @author fjmpaez */ public class DataGenerator { public static void createData() { /* * Inicializamos la factoría de sesiones de Hibernate */ HibernateDaoImpl dao = DaoFactory.getDao(); Autor autor1 = new Autor(); autor1.setNombre("Francisco Javier"); autor1.setApellidos("Martínez"); dao.persist(autor1); Autor autor2 = new Autor(); autor2.setNombre("Roberto"); autor2.setApellidos("Canales Mora"); dao.persist(autor2); Autor autor3 = new Autor(); autor3.setNombre("Juan"); autor3.setApellidos("Alonso Ramos"); dao.persist(autor3); Noticia noticia1 = new Noticia(); noticia1.setTitular("Alberto Contador casi sentencia el Tour"); noticia1.setEntradilla("El ciclista español logró pulverizar " + "todos los registros en la crono y consigue su doblete en este " + "Tour de Francia ampliando diferencias con todos sus rivales. " + "Lance Armstrong vuelve al podio, donde ahora es tercero, pero a sólo a 11 segundos de Wiggins"); noticia1.setFechaPublicacion(new Date()); noticia1.setAutor(autor1); noticia1.setCuerpo(readBody("cuerpo1.html")); dao.persist(noticia1); Noticia noticia2 = new Noticia(); noticia2.setTitular("Aumenta la tensión entre Lorenzo y Rossi"); noticia2.setEntradilla("Jorge Lorenzo no entiende las duras críticas de Valentino Rossi en Alemania. " + "Ve posible renovar con Yamaha, pero admite tener otras ofertas"); noticia2.setFechaPublicacion(new Date()); noticia2.setAutor(autor2); noticia2.setCuerpo(readBody("cuerpo2.html")); dao.persist(noticia2); Noticia noticia3 = new Noticia(); noticia3.setTitular("Alonso: Queremos intentar confirmar que el coche ha mejorado"); noticia3.setEntradilla("Queremos intentar confirmar que el coche ha mejorado, " + "que podemos estar en niveles competitivos y que las vueltas rápidas de Nurburgring " + "no fueron producto de las temperaturas bajas o cualquier otro motivo raro"); noticia3.setFechaPublicacion(new Date()); noticia3.setAutor(autor3); noticia3.setCuerpo(readBody("cuerpo3.html")); dao.persist(noticia3); } /** * Lee el texto del fichero y lo devuelve * * @param fileName * @return */ private static String readBody(String fileName) { StringBuilder sb = new StringBuilder(); FileReader fr = null; try { fr = new FileReader(DataGenerator.class.getClassLoader().getResource(fileName).getPath()); char[] c = new char[256]; int cars = fr.read(c); while (cars != -1) { sb.append(c); cars = fr.read(c); } return sb.toString(); } catch (Exception e) { e.printStackTrace(); return ""; } finally { if (fr != null) { try { fr.close(); } catch (IOException e) { e.printStackTrace(); } } } } } |
A continuación las clases del Dao
El interfaz HibernateCallback.
1 2 3 4 5 6 7 8 |
package com.autentia.adictos.hs.dao; import org.hibernate.Session; interface HibernateCallback { public Object doInHibernate(Session sess); } |
La clase HibernateDaoImpl que será un Dao un poco limitado. A nosotros, para este tutorial únicamente nos interesa el método findByFullText
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
package com.autentia.adictos.hs.dao; import java.util.ArrayList; import java.util.List; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.queryParser.MultiFieldQueryParser; import org.apache.lucene.queryParser.ParseException; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.search.FullTextSession; import org.hibernate.search.Search; public class HibernateDaoImpl { private final SessionFactory sessionFactory; public HibernateDaoImpl(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } public void beginTransaction() { sessionFactory.getCurrentSession().beginTransaction(); } public void commitTransaction() { sessionFactory.getCurrentSession().getTransaction().commit(); } public void rollbackTransaction() { sessionFactory.getCurrentSession().getTransaction().rollback(); } @SuppressWarnings("unchecked") public <T> List<T> loadAll(final Class<T> entityClass) { return (List<T>)execcute(new HibernateCallback() { public Object doInHibernate(Session sess) { final String hql = "from " + entityClass.getSimpleName(); final Query qry = sess.createQuery(hql); return qry.list(); } }); } public void persist(final Object entity) { execcute(new HibernateCallback() { public Object doInHibernate(Session sess) { sess.saveOrUpdate(entity); return null; } }); } /** * Método para realizar búsquedas indexadas * * @param <T> * @param entityClass. Entidad base sobre la que realizar las búsquedas * @param entityFields. Campos de la entidad en los que buscar * @param textToFind. Texto a buscar * @param analyzerName. Nombre del analizador a usar para la búsqueda. Si null entonces StandardAnalyzer * @param orderBy. Campo por el que ordenar. Sólo los campos marcados como UNTOKENIZED o NO_NORM pueden ser usados para * ordenar. Si null, entonces se ordena por relevancia. * @return */ @SuppressWarnings("unchecked") public <T> List<T> findByFullText(final Class<T> entityClass, final String[] entityFields, final String textToFind, final String analyzerName, final String orderBy) { return (List<T>)execcute(new HibernateCallback() { public Object doInHibernate(Session sess) { FullTextSession fullTextSession = Search.getFullTextSession(sess); Analyzer analyzer = null; if (analyzerName != null) { analyzer = fullTextSession.getSearchFactory().getAnalyzer(analyzerName); } else { analyzer = new StandardAnalyzer(); } final MultiFieldQueryParser parser = new MultiFieldQueryParser(entityFields, analyzer); final org.apache.lucene.search.Query luceneQuery; try { luceneQuery = parser.parse(textToFind); } catch (ParseException e) { System.out.println("Cannot parse [" + textToFind + "] to a full text query"); return new ArrayList<T>(0); } final FullTextQuery query = fullTextSession.createFullTextQuery(luceneQuery, entityClass); if (orderBy != null) query.setSort(new Sort(orderBy)); else query.setSort(Sort.RELEVANCE); return query.list(); } }); } private Object execcute(final HibernateCallback callback) { final Session sess; try { sess = sessionFactory.getCurrentSession(); } catch (RuntimeException e) { System.out.println("Error recuperando la sesión"); throw e; } boolean isInternalTransaction = false; Transaction tx = null; try { tx = sess.getTransaction(); } catch (RuntimeException e) { System.out.println("Error recuperando la transacción"); throw e; } try { if (tx == null || !tx.isActive()) { isInternalTransaction = true; tx = sess.beginTransaction(); } } catch (RuntimeException e) { System.out.println("Error iniciando la transacción"); throw e; } final Object callbackResult; try { callbackResult = callback.doInHibernate(sess); } catch (RuntimeException e) { tx.rollback(); System.out.println("Error ejecutando el callBack"); throw e; } if (isInternalTransaction) { tx.commit(); } return callbackResult; } } |
HibernateIgniter será la clase que se encargue de inicializar la factoría de sesiones de Hibernate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package com.autentia.adictos.hs.dao; import org.hibernate.SessionFactory; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.cfg.Configuration; final class HibernateIgniter { private static SessionFactory sessionFactory; private HibernateIgniter() { } static void init() { try { final Configuration cfg = new AnnotationConfiguration(); sessionFactory = cfg.configure().buildSessionFactory(); System.out.println("Inicializado..."); } catch (RuntimeException e) { System.out.println("Error inicializando hibernate"); throw e; } } static SessionFactory getSessionFactory() { return sessionFactory; } static void close() { sessionFactory.close(); } } |
DaoFactory será la clase encargada de construir Daos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package com.autentia.adictos.hs.dao; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.context.ThreadLocalSessionContext; public final class DaoFactory { private static HibernateDaoImpl dao; private DaoFactory() { // Clase de utilidad, singleton, no se pueden crear instancias } public static void init() { HibernateIgniter.init(); final SessionFactory sessionFactory = HibernateIgniter.getSessionFactory(); dao = new HibernateDaoImpl(sessionFactory); } public static void close() { HibernateIgniter.close(); } static void openSession() { final Session sess = HibernateIgniter.getSessionFactory().openSession(); ThreadLocalSessionContext.bind(sess); } static void closeSession() { HibernateIgniter.getSessionFactory().getCurrentSession().close(); ThreadLocalSessionContext.unbind(HibernateIgniter.getSessionFactory()); } public static HibernateDaoImpl getDao() { return dao; } } |
5. Anotando las clases para la indexación.
Para preparar nuestra clase Noticia para indexación haremos las siguientes cosas:
-
La marcaremos como @Indexed para indicar a Hibernate Search que nuestra clase es indexable. Desde este momento y al estar en el classpath Hibernate Search,
se activarán los listeners de indexación cada vez que se modifique o se guarde una entidad de este tipo. - Seleccionaremos el atributo que queremos marcar como identificador en lucene marcándolo como @DocumentId. Lo normal es que sea el mismo que es el @Id de Hibernate.
- Cada atributo que queramos indexar lo marcaremos como @Field. Algunos nos interesarán que sean tokenizados (titular, entradilla y cuerpo) y otros no (fecha).
-
El atributo fecha ha de ser pasado a String durante la indexación. Usaremos un Bridge de fechas @DateBridge que incluye Hibernate Search y le diremos que queremos sólo indexar
como máxima resolución a día (no nos interesa la hora exacta) -
Daremos más relevancia a las ocurrencias del titular, después a la entradilla y por último el cuerpo. Esto lo haremos usando el atributo @Boost y
entraría en juego si le pidiéramos ordenar por relevancia. -
Marcaremos el atributo autor para ser indexado juntamente con Noticia (para poder buscar noticias por autor). Esto lo haremos con la
anotación @IndexEmbedded. En el otro lado, en la clase autor, para que Hibernate Search reindexe una noticia cuando un autor sea modificado marcaremos
el atributo noticias de la clase autor como @ContainedIn (esto nos obliga a marcar la relación como bidireccional. Si no lo hiciésemos así, deberíamos ser nosotros
los encargados de reindexar una noticia cuando un autor sea modificado). -
Además definiremos un Analizador que denominaremos «Analizador_Noticia». Nuestro analizador estará formado por un conjunto de
Filtros creados por el proyecto solr y lucene-snowbal que complementarán la forma en la que serán indexados nuestros atributos.
Nuestro analizador contiene los siguientes filtros:- HTMLStripStandardTokenizerFactory: Extrae el texto del HTML. Ideal para el atributo cuerpo que es una página HTML
- ISOLatin1AccentFilterFactory: Elimina acentos, diéresis etc… durante la tokenización
- StopFilterFactory: No indexa todas aquellas palabras que contenga el fichero indicado
- LowerCaseFilterFactory: Pasa a minúsculas todos los textos
- SnowballPorterFilterFactory: Analiza las palabras para extraer la raíz de la misma y buscar palabras con la misma raíz (gato, gata)
Todo esto lo haremos con la anotación @AnalyzerDef.
- Por último indicaremos en nuestros atributos que use el Analizador definido durante la indexación. Esto lo haremos con la anotación @Analyzer
A continuación muestro como quedan nuestras clases:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
package com.autentia.adictos.hs.model; import java.util.Date; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Temporal; import javax.persistence.TemporalType; import org.apache.solr.analysis.HTMLStripStandardTokenizerFactory; import org.apache.solr.analysis.ISOLatin1AccentFilterFactory; import org.apache.solr.analysis.LowerCaseFilterFactory; import org.apache.solr.analysis.SnowballPorterFilterFactory; import org.apache.solr.analysis.StopFilterFactory; import org.hibernate.search.annotations.Analyzer; import org.hibernate.search.annotations.AnalyzerDef; import org.hibernate.search.annotations.Boost; import org.hibernate.search.annotations.DateBridge; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Index; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.IndexedEmbedded; import org.hibernate.search.annotations.Parameter; import org.hibernate.search.annotations.Resolution; import org.hibernate.search.annotations.Store; import org.hibernate.search.annotations.TokenFilterDef; import org.hibernate.search.annotations.TokenizerDef; @Entity @Indexed @AnalyzerDef(name = "Analizador_Noticia", tokenizer = @TokenizerDef(factory = HTMLStripStandardTokenizerFactory.class), filters = { @TokenFilterDef(factory = ISOLatin1AccentFilterFactory.class), @TokenFilterDef(factory = StopFilterFactory.class, params = { @Parameter(name = "words", value = "spanish-stoplist.txt"), @Parameter(name = "ignoreCase", value = "true") }), @TokenFilterDef(factory = LowerCaseFilterFactory.class), @TokenFilterDef(factory = SnowballPorterFilterFactory.class, params = { @Parameter(name = "language", value = "Spanish") }) }) public class Noticia { @Id @GeneratedValue(strategy = GenerationType.AUTO) @DocumentId private Long id; @Temporal(TemporalType.TIMESTAMP) @Field(index = Index.UN_TOKENIZED, store = Store.NO) @DateBridge(resolution = Resolution.DAY) @Analyzer(definition = "Analizador_Noticia") private Date fechaPublicacion; @Field(index = Index.TOKENIZED, store = Store.NO) @Boost(2.0f) @Analyzer(definition = "Analizador_Noticia") private String titular; @Field(index = Index.TOKENIZED, store = Store.NO) @Boost(1.5f) @Analyzer(definition = "Analizador_Noticia") private String entradilla; /** * El cuerpo de la noticia vendrá en HTML */ @Field(index = Index.TOKENIZED, store = Store.NO) @Analyzer(definition = "Analizador_Noticia") private String cuerpo; @ManyToOne @JoinColumn(name = "id_autor") @IndexedEmbedded private Autor autor; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Date getFechaPublicacion() { return fechaPublicacion; } public void setFechaPublicacion(Date fechaPublicacion) { this.fechaPublicacion = fechaPublicacion; } public String getTitular() { return titular; } public void setTitular(String titular) { this.titular = titular; } public String getEntradilla() { return entradilla; } public void setEntradilla(String entradilla) { this.entradilla = entradilla; } public String getCuerpo() { return cuerpo; } public void setCuerpo(String cuerpo) { this.cuerpo = cuerpo; } public Autor getAutor() { return autor; } public void setAutor(Autor autor) { this.autor = autor; } @Override public String toString() { return "Noticia [titular=" + titular + ", entradilla=" + entradilla + ", fechaPublicacion=" + fechaPublicacion + ", autor=" + autor + "]"; } } |
6. Probando todo esto.
Vamos ahora a crear una clase de test unitarios que me permita probar todo esto y una clase para inicializar los datos de prueba.
Crearemos tres autores y tres noticias. Los cuerpos de las noticias los he descargado de un periódico de deportes en html y lo leo del disco.
La clase que crea los datos de prueba.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
package com.autentia.adictos.hs.test.utils; import java.io.FileReader; import java.io.IOException; import java.util.Date; import com.autentia.adictos.hs.dao.DaoFactory; import com.autentia.adictos.hs.dao.HibernateDaoImpl; import com.autentia.adictos.hs.model.Autor; import com.autentia.adictos.hs.model.Noticia; /** * Clase para creación de datos de pruebas * * @author fjmpaez */ public class DataGenerator { public static void createData() { /* * Inicializamos la factoría de sesiones de Hibernate */ HibernateDaoImpl dao = DaoFactory.getDao(); Autor autor1 = new Autor(); autor1.setNombre("Francisco Javier"); autor1.setApellidos("Martínez"); dao.persist(autor1); Autor autor2 = new Autor(); autor2.setNombre("Roberto"); autor2.setApellidos("Canales Mora"); dao.persist(autor2); Autor autor3 = new Autor(); autor3.setNombre("Juan"); autor3.setApellidos("Alonso Ramos"); dao.persist(autor3); Noticia noticia1 = new Noticia(); noticia1.setTitular("Alberto Contador casi sentencia el Tour"); noticia1.setEntradilla("El ciclista español logró pulverizar " + "todos los registros en la crono y consigue su doblete en este " + "Tour de Francia ampliando diferencias con todos sus rivales. " + "Lance Armstrong vuelve al podio, donde ahora es tercero, pero a sólo a 11 segundos de Wiggins"); noticia1.setFechaPublicacion(new Date()); noticia1.setAutor(autor1); noticia1.setCuerpo(readBody("cuerpo1.html")); dao.persist(noticia1); Noticia noticia2 = new Noticia(); noticia2.setTitular("Aumenta la tensión entre Lorenzo y Rossi"); noticia2.setEntradilla("Jorge Lorenzo no entiende las duras críticas de Valentino Rossi en Alemania. " + "Ve posible renovar con Yamaha, pero admite tener otras ofertas"); noticia2.setFechaPublicacion(new Date()); noticia2.setAutor(autor2); noticia2.setCuerpo(readBody("cuerpo2.html")); dao.persist(noticia2); Noticia noticia3 = new Noticia(); noticia3.setTitular("Alonso: Queremos intentar confirmar que el coche ha mejorado"); noticia3.setEntradilla("Queremos intentar confirmar que el coche ha mejorado, " + "que podemos estar en niveles competitivos y que las vueltas rápidas de Nurburgring " + "no fueron producto de las temperaturas bajas o cualquier otro motivo raro"); noticia3.setFechaPublicacion(new Date()); noticia3.setAutor(autor3); noticia3.setCuerpo(readBody("cuerpo3.html")); dao.persist(noticia3); } /** * Lee el texto del fichero y lo devuelve * * @param fileName * @return */ private static String readBody(String fileName) { StringBuilder sb = new StringBuilder(); FileReader fr = null; try { fr = new FileReader(DataGenerator.class.getClassLoader().getResource(fileName).getPath()); char[] c = new char[256]; int cars = fr.read(c); while (cars != -1) { sb.append(c); cars = fr.read(c); } return sb.toString(); } catch (Exception e) { e.printStackTrace(); return ""; } finally { if (fr != null) { try { fr.close(); } catch (IOException e) { e.printStackTrace(); } } } } } |
Y por último empezaremos con los Tests.
Primero inicializaremos Hibernate y crearemos los datos al comienzo de los tests.
Luego creamos nuestro primer test para comprobar que realmente se han creado las noticias:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package com.autentia.adictos.hs.test; import java.util.List; import junit.framework.Assert; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import com.autentia.adictos.hs.dao.DaoFactory; import com.autentia.adictos.hs.dao.HibernateDaoImpl; import com.autentia.adictos.hs.model.Noticia; import com.autentia.adictos.hs.test.utils.DataGenerator; public class SearchNoticiasTest { public static final String[] whereToSearch = { "titular", "entradilla", "cuerpo", "autor.nombre", "autor.apellidos" }; @BeforeClass public static void startUp() { DaoFactory.init(); DataGenerator.createData(); } @AfterClass public static void tearDown() { DaoFactory.close(); } @Test public void test1HayNoticias() { HibernateDaoImpl dao = DaoFactory.getDao(); List<Noticia> noticias = dao.loadAll(Noticia.class); Assert.assertTrue("El número de noticias total no coincide", noticias.size() == 3); } } |
A continuación el resto de los tests que se explican en cada uno de ellos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
... /* * Búsqueda normal por palabras. Buscamos las palabras aumentar y lograr, que no están pero si aumentó y logró */ @Test public void test2BusquedaSencilla() { HibernateDaoImpl dao = DaoFactory.getDao(); List<Noticia> noticias = dao.findByFullText(Noticia.class, whereToSearch, "aumentar lograr", "Analizador_Noticia", "fechaPublicacion"); Assert.assertTrue("El número de noticias encontradas no coincide", noticias.size() == 2); } // Búsqueda normal por palabras. Busco en el cuerpo il dottore. @Test public void test3BusquedaSencilla() { HibernateDaoImpl dao = DaoFactory.getDao(); List<Noticia> noticias = dao.findByFullText(Noticia.class, whereToSearch, "il dottore", "Analizador_Noticia", "fechaPublicacion"); Assert.assertTrue("El número de noticias encontradas no coincide", noticias.size() == 1); } // Búsqueda por Autor. Busco Martínez Páez (no me preocupo de los acentos, porque al pasarle // el "Analizador_Noticia" Hibernate Search se encarga de preparar las palabras de manera correcta. @Test public void test4BusquedaPorAutor() { HibernateDaoImpl dao = DaoFactory.getDao(); List<Noticia> noticias = dao.findByFullText(Noticia.class, whereToSearch, "Martínez Páez", "Analizador_Noticia", "fechaPublicacion"); Assert.assertTrue("El número de noticias encontradas no coincide", noticias.size() == 1); } // Busco palabras que no debe de haber indexado. @Test public void test5BusquedaStopWord() { HibernateDaoImpl dao = DaoFactory.getDao(); List<Noticia> noticias = dao.findByFullText(Noticia.class, whereToSearch, "éste esta este algunos alguna", "Analizador_Noticia", "fechaPublicacion"); Assert.assertTrue("El número de noticias encontradas no coincide", noticias.size() == 0); } // Busco palabras que no debe de haber indexado. @Test public void test6BusquedaHTML() { HibernateDaoImpl dao = DaoFactory.getDao(); List<Noticia> noticias = dao.findByFullText(Noticia.class, whereToSearch, "<div> href <p>", "Analizador_Noticia", "fechaPublicacion"); Assert.assertTrue("El número de noticias encontradas no coincide", noticias.size() == 0); } // Busco por rango de fechas. [20080101 TO 20101231] // Desde el 1 de enero de 2008 hasta el 31 de diciembre de 2010. @Test public void test7BusquedaFechas() { HibernateDaoImpl dao = DaoFactory.getDao(); List<Noticia> noticias = dao.findByFullText(Noticia.class, whereToSearch, "[20080101 TO 20101231]", "Analizador_Noticia", "fechaPublicacion"); Assert.assertTrue("El número de noticias encontradas no coincide", noticias.size() == 3); } ... |
Hola Francisco Javier. En primer lugar, enhorabuena por los éxitos logrados.
Necesito tu ayuda, por favor:
Tengo una tabla (prueba) con 2 campos, un índice (id) y un campo TEXT (descripcion) indexado como fulltext en mysql.
Estoy trabajando con Java 1.8, hibernate 5, spring en una aplicación MVC que funciona perfectamente. He logrado encajar las versiones y compila sin errores (y este es el problema.. que no da errores).
Pretendo hacer un ejemplo fácil para hacer una búsqueda en la tabla prueba usando el índice fulltext desde hibernate. He seguido tu ejemplo (y unos 70 u 80 más de internet) y en todos el efecto es el mismo. Cuando hago esto:
FullTextSession fullTextSession = Search.getFullTextSession(sessionFactory.getCurrentSession());
QueryBuilder queryBuilder = fullTextSession.getSearchFactory().buildQueryBuilder().forEntity(Prueba.class).get();
org.apache.lucene.search.Query luceneQuery = queryBuilder
.keyword().wildcard()
.onFields(«descripcion»)
.matching(«hola»)
.createQuery();
org.hibernate.Query queryfull = fullTextSession.createFullTextQuery(luceneQuery, Prueba.class);
List list = (List) queryfull.list();
el array «list» está vacío (y es imposible, puesto que hay coincidencias). Si hago un System.out.println(queryfull);, me muestra esto: «descripcion»:»hola».
No da ningún error, ni el compilador, ni netbeans, ni hibernate, ni javascript.. nada!!! símplemente devuelve un list vacío. Estoy desesperado. ¿Qué estoy haciendo mal?