Tuesday, November 14, 2017

Sitecore 9 SOLR search example

One of the key features that any web site needs is a strong internal search engine. This posts focuses on how to get a minimal working example of an internal Sitecore search using SOLR in version 9 of Sitecore.
  1. The first step is to create a core for SOLR. This core is an instance of a Lucene index and contains dedicated configurations for that core.
  2. The next step is to create an index configuration for SOLR search, this will contain what data is getting indexed and which fields of that data.
  3. The index is now ready to be rebuilt (to get the data added into it). If the Sitecore admin area shows an error the index configuration XML may not be valid. The rebuild will also give a count of items processed into the index.
  4. Now code can be used to access the SOLR search index and query for a given term. The example below is the full code implementation for search in Sitecore with SOLR and will be explained in detail.
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Linq;
using Sitecore.ContentSearch.Linq.Utilities;
using Sitecore.ContentSearch.SearchTypes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Sitecore.Base.Helpers
{
    public class SearchHelper
    {
        /// <summary>
        /// Complete a search
        /// </summary>
        /// <param name="searchTerm">Search term</param>
        /// <returns>Search results object</returns>
        public static SearchResults DoSearch(string searchTerm)
        {
            var myResults = new SearchResults
            {
                Results = new List<SearchResult>()
            };

            var searchIndex = ContentSearchManager.GetIndex("sitecore_test_index"); // Get the search index
            var searchPredicate = GetSearchPredicate(searchTerm); // Build the search predicate

            using (var searchContext = searchIndex.CreateSearchContext()) // Get a context of the search index
            {
                var searchResults = searchContext.GetQueryable<SearchModel>().Where(searchPredicate); // Search the index for items which match the predicate

                // This will get all of the results, which is not reccomended
                var fullResults = searchResults.GetResults();

                // This is better and will get paged results - page 1 with 10 results per page
                //var pagedResults = searchResults.Page(1, 10).GetResults();

                foreach (var hit in fullResults.Hits)
                {
                    myResults.Results.Add(new SearchResult
                    {
                        Description = hit.Document.Description,
                        Title = hit.Document.ItemName,
                        Url = hit.Document.ItemUrl
                    });
                }

                return myResults;
            }
        }

        /// <summary>
        /// Search logic
        /// </summary>
        /// <param name="searchTerm">Search term</param>
        /// <returns>Search predicate object</returns>
        public static Expression<Func<SearchModel, bool>> GetSearchPredicate(string searchTerm)
        {
            var predicate = PredicateBuilder.True<SearchModel>(); // Items which meet the predicate

            // Search the whole phrase - LIKE
            predicate = predicate.Or(x => x.DispalyName.Like(searchTerm)).Boost(1.2f);
            predicate = predicate.Or(x => x.Description.Like(searchTerm)).Boost(1.2f);
            predicate = predicate.Or(x => x.Title.Like(searchTerm)).Boost(1.2f);

            // Search the whole phrase - CONTAINS
            predicate = predicate.Or(x => x.DispalyName.Contains(searchTerm)).Boost(2.0f);
            predicate = predicate.Or(x => x.Description.Contains(searchTerm)).Boost(2.0f);
            predicate = predicate.Or(x => x.Title.Contains(searchTerm)).Boost(2.0f);

            return predicate;
        }

        /// <summary>
        /// Search item mapped to SOLR index
        /// </summary>
        public class SearchModel : SearchResultItem
        {
            [IndexField("_name")]
            public virtual string ItemName { get; set; }

            [IndexField("_displayname")]
            public virtual string DispalyName { get; set; }

            [IndexField("itemurl")]
            public virtual string ItemUrl { get; set; }

            [IndexField("description")]
            public virtual string Description { get; set; } // Custom field on my template

            [IndexField("title")]
            public virtual string Title { get; set; } // Custom field on my template
        }

        /// <summary>
        /// Custom search result model for binding to front end
        /// </summary>
        public class SearchResult
        {
            public string Title { get; set; }

            public string Url { get; set; }

            public string Description { get; set; }
        }

        /// <summary>
        /// Custom search result model for binding to front end
        /// </summary>
        public class SearchResults
        {
            public List<SearchResult> Results { get; set; }
        }
    }
}
This code is best explained by breaking it down into sections:
  • SearchModel - this is the model that each item in the index is mapped to. It contains a base class with all of the standard Sitecore fields indexed. The items directly on the model are my custom fields in the index. The itemurl field is a clean implementation of the URL of the item/page that uses a Sitecore computed index field.
  • SearchResults - This is a model containing a list of search results (SearchResult model mapped from the SearchModel  with only the fields we need) which is bound to the view. It would normally contain more information such as total results, current page, etc.
  • GetSearchPredicate - Is where the main search logic is built up into a predicate. This is a simple example where like or contains is run on the whole search term. More advanced logic could be added to split a phrase into multiple terms and so on. A predicate can also be made up of multiple predicates if required.
  • DoSearch - The main piece of code to actually run the search. This gets the search index and queries it with the built up predicate. Any hits are mapped through to the cleaner model assigned to the search results view. In this case p;aging is not implemented, but the code to do this is commented out.
This is just an example of a minimal implementation of SOLR search in Sitecore 9 (from a back-end perspective). This can be expanded upon to provide more advanced concepts such as working out the field which is most relevant to the user's query (and displaying this on the front-end) or highlighting the search terms in the results. 

No comments:

Post a Comment