Downsides/Gotchas of using Projections in Episerver Find

Episerver Find is my preferred way to write searches for Episerver. It allows me to write search queries quickly using a LINQ like syntax I am familiar with, while leveraging Episerver’s cloud servers to do the heavy lifting.

While this does shift some of our server load to Episerver’s servers, the standard implementation still relies on my servers to load model data, and build view models. Generally this isn’t slow, but processing complex view models can hurt performance, and impact server load, especially as result sets increase in size, and complexity.

Projections allow me to specify which fields Episerver Find should retrieve data for, for a given query. I try to use them wherever it makes sense, since it allows us to display results with very little server load. There are a few downsides/gotchas that I’ve had to work through though.

Extra Extension Methods

The LINQ-like methods that can be used in conjunction with projections are much more limited than a normal Find search allows. Even simple string manipulation extension methods are not allowed, and will result in the projection not working properly.

This is easy to test with Fiddler. If the field being requested in the projection is not included in the response, the extension method is either not getting indexed properly, or is using an incompatible extension method in the projection.

There should be an extension method for every field to be used in the results, and it should be indexed during initialization.

public static string MyExtensionMethod(this BlogPostPage blogPostPage)
        {
            // Implement your logic here
        }
// Add to your initialization so it is indexed properly
var searchClient = ServiceLocator.Current.GetInstance<IClient>();
            searchClient.Conventions.ForInstancesOf<BlogPostPage>()
                .IncludeField(x => x.MyExtensionMethod());
Index must always be up to date

Since Find searches with projections rely solely on the data in your Find index, and not the Episerver database to create your view models, your Find index always up to date.

In most use cases, this is not an issue as Find re-indexes data for you when changes are made. This isn’t always the case when your extension methods require information from other models however.

IE an extension method for a ProductContent that requires data from a VariationContent.

This would require VariationContent updates to trigger reindexing for their associated ProductContents in order to keep the Find index correct.

Difficult, but not impossible to search in specific fields for multiple types

Using projections across multiple types, and searching in specific fields using inField do not currently work well together. This is due to inField being only available on ITypeSearch.

The easiest way I have found to implement it this is to is somewhat messy, but effective:

  • Search for a base type which all of your models inherit from,
  • Cast as needed to search in specific fields,
  • Branch/Cast as needed in projections.
var search = _searchClient.Search<PageData>(language)
                .For(searchTerm.Quote())
                .InField(x => x.Name)
                .InField(x => x.MetaDescription)
                .InField(x => ((AboutPage)x).SubTitleText)
                .InField(x => ((BlogPostPage)x).FindMainBodyText())
                .Select(x => new Article
                {
                    Title = x.Name,
                    Description = x.PageTypeName == nameof(AboutPage)
                        ? ((AboutPage)x).SubTitleText
                        : x.PageTypeName == nameof(BlogPostPage)
                            ? ((BlogPostPage)x).FindMainBodyText()
                            : x.MetaDescription,
                    Link = x.FindUrl()
                });
Custom tracking must be implemented

Since projections rely entirely on the data in your Find index, we need to create extension methods for hitId, hitType for Find statistics, and implement custom tracking as explained by Sebastian Sebusæter Enberget.

public static string HitId(this IContent content)
{
    return SearchClient.Instance.Conventions.IdConvention.GetId(content);
}

public static string HitType(this IContent content)
{
    return SearchClient.Instance.Conventions.TypeNameConvention.GetTypeName(content.GetType());
}

Overall, these changes result in more work to implement, but I have seen really good performance when projections are implemented correctly.