Use Lucene.Net in for faster data search

by: Dhanashri Karale in: Programming tags: C#,

Introduction

This tutorial explains how to use Lucene.Net to achieve faster search responses in ASP.NET or MVC or C# applications.

Why Lucene.Net?

All business or consumer applications require data search and it can be implemented using SQL LIKE clause or SQL Full-Text Search. But we found that it is much faster to search data when we use Lucene.Net library.

Lucene is a full-text search library which uses inverted index for search due to which it is able to achieve fast search responses. You can use Lucene.Net library to create indexes and search to retrieve the data which can be sorted by relevance or by a sort field.

“Lucene.Net is a port of the Lucene search engine library, written in C# and targeted at .NET runtime users. The Lucene search library is based on an inverted index.”

Read more about Lucene.Net from Apache Lucene.Net or Lucene.Net GitHub.

Let’s start with the coding :)

Setup C# Lucene Project

Before you start writing your first example using Lucene.Net library, you have to make sure that you have setup your Lucene environment properly.

Let’s start by creating a new project with name - “LuceneConsoleApplication”. Open VS and New Project -> Visual C# -> Windows -> Console Application.

Add "Lucene.Net" references using NuGet Package Manager in your project.

Add below key in App.config file. This is the path where we will store the Lucene indexes. It will create "LuceneIndexes" in bin directory. We can even pull the data from SQL Server database and create Lucence indexes. In this tutorial and sample given below, we have created a list using C# code.

                
    <add key="LuceneIndexStoragePath" value="LuceneIndexes" />
    

 

In code given below, the StartLuceneIndexCreateProcess() method will create Lucene indexes.

    
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                LuceneIndexer startIndexer = new LuceneIndexer();
                Console.WriteLine("Starting Lucene index creation process for search...\r\n");
                Console.WriteLine("Lucene index has been created from the following list:");
                // This method will create Lucene indexes according to your given list items.
                startIndexer.CreateLuceneIndexes();
                Console.WriteLine("");
                // You can check CFX and CFS file in folder "..\LuceneConsoleApplication\App_Data". 
                // Open the file in Notepad or any other text editor and you can see list of items.
                Console.WriteLine("Lucene Index creation successful!\r\n");
                GetSearchLucene getSearchLucene = new GetSearchLucene();
                Console.WriteLine("Enter a name to search:");
                // Get input string from user
                string inputParam = Console.ReadLine();
                List result = getSearchLucene.GetSearchLuceneText(inputParam);
                Console.WriteLine("");
                Console.WriteLine("You searched for:");
                if (result.Count > 0)
                {
                    foreach (var item in result)
                    {
                        Console.WriteLine(item.Actor);
                    }
                }
                else
                {
                    Console.WriteLine("Sorry! No matching records found.");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }

File #1 - Program.cs – Main() method

 

 

Create Indexes for Search

Now, let’s add list of items to CreateDocument() method.

Directory represents the storage location of the indexes. Write your desired directory path in the App.config file. Here we will create our index of our list items. User given input will searched with "Actor" key field.

        private void StartLuceneIndexCreateProcess()
        {
            string luceneIndexStoragePath = @ConfigurationManager.AppSettings["LuceneIndexStoragePath"];
            bool folderExists = System.IO.Directory.Exists(luceneIndexStoragePath);
            if (!folderExists)
                System.IO.Directory.CreateDirectory(luceneIndexStoragePath);
            analyzer = new Lucene.Net.Analysis.Standard.StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
            Lucene.Net.Store.Directory directory = Lucene.Net.Store.FSDirectory.Open(new System.IO.DirectoryInfo(luceneIndexStoragePath));
            writer = new Lucene.Net.Index.IndexWriter(directory, analyzer, true, Lucene.Net.Index.IndexWriter.MaxFieldLength.LIMITED);
            try
            {
                // We will populate below list to create Lucene index.
                List actorsList = new List();
                actorsList.Add("Johnny Depp");
                actorsList.Add("Robert Downey Jr.");
                actorsList.Add("Johnny Depp");
                actorsList.Add("Tom Cruise");
                actorsList.Add("Brad Pitt");
                actorsList.Add("Tom Hanks");
                actorsList.Add("Denzel Washington");
                actorsList.Add("Russell Crowe");
                actorsList.Add("Kate Winslet");
                actorsList.Add("Christian Bale");
                actorsList.Add("Hugh Jackman");
                actorsList.Add("Will Smith");
                actorsList.Add("Sean Connery");
                foreach (var item in actorsList)
                {
                    Console.WriteLine(item);
                    writer.AddDocument(CreateDocument(item.ToString()));
                }
            }
            catch
            {
                Lucene.Net.Index.IndexWriter.Unlock(directory);
                throw;
            }
            finally
            {
                writer.Optimize();
                analyzer.Close();
                writer.Dispose();
                analyzer.Dispose();
            }
        }
        private Lucene.Net.Documents.Document CreateDocument(string actorName)
        {
            try
            {
                Lucene.Net.Documents.Document doc = new Lucene.Net.Documents.Document();
                doc.Add(new Lucene.Net.Documents.Field("actors", actorName, Lucene.Net.Documents.Field.Store.YES, Lucene.Net.Documents.Field.Index.ANALYZED));
                return doc;
            }
            catch
            {
                throw;
            }
        }

File #2 – LuceneIndexer.cs - Contains code to create Lucene indexes

 

Search

Provided input will pass to GetSearchLuceneText() method. GetLuceneIndexPath() method will get the path of stored Lucene index i.e "bin\Debug\LuceneIndexes" folder. Input given by user will search in "LuceneIndexes" folder using Search() method.

    public class GetSearchLucene
    {
        public List GetSearchLuceneText(string actorName)
        {
            LuceneSearcher searchIndex = new LuceneSearcher(GetLuceneIndexPath().FullName);
            List results = new List();
            if (actorName != string.Empty)
            {
                results = searchIndex.Search(actorName, "actors").ToList();
            }
            return results;
        }
        // Get directory path for Lucene index creation
        public DirectoryInfo GetLuceneIndexPath()
        {
            return new DirectoryInfo(ConfigurationManager.AppSettings["LuceneIndexStoragePath"]);
        }
    }

File #3 -  GetSearchLucene.cs - Contains code to invoke search on Lucene indexes

 

Search method

IndexSearcher class acts as a core component which searcher indexes created during indexing process. Lucene directory will point to location where indexes are to be stored. Initialize the QueryParser object created with a standard analyzer having version information and index name on which this query is to run.

       
        public List Search(string searchText, string searchField)
        {
            List output = new List();
            string searchQuery = string.Empty;
            try
            {
                if (string.IsNullOrEmpty(searchText.Trim()))
                {
                    throw new Exception("You forgot to enter something to search for...");
                }
                searchQuery = searchText;
            }
            catch
            {
                throw;
            }
            try
            {
                // Open the IndexReader with readOnly=true. 
                // This makes a big difference when multiple threads are sharing the same reader, 
                // as it removes certain sources of thread contention.
                searcher = new Lucene.Net.Search.IndexSearcher(_directory, true);
                analyzer = new Lucene.Net.Analysis.Standard.StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
                output = (GetSearchResultByField(searchQuery, searchField, searcher, analyzer));
            }
            catch
            {
                throw;
            }
            finally
            {
                analyzer.Close();
                analyzer.Dispose();
                searcher.Dispose();
            }
            return output;
        }

File #4.1 – LuceneSearcher.cs - Contain code to search on Lucene indexes

 

Mapping With Lucene

User given input will be mapped with search field and user input search will be fetched from document object.

        private Lucene.Net.Search.Query ParseQuery(string searchQuery, Lucene.Net.QueryParsers.QueryParser parser)
        {
            Lucene.Net.Search.Query query;
            try
            {
                query = parser.Parse(searchQuery.ToLower().Trim() + "*");
            }
            catch (Lucene.Net.QueryParsers.ParseException)
            {
                query = parser.Parse(Lucene.Net.QueryParsers.QueryParser.Escape(searchQuery.Trim()));
                throw;
            }
            return query;
        }
        private IEnumerable MapLuceneDataToIDList(IEnumerable hits, Lucene.Net.Search.IndexSearcher searcher)
        {
            return hits.Select(hit => MapLuceneData(searcher.Doc(hit.Doc))).ToList();
        }
        // Mapping Lucene data
        private LuceneData MapLuceneData(Lucene.Net.Documents.Document doc)
        {
            return new LuceneData
            {
                Actor = (doc.Get("actors"))
            };
        }

File #4.2 – LuceneSearcher.cs - Contains code to prase and map Lucene search data

 

You can download our sample LuceneConsoleApplication project from GitHub.

Happy Coding :)