Prerequisites:
- Sitecore 10.2 instance
- Basic understanding of the following:
Challenge:
We can provide multiple search filters in the search scope query for an SXA search results component. We can use the predefined SXA search tokens in the filter.
If a single search filter starts with | then the Operation applied to the filter is |
+ | must (AND operation) |
– | not (NOT operation) |
Nothing is set or default | should (OR operation) |
The challenge with predefined SXA tokens like ItemsWithTheSameValueInField is that it always considers the operation “must”. As a result, even if we toggle the filter i.e. change the operation as shown below it does not affect the search result.
Same applies to SXA tokens like TaggedTheSameAsCurrentPage and TaggedWithAtLeastOneTagFromCurrentPage. Basically, the implementation of these two tokens is identical except for the filter type i.e. operation has different values. “must” operation is set in the case of TaggedTheSameAsCurrentPage, and the “should” operation is set in the case of TaggedWithAtLeastOneTagFromCurrentPage. The operation is hardcoded in the backend. This is awesome in scenarios where we do not need to toggle the filter operation and use these specific SXA tokens.
These 3 tokens compare the current page field value with the search results items’ field value to filter the result.
Solution:
We can implement new Search tokens inspired by the predefined token implementation with some modifications. We can supply the operation set by the content editor user in the search scope as in the following implementations.
Code commented with “setting the operation given in the scope” is the key, and also do check other comments for reference 🙂 .Â
CustomItemsWithTheSameValueInField – For a simple field type or field types where only a single ID is saved.
using Sitecore.ContentSearch.Utilities; using Sitecore.Data.Fields; using Sitecore.XA.Foundation.Search.Attributes; using Sitecore.XA.Foundation.Search.Pipelines.ResolveSearchQueryTokens; using System; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; namespace CustomSXA.Foundation.Search.SearchQueryToken { public class CustomItemsWithTheSameValueInField : ResolveSearchQueryTokensProcessor { protected string TokenPart { get; } = nameof(CustomItemsWithTheSameValueInField); protected string Operation { get; set; } = "should"; [SxaTokenKey] protected override string TokenKey => FormattableString.Invariant(FormattableStringFactory.Create("{0}|FieldName", (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]; //SearchStringModel holds a specific filter detail given in the scope if (model.Type.Equals("sxa") && this.ContainsToken(model)) { string str = model.Value.Replace(this.TokenPart, string.Empty).TrimStart('|'); Field field = args.ContextItem.Fields[str]; if (field != null) { this.Operation = model.Operation; //setting the operation given in the scope args.Models.Insert(index, this.BuildModel(str, field.Value)); //pass the field value for filter args.Models.Remove(model); } } } } 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; } }
Following is a short demo where fields Team and Buddy are of a field type Single line text and Droplink respectively.
ItemsWithIDsInFieldAsCurrentPage – For a complex field types where a single ID or multiple IDs are selected.
using Sitecore.ContentSearch.Utilities; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.XA.Foundation.Search.Attributes; using Sitecore.XA.Foundation.Search.Pipelines.ResolveSearchQueryTokens; using Sitecore.XA.Foundation.SitecoreExtensions.Extensions; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; namespace CustomSXA.Foundation.Search.SearchQueryToken { public class ItemsWithIDsInFieldAsCurrentPage : ResolveSearchQueryTokensProcessor { protected string TokenPart => nameof(ItemsWithIDsInFieldAsCurrentPage); protected string Operation { get; set; } = "should"; [SxaTokenKey] protected override string TokenKey => FormattableString.Invariant(FormattableStringFactory.Create("{0}|FieldName", (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.Is("sxa") && this.ContainsToken(model)) { string str = model.Value.Replace(this.TokenPart, string.Empty).TrimStart('|'); MultilistField field = (MultilistField)args.ContextItem.Fields[str]; if (field != null) { Item[] items = field.GetItems(); foreach (Item obj in ((IEnumerable<Item>)items).Reverse<Item>()) //loop through all the selected IDs { this.Operation = model.Operation; //setting the operation given in the scope args.Models.Insert(index, this.BuildModel(str, Convert.ToString(obj.ID))); //pass the selected item ID for filter } if (items.Length >= 2) index += items.Length - 1; args.Models.Remove(model); } } } } 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; } }
Following is a short demo where fields Services and Skills are of a field type Multilist.
Patch the above classes as below.
<?xml version="1.0"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <resolveSearchQueryTokens> <processor type="CustomSXA.Foundation.Search.SearchQueryToken.CustomItemsWithTheSameValueInField, CustomSXA.Foundation.Search" resolve="true" /> <processor type="CustomSXA.Foundation.Search.SearchQueryToken.ItemsWithIDsInFieldAsCurrentPage, CustomSXA.Foundation.Search" resolve="true" /> </resolveSearchQueryTokens> </pipelines> </sitecore> </configuration>
Refer to the list of the NuGet packages from here.
Check out the repository for the above implementation in the CustomSXA.Foundation.Search project.
Hope this helps.
Happy Sitecore Search Query Scoping 🙂