Hibernate Search, Bridges, Analizadores y más

1
8996

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:

      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 + "]";
    	}
    
    }
     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:

      
	4.0.0
	HibernateSearchTest
	HibernateSearchTest
	jar
	1.0-SNAPSHOT
	HibernateSearchTest
	http://maven.apache.org

	

		
			
				true
				
				maven-compiler-plugin
				
					1.6
					1.6
					UTF-8
				
			
			
			
				maven-resources-plugin
				
					UTF-8
				
			
		

	

	
		
		
			junit
			junit
			4.4
			test
		
		
		
		
			hsqldb
			hsqldb
			1.8.0.7
			test
		
		
		
			org.hibernate
			hibernate-core
			3.3.1.GA
		
		
			org.hibernate
			hibernate-annotations
			3.4.0.GA
		
		
		
			org.hibernate
			hibernate-search
			3.1.0.GA
		
		
		
		
			org.slf4j
			slf4j-log4j12
			1.4.3
			
				
					com.sun.jdmk
					jmxtools
				
				
					com.sun.jmx
					jmxri
				
			
		
		
		
			javassist
			javassist
			3.8.1.GA
			runtime
		
		
		
				 
		
			org.apache.solr
			solr-common
			1.3.0
		
		
			org.apache.solr
			solr-core
			1.3.0
			
			
			
					org.apache.solr
					solr-lucene-core
			
			
		
		
			org.apache.lucene
			lucene-snowball
			2.4.1
		
		
	
    

4. Configuración de Hibernate, el Dao y preparación de las pruebas.

A continuación configuraremos Hibernate (hibernate.cfg.xml)

     
  
  
  					
  		
  		true
  		true		
  		
  		create-drop		
  		
  		org.hibernate.dialect.HSQLDialect
  		
  		org.hsqldb.jdbcDriver
  		jdbc:hsqldb:file:/tmp/appName/db/hsqldb/noticias;shutdown=true
  		sa
  		
  		
      org.hibernate.transaction.JDBCTransactionFactory 		
	
		thread
  		
  				
  		org.hibernate.search.store.RAMDirectoryProvider
  		org.hibernate.cache.HashtableCacheProvider	
  		
  		
  					
  	
  

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.

  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.

    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

        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  List loadAll(final Class entityClass) {
    		return (List)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 
	 * @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  List findByFullText(final Class entityClass, final String[] entityFields, final String textToFind,
			final String analyzerName, final String orderBy) {
		return (List)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(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

   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.

   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:

  1. 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.
  2. 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.
  3. Cada atributo que queramos indexar lo marcaremos como @Field. Algunos nos interesarán que sean tokenizados (titular, entradilla y cuerpo) y otros no (fecha).
  4. 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)
  5. 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.
  6. 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).
  7. 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.

  8. 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:

    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.

    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:

  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 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.

   
   ...
   /*
	 * 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 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 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 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 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 noticias = dao.findByFullText(Noticia.class, whereToSearch, "
href

", "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 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); } ...

1 COMENTARIO

  1. 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?

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad