Analyzers en Lucene.Net

En este post estábamos viendo la integración de Lucene.Net con NHibernate mediante NHibernate Search.

Lucene puede indexar texto desde muchas fuentes: PDF, HTML, Documentos de Word entre los más conocidos y esto es lo que hace a Lucene muy usado por muchas aplicaciones reales para solucionar problemas de búsqueda. Luego que Lucene parsea los documentos desde un medio rico, debe convertir este stream en un formato de tokens de texto plano para que los pueda digerir y así indexar el contenido. El paso previo a la indexación es el análisis, y para esto se utilizan Analyzers. Lucene provee algunas clases que se pueden utilizar por ejemplo: WhitespaceAnalyzer que se encarga de tokenizar el texto sin tener en cuenta los espacios en blanco; StopAnalyzer que elimina las StopWords en Inglés del texto para poder indexarlo, por ejemplo: the, an, a, that, this, entre otras.

Usando NHibernate Search, podemos realizar consultas contra los índices que mantiene Lucene, ya sea en Memoria o en el propio sistema de archivos. Este es un ejemplo de una consulta usando NHibernate Search:

QueryParser qp = new QueryParser("Summary", new StopAnalyzer()); IQuery NHQuery = s.CreateFullTextQuery(qp.Parse("series"), typeof(Book)); IList result = NHQuery.List();

QueryParser recibe como parámetro un Analyzer, en este caso StopAnalyzer. Utilizando este Analyzer, Lucene encuentra los términos de búsqueda dentro del texto de búsqueda. Esto no tiene nada que ver con el Analyzer que se configura Lucene al comienzo, que indica la manera en que se van a filtrar los tokens que van almacenarse al Index. Este analyzer realiza un filtro en el string de búsqueda para encontrar las keywords de referencia a buscar.

Para entender un poco más sobre los Analyzer, realicé esta aplicación de consola basado en los ejemplos en Java de Lucene In Action, bueno… en realidad solo fue una traducción de Java a C# de los ejemplos que había. La idea es probar que tokens de salida producen distintos Analyzers ante una entrada de texto:

En este ejemplo utilicé dos analyzer de Lucene y uno personalizado a unas cuantas Stop Words del Español:  SpanishStopAnalyzer. El programa que utilicé para correr ejemplo pueden hacer un checkout de aquí.

NHibernate Search

English post

Una de las características nuevas de NHibernate portadas de Hibernate 3.2 es NHibernate Search. Lucene es el motor de búsqueda de textos usado por NHibernate que permite a las aplicaciones a realizar consultas sobre texto almacenado. Esta característica está disponible en NHibernate 1.2.1 o en NHibernate 2.0 (versión del SVN).

El ejemplo de NHibernate Search con NHibernate 2.0 está disponible para la descarga aquí:

NHibernate Search Demo

Para poder hacer búsquedas sobre el texto de nuestras entidades debemos decorar nuestros objetos de negocio con algunos atributos como se muestra aquí:

using NHibernate.Search; using NHibernate.Search.Attributes; [Indexed] public class Book { private string author; private int id; private string name; private string summary; public Book() { } public Book(int id,string author, string name, string summary) { this.id = id; this.author = author; this.name = name; this.summary = summary; } [DocumentId] public virtual int Id { get { return id; } set { id = value; } } [Field(Index.Tokenized, Store = Store.Yes)] public virtual string Author { get { return author; } set { author = value; } } [Field(Index.Tokenized, Store = Store.Yes)] public virtual string Summary { get { return summary; } set { summary = value; } } [Field(Index.Tokenized, Store = Store.Yes)] public virtual string Name { get { return name; } set { name = value; } } }

No se puede realizar la configuración vía el archivo hibernate.cfg.xml todavía. La configuración debe lucir como se muestra abajo. Hay unos pasos adicionales a tener en cuenta a la hora de configurar NHibernate Search en nuestra aplicación.

cfg = new Configuration(); cfg.SetProperty("hibernate.search.default.directory_provider", typeof(RAMDirectoryProvider).AssemblyQualifiedName); cfg.SetProperty(NHibernate.Search.Environment.AnalyzerClass, typeof(StopAnalyzer).AssemblyQualifiedName); cfg.Configure(); sf = cfg.BuildSessionFactory(); SearchFactory.Initialize(cfg, sf);

Creé 3 libros (Book) para poder hacer este ejemplo funcional:

using (IFullTextSession s = Search.CreateFullTextSession(sf.OpenSession(new SearchInterceptor()))) { using(ITransaction tx = s.BeginTransaction()){ Book b1 = new Book(1, "Eric Evans", "Domain-Driven Design: Tackling Complexity in the Heart of Software", @"This book provides a broad framework for making design decisions and a technical vocabulary for discussing domain design. It is a synthesis of widely accepted best practices along with the author’s own insights and experiences." ); s.Save(b1); Book b2 = new Book(2, "Pierre Kuate", "NHibernate in Action", @"In the classic style of Manning’s ‘In Action’ series, NHibernate in Action introduces .NET developers to the NHibernate Object/Relational Mapping tool. As NHibernate is a port of Hibernate from Java to .NET."); s.Save(b2); Book b3 = new Book(3, "John Doe", "Foo book NHibernate", "Foo series book"); s.Save(b3); s.Flush(); tx.Commit(); } }

Realizamos algunas consultas de texto sobre este escenario. El ejemplo a continuación es utilizando la interfase IQuery. El motor de búsqueda de texto tratará de encontrar la palabra "series" en las propiedad Summary.

using (IFullTextSession s = Search.CreateFullTextSession(sf.OpenSession(new SearchInterceptor()))) { QueryParser qp = new QueryParser("id", new StopAnalyzer()); IQuery NHQuery = s.CreateFullTextQuery(qp.Parse("Summary:series"), typeof(Book)); IList result = NHQuery.List(); Debug.Assert(result.Count == 2); }

Y este ejemplo a continuación es usando Criteria. El ejemplo trata de encontrar el texto "NHibernate" en las propiedades Summary (resume) y Name (nombre del libro)

using (IFullTextSession s = Search.CreateFullTextSession(sf.OpenSession(new SearchInterceptor()))) { IList result = s.CreateCriteria(typeof(Book)) .Add(Search.Query("Summary:NHibernate or Name:NHibernate")) .List(); Debug.Assert(result.Count == 2); }

Como se puede apreciar, para realizar las búsquedas se debe usar la interfase  IFullTextSession en vez de las conocidas hasta el momento ISession o IStatelessSession.