S#arp Architecture

Edit

Pre-Requisite Reading

Due to the complexity of NHibernate Search, I really recommend reading Hibernate Search in Action. A 500 page book on search isn't the most thrilling guide. If you're in a hurry, I would suggest jumping to page 104 where it begins talking about mapping entity relationships. I have few fields that are text heavy, but I do have a lot of deep relationships that benefit from a Lucene query. I assume most business databases are heavy on the number fields, but light on the content of the fields.

Ayende (of course) has a good overview of NHibernate Search here.

Edit

Basic Concept of NHibernate Search

Functionally, you're simply creating a way to index and query the cornerstone entities of your database. You supply a search term and you're sent back a IList<Entity>, the search term will query over all the fields and relationships you specify. For example, I've saved a lot of code and complexity by having Lucene handle the queries on my vendor/supplier landing page. A user will type in, "texas bulbs h102a" and it'll do a very good job of not only finding suppliers from Texas that have "lightbulbs" as a category, but if a review or order contains an h102a bulb, it'll pick that up too.

Edit

Required Assemblies

For whatever reason, the NHibernate Search assemblies are hidden in the source of the NHibernate Contrib project. Here's the Sourceforge Repository.

  1. Go to "<Unzip Location>\nhcontrib\trunk\src\NHibernate.Search\src"
  2. Open NHibernate.Search.sln and build the solution
  3. Go to "<Unzip Location>\nhcontrib\trunk\src\NHibernate.Search\src\NHibernate.Search\bin\Debug-2.0", take out NHibernate.Search.dll and Lucene.Net.dll

    If you're having trouble, I've built the required assemblies against the current NHibernate build (2.1.2) used in Sharp 1.5 here.


  4. Place the assemblies in the "lib" folder of your Sharp project
  5. Add references to the assemblies in your "Core", "Data" and "Web" projects.

Edit

Configuring and Building the Index

Maintaining the index falls in two parts, 1. Updating the index when you perform inserts, updates and deletes and 2. Building the index from your existing entities.

  1. Create an index directory in the root of "Northwind.Web", for example "LuceneIndex," make sure you give the necessary permissions to your ASP.NET account. Exclude this directory from source control or you will get access errors later on.
  2. Navigate to "Northwind.Web\Web.Config"
  3. Add the following right above </configSections>:

    <section name="nhs-configuration" type="NHibernate.Search.Cfg.ConfigurationSectionHandler, NHibernate.Search" requirePermission="false" />


  4. Right below <appSettings/> add the following:

        <nhs-configuration xmlns='urn:nhs-configuration-1.0'>
            <search-factory>
                <property  name="hibernate.search.default.indexBase">~\LuceneIndex</property>
            </search-factory>
        </nhs-configuration>


  5. Navigate to your NHibernate.Config, before </session-factory> add:

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

Edit

Add a Search Repository

It might be best from a DRY standpoint to place the following code inside your existing entity repository. For pedagogical purposes, I'm setting up a separate Lucene repository. I should note that a lot of this and the previous configuration is lifted from Howard van Rooijen's contributions here.

  1. In "Northwind.Data" add a repository called "LuceneSupplierRepository.cs":

    namespace Northwind.Data
    {
        public class LuceneSupplierRepository : NHibernateRepository<Supplier>, ILuceneSupplierRepository
        {
        }
    }


  2. Let's add a method to this repository that will create the initial index, if an index already exists, it will be deleted. We'll iterate through all the Suppliers to accomplish this:

            public void BuildSearchIndex() {

    FSDirectory entityDirectory = null; IndexWriter writer = null;

    var entityType = typeof(Supplier);

    var indexDirectory = new DirectoryInfo(GetIndexDirectory());

    if (indexDirectory.Exists) { indexDirectory.Delete(true); }

    try { entityDirectory = FSDirectory.GetDirectory(Path.Combine(indexDirectory.FullName, entityType.Name), true); writer = new IndexWriter(entityDirectory, new StandardAnalyzer(), true); } finally { if (entityDirectory != null) { entityDirectory.Close(); }

    if (writer != null) { writer.Close(); } }

    IFullTextSession fullTextSession = Search.CreateFullTextSession(this.Session); // Iterate through Suppliers and add them to Lucene's index foreach (Supplier instance in Session.CreateCriteria(typeof(Supplier)).List<Supplier >()) { fullTextSession.Index(instance); } }


  3. We'll also add the GetIndexDirectory() method to grab the Lucene directory referenced in the configuration:


  4.       private string GetIndexDirectory() { 
            INHSConfigCollection nhsConfigCollection = CfgHelper.LoadConfiguration(); 
            string property = nhsConfigCollection.DefaultConfiguration.Properties"hibernate.search.default.indexBase"" title=""hibernate.search.default.indexBase"">"hibernate.search.default.indexBase"; 
            var fi = new FileInfo(property); 
            return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fi.Name); 
        } 


  5. Finally, we'll add a method to query the index:

            public IList<Supplier> Query(string query) {
                var parser = new MultiFieldQueryParser(new[] { "Query" }, new StandardAnalyzer());
                Query query = parser.Parse(term);
                IFullTextSession session =  Search.CreateFullTextSession(this.Session);
                IQuery fullTextQuery = session.CreateFullTextQuery(query, new[] { typeof(Supplier) });
                IList<Supplier> results = fullTextQuery.List<Supplier>();

    return results; }

Edit

Create a Data Interface

  1. Navigate to "Northwind.Core\DataInterfaces"
  2. Create "ILuceneSupplierRepository.cs"
  3. Add the following:

    namespace Northwind.Core.DataInterfaces
    {
        public interface ILuceneSupplierRepository : INHibernateRepository<Supplier>
        {
            void BuildSearchIndex();
            IList<Supplier> Query(string query);
        }
    }

Edit

Add LuceneSupplierController

  1. Add a LuceneSupplierController.cs:

    namespace Northwind.Web.Controllers
    {
        public class LuceneSupplierController : Controller
        {
            public LuceneSupplierController(ILuceneSupplierRepository luceneSupplierRepository) {
                  this.luceneSupplierRepository = luceneSupplierRepository;
            }

    public ActionResult BuildSearchIndex() { luceneSupplierRepository.BuildSearchIndex(); return RedirectToAction("Index", "Home"); }

    public ActionResult Search(string query) { List<Supplier> Suppliers = searchRepository.Query(query).ToList(); return View(Suppliers); }

    private readonly ILuceneSupplierRepository luceneSupplierRepository; } }


  2. Wire up a view to display the search results
  3. Navigate to localhost:portnumber/LuceneSupplier/BuildSearchIndex
  4. This will (quickly) build your index, it would be beneficial to pass status messages here
  5. You should see a Suppliers folder in the LuceneIndex folder of the project
  6. To verify the index, download Luke and point it to the LuceneIndex

ScrewTurn Wiki version 2.0.36. Some of the icons created by FamFamFam.