Skip to main content

Sitecore

Query string-based custom SXA Search tokens for various search results scenarios

Istock 648272290 Featured Image

Challenge:

Setting up a generic search result page with a Search box and Search Results components can be simple. Occasionally, we get the specific requirement for the search page which focuses on some particular fields and specific sets of search results.

Let’s consider a page having a search results component showing results filtered based on a query string like fullname=Rakesh Gupta. Here, fullname is a computed field that has concatenated values from fields “First name”, “Middle name” and “Last Name” with a space delimiter.

Following are the search results cases we will explore the implementation.

  1. Results where the given field’s value matches the exact input words and are case insensitive. e.g. When “Rakesh Gupta” or “rakesh gupta” is searched, then the result contains all the entries matching “Rakesh Gupta”.
  2. Results where the given field’s value matches any of the input words and are case insensitive. e.g. When “Rakesh Gupta” or “rakesh gupta” is searched, then the result contains either “rakesh” or “gupta”.
  3. Results where the given field’s value starts with any of the input words.
  4. Results where the given field value contains any of the input words as a substring.
  5. Results where the given field value matches the exact input words and are case-sensitive.

Prerequisite:

  1. Sitecore 10.2 with an SXA tenant site.
  2. Good to quick read the post – Custom SXA token for search scope query to support all the filter operations

Solution:

To test the implementation, create a new page template named “Developer” inheriting the existing Page template. Add fields “First Name”, “Middle Name” and “Last Name” of Single line text field type. Somewhere under the Home item, create a new page from the existing Page template for search result purposes. Add the search results component to it. Create multiple items from the Developer template. Do provide the newly added fields’ values in these items.

Do rebuild your indexes after you deploy the solution or do reindex the tree at the parent item level if you are testing against the master database to save some indexing time 🙂

You may check out the GitHub repository for the source code and configuration files.

FullName computed field class

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Data.Items;

namespace CustomSXA.Foundation.Search.ComputedFields
{
    public class FullName : AbstractComputedIndexField
    {
        public override object ComputeFieldValue(IIndexable indexable)
        {
            Item obj = (Item)(indexable as SitecoreIndexableItem);
            if (obj == null)
                return (object)null;

            string fullname = (obj["First Name"] + " " + obj["Middle Name"] + " " + obj["Last Name"]).Replace("  ", " ");
            if (string.IsNullOrWhiteSpace(fullname.Trim()))
                return null;
            return fullname;
        }
    }
}

Patch the above class as below.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <contentSearch>
      <indexConfigurations>
        <defaultSolrIndexConfiguration type="Sitecore.ContentSearch.SolrProvider.SolrIndexConfiguration, Sitecore.ContentSearch.SolrProvider">
          <documentOptions type="Sitecore.ContentSearch.SolrProvider.SolrDocumentBuilderOptions, Sitecore.ContentSearch.SolrProvider">
            <fields hint="raw:AddComputedIndexField">
              <!--fullname is used in case 1, case 2, case 3 and case 4 for demo purpose. Note the return type here is text-->
              <field fieldName="fullname" returnType="text">CustomSXA.Foundation.Search.ComputedFields.FullName, CustomSXA.Foundation.Search</field>
              <!--exactfullname is used in case 5 - for exact search phrase match and is case sensitive. Note the return type here is string-->
              <field fieldName="exactfullname" returnType="string">CustomSXA.Foundation.Search.ComputedFields.FullName, CustomSXA.Foundation.Search</field>
            </fields>
          </documentOptions>
        </defaultSolrIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
  </sitecore>
</configuration>

For each of the above cases, let’s have a custom SXA Search token and a scope item.

Following is the base class inherited by the concrete classes for the SXA token implementation.

using Sitecore.ContentSearch.Utilities;
using Sitecore.XA.Foundation.Search.Attributes;
using Sitecore.XA.Foundation.Search.Pipelines.ResolveSearchQueryTokens;
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Web;

namespace CustomSXA.Foundation.Search.SearchQueryToken
{
    public abstract class ItemsWithQueryStringValueInField : ResolveSearchQueryTokensProcessor
    {
        protected abstract string TokenPart { get; }

        protected abstract string Operation { get; set; }

        [SxaTokenKey]
        protected override string TokenKey => FormattableString.Invariant(FormattableStringFactory.Create("{0}|ParamName", (object)this.TokenPart));

        public override void Process(ResolveSearchQueryTokensEventArgs args)
        {
            if (args.ContextItem == null)
                return;
            for (int index = 0; index < args.Models.Count; ++index)
            {
                SearchStringModel model = args.Models[index];
                if (model.Type.Equals("sxa") && this.ContainsToken(model))
                {
                    string paramName = model.Value.Replace(this.TokenPart, string.Empty).TrimStart('|');
                    if (string.IsNullOrEmpty(paramName))
                        return;
                    this.Operation = model.Operation; //setting the operation given in the scope
                    UpdateFilter(paramName, model, args, index);
                }
            }
        }

        protected abstract void UpdateFilter(string paramName, SearchStringModel model, ResolveSearchQueryTokensEventArgs args, int index);

        protected virtual SearchStringModel BuildModel(
          string replace,
          string fieldValue)
        {
            return new SearchStringModel("custom", FormattableString.Invariant(FormattableStringFactory.Create("{0}|{1}", (object)replace.ToLowerInvariant(), (object)fieldValue)))
            {
                Operation = this.Operation
            };
        }

        protected override bool ContainsToken(SearchStringModel m) => Regex.Match(m.Value, FormattableString.Invariant(FormattableStringFactory.Create("{0}\\|[a-zA-Z ]*", (object)this.TokenPart))).Success;

        protected string GetURLRefererQueryStringParamValue(string paramName)
        {
            var queryCollection = HttpUtility.ParseQueryString(HttpContext.Current.Request.UrlReferrer.Query);
            if (queryCollection.AllKeys.Contains(paramName, StringComparer.OrdinalIgnoreCase))
            {
                return queryCollection.Get(paramName) ?? queryCollection.Get(paramName.ToLower()); //Please do sanitize the query string value or validate it based on the specific requirement.
            }
            return null;
        }
    }
}

Case 1: ItemsWithQueryStringValueInFieldAllWords

Create the scope item named ItemsWithQueryStringValueInFieldAllWords under /sitecore/content/{tenant}/{site}/Settings/Scopes and provide the below scope query value. Do update the template ID with your Developer template or another template whose items you want to consider for the results.

+template:{4746BE25-C48D-4E48-9B4B-E531C57C6132};+sxa:CurrentLanguage;+sxa:ItemsWithQueryStringValueInFieldAllWords|fullname

ItemsWithQueryStringValueInFieldAllWords class

using Sitecore.ContentSearch.Utilities;
using Sitecore.XA.Foundation.Search.Pipelines.ResolveSearchQueryTokens;
using System.Web;

namespace CustomSXA.Foundation.Search.SearchQueryToken
{
    public class ItemsWithQueryStringValueInFieldAllWords : ItemsWithQueryStringValueInField
    {
        protected override string TokenPart => nameof(ItemsWithQueryStringValueInFieldAllWords);

        protected override string Operation { set; get; }

        protected override void UpdateFilter(string paramName, SearchStringModel model, ResolveSearchQueryTokensEventArgs args, int index)
        {
            string queryStringValue = HttpContext.Current.Request.QueryString[paramName];
            if (string.IsNullOrEmpty(queryStringValue)) //Please do sanitize the query string value or validate it based on the specific requirement.
                queryStringValue = GetURLRefererQueryStringParamValue(paramName);
            if (string.IsNullOrEmpty(queryStringValue))
                return;
            args.Models.Insert(index, this.BuildModel(paramName, queryStringValue)); //pass the field value for filter
            args.Models.Remove(model);
        }
    }
}

Patch the class given at the end, or check CustomSXA.Foundation.Search.config file.

Go to the Search result page and edit the search results control properties and set ItemsWithQueryStringValueInFieldAllWords in the Scope field, save and test the page as shown below.

 Itemswithquerystringvalueinfieldallwords

Itemswithquerystringvalueinfieldallwords

Similarly, test for other cases as given below.

Case 2: ItemsWithQueryStringValueInFieldAnyWord

+template:{4746BE25-C48D-4E48-9B4B-E531C57C6132};+sxa:CurrentLanguage;sxa:ItemsWithQueryStringValueInFieldAnyWord|fullname

ItemsWithQueryStringValueInFieldAnyWord class

using Sitecore.ContentSearch.Utilities;
using Sitecore.XA.Foundation.Search.Pipelines.ResolveSearchQueryTokens;
using System.Web;

namespace CustomSXA.Foundation.Search.SearchQueryToken
{
    public class ItemsWithQueryStringValueInFieldAnyWord : ItemsWithQueryStringValueInField
    {
        protected override string TokenPart => nameof(ItemsWithQueryStringValueInFieldAnyWord);

        protected override string Operation { set; get; }

        protected override void UpdateFilter(string paramName, SearchStringModel model, ResolveSearchQueryTokensEventArgs args, int index)
        {
            string queryStringValue = HttpContext.Current.Request.QueryString[paramName];
            if (string.IsNullOrEmpty(queryStringValue))
                queryStringValue = GetURLRefererQueryStringParamValue(paramName);
            if (string.IsNullOrEmpty(queryStringValue))
                return;
            //split the search phrase into words and pass each word for filter with OR condition i.e. should operation
            //so we can filter result containing any of them.
            //note the should operation is set in the base class and was supplied from the scope query filter i.e. no toggle filter set in the scope so default is 'should'
            string[] allWords = queryStringValue.Split(' ');        
            for (int i = 0; i < allWords.Length; i++)
            {
                args.Models.Insert(index, this.BuildModel(paramName, allWords[i])); //pass the field and field value for filter
                args.Models.Remove(model);
            }
            args.Models.Insert(index, this.BuildModel(paramName, queryStringValue)); //pass the query string value i.e. the input words phrase
            args.Models.Remove(model);
        }
    }
}
Itemswithquerystringvalueinfieldanyword

Itemswithquerystringvalueinfieldanyword

Case 3: ItemsWithQueryStringValueInFieldStartsWithAnyWord

+template:{4746BE25-C48D-4E48-9B4B-E531C57C6132};+sxa:CurrentLanguage;sxa:ItemsWithQueryStringValueInFieldStartsWithAnyWord|fullname

ItemsWithQueryStringValueInFieldStartsWithAnyWord class

using Sitecore.ContentSearch.Utilities;
using Sitecore.XA.Foundation.Search.Pipelines.ResolveSearchQueryTokens;
using System.Web;

namespace CustomSXA.Foundation.Search.SearchQueryToken
{
    public class ItemsWithQueryStringValueInFieldStartsWithAnyWord : ItemsWithQueryStringValueInField
    {
        protected override string TokenPart => nameof(ItemsWithQueryStringValueInFieldStartsWithAnyWord);

        protected override string Operation { set; get; }

        protected override void UpdateFilter(string paramName, SearchStringModel model, ResolveSearchQueryTokensEventArgs args, int index)
        {
            string queryStringValue = HttpContext.Current.Request.QueryString[paramName];
            if (string.IsNullOrEmpty(queryStringValue))
                queryStringValue = GetURLRefererQueryStringParamValue(paramName);
            if (string.IsNullOrEmpty(queryStringValue))
                return;
            //split the search phrase into words and pass each word for filter with OR condition i.e. should operation
            //so we can filter result containing any of them.
            //note the should operation is set in the base class and was supplied from the scope query filter i.e. no toggle filter set in the scope so default is 'should'
            string[] allWords = queryStringValue.Split(' ');        
            for (int i = 0; i < allWords.Length; i++)
            {
                if (!string.IsNullOrEmpty(allWords[i]))
                {
                    //pass the field name and value for filter.
                    //Since * is applied in end, it will consider result items where field value starts with the given input words.
                    args.Models.Insert(index, this.BuildModel(paramName, allWords[i] + "*")); 
                    args.Models.Remove(model);
                }
            }
            args.Models.Insert(index, this.BuildModel(paramName, queryStringValue)); //pass the query string value i.e. the input words phrase
            args.Models.Remove(model);
        }
    }
}
Itemswithquerystringvalueinfieldstartswithanyword

Itemswithquerystringvalueinfieldstartswithanyword

Case 4: ItemsWithQueryStringValueInFieldSubstringAnyWord

+template:{4746BE25-C48D-4E48-9B4B-E531C57C6132};+sxa:CurrentLanguage;sxa:ItemsWithQueryStringValueInFieldSubstringAnyWord|fullname

ItemsWithQueryStringValueInFieldSubstringAnyWord class

using Sitecore.ContentSearch.Utilities;
using Sitecore.XA.Foundation.Search.Pipelines.ResolveSearchQueryTokens;
using System.Web;

namespace CustomSXA.Foundation.Search.SearchQueryToken
{
    public class ItemsWithQueryStringValueInFieldSubstringAnyWord : ItemsWithQueryStringValueInField
    {
        protected override string TokenPart => nameof(ItemsWithQueryStringValueInFieldSubstringAnyWord);

        protected override string Operation { set; get; }

        protected override void UpdateFilter(string paramName, SearchStringModel model, ResolveSearchQueryTokensEventArgs args, int index)
        {
            string queryStringValue = HttpContext.Current.Request.QueryString[paramName];
            if (string.IsNullOrEmpty(queryStringValue))
                queryStringValue = GetURLRefererQueryStringParamValue(paramName);
            if (string.IsNullOrEmpty(queryStringValue))
                return;
            //split the search phrase into words and pass each word for filter with OR condition i.e. should operation
            //so we can filter result containing any of them.
            //note the should operation is set in the base class and was supplied from the scope query filter i.e. no toggle filter set in the scope so default is 'should'
            string[] allWords = queryStringValue.Split(' ');
            for (int i = 0; i < allWords.Length; i++)
            {
                if (!string.IsNullOrEmpty(allWords[i]))
                {
                    //pass the field name and value for filter.
                    //Since * is applied in start and end, it will be considered as substring.
                    args.Models.Insert(index, this.BuildModel(paramName, "*" + allWords[i] + "*"));
                    args.Models.Remove(model);
                }
            }
            args.Models.Insert(index, this.BuildModel(paramName, queryStringValue)); //pass the query string value i.e. the input words phrase
            args.Models.Remove(model);
        }
    }
}
Itemswithquerystringvalueinfieldsubstringanyword

Itemswithquerystringvalueinfieldsubstringanyword

Case 5 : ItemsWithQueryStringValueInFieldExactMatch

+template:{4746BE25-C48D-4E48-9B4B-E531C57C6132};+sxa:CurrentLanguage;+sxa:ItemsWithQueryStringValueInFieldExactMatch|exactfullname

ItemsWithQueryStringValueInFieldExactMatch class

using Sitecore.ContentSearch.Utilities;
using Sitecore.XA.Foundation.Search.Pipelines.ResolveSearchQueryTokens;
using System.Web;

namespace CustomSXA.Foundation.Search.SearchQueryToken
{
    public class ItemsWithQueryStringValueInFieldExactMatch : ItemsWithQueryStringValueInField
    {
        protected override string TokenPart => nameof(ItemsWithQueryStringValueInFieldExactMatch);

        protected override string Operation { set; get; }

        protected override void UpdateFilter(string paramName, SearchStringModel model, ResolveSearchQueryTokensEventArgs args, int index)
        {
            string queryStringValue = HttpContext.Current.Request.QueryString[paramName];
            if (string.IsNullOrEmpty(queryStringValue))
                queryStringValue = GetURLRefererQueryStringParamValue(paramName);
            if (string.IsNullOrEmpty(queryStringValue))
                return;
            args.Models.Insert(index, this.BuildModel(paramName, queryStringValue)); //pass the query string value i.e. the input words phrase
            args.Models.Remove(model);
        }
    }
}
Itemswithquerystringvalueinfieldexactmatch

Itemswithquerystringvalueinfieldexactmatch

ItemsWithMultipleQueryStringValueInField

Moreover, if we want to supply multiple fields or computed field filters we can use the above tokens multiple times in the same scope query. Otherway, we can have a single token implementation and supply field names separated by commas as below.

+template:{4746BE25-C48D-4E48-9B4B-E531C57C6132};+sxa:CurrentLanguage;+sxa:ItemsWithMultipleQueryStringValueInField|first name,middle name,last name

ItemsWithMultipleQueryStringValueInField class

using Sitecore.ContentSearch.Utilities;
using Sitecore.XA.Foundation.Search.Pipelines.ResolveSearchQueryTokens;
using System.Web;

namespace CustomSXA.Foundation.Search.SearchQueryToken
{
    public class ItemsWithMultipleQueryStringValueInField : ItemsWithQueryStringValueInField
    {
        protected override string TokenPart => nameof(ItemsWithMultipleQueryStringValueInField);

        protected override string Operation { set; get; }

        protected override void UpdateFilter(string paramName, SearchStringModel model, ResolveSearchQueryTokensEventArgs args, int index)
        {
            string[] queryStringParams = paramName?.Split(new char[] { ',' });
            if (queryStringParams?.Length > 0)
            {
                foreach (var param in queryStringParams)
                {
                    string queryStringValue = HttpContext.Current.Request.QueryString[param] ?? HttpContext.Current.Request.QueryString[param.ToLower()];
                    if (string.IsNullOrEmpty(queryStringValue))
                        queryStringValue = GetURLRefererQueryStringParamValue(param);
                    if (string.IsNullOrEmpty(queryStringValue))
                        continue;
                    args.Models.Insert(index, this.BuildModel(param, queryStringValue)); //pass the field value for filter
                    args.Models.Remove(model);
                }
            }
        }
    }
}
Itemswithmultiplequerystringvalueinfield

Itemswithmultiplequerystringvalueinfield

Patch all the above classes as shown below.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <resolveSearchQueryTokens>
        <processor type="CustomSXA.Foundation.Search.SearchQueryToken.ItemsWithQueryStringValueInFieldAllWords, CustomSXA.Foundation.Search" resolve="true" />
        <processor type="CustomSXA.Foundation.Search.SearchQueryToken.ItemsWithQueryStringValueInFieldAnyWord, CustomSXA.Foundation.Search" resolve="true" />
        <processor type="CustomSXA.Foundation.Search.SearchQueryToken.ItemsWithQueryStringValueInFieldStartsWithAnyWord, CustomSXA.Foundation.Search" resolve="true" />
        <processor type="CustomSXA.Foundation.Search.SearchQueryToken.ItemsWithQueryStringValueInFieldSubstringAnyWord, CustomSXA.Foundation.Search" resolve="true" />
        <processor type="CustomSXA.Foundation.Search.SearchQueryToken.ItemsWithQueryStringValueInFieldExactMatch, CustomSXA.Foundation.Search" resolve="true" />
        <processor type="CustomSXA.Foundation.Search.SearchQueryToken.ItemsWithMultipleQueryStringValueInField, CustomSXA.Foundation.Search" resolve="true" />        
      </resolveSearchQueryTokens>
    </pipelines>
  </sitecore>
</configuration>

Check the NuGet packages from here.

NOTE:

We can also pass the query string parameters with hash i.e. as part of the hash value as shown below. This does not reload the page, instead gets the result from //sxa/search/results API and updates the search results component.

Query Strings Parameters with Hash

Query Strings Parameters with Hash

The direct query string is accessed via referer in code.

Direct Query Strings Parameters

Direct Query Strings Parameters

One can have the form from which we can accept the inputs and pass them to hash query strings or direct URL query strings to filter the search results.

Please do sanitize the query string value or validate it based on your requirement in code from a security perspective.

The overall idea of this post is to understand the different output cases and how it is implemented at the code level so that when we have a very niche requirement then we can tailor the code accordingly.

Hope this helps. Happy Sitecore search scoping!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Sandeepkumar Gupta

Sandeepkumar Gupta is a Lead Technical Consultant at Perficient. He enjoys research and development work in Sitecore. He has contributed to Sitecore projects based on SXA, ASP.NET MVC, and Web Forms. He has also contributed to Sitecore upgrade projects.

More from this Author

Categories
Follow Us
TwitterLinkedinFacebookYoutubeInstagram