EditPre-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.
EditBasic 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.
EditRequired Assemblies
For whatever reason, the NHibernate Search assemblies are hidden in the source of the NHibernate Contrib project. Here's the
Sourceforge Repository.
- Go to "<Unzip Location>\nhcontrib\trunk\src\NHibernate.Search\src"
- Open NHibernate.Search.sln and build the solution
- 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.
- Place the assemblies in the "lib" folder of your Sharp project
- Add references to the assemblies in your "Core", "Data" and "Web" projects.
EditConfiguring 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.
- 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.
- Navigate to "Northwind.Web\Web.Config"
- Add the following right above
</configSections>:
<section name="nhs-configuration" type="NHibernate.Search.Cfg.ConfigurationSectionHandler, NHibernate.Search" requirePermission="false" />
- 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>
- 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'/>
EditAdd 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.
- In "Northwind.Data" add a repository called "LuceneSupplierRepository.cs":
namespace Northwind.Data
{
public class LuceneSupplierRepository : NHibernateRepository<Supplier>, ILuceneSupplierRepository
{
}
}
- 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);
}
}
- We'll also add the GetIndexDirectory() method to grab the Lucene directory referenced in the configuration:
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);
}
- 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;
}
EditCreate a Data Interface
- Navigate to "Northwind.Core\DataInterfaces"
- Create "ILuceneSupplierRepository.cs"
- Add the following:
namespace Northwind.Core.DataInterfaces
{
public interface ILuceneSupplierRepository : INHibernateRepository<Supplier>
{
void BuildSearchIndex();
IList<Supplier> Query(string query);
}
}
EditAdd LuceneSupplierController
- 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;
}
}
- Wire up a view to display the search results
- Navigate to localhost:portnumber/LuceneSupplier/BuildSearchIndex
- This will (quickly) build your index, it would be beneficial to pass status messages here
- You should see a Suppliers folder in the LuceneIndex folder of the project
- To verify the index, download Luke and point it to the LuceneIndex