SXA is all about accelerating the development by providing loads of features and component OOTB. In the past few years, I have been using search components a lot. E.g. property listing, product listing and many more.
Search component works with the Search Scope where user can provide the search query and limit the result. The search query can be extended by using SXA search tokens.
Nice article by Gert Gullentops : https://ggullentops.blogspot.com/2019/01/extending-sxa-search-query-tokens.html
Limitation:
I have been working on a real-estate website that required immense search functionality. It involved numerous business logic to search the properties based on given filters. Sitecore search query interface generates linear query E.g.
q=(((_path:(3936107d3534414c860878b3fdf61c76) AND _language:(en)) AND _latestversion:(True)) AND _templatename:(Page))&start=0&rows=1000000&fl=*,score&fq=_indexname:(sitecore_master_index)&facet=true&facet.field=active_b&f.active_b.facet.mincount=0&wt=xml
Currently, there is no way where user can generate deeply nested queries, to my knowledge, if you please know to write in the comment. E.g.
q=(
(condition 1)
AND
(
(Condition 2 OR Condition3 OR Condition4)
AND
(Condition5 OR Condition 6 OR Condition7)
)
AND
(
(Condition 8 AND Condition9 AND Condition10)
OR
(Condition11 AND Condition 12 AND Condition13)
))
&start=0&rows=1000000&fl=*,score&fq=_indexname:(sitecore_master_index)&facet=true&facet.field=active_b&f.active_b.facet.mincount=0&wt=xml
Current Implementation
Search Result component consumes Sitecore.XA.Foundation.Search.Services.SearchService.SearchService to post a query and get the processed output. Let understand one of its important method GetQuery below.


Solution
Sitecore and SXA are all about extension and we need some way to inject our queries. I wanted to do less customization as much as possible, thus during the next upgrade, it should not be a big issue. The idea is to inject our pipeline or piece of code which calls external code from the feature or Projects, which add extra predicates.
The solution is implemented in two parts:
First, extend the foundation of the Search Service. (I will use for loop for the demo purpose, suggested approach is the pipeline)
Extended Search Service
//Inherit Search Service and override constructor and GetQuery method.
public class ExtendedSearchService : SearchService
{
private IEnumerable<ISearchFilter> Filters;
public ExtendedSearchService () : base()
{
//Get all filters registered by the developers, from DI
Filters = ServiceLocator.ServiceProvider.GetServices<ISearchFilter>();
}
//Override the implementation.
public overide virtual IQueryable<ContentPage> GetQuery(SearchQueryModel searchQueryModel, out string indexName)
{
Item contextItem = GetContextItem(searchQueryModel.ItemID);
ISearchIndex searchIndex = IndexResolver.ResolveIndex(contextItem);
IList<Item> list = searchQueryModel.ScopesIDs.Select(Context.Database.GetItem).ToList();
indexName = searchIndex.Name;
IEnumerable<SearchStringModel> models = list.Select((Item i) => i["ScopeQuery"]).SelectMany(SearchStringModel.ParseDatasourceString);
models = ResolveSearchQueryTokens(contextItem, models);
IQueryable<ContentPage> source = LinqHelper.CreateQuery<ContentPage>(searchIndex.CreateSearchContext(), models);
string text = NormalizeSearchPhrase(searchQueryModel.Query);
source = source.Where(IsGeolocationRequest ? GeolocationPredicate(searchQueryModel.Site) : PageOrMediaPredicate(searchQueryModel.Site));
source = source.Where(ContentPredicate(text));
source = source.Where(LanguagePredicate(searchQueryModel.Languages));
source = source.Where(LatestVersionPredicate());
/*custom code starts*/
//Custom argument type.
FilterArgs args = new FilterArgs()
{
Source = source,
QueryString = WebUtil.ParseUrlParameters(HttpContext.Current.Request.RawUrl) ,
SearchQueryModel = searchQueryModel,
ContextItem = contextItem,
ScopeItemlist = list
};
//Extension for external Filters, which can add more complex predicates.
// below code can be replaced with pipeline implementation.
// Open for change, close for modification
foreach (var filter in Filters)
{
if (filter.CanApply(args))
{
filter.Apply(args);
}
}
source = args.Source;
/*custom code ends*/
source = source.ApplyFacetFilters(Context.Request.QueryString, searchQueryModel.Coordinates, searchQueryModel.Site);
return BoostingService.BoostQuery(list, text, contextItem, source);
}
}
Argument Class:
public class FilterArgs
{
public IQueryable<ContentPage> Source { get; set; }
public Item ContextItem { get; set; }
public NameValueCollection QueryString { get; set; }
public IList<Item> ScopeItemlist { get; set; }
public SearchQueryModel SearchQueryModel { get; set; }
}
Search Filter Interface
public interface ISearchFilter
{
bool CanApply(FilterArgs args); //if current scope id or name matches then apply method will be called.
void Apply(FilterArgs args); //Add complex predicate to the existing source.
}
Register the new extended search service.
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
xmlns:set="http://www.sitecore.net/xmlconfig/set/">
<sitecore>
<services>
<configurator type= "SxaExtension.Foundation.Search.DI.ServicesConfigurator, SxaExtension.Foundation.Search"/>
<register serviceType="Sitecore.XA.Foundation.Search.Services.ISearchService, Sitecore.XA.Foundation.Search" set:implementationType="SxaExtension.Foundation.Search.Services.ExtendedSearchService, SxaExtension.Foundation.Search" lifetime="Singleton"/>
</services>
</sitecore>
</configuration>
Now lets implement the feature or project part, where we can implement ISearchFilter interface and register our filter in DI.
Sample and simple use case(for better understanding) , when user search for property in the given suburb , but also wants to find the properties from the surrounding suburbs. Additionally property should be active and surrounding suburbs must be within 20 Kilometres radius from the given suburb.
Sample Filter
namespace SxaExtension.Feature.Property.SearchFilters
{
public class PropertySearchFilters : ISearchFilter
{
private readonly ID PropertySearchScopeId = new ID("ID of your scope");
private readonly ISuburbService SuburbService;
public PropertySearchFilters()
{
SuburbService = ServiceLocator.ServiceProvider.GetService<ISuburbService>();
}
//Decides, should apply filter or not, based on scope id and query value.
//This can be have custom conditions based on your business logic.
public bool CanApply(FilterArgs args)
{
string queryValue = GetQSParam("q");
if (args.SearchQueryModel.ScopesIDs.Contains(PropertySearchScopeId) && !string.IsNullOrWhiteSpace(queryValue))
{
return true;
}
return false;
}
public void Apply(FilterArgs args)
{
List<string> suburbs = new List<string>();
string query = GetQSParam("q");
//Find requested suburb
var suburb = SuburbService.GetSuburbs(args.ContextItem, query, false)?.FirstOrDefault();
//Find surronding suburbs of given suburb
suburbs = SuburbService.GetSurroundingSuburbs(args.ContextItem, query, false);
//Builds OR predicate.
var predicate = PredicateBuilder.False<ContentPage>();
foreach (var s in suburbs)
{
//p[(ObjectIndexerKey)"surroundsuburbIds"] is the way to access properties not listed on content page model.
predicate = predicate.Or(p => p[(ObjectIndexerKey)"surroundsuburbIds"].MatchWildcard($"\"{"|" + s + "|"}\""));
}
args.Source = args.Source.Where(predicate);
args.Source = args.Source.Where((p => ((string)p[(ObjectIndexerKey)"status"]) == "Active"));
args.SearchQueryModel.Coordinates = suburb.Location;
if (args.SearchQueryModel.Coordinates != null)
{
//Check for radius.
args.Source = args.Source
.WithinRadius(s => s.Location, args.SearchQueryModel.Coordinates, 200000,true)
.OrderByDistance(d => d.Location, suburb.Location);
}
}
private static string GetQSParam(string param)
{
var qsParams = WebUtil.ParseUrlParameters(HttpContext.Current.Request.RawUrl);
var queryValue = qsParams[param];
return queryValue;
}
}
}
Service Configurator
namespace SxaExtension.Feature.Property.DI
{
public class ServicesConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddTransient<IPropertyRepository, PropertyRepository>();
serviceCollection.AddMvcControllers("SxaExtension.Feature.Property*");
RegisterSearchFilters(serviceCollection);
}
private void RegisterSearchFilters(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<ISearchFilter, PropertySearchFilters>();
}
}
}
Register Configuration
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<services>
<configurator type= "SxaExtension.Feature.Property.DI.ServicesConfigurator, SxaExtension.Feature.Property"/>
</services>
</sitecore>
</configuration>
I hope this post helps you to understand the purpose and essence of the customization.