Advertisement

#9 Interacting With the Search Index

In this article we will add the ability to add and delete entries within our search index. We will also add the ability to clear the entire index and finally of course to be able to perform a search against it.

Index directory

  • WebUi
    • Features
      • Search
        • SearchManager.cs

The first thing we have to do is decide where the files that make up our search index are going to live. What I normally do is create a folder at the root of the application which is what we are going to do here as well (a).

SearchManager.cs

using Lucene.Net.Index;
using Lucene.Net.Store;
using Microsoft.AspNetCore.Hosting;
using System.IO;

namespace WebUi.Features.Search
{
    public class SearchManager
    {
        private static FSDirectory _directory;
        private readonly IHostingEnvironment _env;

        public SearchManager(IHostingEnvironment env)
        {
            _env = env;
        }

        private FSDirectory Directory
        {
            get
            {
                if (_directory != null)
                {
                    return _directory;
                }

                var info = System.IO.Directory.CreateDirectory(LuceneDir);
                return _directory = FSDirectory.Open(info);
            }
        }

        private string LuceneDir => Path.Combine(_env.ContentRootPath, "Lucene_Index");
    }
}
(a) Specifying the directory that will contain our search index and how we are going to get a handle to it.

Adding

  • WebUi
    • Features
      • Search
        • ISearchManager.cs

In order for us to be able to use a search index to find what a user is looking for we first have to create the index. Our first order of business is to create an interface that will take in one or more searchable items (b).

ISearchManager.cs

using WebUi.Features.Search.Searchables;

namespace WebUi.Features.Search
{
    public interface ISearchManager
    {
        void AddToIndex(params Searchable[] searchables);
    }
}
(b) Our search manager interface specifies a single method for adding searchable items to the index.
  • WebUi
    • Features
      • Search
        • SearchManager.cs

Of course implementing the functionality of the interface is a bit more complex that just specifying its method signatures. In addition to the actual add method we are going to also create a helper method to create a writer and use it (c).

SearchManager.cs

...
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Util;
using System;
using WebUi.Features.Search.Searchables;

namespace WebUi.Features.Search
{
    public class SearchManager : ISearchManager
    {
        ...
        public void AddToIndex(params Searchable[] searchables)
        {
            UseWriter(x =>
            {
                foreach (var searchable in searchables)
                {
                    var doc = new Document();
                    foreach (var field in searchable.GetFields())
                    {
                        doc.Add(field);
                    }
                    x.AddDocument(doc);
                }
            });
        }

        private void UseWriter(Action<IndexWriter> action)
        {
            using (var analyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48))
            {
                using (var writer = new IndexWriter(Directory, new IndexWriterConfig(LuceneVersion.LUCENE_48, analyzer)))
                {
                    action(writer);
                    writer.Commit();
                }
            }
        }
    }
}
(c) Here we are creating the ability to add an item to our search index.
Advertisement

Deleting

  • WebUi
    • Features
      • Search
        • ISearchManager.cs

The way that we will be using our index is to first, as we have already created, is to add an item to the index. If later on we need to update that entry we will delete the previous one and add the new one. Our next step is then to create a way for deleting an entry by first specifying its signature in our interface file (d).

ISearchManager.cs

using WebUi.Features.Search.Searchables;

namespace WebUi.Features.Search
{
    public interface ISearchManager
    {
        void DeleteFromIndex(params Searchable[] searchables);
    }
}
(d) Method signature for our delete method.
  • WebUi
    • Features
      • Search
        • SearchManager.cs

In our present example we know that each entry in our index will contain an id that will uniquely identify the entry. Using this knowledge it is fairly easy to implement the delete functionality (e).

SearchManager.cs

...

namespace WebUi.Features.Search
{
    public class SearchManager : ISearchManager
    {
        ...
        public void AddToIndex(params Searchable[] searchables)
        {
            DeleteFromIndex(searchables);
            ...
        }

        public void DeleteFromIndex(params Searchable[] searchables)
        {
            UseWriter(x =>
            {
                foreach (var searchable in searchables)
                {
                    x.DeleteDocuments(new Term(Searchable.FieldStrings[Searchable.Field.Id], searchable.Id.ToString()));
                }
            });
        }
        ...
    }
}
(e) Deleting an entry in our index is fairly simple using the id of the provided searchables.

Clearing

  • WebUi
    • Features
      • Search
        • ISearchManager.cs

It is possible that we could use the delete method that we just created to remove all entries from the index but if we check the Lucene documentation we will find that calling the DeleteAll method is more performant so that is what we are going to do. So of course first up is specifying the signature (f).

ISearchManager.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
...
namespace WebUi.Features.Search
{
    public interface ISearchManager
    {
        ...
        void Clear();
        ...
    }
}
(f) Adding the method signature for clearing the index.
  • WebUi
    • Features
      • Search
        • SearchManager.cs

Adding the clear functionality is as simple as calling the appropriate method provided by Lucene (g).

SearchManager.cs


...
namespace WebUi.Features.Search
{
    public class SearchManager : ISearchManager
    {
        ...
        public void Clear()
        {
            UseWriter(x => x.DeleteAll());
        }
        ...
    }
}
(g) To clear the index we are just going to call the DeleteAll method.

Searching

  • WebUi
    • Features
      • Search
        • ISearchManager.cs

It is all well and good to be able to add to our search index but if we cannot read from it that would make it pretty useless. To fix this it is time to modify our interface to add a search method (h).

ISearchManager.cs

...
namespace WebUi.Features.Search
{
    public interface ISearchManager
    {
        ...
        SearchResultCollection Search(string searchQuery, int hitsStart, int hitsStop, string[] fields);
    }
}
(h) Adding the signature for our search method.
  • WebUi
    • Features
      • Search
        • SearchManager.cs

Last but not least it is time to add the logic for searching the index (i). Along with specifying the search query itself we are also providing a start and stop value which we can use to add the ability to have pagination for our search results.

SearchManager.cs

...

using Lucene.Net.QueryParsers.Classic;
using Lucene.Net.Search;
using System.Linq;

namespace WebUi.Features.Search
{
    public class SearchManager : ISearchManager
    {
        ...
        public SearchResultCollection Search(string searchQuery, int hitsStart, int hitsStop, string[] fields)
        {
            if (string.IsNullOrEmpty(searchQuery))
            {
                return new SearchResultCollection();
            }

            const int hitsLimit = 100;
            SearchResultCollection results;
            using (var analyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48))
            {
                using (var reader = DirectoryReader.Open(Directory))
                {
                    var searcher = new IndexSearcher(reader);
                    var parser   = new MultiFieldQueryParser(LuceneVersion.LUCENE_48, fields, analyzer);
                    var query    = parser.Parse(QueryParserBase.Escape(searchQuery.Trim()));
                    var hits     = searcher.Search(query, null, hitsLimit, Sort.RELEVANCE).ScoreDocs;
                    results = new SearchResultCollection
                    {
                        Count = hits.Length,
                        Data  = hits.Where((x, i) => i >= hitsStart && i < hitsStop)
                            .Select(x => new SearchResult(searcher.Doc(x.Doc)))
                            .ToList()
                    };
                }
            }
            return results;
        }
        ...
    }
}
(i) Adding the logic for searching our index.
Exciton Interactive LLC
Advertisement