Stay up to date with weekly industry insights

the latest trends in web design, inbound marketing, mobile strategy and more...

Getting Started Using Vulcan Search in Episerver

Episerver Vulcan

At WSOL, we’re big proponents of the Episerver content management system (CMS), since it offers best-of-breed website management tools for organizations with enterprise requirements. As we continue to work with the Episerver platform, we’ve been able to find ways to expand and refine its capabilities, and one of the most powerful additions that we’ve worked with is Vulcan search.

Vulcan is a relatively new (almost a year old now), open source search client built to index Episerver CMS content to Elasticsearch. It was created to fill a void in the default Episerver search offerings of Lucene and Episerver Find, which also utilizes Elasticsearch. Vulcan offers a modern search experience, similar to Find, but with the flexibility of choosing a hosting environment, such as an on-premises or cloud hosted service.

It’s fairly simple to get started with Vulcan; only a few basic components are required:

  • Elasticsearch 2.x server (the latest 5.x is not currently supported)
  • Episerver CMS web project

Let’s look at how to set up Vulcan using Episerver’s example Alloy site with no search configured and a local Elasticsearch server:

Setting Up a Local Elasticsearch

Elasticsearch is pretty easy to set up on Windows machine. You can do so by simply downloading a supported version in a zip file and extracting the files.

Once Java is installed and Elasticsearch files are extracted, execute the bin\elasticsearch.bat file, and if all is working correctly, you will see a command prompt similar to this:

Elasticsearch install

The key message to pay attention to is the line which notes that the health status has changed from RED to YELLOW, which lets you know Elasticsearch is running. Don’t worry about the YELLOW status; it basically means that Elasticsearch is not running in a clustered environment, which isn’t needed for local development, but is highly recommended for production environments.

Adding Vulcan to Episerver

Now, with Elasticsearch up and running, we can install Vulcan to the Episerver CMS website project by following these steps:

  • Open the project in Visual Studio.
  • Right-click on the Episerver website project.
  • Select Manage NuGet Packages.
  • Select the Episerver Feed as the Package Source.
  • Enter “Vulcan” in the search box, and you should see results similar to the image below:

Add Vulcan to Episerver

  • Install the base package Vulcan.Core. It is also recommended to install TcbInternetSolutions.Vulcan.UI, since it has many nice benefits, such as displaying indexing modifiers, showing indexed item totals, and adding synonyms.
    • Note: If the Forms NuGet package is required, you will need to set the Dependency Behavior to IgnoreDependencies in the install options to upgrade Newtonsoft.Json to version 9.0.1, which is required by later version of the NEST Nuget package, which the core Vulcan package requires.
  • After the package(s) are installed, open the web.config and locate the AppSettings.
    • Change the following lines from:
      • <add key="VulcanUrl" value="SET THIS" />
      • <add key="VulcanIndex" value="SET THIS" />
    • To:
      • <add key="VulcanUrl" value="http://localhost:9200" />
      • <add key="VulcanIndex" value="vulcan.demo.local" />
    • Tip: Append the environment name to the VulcanIndex, so each environment can use its own index. Values can be changed using config transforms to match the environment during deployments.
  • At this point, the site can be compiled, and content can be indexed with Vulcan, but you will not yet be able to utilize the search results on the site.

Indexing CMS Content

Episerver CMS content gets indexed initially by an Episerver scheduled job. Also, the index stays up to date using Episerver content events for PublishedContent, MovedContent, DeletedContent, and DeletedContentLanguage. The screenshot below displays the Vulcan Index Content scheduled job to execute to populate the search index:

Vulcan index content

Creating a Vulcan Search Service

The default Alloy site provides a good demonstration of how to create a MVC search controller using a search service class with the default Lucene implementation. To make the search service work with Vulcan, we need to abstract the search service a little further using the following interfaces:

using EPiServer.Core;
using EPiServer.ServiceLocation;
using System.Collections.Generic;
using System.Web;

namespace VulcanDemo.Business
{
    public interface ISearchService
    {
        bool IsActive { get; }

        ISearchResults Search(string searchText, IEnumerable searchRoots, HttpContextBase context, string languageBranch, int currentPage, int maxResults);
    }

    public interface ISearchResults
    {
        IEnumerable Items { get; }

        long TotalHits { get; }

        string Version { get; }
    }
    
    [ServiceConfiguration(typeof(ISearchResults))]
    public class CustomSearchResults : ISearchResults
    {
        public CustomSearchResults(IEnumerable items, long totalHits, string version)
        {
            Items = items;
            TotalHits = totalHits;
            Version = version;
        }

        public IEnumerable Items { get; }

        public long TotalHits { get; }

        public string Version { get; }
    }
}

These abstractions will then be applied to the existing search service and a new VulcanSearchService class:

using EPiServer;
using EPiServer.Core;
using EPiServer.ServiceLocation;
using System.Collections.Generic;
using System.Globalization;
using System.Web;
using TcbInternetSolutions.Vulcan.Core;
using TcbInternetSolutions.Vulcan.Core.Extensions;
using TcbInternetSolutions.Vulcan.Core.Implementation;

namespace VulcanDemo.Business
{
    [ServiceConfiguration(typeof(ISearchService))]
    public class VulcanSearchService : ISearchService
    {
        IVulcanHandler _VulcanHandler;
        IContentLoader _ContentLoader;

        public bool IsActive => true;

        public VulcanSearchService(IVulcanHandler vulcanHandler, IContentLoader contentLoader)
        {
            _VulcanHandler = vulcanHandler;
            _ContentLoader = contentLoader;
        }

        public ISearchResults Search(string searchText, IEnumerable searchRoots, HttpContextBase context, string languageBranch, int currentPage, int maxResults)
        {
            // get a client using the given language
            var client = _VulcanHandler.GetClient(new CultureInfo(languageBranch));

            // uses GetSearchHits extension, source located at: 
            // https://github.com/TCB-Internet-Solutions/vulcan/blob/master/TcbInternetSolutions.Vulcan.Core/Extensions/IVulcanClientExtensions.cs
            // otherwise a full client.Search could be called to customize further
            var siteHits = client.GetSearchHits(searchText, currentPage, maxResults, searchRoots: searchRoots);            

            return new CustomSearchResults(ConvertToSearchResponseItem(siteHits.Items), siteHits.TotalHits, "Vulcan");
        }

        private IEnumerable ConvertToSearchResponseItem(List items)
        {
            foreach(var item in items)
            {
                yield return _ContentLoader.Get(new ContentReference(item.Id.ToString()));
            }
        }
    }
}

For brevity, the source of the Lucene based search service can be found on GitHub.

The final action necessary to display Vulcan search results is to add the ISearchService to the MVC search controller using dependency injection:

using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using EPiServer.Core;
using EPiServer.Framework.Web;
using VulcanDemo.Business;
using VulcanDemo.Models.Pages;
using VulcanDemo.Models.ViewModels;
using EPiServer.Web;
using EPiServer.Web.Routing;

namespace VulcanDemo.Controllers
{
    public class SearchPageController : PageControllerBase
    {
        private const int MaxResults = 40;
        private readonly ISearchService _searchService;
        private readonly UrlResolver _urlResolver;
        private readonly TemplateResolver _templateResolver;

        public SearchPageController(
            ISearchService searchService, 
            TemplateResolver templateResolver,
            UrlResolver urlResolver)
        {
            _searchService = searchService;
            _templateResolver = templateResolver;
            _urlResolver = urlResolver;
        }

        [ValidateInput(false)]
        public ViewResult Index(SearchPage currentPage, string q, int pageNumber = 1)
        {
            var model = new SearchContentModel(currentPage)
                {
                    SearchServiceDisabled = !_searchService.IsActive,
                    SearchedQuery = q
                };

            if(!string.IsNullOrWhiteSpace(q) && _searchService.IsActive)
            {
                var hits = Search(q.Trim(),
                    new[] { SiteDefinition.Current.StartPage, SiteDefinition.Current.GlobalAssetsRoot, SiteDefinition.Current.SiteAssetsRoot }, 
                    ControllerContext.HttpContext, 
                    currentPage.LanguageID,
                    pageNumber).ToList();
                model.Hits = hits;
                model.NumberOfHits = hits.Count();
            }

            return View(model);
        }

        /// 
        /// Performs a search for pages and media and maps each result to the view model class SearchHit.
        /// 
        /// 
        /// The search functionality is handled by the injected SearchService in order to keep the controller simple.
        /// Uses EPiServer Search. For more advanced search functionality such as keyword highlighting,
        /// facets and search statistics consider using EPiServer Find.
        /// 
        private IEnumerable Search(string searchText, IEnumerable searchRoots, HttpContextBase context, string languageBranch, int pageNumber)
        {
            var searchResults = _searchService.Search(searchText, searchRoots, context, languageBranch, pageNumber, MaxResults);

            return searchResults.Items.SelectMany(CreateHitModel);
        }

        private IEnumerable CreateHitModel(IContent content)
        {
            if (content != null && HasTemplate(content) && IsPublished(content as IVersionable))
            {
                yield return CreatePageHit(content);
            }
        }

        private bool HasTemplate(IContent content)
        {
            return _templateResolver.HasTemplate(content, TemplateTypeCategories.Page);
        }

        private bool IsPublished(IVersionable content)
        {
            if (content == null)
                return true;
            return content.Status.HasFlag(VersionStatus.Published);
        }

        private SearchContentModel.SearchHit CreatePageHit(IContent content)
        {
            return new SearchContentModel.SearchHit
                {
                    Title = content.Name,
                    Url = _urlResolver.GetUrl(content.ContentLink),
                    Excerpt = content is SitePageData ? ((SitePageData) content).TeaserText : string.Empty
                };
        }
    }
}

Accessing Vulcan in the Episerver UI

If the NuGet package TcbInternetSolutions.Vulcan.UI is installed, we can view some very helpful information in the Episerver UI.  Selecting the Vulcan menu item displays information about:

  • Each index (one will exist for each language enabled on the site, plus one invariant language for uploaded assets).
  • Index modifiers – any class that implements the IVulcanIndexingModifier
  • POCO Indexers – any class that implements the IVulcanPocoIndexer interface to index custom POCO (plain old CLR objects), that are not Episerver CMS content.

Vulcan configuration

Display the Results

Since we abstracted the search service and didn’t change the search results view model, we didn’t have to change the result page code. The screenshot below displays what a search would look like if any ISearchService is used:

Alloy search results

Moving to Production

When we are ready to move the Vulcan search to production, we need to consider hosting options beyond just a local Elasticsearch instance. The simplest and easiest solution is to use cloud based hosting, which is beyond the scope of this article, but will be the fastest way to implement Vulcan, since we only need the VulcanUrl and VulcanIndex web.config app settings. Another possible solution is to host on-premises behind a firewall, with a cluster of Linux servers, such as Red Hat or Ubuntu. The important point here is that there are many options that can work for any setup.

We hope this look at how to implement Vulcan search on your Episerver website has been helpful, but if you have any questions, please feel free to contact us. Do you have any tips of your own for getting the most out of Episerver? Please share them in the comments below!

About the Author

Brad McDavid
Brad McDavid
Brad is WSOL's Product Manager, you can read more about him here.