Advertisement

#8 Full Text Search Using Lucene.Net

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.

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.

(a) Image showing the names and version numbers of the Lucene.Net packages that we need to install.

Time for a search result

  • WebUi
    • Features
      • Search
        • SearchResult.cs

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);
        }
    }
}
(b) The search result class will be used to hold the data contained within the search index.

A single search result is not very interesting

  • WebUi
    • Features
      • Search
        • SearchResultCollection.cs

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;
        }
    }
}
(c) The search result collection will hold all the search results from our index.

Displaying the results

  • WebUi
    • Pages
      • Search.cshtml.cs

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();
        }
    }
}
(d) The code behind for our search page will start by hard coding an empty search result collection.
Advertisement
  • WebUi
    • Pages
      • Search.cshtml

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>
(e) Simple page for displaying our search results.

What are we searching for?

  • WebUi
    • Features
      • Search
        • Searchables
          • Searchable.cs

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)
            };
        }
    }
}
(f) The base class that all items that we want to be indexed and therefore searchable will inherit from.

Time for a searchable article

  • WebUi
    • Features
      • Search
        • Searchables
          • SearchableArticle.cs

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; }
    }
}
(g) The searchable class that we will use to store the information from our article that we want to have analyzed and stored within our search index.
Exciton Interactive LLC
Advertisement