Creación: 13-06-2008
Índice de contenidos
1.Introducción
2. Entorno
3.Configuración del entorno de desarrollo
4.Configuración de Hibernate Search
5.Construyendo un POJO que sea «indexable»
6.Como hacer consultas en nuestros índices
7.Recursos
8.Conclusiones
9.Sobre el autor
1. Introducción
Sí, has oído bien, en este tutorial vamos a ver una introducción de como montar un sistema para poder hacer búsquedas textuales tipo Google sobre la información que tenemos almacenada en nuestra base de datos.
Para ello vamos a utilizar Hibernate Search (http://www.hibernate.org/410.html).
Este proyecto hace algún tiempo que vio luz, y lo que hace es combinar la potencia de Hibernate (http://www.hibernate.org) para la gestión de bases de datos, con la potencia de Apache Lucene (http://lucene.apache.org/java/docs/index.html) como motor de búsquedas textuales basado en índices inversos.
Gracias a Hiberante Search conseguimos unificar estas dos grandes herramientas unificando manejo e interfaces. Y resulta trivial dotar de esta funcionalidad a nuestras aplicaciones.
Tal como nos cuenta la gente de Hibernate Search en su página principal, algunas de las principales ventajas son:
- Solventa el problema del desajuste de estructuras entre Hibernate y Lucene. Es decir, se encarga de hacer la traducción entre nuestros objetos y los índices de Lucene.
- Acaba con los problemas de duplicidad, manejando los índices, manteniendo sincronizados los cambios con la base de datos, y optimizando el acceso al índice, de forma transparente para el desarrollador.
- Unifica los APIs, de manera que se pueden hacer búsquedas en el índice y recuperar los objetos pertinentes como si se tratara de una consulta normal de Hiberante (además siempre tenemos la opción de acceder directamente al API Lucene, si fuera necesario).
Pero bueno, empecemos ya que me estoy poniendo nervioso 😉
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil Asus G1 (Core 2 Duo a 2.1 GHz, 2048 MB RAM, 120 GB HD).
- Nvidia GEFORCE GO 7700
- Sistema Operativo: GNU / Linux, Debian (unstable), Kernel 2.6.24, KDE 3.5
- Java Sun 1.6.0_06
- Hibernate 3.2.6.ga
- Hibernate Search 3.0.1 GA
- JUnit 4.4
3. Configuración del entorno de desarrollo
La configuración del entorno, como siempre, la vamos a hacer con Maven. De esta forma resulta tan sencillo como añadir a nuestro pom.xml
las siguientes líneas:
... <repositories> ... <repository> <id>repository.jboss.org</id> <name>JBoss Maven Repository</name> <url>http://repository.jboss.org/maven2</url> <layout>default</layout> </repository> ... </repositories> <dependencies> ... <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate</artifactId> <version>3.2.6.ga</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search</artifactId> <version>3.0.1.GA</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-annotations</artifactId> <version>3.3.1.GA</version> </dependency> ... <dependencies> ...
4. Configuración de Hibernate Search
Hibernate Search ya viene configurado para poder trabajar en la mayoría de los casos. Por esto para empezar a trabajar tan sólo tendremos que añadir a nuestrohibernate.cfg.xml
las siguientes líneas:
<property name="hibernate.search.default.directory_provider">org.hibernate.search.store.RAMDirectoryProvider</property> <!-- <property name="hibernate.search.default.directory_provider">org.hibernate.search.store.FSDirectoryProvider</property>--> <!-- <property name="hibernate.search.default.indexBase">/tmp/autentia-hibernate-search/indexes</property>-->
En la línea 1 le estoy diciendo que quiero que me guarde los índices en memoria. Es decir, al terminar la aplicación se perderán los índices. Esto no suele ser el comportamiento deseado, pero para hacer test automáticos nos va a venir muy bien 🙂
En la línea 2 y 3 os pongo comentado como configuramos Hibernate Search para que mantenga los índices en el disco duro. En concreto en la línea 3 es donde le decimos la ruta en el disco donde se guardarán los índices. Os recomiendo que sea una ruta fuera de vuestro servidor de aplicaciones, y deberíais hacer copias de seguridad de ella.
Si guardáis los índices en disco siempre podéis explorarlos y jugar con ellos, gracias a la herramienta Luke (http://www.getopt.org/luke/).
Os dejo un pantallazo para que os hagáis una idea:
Por supuesto que hay un montón de cosas más que podemos configurar, tanto de Hibernate Search como de Lucene. Pero entrar en esto escapa de la intención de este tutorial.
Para saber más al respecto os recomiendo especialmente la documentación de Hibernate Search (http://www.hibernate.org/hib_docs/search/reference/en/html_single/)
que es bastante clarificadora y es bastante corta (te la lees en un ratito). Y por supuesto la documentación de Luece para entender bien todos los conceptos, y exprimirlo al máximo
(http://lucene.apache.org/java/2_3_2/).
5. Construyendo un POJO que sea «indexable»
Ahora vamos a crear un POJO que el sistema sea capaz de indexar.
Usaremos anotaciones y nos queda algo tan sencillo como:
@Entity @Indexed public class Book { @Id @GeneratedValue @DocumentId private Integer id; @Field private String title; @Field private String summary; @Field @DateBridge(resolution = Resolution.DAY) @Temporal(TemporalType.TIMESTAMP) private Date publicationDate; ... // Resto de constructores, métodos, getter y setters, etc ...
Vamos a comentar exclusivamente las anotaciones de Hiberante Search:
- Línea 2,
@Indexed
: Esta anotación indica que este POJO debe ser indexado por Lucene cada vez que hagamos una operación con Hibernate. Es decir, al persistir, actualizar o borrar el POJO, Hibernate Search se encargará de mantener actualizado y sincronizado el índice de Lucene. - Línea 7,
@DocumentId
: Indicamos que atributo será el identificador para la indexación. Esto es un tema de diseño de Hibernate Search, y es obligatorio. - Línea 9,
@Field
: Hibernate Search sólo guardará en el índice los atributos que tengamos marcados con esta anotación. Por defecto esta anotación indexa el atributo
«tokenizado» (es decir, separado en palabras), y no guarda el contenido del atributo en el índice. Esto último significa que para ver el contenido del atributo Hibernate Search tendrá que hacer una búsqueda en la base de datos (esto suele ser lo habitual y solo guardaremos el contenido en el índice si queremos hacer proyecciones). - Línea 16,
@DateBridge
: Los índices de Lucene sólo guardan texto (por eso son búsquedas textuales ;), lo que hace Lucene es guardar la representación como texto de los atributos de nuestra clase. Todo esto es configurable, por ejemplo en el caso de las fechas, gracias a esta anotación, podemos decirle que parte de la fecha queremos indexar en el índice, por defecto indexará toda la precisión delDate
(hasta segundo y
milisegundos), pero esto no es demasiado útil, así que le podemos marcar cuanta precisión queremos indexar. En el índice le estamos indicando que sólo guarde hasta el día, este incluido. Hay que tener en cuenta que para buscar estas fechas el texto de la consulta lo tendremos que expresar en el formatoyyyyMMddHHmmssSSS
, en tiempo GMT.
Os recuerdo que para saber más sobre estas anotaciones, sus parámetros, posibles configuraciones, … lo mejor es acudir a la configuración de Hibernate Search.
Pues ya está señores, ya hemos terminado. Fácil ¿verdad? Ahora vamos a ver como podemos hacer algunas consultas.
6. Como hacer consultas en nuestros índices
Imaginemos que tenemos un Dao implementado con Spring (en los recursos podéis acceder a todo el código). Podríamos tener el siguiente método para hacer búsquedas textuales sobre una entidad concreta:
@Transactional(readOnly = true) public <T> List<T> findByFullText(Class<T> entityClass, String[] entityFields, String textToFind) { final FullTextSession fullTextSession = Search.createFullTextSession(getSession()); final MultiFieldQueryParser parser = new MultiFieldQueryParser(entityFields, new StandardAnalyzer()); final org.apache.lucene.search.Query luceneQuery; try { luceneQuery = parser.parse(textToFind); } catch (ParseException e) { log.error("Cannot parse [" + textToFind + "] to a full text query", e); return new ArrayList<T>(0); } final Query query = fullTextSession.createFullTextQuery(luceneQuery, entityClass); return query.list(); }
- Línea 3: Creamos una sesión para hacer búsquedas textuales. Esta sesión se hace en base a la sesión de Hibernate.
- Línea 4 a 11: Preparamos la consulta de Lucene, en función al texto que nos han pasado y los campos que queremos inspeccionar.
- Línea 12: Lanzamos la consulta de Hibernate sobre la entidad que nos han dicho. No es obligatorio hacer consultas sobre entidades determinadas, podríamos lanzar consultar sobre cualquier entidad; pero es interesante, de cara al rendimiento, reducir las búsquedas siempre que sea posible, limitando las entidades sobre las que se tiene que lanzar la consulta.
Sobre las búsquedas, y en general aplicable para cualquier operación que hagamos con Hibernate, es muy recomendable hacerlas siempre en el marco de una transacción (el ejemplo lo conseguimos gracias a Spring con la anotación de la línea 1). De esta forma se optimiza el rendimiento.
Por ejemplo, si hacemos varias inserciones en la base de datos dentro de la misma transacción, el índice de Lucene sólo se actualizará con el commit
de la transacción. Además, si algo sale mal, y se hacer rollback
de la transacción, el índice no se llegará a tocar.
7. Recursos
Aquí os dejo todos los fuentes del ejemplo, donde podréis encontrar toda la configuración y los test completos donde podréis jugar haciendo diferentes búsquedas.
8. Conclusiones
Evidentemente hay mucho más de lo que hemos visto aquí. Pero sólo con lo que hemos tratado en este tutorial ya tenemos para hacer grandes cosas.
Además gracias a las anotaciones conseguimos desarrollar muy rápido, y el código es sencillo, limpio, y mantenible.
Espero que os haya gustado, a mi desde luego me parece una opción muy interesante.
9. Sobre el autor
Alejandro Pérez García, Ingeniero en Informática (especialidad de Ingeniería del Software)
Socio fundador de Autentia (Formación, Consultoría, Desarrollo de sistemas transaccionales)
mailto:alejandropg@autentia.com
Autentia Real Business Solutions S.L. – «Soporte a Desarrollo»
Excelente ejemplo, una pregunta estoy usando JPA (Hibernate 4.2) y Spring , como haria para que al momento de levantar el proyecto indexe la base de datos si es que los indices tienen que actualizarse?
Hasta donde yo sé, eso no se puede hacer con JPA o Hibernate o Spring, por lo menos no directamente.
Lo podrías poner en un bean de spring de forma que, cuando se levante el contexto, este bean se encargue de hacer estas funciones. Pero yo no te recomendaría esta fórmula.
Yo seguramente lo haría en un script de arranque, de forma que la gestión de al bbdd no quedara acoplada a la propia aplicación. O incluso en un script de mantenimiento de la bbdd pero que no tenga nada que ver con el arranque de la aplicación, ya que me parece raro que haya que hacer la reindexación cada vez se levante la aplicación.
Muchas gracias alejandro, si creo que lo mejor es dejar esa parte como un script o funcionalidad de mantenimiento.
Por otro lado hibernate search si tiene soporte para JPA, basicamente lo que te ahorras es el casteo de la sesion del entity manager a el de hibernate xD, pero por ciertas cosas me pregunto si hibernate-search soporta los *Example queries*, es decir lleno un pojo con campos en una busqueda y se lo paso directamente para que lo haga.
* https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/querycriteria.html#querycriteria-examples
¿Por que? o ¿cual es el caso?,(quiza la aproximación que esto haciendo está equivocada).
Tengo una busqueda de personas y se capturaran nombre , apellidos, ciudad , etc, la idea
es llenar el pojo con esos datos y pasarlos directamente a hibernate-search para que haga la
busqueda.
* Actualmente uso JPA 2, hibernate 4.2.7.Final y hibernate-search 4.4.6.Final
Hola Alejandro. Estoy trabajando en un ejemplo muy sencillo para buscar en una tabla con un índice textfull sobre un campo tipo TEXT de mysql, desde hibernate. He seguido varios ejemplos, entre ellos el tuyo, que me ha parecido muy sencillo pero al lanzanlo me sale un error aquí:
java.lang.ClassNotFoundException: org.apache.lucene.search.BoostQuery
justo cuando ejecuta esta línea de tu ejemplo:
MultiFieldQueryParser parser = new MultiFieldQueryParser(entityFields, new StandardAnalyzer());
También he probado con esta, para evitar el array y hacerlo más sencillo con idéntico resultado:
QueryParser parser = new QueryParser(«udfilename», new StandardAnalyzer());
¿Te ha ocurrido alguna vez? ¿puedes darme alguna pista, por favor?
Por el mensaje de error parece un problema de dependencias ya que no está encontrando la clase, pero teniendo en cuenta que el tutorial tiene 8 años, puede ser que esa clase ya no exista. Te recomiendo que revises la documentación actual de Lucene y veas que lo tienes correctamente configurado.
Te animo a que cuando resuelvas el problema compartas con todos nosotros la solución con un tutorial que actualice al mío.
Saludos y suerte!
Muchas gracias