#8 Full Text Search Using Lucene.Net
Sunday, December 9, 2018
In this article and a couple of following articles we will be adding full text search capability to our web application. The project that we will utilizing to add this capability is Lucene.Net which is a port of the Lucene search engine library.
Parts
- Part 29: Offset Pager Urls
- Part 28: Offset Pager Start
- Part 27: Mock Context Builder
- Part 26: Mock Repository
- Part 25: Mock Async
- Part 24: Picture Tag Helper
- Part 23: Img DPR Tag Helper
- Part 22: Img Responsive Tag Helper
- Part 21: Img Optimized Display
- Part 20: Img Optimization
- Part 19: Img Lazy Loading
- Part 18: Img Responsive
- Part 17: Bottom Nav
- Part 16: Main Nav Cookie
- Part 15: Main Nav Mobile
- Part 14: Main Nav Search
- Part 13: Main Nav Auth
- Part 12: Main Nav Anchors
- Part 11: Main Nav Logo
- Part 10: Search Results
- Part 9: Search Manager
- Part 8: Search Start
- Part 7: Seeding the Database
- Part 6: Domain Database
- Part 5: Emailing Exceptions
- Part 4: Mailkit
- Part 3: View Renderer
- Part 2: Upgrade to 2.1
- Part 1: Quick Start
Adding Lucene.Net to our project
We begin by adding several nuget packages to our project which are shown in (a). As you can see at the time of this article the version of the packages that we are installing is currently in beta. If this is still the case when you are reading this in order for them to show up for download you will need to check the include pre-releases options.
Time for a search result
- WebUi
- Features
- Search
- SearchResult.cs
- Search
- Features
If we are going to be able to support searching we are going to need a search result (b). The most important part of the search result class is the parse function which will take in a parse action which will take the information stored within the search index and use it to populate the properties of our search result.
SearchResult.cs
using System;
using Lucene.Net.Documents;
namespace WebUi.Features.Search
{
public class SearchResult
{
private readonly Document _doc;
public SearchResult(Document doc)
{
_doc = doc;
}
public string DescriptionPath { get; set; }
public string LinkHref { get; set; }
public string LinkText { get; set; }
public void Parse(Action<Document> parseAction)
{
parseAction(_doc);
}
}
}
A single search result is not very interesting
- WebUi
- Features
- Search
- SearchResultCollection.cs
- Search
- Features
Now that we have a class for our search results we are going to need a container class to hold all of the results as well as a count property that will hold the number of total hits for a given search (c).
SearchResultCollection.cs
using System.Collections.Generic;
namespace WebUi.Features.Search
{
public class SearchResultCollection
{
private List<SearchResult> _data;
public int Count { get; set; }
public List<SearchResult> Data
{
get => _data ?? (_data = new List<SearchResult>());
set => _data = value;
}
}
}
Displaying the results
- WebUi
- Pages
- Search.cshtml.cs
- Pages
The next thing for us to tackle is to be able to display the search results. We will start by adding a couple of properties in the get method of our search page model (d). For the moment we will just set the results to a new search result collection.
Search.cshtml.cs
using WebUi.Features.Search;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace WebUi.Pages
{
public class SearchModel : PageModel
{
public SearchResultCollection Results { get; set; }
[BindProperty(SupportsGet = true)] public string Search { get; set; }
public void OnGet()
{
Results = new SearchResultCollection();
}
}
}
- WebUi
- Pages
- Search.cshtml
- Pages
Now we just need a simple page to display our results (e).
Search.cshtml
@page "{search}"
@model SearchModel
@{
ViewData["Title"] = $"Search for '{Model.Search}'";
}
<h1>Search for: @Model.Search</h1>
<h3>Found @Model.Results.Count result(s).</h3>
<ul>
@foreach (var result in Model.Results.Data)
{
<li>
<a href="@result.LinkHref">
<div>@result.LinkText</div>
<div>@result.LinkHref</div>
<p><partial name="@result.DescriptionPath"/></p>
</a>
</li>
}
</ul>
What are we searching for?
- WebUi
- Features
- Search
- Searchables
- Searchable.cs
- Searchables
- Search
- Features
In our projects we may need to be able to search for different things such as articles, products, comments ... etc. To facilitate being able to search for a set of heterogenous items we are going to start by defining a base class for searchable items (f). In this base class we are going to specify all of the fields that we will either search for or just store in the search index. We will then use these enum values to retrieve the strings that we need through a couple of dictionaries.
Searchable.cs
using System.Collections.Generic;
using Lucene.Net.Documents;
using Lucene.Net.Index;
namespace WebUi.Features.Search.Searchables
{
public abstract class Searchable
{
public static readonly Dictionary<Field, string> FieldStrings = new Dictionary<Field, string>
{
{Field.Description, "Description"},
{Field.DescriptionPath, "DescriptionPath"},
{Field.Href, "Href"},
{Field.Id, "Id"},
{Field.Title, "Title"}
};
public static readonly Dictionary<Field, string> AnalyzedFields = new Dictionary<Field, string>
{
{Field.Description, FieldStrings[Field.Description] },
{Field.Title, FieldStrings[Field.Title] }
};
public abstract string Description { get; }
public abstract string DescriptionPath { get; }
public abstract string Href { get; }
public abstract int Id { get; }
public abstract string Title { get; }
public enum Field
{
Description,
DescriptionPath,
Href,
Id,
Title
}
public IEnumerable<IIndexableField> GetFields()
{
return new Lucene.Net.Documents.Field[]
{
new TextField(AnalyzedFields[Field.Description], Description, Lucene.Net.Documents.Field.Store.NO),
new TextField(AnalyzedFields[Field.Title], Title, Lucene.Net.Documents.Field.Store.YES){ Boost = 4.0f },
new StringField(FieldStrings[Field.Id], Id.ToString(), Lucene.Net.Documents.Field.Store.YES),
new StringField(FieldStrings[Field.DescriptionPath], DescriptionPath, Lucene.Net.Documents.Field.Store.YES),
new StringField(FieldStrings[Field.Href], Href, Lucene.Net.Documents.Field.Store.YES)
};
}
}
}
Time for a searchable article
- WebUi
- Features
- Search
- Searchables
- SearchableArticle.cs
- Searchables
- Search
- Features
As a concrete example we are now ready to create a class for indexing our articles (g).
Here we will use the view
renderer that we created previously to convert the intro razor file into a string within the constructor. In the
GetFields
method we see the use of both the
AnalzyedFields
and FieldStrings
dictionaries. The purpose of having both is to make it a little more obvious what data is being analyzed and what
data is just being stored within the index.
SearchableArticle.cs
using System.Collections.Generic;
using WebUi.Domain;
using WebUi.Infrastructure;
using Lucene.Net.Documents;
using Lucene.Net.Index;
namespace WebUi.Features.Search.Searchables
{
public class SearchableArticle : Searchable
{
public SearchableArticle(Article article, IViewRenderer viewRenderer)
{
var descriptionPath = $"Pages/Articles/Intro{article.Id}";
DescriptionPath = descriptionPath;
Description = viewRenderer.Render<string>(descriptionPath, null);
Href = $"/articles/read/{article.Id}";
Id = article.Id;
Title = article.Title;
}
public override string Description { get; }
public override string DescriptionPath { get; }
public override string Href { get; }
public override int Id { get; }
public override string Title { get; }
}
}