How to enable CMS search for catalogs
This will be a more concrete example on how a developer can change the search function in the catalogs gadget, but I will also show how this adds real value for editors so if you aren't a developer read through and focus on the why and what instead of the where and how.
Background
I have an old site that we have migrated to a newer EPiServer version as the new asset system was introduced we needed to install the EPiServer.Search NuGet package so that the media search worked. As I was experimenting with EPiServer.Search I noticed that the Catalog UI was indeed triggering the right CMS IContent events and indexing catalog content into the same search index, a fact that even EPiServer developer support was unaware of and required proof of when I asked them, so don't feel bad if you never heard of it either .
However, the search in the Catalogs gadget was already working before we installed the NuGet since that uses an entirely different search index. Now when we have installed EPiServer.Search it makes two search indexes that is being populated with catalog content but only one that is ever being queried for it. This is my story about how I changed it so that both the Catalogs and the Media gadget queried the same CMS search index.
Enabling the custom search
This is really straightforward but it took me a long time getting there:
[SearchProvider]
public class CustomCatalogSearchProvider : EPiServerSearchProviderBase<EntryContentBase,ContentType>
{
private readonly LocalizationService _localizationService;
public override string Area => "Commerce/Catalog";
public override string Category => _localizationService.GetString("/Commerce/Edit/Provider/SearchProductCatalog/Category");
protected override string IconCssClass => "epi-resourceIcon epi-resourceIcon-page";
public CustomCatalogSearchProvider(LocalizationService localizationService, SiteDefinitionResolver siteDefinitionResolver, IContentTypeRepository<ContentType> contentTypeRepository, EditUrlResolver editUrlResolver, IContentRepository contentRepository, ILanguageBranchRepository languageBranchRepository, SearchHandler searchHandler, ContentSearchHandler contentSearchHandler, SearchIndexConfig searchIndexConfig, UIDescriptorRegistry uiDescriptorRegistry) : base(localizationService, siteDefinitionResolver, contentTypeRepository, editUrlResolver, contentRepository, languageBranchRepository, searchHandler, contentSearchHandler, searchIndexConfig, uiDescriptorRegistry)
{
_localizationService = localizationService;
}
public CustomCatalogSearchProvider(LocalizationService localizationService, SiteDefinitionResolver siteDefinitionResolver, IContentTypeRepository<ContentType> contentTypeRepository, EditUrlResolver editUrlResolver, ServiceAccessor<SiteDefinition> currentSiteDefinition, IContentRepository contentRepository, ILanguageBranchRepository languageBranchRepository, SearchHandler searchHandler, ContentSearchHandler contentSearchHandler, SearchIndexConfig searchIndexConfig, UIDescriptorRegistry uiDescriptorRegistry, LanguageResolver languageResolver, UrlResolver urlResolver, TemplateResolver templateResolver) : base(localizationService, siteDefinitionResolver, contentTypeRepository, editUrlResolver, currentSiteDefinition, contentRepository, languageBranchRepository, searchHandler, contentSearchHandler, searchIndexConfig, uiDescriptorRegistry, languageResolver, urlResolver, templateResolver)
{
_localizationService = localizationService;
}
public CustomCatalogSearchProvider(LocalizationService localizationService, ISiteDefinitionResolver siteDefinitionResolver, IContentTypeRepository<ContentType> contentTypeRepository, EditUrlResolver editUrlResolver, ServiceAccessor<SiteDefinition> currentSiteDefinition, IContentRepository contentRepository, ILanguageBranchRepository languageBranchRepository, SearchHandler searchHandler, ContentSearchHandler contentSearchHandler, SearchIndexConfig searchIndexConfig, UIDescriptorRegistry uiDescriptorRegistry, LanguageResolver languageResolver, UrlResolver urlResolver, TemplateResolver templateResolver) : base(localizationService, siteDefinitionResolver, contentTypeRepository, editUrlResolver, currentSiteDefinition, contentRepository, languageBranchRepository, searchHandler, contentSearchHandler, searchIndexConfig, uiDescriptorRegistry, languageResolver, urlResolver, templateResolver)
{
_localizationService = localizationService;
}
}
All we need to do is register our own class with the [SearchProvider] attribute, which will enable EPiServer to set everything up during the regular initialization.
It is the Area:"Commerce/Catalog" property that informs the Catalog UI that this SearchProvider should be called when the "Catalogs" gadget is used.
Everything needed to do the actual query is already implemented in EPiServerSearchProviderBase we just had to configure it to do it for EntryContentBase.
EPiServer uses the base class in their own search providers for pages, blocks and media files that are all included in the CMS.
Disabling the legacy search
I personally consider this to be legacy as EPiServer inherited it when they bought the commerce platform, they haven’t written it themselves which is also why support can be a trial, but it is however not obsolete as they have yet to provide an alternative.
Unfortunately, I have found no easy way to do this. I will have to look into implementing a dummy version of Mediachase.Search.SearchProvider and configure EPiServer to use it by entering it in the Mediachase.Search.config file. If you do this don't forget to turn off the two indexing jobs under CMS->Admin->Scheduled Jobs.
Adding node search
The legacy search only indexed entries so that was all that could be queried, however during my experiments I noticed that the CMS index was indeed being populated not only with entries but also with nodes, so naturally i set out on a quest to modify the Catalogs gadget to return nodes as well as entries when the editors did a search.
To do this all I did was to change the setting for EPiServerSearchProviderBase to the right base class:
public class CustomCatalogSearchProvider : EPiServerSearchProviderBase<CatalogContentBase, ContentType>
Searching in the Catalogs gadget did indeed return not only the entries as before but also a node. However, double-clicking on it does not navigate to the node in the Catalog UI. I am sorry but it will take someone better on JavaScript than me to make that happen, since I haven't mustered the courage to enter the dojo yet.
All is not for nothing though since even though the navigation doesn't work the drag-and-drop does, which enables editors to easily find nodes when they are populating content areas on pages inside cms and also when putting together promotions in marketing.
Conclusion
With very little code, it took me personally some significant effort to work all of this out but hopefully it was easier for you , we added some real value for editors as they can now search for nodes in the catalogs gadget and drag-and-drop them into content areas both in the CMS site but also for promotions in marketing. We also made life a bit simpler for us developers later on as we removed the dependency on an old search index.
Known limitations:
- Double-clicking a node for navigating to it doesn't work.
- If you like me have a preexisting site and adds EPiServer. Search you have to do your own indexing to populated it initially.
- This have not been thoroughly tested and there might very well be pitfalls down the road.
Footnote: this was done in CMS 10.10.1 and Commerce 11.2.0.
Fun fact: my dummy implementation of the old Search Provider got called three times for every search phrase during my debugging.
Update:
The original code will work for the gadget on the right side of the admin ui but not always for the left hand side when in the Catalog UI, it requires something extra.
To make sure our new SearchProvider is used instead of the old one we need to implement EPiServer.Framework.ISortable:
SortOrder defaults to 1000 if ISortable isn't implemented, Episervers search providers doesn't implement it. (Don't even think about asking me about how i found out that this interface existed and what the default value for search providers is... )
Great post, Erik!
The results are by default shown by the
Name
property. However, in a multi-language catalog you might want to useDisplayName
instead (to get it localized). To do this, you just need to override theCreateSearchResult
method.protected override SearchResult CreateSearchResult(EntryContentBase contentData)
{
var result = base.CreateSearchResult(contentData);
result.Title = HttpUtility.HtmlEncode(contentData.DisplayName);
return result;
}