NHibernate Burrow and NHibernate Search

Spanish version

In this post I’m going to explain how I managed to make these projects to work together using NHibernate events instead of the classic interceptors. Through these events, we tell NHibernate.Search to index the elements marked for that purpose without touching the session. We only need to get it in order to perform a search in the index and we get it from NHibernate.Burrow.
The first thing we need is to configure the application properly (App.config):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="NHibernate.Burrow" type="NHibernate.Burrow.Configuration.NHibernateBurrowCfgSection, NHibernate.Burrow" />
    <section name="nhs-configuration" type="NHibernate.Search.Cfg.ConfigurationSectionHandler, NHibernate.Search" />
  </configSections>
  <NHibernate.Burrow>
<persistenceUnits>
      <add name="PersistenceUnit1"
           nh-config-file="hibernate.cfg.xml" />
    </persistenceUnits>
  </NHibernate.Burrow>

  <nhs-configuration xmlns='urn:nhs-configuration-1.0'>
    <search-factory>
<property name='hibernate.search.default.directory_provider'>NHibernate.Search.Store.RAMDirectoryProvider, NHibernate.Search</property>
<property name='hibernate.search.default.indexBase'>~/Index</property>
    </search-factory>
  </nhs-configuration>
</configuration>

NHibernate.Search does not manage the configuration section properly, so it looks for a section called nhs-configuration, so don’t change the name. On the other side, we should provide a valid hibernate.cfg.xml with NHibernate configuration. For example, for testing you can use an in-memory SQLite database:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
  <session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<property name="dialect">NHibernate.Dialect.SQLiteDialect</property>
<property name="connection.driver_class">NHibernate.Driver.SQLite20Driver</property>
<property name="connection.connection_string">Data Source=:memory:;Version=3;New=True;</property>
<property name="connection.release_mode">on_close</property>-->
<mapping assembly="Mapped.Assembly" />
<mapping assembly="Mapped.Assembly2" />

  </session-factory>
</hibernate-configuration>

Don’t forget to add all the assemblies in the mapping-assembly sections.
After that, the most interesting part is Burrow session initialization in order to add reference to the events. For example, this is what I have at the beginning of each test case that hits the database:

            var framework = new BurrowFramework();

            Configuration config = framework.BurrowEnvironment.GetNHConfig("PersistenceUnit1");

            // Poner los EventListeners
            config.SetListener(ListenerType.PostDelete, new FullTextIndexEventListener());
            config.SetListener(ListenerType.PostInsert, new FullTextIndexEventListener());
            config.SetListener(ListenerType.PostUpdate, new FullTextIndexEventListener());

            framework.BurrowEnvironment.RebuildSessionFactories();

The last line is important because it updates the session with the changes made. Ok, this may not be the best approach and adding the events in the nhibernate.cfg.xml would be better. It’s up to you. It should be something like this (the code goes into the session-factory section):

<listener class="NHibernate.Search.Event.FullTextIndexEventListener, NHibernate.Search" type="post-update" />
<listener class="NHibernate.Search.Event.FullTextIndexEventListener, NHibernate.Search" type="post-insert" />
<listener class="NHibernate.Search.Event.FullTextIndexEventListener, NHibernate.Search" type="post-delete" />

With this code you don’t need to rebuild the session factory.
With this configuration, the indexable entities (see Dario Quintana’s post on NHibernate.Search) are managed automatically by the library without doing anything manually and in a transparent manner.
Finally, you only need to test the search over the indexes. In the simple test below, the code creates a NHibernate.Search session and makes a search over an indexed entity, following Dario’s example:

        [Test]
        public void SearchIndexedUser()
        {
            var session = Search.CreateFullTextSession(new BurrowFramework().GetSession());

            var qp = new QueryParser("UserName", new StopAnalyzer());
            var nhQuery = session.CreateFullTextQuery(qp.Parse("test"), typeof(User));

            var results = nhQuery.List();

            Assert.AreEqual(1, results.Count);
        }

You must take in account on important thing: NHibernate.Search uses the current transaction if there’s one and NHibernate.Burrow makes use of them, so if you want to persist the indexes before making the search, it’s not enough evicting the entity. You should do the following:

            new BurrowFramework().CloseWorkSpace();
            new BurrowFramework().InitWorkSpace();

With this piece of code we finish Burrow’s pending transaction and create a new one in order to persist all the index changes.
And this is all. With this samples you can integrate in a simple manner two interesting projects on top of NHibernate, that can be a bit tricky if you need to deal directly with the session and are using Burrow. As well, using the new event system introduced in the 2.0 branch of NHibernate avoids the use of interceptors that in many cases was painful.
Note that both projects were compiled from the trunk of nhcontrib against NHibernate 2.0.1GA.
I’ll be waiting for your comments.

NHibernate Burrow y NHibernate Search

English version

En este post voy a explicar cómo he conseguido hacer funcionar ambos proyectos a la vez usando los eventos de NHibernate que sustituyen a los clásicos interceptors. A través de estos eventos, permitimos que NHibernate.Search indexe los elementos marcados para tal efecto sin tener que obtener explícitamente la sesión, cosa que solamente deberemos hacer a la hora de buscar en los índices a través de la sesión de NHibernate.Burrow.
Para empezar, habría que configurar adecuadamente la aplicación (App.config):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="NHibernate.Burrow" type="NHibernate.Burrow.Configuration.NHibernateBurrowCfgSection, NHibernate.Burrow" />
    <section name="nhs-configuration" type="NHibernate.Search.Cfg.ConfigurationSectionHandler, NHibernate.Search" />
  </configSections>
  <NHibernate.Burrow>
    <persistenceUnits>
      <add name="PersistenceUnit1"
           nh-config-file="hibernate.cfg.xml" />
    </persistenceUnits>
  </NHibernate.Burrow>
  <nhs-configuration xmlns='urn:nhs-configuration-1.0'>
    <search-factory>
      <property name='hibernate.search.default.directory_provider'>NHibernate.Search.Store.RAMDirectoryProvider, NHibernate.Search</property>
      <property name='hibernate.search.default.indexBase'>~/Index</property>
    </search-factory>
  </nhs-configuration>
</configuration>

El NHibernate.Search no maneja demasiado bien el tema de la sección y busca una llamada nhs-configuration, así que es mejor no cambiarle el nombre. Por otro lado, habrá que crear un fichero hibernate.cfg.xml con la configuración de NHibernate. Por ejemplo, para testado se puede usar una base de datos en memoria de SQLite:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
  <session-factory>
    <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
    <property name="dialect">NHibernate.Dialect.SQLiteDialect</property>
    <property name="connection.driver_class">NHibernate.Driver.SQLite20Driver</property>
    <property name="connection.connection_string">Data Source=:memory:;Version=3;New=True;</property>
    <property name="connection.release_mode">on_close</property>-->

    <mapping assembly="Mapped.Assembly" />
    <mapping assembly="Mapped.Assembly2" />
  </session-factory>
</hibernate-configuration>

Sin olvidar las secciones de mapping-assembly donde se indicaran los ensamblados a mapear.
Después, lo más interesante es la inicialización de Burrow para que tenga en cuenta los eventos de Search. Por ejemplo esto es lo que tengo al inicio de cada caso de test:

            var framework = new BurrowFramework();

            Configuration config = framework.BurrowEnvironment.GetNHConfig("PersistenceUnit1");

            // Poner los EventListeners
            config.SetListener(ListenerType.PostDelete, new FullTextIndexEventListener());
            config.SetListener(ListenerType.PostInsert, new FullTextIndexEventListener());
            config.SetListener(ListenerType.PostUpdate, new FullTextIndexEventListener());

            framework.BurrowEnvironment.RebuildSessionFactories();

La última línea es importante porque actualiza la sesión de NHibernate con los cambios realizados. Esto en realidad también se podría haber puesto en el fichero nhibernate.cfg.xml poniendo dentro de la sección session-factory lo siguiente:

        <listener class="NHibernate.Search.Event.FullTextIndexEventListener, NHibernate.Search" type="post-update" />
        <listener class="NHibernate.Search.Event.FullTextIndexEventListener, NHibernate.Search" type="post-insert" />
        <listener class="NHibernate.Search.Event.FullTextIndexEventListener, NHibernate.Search" type="post-delete" />

Con eso no haría falta añadir manualmente los manejadores ni regenerar los session factories.
En resumen, de este modo las entidades indexables (ver el post de Dario Quintana al respecto de NHibernate.Search) son tratadas automáticamente por la librería sin tener que hacer nada especial.
Ahora solamente quedaría hacer búsquedas sobre los índices. Por ejemplo en este caso este simple test crea una sesión de NHibernate.Search y hace una búsqueda sobre una entidad indexada, basándome en el ejemplo de Darío:

        [Test]
        public void SearchIndexedUser()
        {
            var session = Search.CreateFullTextSession(new BurrowFramework().GetSession());

            var qp = new QueryParser("UserName", new StopAnalyzer());
            var nhQuery = session.CreateFullTextQuery(qp.Parse("test"), typeof(User));

            var results = nhQuery.List();

            Assert.AreEqual(1, results.Count);
        }

Hay que tener en cuenta una cosa importante: Nhibernate.Search tiene en cuenta las transacciones y NHibernate.Burrow hace uso intensivo de las mismas, así que si en el test se crean los datos que luego se quieren buscar en el índice habrá que hacer lo siguiente:

            new BurrowFramework().CloseWorkSpace();
            new BurrowFramework().InitWorkSpace();

Con este código terminamos todas las transacciones pendientes por acometer de Burrow y se crea una nueva de modo que todos los cambios pendientes en los índices se persisten.
Y eso es todo. Con esto se pueden integrar de forma sencilla estos dos interesantes y útiles proyectos que de otro modo sería complicado ya que no se puede modificar directamente la sesión de Burrow además de ser incómodo el uso de interceptors ya que hay que trabajar un poco para poder usar varios a la vez, por lo que el mecanismo de eventos es mucho más limpio.
Finalmente comentar que ambas cosas las he hecho compilando el código del trunk tanto de NHibernate.Burrow como de NHibernate.Search que se puede conseguir en el SVN de nhcontrib contra la versión actual estable de NHibernate, la 2.0.1GA.
Espero vuestros comentarios.