Building a Reusable Product Search Block Part 2: Find Search Service & Controller

Alright, let's bring it all together and get this product search rocking! In the first part of this series, we built out the search block controller and interface, as well as an init module to register the route for our reusable search block. In this post, we are going to build the search service that creates an ITypeSearch query from the user input and returns the results to a simple view. There are a few Episerver nuget packages we need to install first that make it easier to use Find with Commerce products. They are:

 Next, let's create a couple of interfaces for working with the search results. We will create one for the Find search hit and one for the search results page. I created both of these in the Common project of my solution. 

public interface ISearchHit
{
     string Name { get; set; }
     string Url { get; set; }
     string Teaser { get; set; }
}
public interface ISearchResults
{
     int TotalResults { get; set; }
     List<Hit> Results { get; set; }
}

Let's do one more thing before we move to the search service. We are going to override a part of the built-in ISearchContent interface called SearchHitUrl on our product variant type. That way, we can specify the exact URL we want to use on our search results page. You can find a reference to all the ISearchContent properties here. This is what I implemented on my variant class, ProductItemData:

using EPiServer.Commerce.Catalog.DataAnnotations;
using EpiCommerceSandbox.Models.Catalog.Abstracts;
using WSOL.EPiServerCms.Web.Extensions;

namespace EpiCommerceSandbox.Models.Catalog
{
    [CatalogContentType(
        DisplayName = "Product Item", 
        GUID = "a959963b-bd4f-4f1d-b366-04698586bf51", 
        MetaClassName = "Product Item")]
    public class ProductItemData : CoreItemData
    {
        // For Find Unified Searches
        public virtual string SearchHitUrl { get { return this.ContentLink.GetUrl(); } }
    }
}

Now, let's create a class called FindSearchService. In this class, we will create an instance of an Episerver Find IClient. Next, let's create a method called SearchVariants. This method takes the values passed from our product search block and uses them to create an ITypeSearch query. Here are a few things to note. For the keyword field, we are creating a wildcard search that looks at the products display name, as well as the description. When we set the results variable, we are doing a few things here. We are filtering the content for visitor groups, excluding deleted content and only returning content that is currently published. You also may notice that two of the items we pass to SearchVariants are pageSize and page. This is so that when we return results, we are only pulling the ones we need to populate the current page with pagination. Here is the full class:

using EpiCommerceSandbox.Models.Catalog;
using EPiServer.Find;
using EPiServer.Find.Cms;
using EPiServer.Find.Framework;
using WSOL.EPiServerCms.Web.Extensions;

namespace EpiCommerceSandbox.Services
{
    public class FindSearchService
    {
        private IClient _client = SearchClient.Instance;

        public IContentResult<ProductItemData> SearchVariants(int pageSize, int page, string keyword = "", string manufacturer = "")
        {
            ITypeSearch<ProductItemData> query = _client.Search();
            // Keyword
            if (!keyword.Equals(string.Empty))
            {
                query = query.For(keyword, q =>
                {
                    q.Query = "*" + keyword + "*";
                }).InField(x => x.DisplayName).AndInField(x => x.Description);
            }
            // Manufacturer
            if (!manufacturer.Equals(string.Empty))
            {
                var manufacturerFilter = _client.BuildFilte<ProductItemData>r();
                manufacturerFilter = manufacturerFilter.And(x => x.Manufacturer.Match(manufacturer));
                query = query.Filter(manufacturerFilter);
            }
            // Get the reults
            var results = query.FilterForVisitor().ExcludeDeleted().CurrentlyPublished().Skip((page - 1) * pageSize).Take(pageSize).GetContentResult();

            return results;
        }
    }
}

Now, let's create a page type called SearchResultsPageData and a controller for it called SearchResultsController. The page type is pretty simple at this point, it has no properties and inherits from my main abstract class called SitePageData. Create a view called index.cshtml in Views/SearchResults. Also, create a view model in Models/ViewModels called SearchContentViewModel. This is the model we will use for our search results view. Here is the code for these pieces:

View Model:

using EpiCommerceSandbox.Common.Objects;
using EpiCommerceSandbox.Models.Pages;
using System.Collections.Generic;

namespace EpiCommerceSandbox.Models.ViewModels
{
    public class SearchContentViewModel : PageViewModel<SearchResultsPageData>
    {
        public SearchContentViewModel() { }

        public SearchContentViewModel(SearchResultsPageData currentPage)
            : base(currentPage) { }

        public string SearchedQuery { get; set; }
        public int NumberOfHits { get; set; }
        public int PageSize { get; set; }
        public IEnumerable<Hit> Hits { get; set; }
    }
}

Controller:

using EpiCommerceSandbox.Business;
using EpiCommerceSandbox.Models.Pages;
using EpiCommerceSandbox.Services;
using EPiServer.ServiceLocation;
using EpiCommerceSandbox.Common.Objects;
using System.Web.Mvc;
using EpiCommerceSandbox.Models.ViewModels;
using EPiServer.Web.Routing;
using EPiServer;
using System.Collections.Generic;

namespace EpiCommerceSandbox.Controllers
{
    public class SearchResultsController : PageControllerBase<SearchResultsPageData>
    {
        private readonly PageViewContextFactory _contextFactory;
        private SearchResults _pagedResults = new SearchResults();
        private FindSearchService _searchService = ServiceLocator.Current.GetInstance<FindSearchService>();

        public SearchResultsController(PageViewContextFactory contextFactory)
        {
            _contextFactory = contextFactory;
        }

        public ActionResult Index(SearchResultsPageData currentPage)
        {
            SearchContentViewModel model = new SearchContentViewModel(currentPage);

            // Let's get those query string values
            string keyword = Request.QueryString["keyword"];
            string manufacturer = Request.QueryString["manufacturer"];
                       
            int pageSize = (Request.QueryString["pageSize"] != null) ? int.Parse(Request.QueryString["pageSize"]) : model.PageSize;
            int page = (Request.QueryString["page"] != null) ? int.Parse(Request.QueryString["page"]) : 1;

            var urlResolver = ServiceLocator.Current.GetInstance<UrlResolver>();
            var pageUrl = urlResolver.GetVirtualPath(currentPage.ContentLink);
            model.PageURL = pageUrl.VirtualPath;
            model.PageSize = pageSize;
            model.PageNumber = page;
            if (Request.QueryString["keyword"] != null && Request.QueryString["manufacturer"] != null)
            {
                if (keyword.Equals("") && manufacturer.Equals(""))
                {
                    model.NumberOfHits = 0;
                }
                else
                {
                    var pagedResults = DoProductSearch(pageSize, page, keyword, manufacturer);
                    model.Hits = pagedResults.Results;
                    model.NumberOfHits = pagedResults.TotalResults;
                }
            }
            else
            {
                model.NumberOfHits = 0;
            }

            return View(string.Format("Index", currentPage.GetOriginalType().Name), model);
        }

        private SearchResults DoProductSearch(int pageSize, int page, string keyword, string manufacturer)
        {
            _pagedResults.Results = new List<Hit>();

            var results = _searchService.SearchVariants(pageSize, page, keyword, manufacturer);

            if (results != null)
            {
                _pagedResults.TotalResults = results.TotalMatching;

                foreach (var item in results)
                {
                    _pagedResults.Results.Add(new Hit()
                    {
                        Name = item.Name,
                        Url = item.SearchHitUrl,
                        Teaser = item.Teaser
                    });
                }
            }
            return _pagedResults;
        }
    }
}

As you can see in the controller, I broke out the piece that does the search and populates our results object into it's own private function. The ActionResult is pretty self explanatory. It creates an instance of our SearchContentViewModel, checks the query strings for values, does the search and returns our populated view model to the index view. The view simply loops through the results and displays them in a list. 

There you have it! We have ourselves a nice reusable product search block. While this implementation is pretty basic, hopefully you have a decent idea of how to work with Episerver blocks, Commerce variants and the Find Api and implement them into your own projects. Personally, I think it's a blast to work with this stuff. If you want to see the full code base, you can check out my github repo here.  If you have questions or comments please leave them below. 

Read Part One

0 comment(s) in response to Building a Reusable Product Search Block Part 2: Find Search Service & Controller

Have a comment?