jQuery UI AutoComplete

jQuery AutoComplete using MVC with Lucene Index Computed Fields in Sitecore 8.1

In this post I am going to give an example of how to wire up suggestive search, otherwise known as AutoComplete, using the jQuery UI when using ASP.NET MVC along with Lucene Computed Index Fields in Sitecore 8.1.  An AutoComplete is as quoted from jQuery UI, “Enables users to quickly find and select from a pre-populated list of values as they type, leveraging searching and filtering“.

I started this off by working through the examples out of the most recent book put out by Phil Wicklund and Jason Wilkerson, Professional Sitecore 8 Development.  If you haven’t read this book, get it! It is filled with a lot of knowledge that you can add to your toolbox. I started with some basic examples from that book, and modified to get what I needed to meet my requirements for this task.

My requirement for this task was to create an AutoComplete that had a First Name and Last Name. The problem is that in my indexes they are stored as 2 separate fields in Lucene, as you will see below:

<field fieldName="first name" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
   <Analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
</field>
<field fieldName="last name" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
   <Analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
</field>

In order for me to bring them both together in my AutoComplete I created a Computed Index Field. A Computed Index Field is a custom field that stores data in Lucene that is calculated at index time versus on the fly. I needed to query a full name not a first name field AND a last name field. Below is the code to create a Computed index Field called FullNameField.cs:

FullNameField.cs

using System;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Diagnostics;
 
namespace SitecoreSandbox.Website.Search.ComputedFields
{
    public class FullNameField : IComputedIndexField
    {
        public string FieldName { get; set; }
 
        public string ReturnType { get; set; }
 
        public object ComputeFieldValue(IIndexable indexable)
        {
            Assert.ArgumentNotNull(indexable, "indexable");
 
            try
            {
                var indexItem = indexable as SitecoreIndexableItem;
 
                if (indexItem == null)
                    return null;
 
                var item = indexItem.Item;
 
                if (item != null)
                {
                    return $"{item["First Name"]} {item["Last Name"]}";
                }
            }
            catch (Exception ex)
            {
                Log.Error($"An error occurred when indexing {indexable.Id}: {ex.Message}", ex, this);
            }
 
            return null;
        }
    }
}

Now, that I have my Computed Index Field, I need to add it to my index. First, I added my field to the fieldmap>fieldnames node to be included in the index:

<field fieldName="full name" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
   <Analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
</field>

Then, I added my Computed Index Field to the documentOption>fields node to be included as a Computed Index Field:

<field fieldName="full name" storageType="YES" indexType="UNTOKENIZED">SitecoreSandbox.Website.Search.ComputedFields.FullNameField, SitecoreSandbox.Website</field>

You will notice that I added in indexType=”UNTOKENIZED” and that is because I want the value to not be split up. For example, if the name is John Doe, I want the results to come back as “John Doe” (UNTOKENIZED) versus “John” or “Doe” (TOKENIZED).

Now that my index is set up with my “full name” Computed Index Field all I need to do now is publish out to my Website folder and re-index. Once, I re-index I can see my field with the full names now being stored. I can easily view my index using LUKE – Lucene Index Toolbox, which I highly recommend, and it’s free. Below you will see that full names are left out for privacy purposes but you can see the field IS being indexed by Lucene and I assert to you the full names are there:

Luke (Lucene Index Toolbox)

Luke (Lucene Index Toolbox)

After verifying in Luke that my full names are being indexed, I added my new field to my PeopleSearchResultItem class so I can start using in code:

[IndexField("full name")]
public string FullName { get; set; }

Next. I created my interface for what is going to be used to implement my Search Service:

ISearchService.cs

using System.Collections.Generic;
 
namespace SitecoreSandbox.Website.Search.AutoComplete
{
    public interface ISearchService
    {
        IEnumerable<string> GetSearchSuggestions(string searchTerm);
    }
}

Then, I implemented my Interface in my Search Service class:

SearchService.cs

using System.Collections.Generic;
using System.Linq;
 
namespace SitecoreSandbox.Website.Search.AutoComplete
{
    public class SearchService : ISearchService
    {
        private readonly SearchManager _searchManager = new SearchManager();
 
        /// <summary>
        /// Gets the search suggestions from the computed index field for full name
        /// </summary>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        public IEnumerable<string> GetSearchSuggestions(string searchTerm)
        {
            var suggestions = new List<string>();
 
            var results = _searchManager.GetNamesByLetters(searchTerm);
 
            if (!results.Any())
                return suggestions;
 
            suggestions.AddRange(results.Select(result => result.FullName));
 
            return suggestions;
        }
    }
}

The GetSearchSuggestions method will make a call to my SearchManager class where my Lucene search logic is taking place and return me my results. In this case, I needed to pass in letters as the user types to return me back my results for my AutoComplete. The GetNamesByLetters method is seen below:

/// <summary>
/// Gets the the names with letters used with AutoComplete
/// </summary>
/// <param name="letters"></param>
/// <returns></returns>
public List<PeopleSearchResultItem> GetNamesByLetters(string letters)
{ 
    var searchIndex = ContentSearchManager.GetIndex("people_index");
 
    using (var context = searchIndex.CreateSearchContext())
    {
        return context.GetQueryable<PeopleSearchResultItem>()
            .Where(x => x.FullName.Contains(letters)).ToList();
    }
}

Now, that my logic is in place to return my results I just need to set up my Controller to handle the post and my View to make an AJAX call to the AutoComplete function in the jQuery UI. Below is the Controller logic:

private readonly SearchService _searchService = new SearchService();

[HttpPost]
public JsonResult GetSuggestions(PeopleSearchViewModel viewModel)
{
    if (!string.IsNullOrWhiteSpace(viewModel?.SearchTerm))
    {
        return Json(_searchService.GetSearchSuggestions(viewModel.SearchTerm));
    }
 
    return Json(new {});
}

Below is the jQuery code to handle the event for my field that is utilizing the AutoComplete. I added this to my .cshtml file. Keep in mind that my main layouts are already using jQuery and referencing the library in code so I am not including that script here. Also, the jQuery UI <script> and <link> elements can be found on the jQuery UI website:

    $(function () {
        $('#people-name').autocomplete({
            source: function(request, response) {
                $.ajax({
                    url: "/peoplesearch/getsuggestions",
                    type: "POST",
                    dataType: "json",
                    data: {
                        searchTerm: request.term
                    },
                    success: function(data) {
                        response(data.length === 1 && data[0].length === 0 ? [] : data);
                    }
                });
            }
        });
    });

The input text box HTML is below:

<input id=”people-name” placeholder=”Enter name” type=”text”>

The last thing I needed to do was just make sure that my route was set in the RouteConfig.cs file as such:

// Used with the AutoComplete for People Search
RouteTable.Routes.MapRoute("GetSuggestions", "PeopleSearch/GetSuggestions",
   new {controller = "PeopleSearch", action = "GetSuggestions"});

That’s all it takes to get the jQuery UI AutoComplete plug-in working for you to bring back results from your Lucene indexes. Happy coding!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s