Note: Since writing this post the company behind Truffler, 200OK, has been sold to EPiServer and the product Truffler has been replaced by EPiServer Find. Most of the content of this blog post is however applicable to EPiServer Find as well. For questions regarding Find, be sure to visit the forum on EPiServer World.
This is the first in a series of posts about building search functionality for EPiServer CMS based sites using Truffler. While Truffler can be used for many things such as querying for data and finding related content we’ll here focus on the traditional search page.
To have some context to work in I’ll start by walking through the characteristics of a fictive EPiServer site built with Page Type Builder and EPiImage. We’ll then look at how to add the Truffler .NET API to it and build a search page from the ground up. Be ware that while I won’t go into all of the nitty gritty details of the .NET API I’ll often cover multiple solutions to a given problem.
An example site
In this tutorial we’ll use a site for fictive company, the airline Fly Truffler. It’s site is straight forward and consists of only a few page types. Most notable are the Standard Page and the Destination page types. The first is used for articles of various kinds, such as travel information and news bulletins. The Destination page type is used to describe the various destinations that the airline flies to.
The standard page type has three properties: MainIntro, MainBody and ListingRoot. The latter can be used by editors to set a page whose children should be listed below the main body.
An example of a page using the Standard Page page type used for an individual news item:
An example of a page using the Standard Page page type with a listing:
The Destination page type has more properties. However, for the search page we’ll mainly be interested in the MainIntro and MainBody property. The others, such as coordinates would come in handy when building some cool geosearch though :)
An example of a page using the Destination page type:
The site structure is straight forward. We have four main sections: Destinations, Travel Information, News and About Fly Truffler. The three latter sections are standard pages themselves used to group the child pages below them. This means that a standard page below Travel Information is conceptually a “Travel Information page” although it’s page type is the same as those under for instance the News section.
Page Type Builder
The site uses Page Type Builder to define page types in code. While Truffler doesn’t require Page Type Builder and has no dependency to it, meaning that it will play well with the typed pages support in the next version of the CMS too, it’s really when using Page Type Builder, or other mechanisms for typed pages, where Truffler really shines.
Getting started
In order to build a search page with Truffler we’ll need two things: a Truffler index and the Truffler integration for EPiServer CMS. A development index can be created on the Truffler website and the EPiServer Integration can be installed using NuGet after adding EPiServer’s NuGet feed. I won’t go into any details here as the process is straight forward and well documented.
Initial indexing
With an index created and the EPiServer integration referenced and configured we need to do an initial indexing. This is done by triggering Truffler’s scheduled job for indexing in EPiServer CMS’s admin mode.
The job usually completes within seconds on a small site and after it’s complete we can ensure that everything went OK by looking at the job’s history. In case something went wrong we’ll see a message about failed batches. This means that one or more batches of pages have failed to be indexed. If this happens we need to fix the problem and run the job again. The most common problems are pages that have properties defined as reference types while there’s no value causing a null reference exception. This is easily corrected by changing the (code) property’s type to the nullable equivalent of the reference type, such as from int to int?, which is a best practice when using typed pages anyway.
Note that we only need to run the job to index existing content or when we’ve made modifications that require the index to be updated. The EPiServer integration will listen for events from EPiServer’s DataFactory and index pages whenever they are published meaning that pages are indexed in close to real-time when they are published.
A first implementation of the search page
In order to display search results we’ll need input from the user so the first step in creating the page is to add a textbox and a button:
<asp:Panel DefaultButton="btnSearch" runat="server"> <asp:TextBox ID="tbQuery" runat="server"></asp:TextBox> <asp:Button ID="btnSearch" runat="server" Text="Search"/> </asp:Panel>
If the user has entered a search query we’ll want to display it and the results. Therefore we’ll begin by adding some initial code to the markup of the search page that checks if a codebehind property named Query has a value. If so it displays the value, the number of hits (hard coded) and a so far empty list which we’ll later render the search results in.
<% if (Query != null) { %> <p class="hitcount"> Found <strong>123 hits</strong> for <strong>"<%: Query %>"</strong>. </p> <ul id="search-results"> </ul> <% } %>
In order to handle user interaction and implement the Query property we head into the the code behind file for the search page template. There we add the Query property and have it return the value of a query string parameter named “q”:
protected string Query { get { return Request.QueryString["q"]; } }
Next we override the OnLoad method (or implement the Page_load event handler) to set the textbox’s value if the page isn’t post back. We also bind an event handler to for the button that will redirect to the same page but with the value from the textbox as the query string parameter “q”.
protected override void OnLoad(EventArgs e) { base.OnLoad(e); if (!IsPostBack) { tbQuery.Text = Query; } btnSearch.Click += BtnSearchOnClick; } private void BtnSearchOnClick(object sender, EventArgs eventArgs) { var redirectUrl = Request.RawUrl; redirectUrl = UriSupport.AddQueryString( redirectUrl, "q", tbQuery.Text); Response.Redirect(redirectUrl); }
With this code and the markup in place we now have a search page that responds to user interactions and displays the entered search query. With some styling it should look something like the image below after entering something in the textbox and pressing the search button.
Searching
Next we need to show the correct number of hits instead of the hard coded value and display some search results. In the simplest of situations we could search for all pages and get the actual PageData objects that match the search query. This can easily be done using the EPiServer CMS integration’s GetPagesResult method. Doing so is great in other types of querying situations, especially since the GetPagesResult method will also cache the result. However, for a search page such as this we’ll probably want to display other things than what’s necessarily contained in the pages, or subsets of what is, such as highlights from text that match the search query.
Therefore we’ll create new little class that we’ll use as a view model. We’ll still be searching for pages but we’ll retrieve the results as instances of this class which is tailor made for display on the search page.
public class SearchHit { public string Title { get; set; } public string Url { get; set; } public string Text { get; set; } }
Next we add a property of type SearchResults<SearchHit> to the code behind of our search page. The SearchResults<T> class is a Truffler class so we’ll need to add a using statement for Truffler. While we’re at it we also add a couple of using statements for the EPiServer integration which will come in handy later on.
using Truffler; using Truffler.EPiServer; using Truffler.EPiServer.Cms; //In the class protected SearchResults<SearchHit> Results { get; set; }
We can now implement the number of hits text that we earlier hard coded in the markup. The total number of matching hits is exposed by the SearchResults<T> class’ TotalMatching property.
Found <strong><%= Results.TotalMatching %> hits</strong> for <strong>"<%: Query %>"</strong>.
The SearchResults<T> class implements IEnumerable<T> meaning that we can iterate over the search hits in it with, for instance, a simple for each loop:
<ul id="search-results"> <% foreach (var hit in Results) { %> <li> <h4><a href="<%= hit.Url %>"><%= hit.Title %></a></h4> <p> <%= hit.Text %> </p> </li> <% } %> </ul>
We now have everything in place for displaying search results. There’s just one thing missing: searching. We’ll begin by creating an empty method named Search that we execute given that the page isn’t post back.
protected override void OnLoad(EventArgs e) { base.OnLoad(e); if (!IsPostBack) { tbQuery.Text = Query; Search(); } btnSearch.Click += BtnSearchOnClick; } private void Search() { }
The objective of the Search method is to search for pages that matches the query, or search text, entered by the user and which is exposed by the Query property. Matching pages should be projected to the SearchHit class and the Results property should be given a value. We’ll begin by searching only for pages of the standard page page type.
When using Truffler’s EPiServer integration all interactions with the search engine is typically done through the singleton exposed by Truffler.EPiServer.EPiSearchClient.Instance. To create a search query we use the Search method specifying what type to search for as a type parameter. In order to search based on a text inputted by a user we can use the For method. In order to project from the StandardPage type to our view model, SearchHit, we use the Select method, just like we would have with LINQ. Finally we execute the search request using the GetResult method and assign the result to the Results property.
private void Search() { var result = EPiSearchClient.Instance .Search<StandardPage>() .For(Query) .Select(x => new SearchHit { Title = x.PageName, Url = x.LinkURL }) .GetResult(); Results = result; }
It may not be pretty just yet, but we now have working search page in the sense that search results are displayed for the user. It should look something like this:
Summary
We’ve now built a very basic search page that displays search results matching a search query entered by a user. A lot of the code so far has not been specific to Truffler but has been necessary to display search results and handle user input. The most important, and Truffler specific part of this post was the last code snippet, the Search method. In it we use the EPiSearchClient class to create a search query for a specific type of object or page type using the Search<T>() method. We then add a free text search query to it using the For(string) method. Finally we provided a projection from the StandardPage type to a view model type using the Select method, just like we might have done with LINQ and executed the search query with the GetResult() method.
The search page is pretty basic at this point but and hardly production ready. We’ll fix that in the next post. There we’ll look at how we can improve the free text search by using stemming and how we can search over multiple types. We’ll also improve it visually by adding excerpts of text with keywords highlighted to the search hits as well as implement paging functionality.