Wednesday, January 2, 2019

Extending SXA search queries with SearchQueryTokens

Tokens in Sitecore Search

SXA comes with a set of tokens that you can use in search queries to apply additional search filters.

They can be very useful when creating queries that will be used on multiple pages. In my case, I wanted to extend a search scope with some business logic based on the value of a field on the current page.

The "ItemsWithTheSameValueInField" came close to what I needed but not entirely..  but these tokens can be easily extended. Let's implement our own ResolveSearchQueryTokensProcessor!

ResolveSearchQueryTokens Processor

To start my implementation, I had a (dot)peek at the ItemsWithTheSameValueInField source code as mine would be rather similar. First thing to do is define the key for our SXA token:
[SxaTokenKey]
protected override string TokenKey => FormattableString.Invariant(FormattableStringFactory.Create("{0}|FieldName", "YourTokenName"));

Next thing is override the Process method:
public override void Process(ResolveSearchQueryTokensEventArgs args)
{
  if (args.ContextItem == null)
    return;

  for (var index = 0; index < args.Models.Count; ++index)
  {
    var model = args.Models[index];
    if (model.Type.Equals("sxa", StringComparison.OrdinalIgnoreCase) && ContainsToken(model))
    {
      var fieldName = model.Value.Replace(TokenPart, string.Empty).TrimStart('|');
      var field = args.ContextItem.Fields[fieldName];
      if (field?.Value != null)
      {
        args.Models.Insert(index, BuildModel(fieldName, field.Value));
      }

      args.Models.Remove(model);
    }
  }
}

Let's see what is happening here:

Find our SearchStringModel

The code loops over all the SearchStringModel objects in the arguments. Each SearchStringModel defines a part of the query and has a value, a type and an operation.  When looking at the search box interface like in the above image:
  • the type of the SearchStringModel is the first part (in the image above that would be "Sxa") 
  • the value of the model is the second part (e.g. "ItemsWithTheSameValueInField|FieldName")
  • the operation is "must", "should" or "not"

So we search until we find a model of type sxa. The ContainsToken function adds this check to make sure we have our the token that corresponds with our code:
protected override bool ContainsToken(SearchStringModel m)
{
  return Regex.Match(m.Value, FormattableString.Invariant(FormattableStringFactory.Create("{0}\\|[a-zA-Z ]*", "YourTokenName"))).Success;
}

If the model is found, we fetch the provided FieldName and test if the context item has a value in this field. If that is the case, we build a new SearchStringModel to be inserted in the list. The current model is removed as that has been handled.

Build a SearchStringModel

protected virtual SearchStringModel BuildModel(string fieldName, string fieldValue)
{
  var name = SomeBusinessLogicWithFieldName(fieldName);
  var value = SomeBusinessLogicWithFieldValue(fieldValue);
  return new SearchStringModel("custom", FormattableString.Invariant(FormattableStringFactory.Create("{0}|{1}", name, value)))
  {
    Operation = "must"
  };
}

I am building a SearchStringModel based on the field name and value. The model type is "custom" as we are not using predefined Sitecore fields. You can test such queries in a search box as well. If you would use them in a SXA scope, the scope string would look like "+custom:fieldName|fieldValue".


ResolveSearchQueryToken configuration

To tell Sitecore that we have our own tokens, we have to add the processor to a pipeline:
<sitecore>
  <pipelines>
    <resolveSearchQueryTokens>
   <processor type="YourNameSpace.YourType, YourAssembly" resolve="true" patch:before = "*[1]" />
    </resolveSearchQueryTokens>
  </pipelines>
</sitecore>
With this patch I am including our tokens as the first one in the list.

Once this config patch and the code is available, your Sitecore instance will detect the SxaToken and it will be available in the search list once you type "Sxa".


Conclusion / Issues

I have used this succesfully in scopes with SearchResults components to make them aware of the context item (adapting the actual query based on a field value of the current page). You can of course make the code as complex as you would like
  • pass more parameters in
  • create multiple models based on your data
  • ...

I did find one issue though: in the SearchResults component we have a ContextItem. But that is not always the case. Our code will not break (we do check for null) but the extra filtering is not happening in that case. Sxa's checklist facet component -in 1.7.1- does not send the ContextItem and it does make sense that a facet uses the same data as the corresponding results component.
I've created a Sitecore Support ticket to fix this and did get a patch (ref. 301406).

But (again) a very nice extension point for the Sitecore Experience Accelerator.

2 comments:

  1. The process method is not getting called even though I can see the newly created token. Any idea why?

    ReplyDelete
    Replies
    1. Find me on Slack or StackExchange (https://sitecore.stackexchange.com) for questions like this - those platforms are better suited.

      Delete