#9 Interacting With the Search Index
Saturday, December 15, 2018
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.
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
Index directory
- WebUi
- Features
- Search
- SearchManager.cs
- Search
- Features
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");
}
}
Adding
- WebUi
- Features
- Search
- ISearchManager.cs
- Search
- Features
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);
}
}
- WebUi
- Features
- Search
- SearchManager.cs
- Search
- Features
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();
}
}
}
}
}
Deleting
- WebUi
- Features
- Search
- ISearchManager.cs
- Search
- Features
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);
}
}
- WebUi
- Features
- Search
- SearchManager.cs
- Search
- Features
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()));
}
});
}
...
}
}
Clearing
- WebUi
- Features
- Search
- ISearchManager.cs
- Search
- Features
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();
...
}
}
- WebUi
- Features
- Search
- SearchManager.cs
- Search
- Features
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());
}
...
}
}
Searching
- WebUi
- Features
- Search
- ISearchManager.cs
- Search
- Features
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);
}
}
- WebUi
- Features
- Search
- SearchManager.cs
- Search
- Features
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;
}
...
}
}