Saturday, February 24, 2018

Sitecore-Azure : Highlight search term in Azure Search Results

In my previous, we searched for a Sitecore item based on item id in Azure search index. Let us take a different example this time to see how to achieve search term highlighting in your project if you are using Azure search.

Suppose we have a FAQ page where a user can search for frequently asked questions.  We created a template for FAQ item with fields Question and Answer. Now suppose I want to search “Working hours” in the FAQ list I will fire below query in Azure Search Explorer:-
search=working hours&$select=question,answer&$filter=template_1 eq 'a68fcde9c8e34172aeac251f315ca7e5' and language_1 eq 'en'

In above query:-
1.       Search term is working hours
2.       Field to retrieve are question and answer
3.       We are filtering the results based on the template id of FAQ Item and English language
4.       Let us update search parameter class which we created in previous post
public class SearchParams
    {
        public SearchParams()
        {
            SortBy = new List<SortQuery>();
            FieldsToSearch = new List<string>();
            FieldsToRetrieve = new List<string>();
            SearchFilterQuery = new List<Tuple<string, string, string, string>>();
            Skip = 0;
            Top = 100000;
            FieldsToHighlight = new List<string>();
            HighlightPreTag = "<b>";
            HighlightPostTag = "</b>";
        }

        public List<Tuple<string,string,string,string>> SearchFilterQuery { get; set; }
        public int Top { get; set; }
        public int Skip { get; set; }       
        public IList<SortQuery> SortBy { get; set; }
        public IList<string> FieldsToSearch { get; set; }
        public IList<string> FieldsToRetrieve { get; set; }
        public bool IncludeTotalResultCount { get; set; }
        public IList<string> FieldsToHighlight { get; set; }
        public string HighlightPreTag { get; set; }
        public string HighlightPostTag { get; set; }
    }
5.       Now add fields in which you want to highlight the search text.
public List<FaqItem> GetFaqBySearchTerm(string searchTerm, string language)
    {
        LoggerHelper.WriteDebug("GetFaqBySearchTerm method: Started");
        var faqItems = new Lsit<FaqItem>();
        try
        {
            var fieldsToretrieve = new List<string>();
            fieldsToretrieve.Add(Constants.Question);
            fieldsToretrieve.Add(Constants.Answer);

            var results = GetFaqFromAzure(searchTerm, Constants.FaqTemplateId, fieldsToretrieve, language);
            if (results != null && results.Results.Count > 0)
            {
                foreach (var item in results.Results)               
                {
                                   FaqItem faq = new FaqItem();
                    faq.Question = item.Document.Question;
                        if (item.Highlights != null && item.Highlights.ContainsKey(Constants.Question))
                        {
                            var highlights = item.Highlights[Constants.SearchFaqTitleField];
                            faq.Title =GetHighlighedString(faq.Title, highlights);
                        }
                        faq.Answer = item.Document.Answer;
                        if (item.Highlights != null && item.Highlights.ContainsKey(Constants.Answer))
                        {
                            var highlights = item.Highlights[Constants.Answer];
                            faq.Description =GetHighlighedString(faq.Answer, highlights);                           
                        }
                    faqItems.Add(faq);
                }
            }
        }
        catch (Exception exc)
        {
            LoggerHelper.WriteDebug(string.Format("GetFaqBySearchTerm method Exception occured", exc.Message));
            throw;
        }
        LoggerHelper.WriteDebug("GetFaqBySearchTerm method: End");
        return faqItems;
    }


    
private DocumentSearchResult<FaqSearchResult> GetFaqFromAzure(string searchTerm, string templateId,
        List<string> fieldsToretrieve, string language = "en", List<SortQuery> sortQueries = null)
    {
        DocumentSearchResult<LinkBoxSearchResult> response = null;
        if (!string.IsNullOrEmpty(searchTerm) && !string.IsNullOrEmpty(templateId))
        {
            string searchText = Constants. searchTerm;

            SearchParams searchParameters = new SearchParams();

            //Filter For language
            searchParameters.SearchFilterQuery.Add(new Tuple<stringstringstringstring>(Constants.LanguageField,
            Convert.ToString(Enums.SearchOperator.eq), "'" + language + "'", Enums.MultiCriteriaOperation.and.ToString()));

            //Filter For template
            searchParameters.SearchFilterQuery.Add(new Tuple<stringstringstringstring>(Constants.TemplateField,
            Convert.ToString(Enums.SearchOperator.eq), "'" + GenericUtilities.GetAzureFormattedGuid(templateId) + "'"string.Empty));

            ////Field That We want to Show records in View Page
            searchParameters.FieldsToRetrieve = fieldsToretrieve;

            searchParameters.FieldsToHighlight.Add(Constants.Question);
            searchParameters.FieldsToHighlight.Add(Constants.Answer);

            //How many records want to display page at a time
            searchParameters.Top = 1;
            //If Skip Count has any count value
            searchParameters.Skip = 0;

            if (sortQueries != null)
                searchParameters.SortBy = sortQueries;

            response = GetFaq(searchTerm, searchParameters);
        }
        return response;
    }

    private DocumentSearchResult<FaqSearchResult> GetFaq(string query, SearchParams searchParams)
    {
        ISearchIndexClient searchIndexClient = SearchServiceOperations.CreateSearchIndexClient();
        SearchParameters parameters = new SearchParameters()
        {
            Filter = QueryBuilder.GenerateFilterQuery(searchParams.SearchFilterQuery),
            OrderBy = QueryBuilder.GetSelectedSort(searchParams),
            QueryType = QueryType.Full,
            SearchFields = searchParams.FieldsToSearch,
            IncludeTotalResultCount = searchParams.IncludeTotalResultCount,
            Select = searchParams.FieldsToRetrieve,
            Skip = searchParams.Skip,
            Top = searchParams.Top,
            HighlightFields=searchParams.FieldsToHighlight,
            HighlightPreTag=searchParams.HighlightPreTag,
            HighlightPostTag=searchParams.HighlightPostTag
        };
        return searchIndexClient.Documents.Search<LinkBoxSearchResult>(query, parameters);
    }

        private string GetHighlighedString(string value, IList<string> highlights)
        {
            if (!string.IsNullOrEmpty(value) && highlights.Any())
            {
                var hits = highlights.Select(h => h.Replace("<b>", string.Empty)
                                .Replace("</b>", string.Empty)).ToList();
                for (int i = 0; i < highlights.Count; i++)
                {
                    value = value.Replace(hits[i], highlights[i]);
                }
            }
            return value;
        }
}



Friday, February 23, 2018

Search Sitecore Item In Azure Search Index

I have created an item in Sitecore with below fields and I pulled it from the Azure search based on item Id. Let see how we can do this:-

1.       Created an entity for LinkBox
public class LinkBox
{
    public string Title { get; set; }
    public string Description { get; set; }
    public string ImageUrl { get; set; }
    public string LinkText { get; set; }
    public string LinkUrl { get; set; }
}

2.       Created a search parameters class.
public class SearchParams
{
    public SearchParams()
    {
        SortBy = new List<SortQuery>();
        FieldsToSearch = new List<string>();
        FieldsToRetrieve = new List<string>();
        SearchFilterQuery = new List<Tuple<string, string, string, string>>();
        Skip = 0;
        Top = 100000;
    }

    public List<Tuple<string, string, string, string>> SearchFilterQuery { get; set; }
    public int Top { get; set; }
    public int Skip { get; set; }
    public IList<SortQuery> SortBy { get; set; }
    public IList<string> FieldsToSearch { get; set; }
    public IList<string> FieldsToRetrieve { get; set; }
    public bool IncludeTotalResultCount { get; set; }
}

3.       Created a LinkBoxSearchResult class to map the fields indexed in a document in azure search index.
public class AzureContentSearch
{
    [JsonProperty(PropertyName = "id_1")]
    public string ItemId { get; set; }
}

public class LinkBoxSearchResult : AzureContentSearch
{
    [JsonProperty(PropertyName = "title")]
    public string Title { get; set; }
    [JsonProperty(PropertyName = "description")]
    public string Description { get; set; }
    [JsonProperty(PropertyName = "imageurlfield")]
    public string ImageUrl { get; set; }
    [JsonProperty(PropertyName = "linktextfield")]
    public string LinkText { get; set; }
    [JsonProperty(PropertyName = "linkurlfield")]
    public string LinkUrl { get; set; }
}
4.       Created a static a client for Azure search. Search service name and Admin API key is residing in the app settings for me. You need to add your Azure search-service name and Admin API Key in the app setting file.
public static class SearchServiceOperations
{
    public static ILogger LoggerHelper { get; set; }
    private static readonly object threadlock = new object();
    private static SearchServiceClient serviceOperations;

    static SearchServiceOperations()
    {
        LoggerHelper = UnityResolver.Resolve<ILogger>();
    }

    public static SearchServiceClient CreateSearchServiceClient()
    {
        LoggerHelper.WriteDebug("CreateSearchServiceClient Method Started.");
        try
        {
            if (serviceOperations == null)
            {
                lock (threadlock)
                {
                    string searchServiceName = ConfigurationManager.AppSettings[Constants.SearchServiceName];
                    string adminApiKey = ConfigurationManager.AppSettings[Constants.SearchServiceAdminApiKey];
                    serviceOperations = new SearchServiceClient(searchServiceName, new SearchCredentials(adminApiKey));
                }
            }
        }
        catch (Exception ex)
        {
            LoggerHelper.WriteDebug("CreateSearchServiceClient Method Exception Caught. Exception Message is: " + ex.Message);
        }
        LoggerHelper.WriteDebug("CreateSearchServiceClient Method Ended.");
        return serviceOperations;
    }

    /// <summary>
    /// Method to Get the Index Client for the Particular Index
    /// </summary>
    /// <param name="IndexName">Index Name</param>
    /// <returns>ISearchIndexClient</returns>
    public static ISearchIndexClient CreateSearchIndexClient(string IndexName = Constants.WebIndex)
    {
        try
        {
            LoggerHelper.WriteDebug("CreateSearchIndexClient Method returning the Index.");
            return serviceOperations.Indexes.GetClient(IndexName);
        }
        catch (Exception ex)
        {
            LoggerHelper.WriteDebug("CreateSearchIndexClient Method Exception Caught. Exception Message is: " + ex.Message);
            return null;
        }
    }
}
5.       Added Content Repository to write logic to pull the data from Azure Search Index. You can analyze and map with the query mentioned below which we use in azure search explorer
search=id_1:d68fcae9c8e34172aeac251f315bd7e5&$select=title,description&$filter=template_1 eq 'a68fcde9c8e34172aeac251f315ca7e5' and language_1 eq 'en'

·         search=id_1:d68fcae9c8e34172aeac251f315bd7e5 Here id_1 is the item id field in azure index and value is the item Guid for our link box item in lower case without any special characters.
·         select=title,description  Fields to retrieve in the below code refers to this section of the query
·         filter: In this section of query we have passed language and template id Guid in the ‘and’ clause.

public enum MultiCriteriaOperation
{
    and = 1,
    or = 2,
}
public class SortQuery
{
    public string FieldName { get; set; }
    public SortOrder Order { get; set; }
    public enum SortOrder { Ascending, Descending, }
}
public class ContentRepository : IContentRepository
{
    public ILogger LoggerHelper { get; set; }

    public ContentRepository(ILogger _logger, ISearch _azureSearch)
    {
        SearchServiceOperations.CreateSearchServiceClient();
        LoggerHelper = UnityResolver.Resolve<ILogger>();
    }

    char[] specialCharacters = new char[] { '+', ',', '.', '!', '(', ')', '{', '}', '[', ']', '^', '~', '*', '?', ':', '\\' };


    public LinkBox GetLinkBoxDetailsById(string id, string language)
    {
        LoggerHelper.WriteDebug("GetLinkBoxDetailsById method: Started");
        var linkBoxItem = new LinkBox();
        try
        {
            var fieldsToretrieve = new List<string>();
            fieldsToretrieve.Add(Constants.Title);
            fieldsToretrieve.Add(Constants.Description);
            fieldsToretrieve.Add(Constants.ImageUrlfield);
            fieldsToretrieve.Add(Constants.LinkUrl);
            fieldsToretrieve.Add(Constants.LinkTextField);

            var results = GetLinkBoxByIdFromAzure(id, Constants.LinkBoxTemplateId, fieldsToretrieve, language);
            if (results != null && results.Results.Count > 0)
            {
                var item = results.Results.First();
                if (item != null)
                {
                    linkBoxItem.Title = item.Document?.Title;
                    linkBoxItem.Description = item.Document?.Description;
                    linkBoxItem.ImageUrl = item.Document?.ImageUrl;
                    linkBoxItem.LinkUrl = item.Document?.LinkUrl;
                    linkBoxItem.LinkText = item.Document?.LinkText;
                }
            }
        }
        catch (Exception exc)
        {
            LoggerHelper.WriteDebug(string.Format("GetLinkBoxDetailsById method Exception occured", exc.Message));
            throw;
        }
        LoggerHelper.WriteDebug("GetLinkBoxDetailsById method: End");
        return linkBoxItem;
    }

    private DocumentSearchResult<LinkBoxSearchResult> GetLinkBoxByIdFromAzure(string id, string templateId,
        List<string> fieldsToretrieve, string language = "en", List<SortQuery> sortQueries = null)
    {
        DocumentSearchResult<LinkBoxSearchResult> response = null;
        if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(templateId))
        {
            string searchText = Constants.IdField + ":" + GenericUtilities.GetAzureFormattedGuid(id).ToLower();

            SearchParams searchParameters = new SearchParams();

            //Filter For language
            searchParameters.SearchFilterQuery.Add(new Tuple<string, string, string, string>(Constants.LanguageField,
            Convert.ToString(Enums.SearchOperator.eq), "'" + language + "'", Enums.MultiCriteriaOperation.and.ToString()));

            //Filter For template
            searchParameters.SearchFilterQuery.Add(new Tuple<string, string, string, string>(Constants.TemplateField,
            Convert.ToString(Enums.SearchOperator.eq), "'" + GenericUtilities.GetAzureFormattedGuid(templateId) + "'", string.Empty));

            ////Field That We want to Show records in View Page
            searchParameters.FieldsToRetrieve = fieldsToretrieve;

            //How many records want to display page at a time
            searchParameters.Top = 1;
            //If Skip Count has any count value
            searchParameters.Skip = 0;

            if (sortQueries != null)
                searchParameters.SortBy = sortQueries;

            response = GetLinkBox(searchText, searchParameters);
        }
        return response;
    }

    private DocumentSearchResult<LinkBoxSearchResult> GetLinkBox(string query, SearchParams searchParams)
    {
        ISearchIndexClient searchIndexClient = SearchServiceOperations.CreateSearchIndexClient();
        SearchParameters parameters = new SearchParameters()
        {
            Filter = QueryBuilder.GenerateFilterQuery(searchParams.SearchFilterQuery),
            OrderBy = QueryBuilder.GetSelectedSort(searchParams),
            QueryType = QueryType.Full,
            SearchFields = searchParams.FieldsToSearch,
            IncludeTotalResultCount = searchParams.IncludeTotalResultCount,
            Select = searchParams.FieldsToRetrieve,
            Skip = searchParams.Skip,
            Top = searchParams.Top
        };
        return searchIndexClient.Documents.Search<LinkBoxSearchResult>(query, parameters);
    }
}

Call the content repository from your controller action by passing item id and context language.

In case you feel any item missing while implementing this on your project, write a comment to me so that I can add the missing piece for you.

Thanks