Building an advanced search page using EPiServer Find

Mari Jørgensen 2015-09-04 06:28:11

The requirements

Recently I developed a search page for a commerce site where the customer had the following requirements:

  • Show the results grouped as Products, Stores and Articles
  • Initial show 10 hits with paging for each group
  • Enable statistics with click tracking

As you can see from the images below, the search hits are displayed quite differently for product and store (articles are left out for simplicity, but you get the point).

The problem

Normally you would use EPiServer Find’s multi search to run all searches in one request, but that require all searches to have a common return type, which I do not have (and want to avoid since the types are so unalike). Because of the requirement of paging within each group, Unified Search was neither a great fit.

Running each query separately is something you want to avoid, as it will require several round trips to the server, which may affect performance.

EPiServer Community to the rescue

To solve the limitations with multi search, Per Magne at EPiServer Norway has created an extension that enables multi search queries without projections. I would recommend reading his blog post here.

With the use of dynamic multi search, I can write smooth queries like this:

var myDynamicMultiSearchResult = SearchClient.Instance.DynamicMultiSearch()
          .Search<Product>(p => p.For(query)
.Filter(x => x.LangCode.MatchCaseInsensitive(language.ToLower())) .Filter(x => x.Markets.Match(market)) .Skip((page) * pageSize).Take(pageSize)) // products .Search<StorePage>(s => s.FindPages(query, page, pageSize)) //stores .Search<StandardPage>(c => c.FindPages(query, page, pageSize)) //cms pages .GetDynamicResult();

My FindPages extension method looks like this:

public static ITypeSearch FindPages(this ITypeSearch search, string query, int page, int pageSize) where T : SitePageData
        {
            return search.For(query).InFields(p => p.SearchText())
                               .UsingSynonyms()
                               .FilterForVisitor()
                               .Skip((page - 1) * pageSize)
                               .Take(pageSize);
        }

The statistics part

The last requirement I had to cover was tracking of queries and hits. Tracking of hits refer to tracking user clicks within the search result. If you read through the Find documentation you might notice the following:

When using Unified Search, StatisticsTrack() enables tracking of both the query and the hits. When using StatisticsTrack() on a non-Unified Search, it only enables tracking of the query.

What does this mean? Well, since I’m not using Unified Search, there is no hit click tracking out-of-the box.
Also, tracking each query will in the end ruin the search statistics. I'll try to illustrate why:
If we go back to the searches used in the images above, "jeans" return hits for products and articles and "oslo" return hits for stores and articles. But since "jeans" returns 0 store hits and "oslo" returns 0 product hits, both will be tracked as "Searches without hits". 

To solve this we need to implement custom query and click tracking. Luckily, Henrik Fransas has written an excellent blog post about this, you can read the details here: http://world.episerver.com/blogs/Henrik-Fransas/Dates/2015/2/how-to-do-custom-query-and-click-tracking-with-episerver-find/

I did my implementation following Henriks steps, but since I'm using multi search I had to loop through the result sets to compute total number of hits:

var hitsInTotal = myDynamicMultiSearchResult.Sum(res => res.TotalMatching);

// Tracking
model.TrackId = new TrackContext().Id;
TrackQuery(query, hitsInTotal, model.TrackId);