Nhibernate Search Tutorial with Lucene.Net and NHibernate 3.0



Here's another quickstart tutorial on NHibernate Search for NHibernate 3.0 using Lucene.Net. We're going to be using Fluent NHibernate for NHibernate but attributes for NHibernate Search. There is a Fluent NHibernate Search but I haven't played with it yet. I might update the tutorial, or post another when I do.

You're going to need:

  1. VS2010 - Express edition will do. (we're going to be writing a Console Application)
  2. SQL Server 2008 - Express edition should do
  3. NHibernate 3.0 download this tutorial was written with NHibernate 3.0.0
  4. Fluent NHibernate Stable PreRelease binary v1.x build#694
  5. NHibernate Search 2.0.1 for NHibernate 3.0.0GA
  6. Luke - See the insides of your Lucene Index

 

Step 1 - Setup normal Fluent NHibernate 3.0

I'm not going to go into much detail on this, I've recently written a basic tutorial on setting up Fluent NHibernate 3.0 so I'll shamelessly plug that :)

Step 2 - Add Files to your lib folder

Ok next is to add NHibernate.Search.dll and Lucene.Net.dll to your lib folder and then reference them from your project.

Step 3 - Add NHibernate Search to you App.config or Web.config

Ok now Lucene is referenced, you need to set it up.

Here we're telling NHibernate Search where to store our Index and that we're using the FileSystem to store it and importantly that we're using event based indexing strategy. Shortly we'll be hooking it up so whenever NHibernate does an insert or an update, then our Lucene.net index will be updated accordingly.

Step 4 - Hooking Up NHibernate Search with your Fluent NHibernate Configuration

So below we're using Fluent NHibernate to configure our database connection etc. But importantly we don't instantly assign it to the ISessionFactory instead we add a bit of extra code in to SetListerner(NHibernate.Event.ListenerType.PostUpdate,new FullTextIndexEventListener()) this is the magic bit that keeps the Lucene index in sync with your database.

 

using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Search.Event;
using NHibernate.Tool.hbm2ddl;

namespace SimpleNHibernateClient.ConsoleApplication
{
    public class NHibernateHelper
    {
        private static ISessionFactory _sessionFactory;

        private static ISessionFactory SessionFactory
        {
            get
            {
                if(_sessionFactory==null)
                    InitializeSessionFactory();

                return _sessionFactory;
            }
        }

        private static void InitializeSessionFactory()
        {

            var config = Fluently.Configure()
                .Database(MsSqlConfiguration.MsSql2008
                              .ConnectionString(
                                  @"Server=localhost\SQLExpress;Database=SimpleNHibernate;Trusted_Connection=True;")
                                  .ShowSql()                            
                )
                .Mappings(m =>
                          m.FluentMappings
                              .AddFromAssemblyOf())
                .ExposeConfiguration(cfg => new SchemaExport(cfg)
                                                .Create(true, true))
                .BuildConfiguration();               

//          Add NHibernate.Search listeners
            config.SetListener(NHibernate.Event.ListenerType.PostUpdate, new FullTextIndexEventListener());
            config.SetListener(NHibernate.Event.ListenerType.PostInsert, new FullTextIndexEventListener());
            config.SetListener(NHibernate.Event.ListenerType.PostDelete, new FullTextIndexEventListener());

            var factory = config.BuildSessionFactory();
            _sessionFactory= factory;

        }

        public static ISession OpenSession()
        {
            return SessionFactory.OpenSession();
        }
    }
}

 

Step 5 - Decorate Our Models with NHibernate Search Attributes

Next we need to cover our entities in attributes. If you're wondering where the NHibernate Attributes are, they're not here because we've used the Fluent NHibernate mappings in another file.

We're declaring that the entity is Indexed by Lucene.net and that it's DocumentId is our Car's Id property. It gets more interesting on Field(Index.Tokenized,Store = Store.Yes) here we're saying that we want to tokenize the string in the Lucene index, and that also want to store it. This would enable us to bring the Title back from the index without ever even needing to touch the database. Lucene.net is a document database or store everything document has the potential to have different fields. Where in SQL Server each row in a table has the same set of columns. In Lucene each document in an index can have different fields.

 

using NHibernate.Search.Attributes;

namespace SimpleNHibernateClient.ConsoleApplication
{
    [Indexed]
    public class Car
    {
        [DocumentId]
        public virtual int Id { get; set; }
        [Field(Index.Tokenized, Store = Store.Yes)]
        public virtual string Title { get; set; }
        [Field(Index.Tokenized, Store = Store.Yes)]
        public virtual string Description { get; set; }
        [IndexedEmbedded(Depth = 1, Prefix = "MAKE_")]
        public virtual Make Make { get; set; }
        [IndexedEmbedded(Depth = 1, Prefix = "MODEL_")]
        public virtual Model Model { get; set; }
    }
}
using System.Collections.Generic;
using NHibernate.Search.Attributes;

namespace SimpleNHibernateClient.ConsoleApplication
{
    [Indexed]
    public class Make
    {
        [DocumentId]
        public virtual int Id { get; set; }
        [Field(Index.Tokenized, Store = Store.Yes)]
        public virtual string Name { get; set; }
        public virtual IList Models { get; set; }
    }
}

 

using NHibernate.Search.Attributes;

namespace SimpleNHibernateClient.ConsoleApplication
{
    [Indexed]
    public class Model
    {
        [DocumentId]
        public virtual int Id { get; set; }
        [Field(Index.Tokenized, Store = Store.Yes)]
        public virtual string Name { get; set; }
        public virtual Make Make { get; set; }
    }
}

 

Step 6 - Create some Cars, and then search for them

We're pretty much there now. In the code below we simply create some Car Makes and Models, Ford and Vauxhall with Models Fiesta, Focus and Astra. As we persist these to the database NHibernate Search is also magically persisting them to the Lucene Index.

If you look in your LuceneIndex folder you'll see some folders for each of your entities. Inside each folder you see the files that make up the index, much like the above picture. You can use the Luke tool mentioned below to open the index.

static void Main(string[] args)
        {
            //Create Cars
            using (var s = NHibernateHelper.OpenSession())
            {
                using(var transaction = s.BeginTransaction())
                {
                    //Create Ford Makes and Models
                    var fordMake = new Make
                                       {
                                           Name = "Ford"
                                       };
                    s.Save(fordMake);

                    var fiestaModel = new Model
                                          {
                                              Make = fordMake,
                                              Name = "Fiesta"
                                          };

                    s.Save(fiestaModel);

                    var focusModel = new Model
                    {
                        Make = fordMake,
                        Name = "Focus"
                    };

                    s.Save(focusModel);


                    var vauxhallMake = new Make
                    {
                        Name = "Vauxhall"
                    };
                    s.Save(vauxhallMake);

                    var astraModel = new Model
                    {
                        Make = vauxhallMake,
                        Name = "Astra"
                    };

                    s.Save(astraModel);



                    var fordFiestaCar = new Car
                                      {
                                          Title = "Ford Fiesta",
                                          Description = "Nice Ford Fiesta",
                                          Make = fordMake,
                                          Model = fiestaModel
                                      };
                    var fordFocusCar = new Car
                    {
                        Title = "Ford Focus",
                        Description = "Nice Ford Focus",
                        Make = fordMake,
                        Model = focusModel
                    };
                    var vauxhallAsta = new Car
                    {
                        Title = "Vauxhall Astra",
                        Description = "Nice Vauxhall Astra",
                        Make = vauxhallMake,
                        Model = astraModel
                    };


                    s.Save(fordFiestaCar);
                    s.Save(fordFocusCar);
                    s.Save(vauxhallAsta);
                    transaction.Commit();

                }

                //Search for Ford in the Title field
                var query = "Title:Ford";
                using (var search = NHibernate.Search.Search.CreateFullTextSession(s))
                {                    
                    using (var transaction = s.BeginTransaction())
                    {

                        var carSearchResults = search.CreateFullTextQuery(query)
                            .SetMaxResults(5)
                            .List();   
                     
                        foreach(var car in carSearchResults)
                        {
                            Console.WriteLine(car.Title);
                        }

                        transaction.Commit();
                    }
                }
            }

            Console.ReadLine();
        }    
    }

Step 7 - Examine your Index in Luke

What we can also do here is to open the Lucene index in a wonderful tool called Luke.

In Luke we can see the contents of our Index. You can see we're storing the MODEL_Name as Focus this would enable us to search on the Model without requiring NHibernate search to go and get the models name out the database each time. This could give you a considerable performance boost. We're essentially de-normalizing our database structure to give us better performance. This often makes sense to require a join to Make and Model everytime we do a search for Ford Fiesta would give our simple application a bit of a kicking.

Step 8 - Run your Application and find a Ford

You'll find 2 Ford cars. And that concludes our introduction to NHibernate Search. Obviously this is the tip of the ice berg, there are endless possibilities where you can go from here.

 

Please note, that NHibernate is set to wipe your database clean everytime your run the application. Whereas you Lucene Index is not deleted each time you run the app. So it's still possible for your Index to get out of sync.

Good luck

References

 


blog comments powered by Disqus

Search

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2014