A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Eric Herlitz
Jan 7, 2013
  15354
(4 votes)

Using EPiServer Search in EPiServer 7 MVC

The Full-Text EPiServer 7 Search is a very simple but effective search solution and it should cover the needs of any basic search. The base is built upon the Lucene indexer.

This post will cover the basics of building a simple search page in EPiServer 7 using MVC with a simple pager, we will also allow for some fuzzy searching if the primary result isn’t good enough.

Search API’s

The query classes can be found in the EPiServer.Search.Queries.Lucene namespace. In this post I will use the following classes

  • EPiServer.Search.Queries.Lucene.FieldQuery
  • EPiServer.Search.Queries.Lucene.FuzzyQuery

Why FieldQuery + FuzzyQuery?

The idea is to fill a resultset with data, while the FieldQuery will require explicit hits the FuzzyQuery bends the qurey a bit. Fuzzy queries are excellent if someone is misspelling and do basically create a “similarity search” based on the Levenshtein algorithm.

The logic is set up to first do the FieldQuery, if it doesn’t fill the resultset to a certain degree we add the results from a FuzzyQuery in the end of the same resultset, excluding the ones we already have. We start at fuzziness 0.8f and step down to 0.4f, decreasing 0.1f for each turn until we either reach 0.4f (f for float) or the result set is filled. These values can be adjusted, however going below 0.4f isn’t recommended (by me) since the results tend to be a bit to fuzzy.

Pretty basic logic, it will handle the correct searches and have some tolerance for the faulty searches as well.

 

MVC Parts

As in any MVC project we will need a few parts to make this happen

PageType with PageData

Since I’m creating a very simple search page there is no need for any Page Properties

using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;   
namespace MySite.Content.Pages { [ContentType( DisplayName = "Söksida", GUID = "bd1eec3d-37d1-4717-b3eb-3f5agdf85216", Description = "Sida för sökresultat och sökning")] public class SearchPage : PageData { } }

The Controller

The controller will handle the regular page requests as well as search requests. Please note that the controller sends an instance of the SearchPageViewModel to the view.

using EPiServer.Core; using EPiServer.Web.Mvc; using EPiServer.Web.Routing; using System.Web.Mvc; using MySite.Content.Pages; using MySite.Models.Pages;  
namespace MySite.Controllers.Pages { public class SearchPageController : PageController<SearchPage> { /// <summary> /// Handle regular requests to view the search page /// </summary> /// <param name="currentPage"></param> /// <returns></returns> public ActionResult Index(SearchPage currentPage) { var model = new SearchPageViewModel { Name = currentPage.Name, InvalidQuery = true };   return View(model); }

 

   /// <summary> /// Handle searches /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost] public ActionResult Index(SearchPageViewModel model) { PageRouteHelper pageRouteHelper = EPiServer.ServiceLocation.ServiceLocator.Current.GetInstance<PageRouteHelper>();   PageData currentPage = pageRouteHelper.Page;   model.Name = currentPage != null ? currentPage.Name : "S?k";   if (ModelState.IsValid) { model.Search(model.SearchText, model.Page, model.PageSize); model.InvalidQuery = false; return View("~/Views/SearchPage/index.cshtml", model); } else { model.InvalidQuery = true; return View("~/Views/SearchPage/index.cshtml", model); } } } }

ViewModel

The ViewModel keeps most logic and is the class the view is typed to.

using EPiServer.Globalization; using EPiServer.Search; using log4net; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq;  
namespace MySite.Models.Pages { public class SearchPageViewModel { private static readonly ILog Log = LogManager.GetLogger(typeof(SearchPageViewModel));  

 

public string Name { get; set; }  

 

[Required(ErrorMessage = "Du måste ange en söksträng")] [Display(Name = "Sök")] public string SearchText { get; set; }

 

   /// <summary> /// Will store the modelstate /// </summary> public bool InvalidQuery { get; set; }

 

   /// <summary> /// Storing the search results /// </summary> public List<IndexResponseItem> SearchResult { get; set; }

 

   /// <summary> /// Number of search hits /// </summary> public int SearchHits { get; set; }

 

   /// <summary> /// Current page in the result /// </summary> private int _page = 1; public int Page { get { return _page; } set { _page = value; } }

   /// <summary> /// Number of posts to show on each search page /// </summary> public int PageSize = 10;

 

   /// <summary> /// Main search method /// </summary> /// <param name="q"></param> /// <param name="page"></param> /// <param name="pageSize"></param> public void Search(string q, int page, int pageSize) { // Re-index //var reIndex = new EPiServer.Search.ReIndexManager(); //reIndex.ReIndex();

 

   // Current content language var culture = ContentLanguage.PreferredCulture.Name;

 

   SearchResult = new List<IndexResponseItem>();

 

   // Get the type of UnifiedDirectory, we want to skip this in our search results var unifiedDirectoryType = typeof(EPiServer.Web.Hosting.UnifiedDirectory);

 

   // Do a fieldquery var fieldQuery = new EPiServer.Search.Queries.Lucene.FieldQuery(q); var fieldQueryResult = SearchHandler.Instance.GetSearchResults(fieldQuery, 1, pageSize + 20) .IndexResponseItems .Where(x => (x.Culture.Equals(culture) || string.IsNullOrEmpty(x.Culture)) && !x.ItemType.Contains(unifiedDirectoryType.FullName) ) .ToList();

 

   // Count the result int fieldQueryCount = fieldQueryResult.Count();

 

     // Add to the search result SearchResult.AddRange(fieldQueryResult);

 

   // if less than pageSize results, lets do a fuzzy query if (fieldQueryCount < pageSize) { float startFuzzines = 0.8f;

   for (float i = startFuzzines; i > 0.4f; i = i - 0.1f) { //var fuzzyResult = DoFuzzySearch(q, page, pageSize + 20, i, culture); var fuzzyResult = DoFuzzySearch(q, 1, pageSize + 20, i, culture); SearchResult = MergeSearchResults(SearchResult, fuzzyResult); } }   SearchHits = SearchResult.Count; }

 

   /// <summary> /// Merge search results /// </summary> /// <param name="left"></param> /// <param name="right"></param> /// <returns></returns> private List<IndexResponseItem> MergeSearchResults(List<IndexResponseItem> left, List<IndexResponseItem> right) { // Must remove some duplicates, stupid since union should fix this // EH foreach (var item in left) { right.RemoveAll(x => x.Id.Equals(item.Id)); }   return left.Union(right).ToList(); }

 

   /// <summary> /// Stage some fuzzy searching /// </summary> /// <param name="q"></param> /// <param name="page"></param> /// <param name="pageSize"></param> /// <param name="fuzziness"></param> /// <param name="culture"></param> /// <returns></returns> private List<IndexResponseItem> DoFuzzySearch(string q, int page, int pageSize, float fuzziness, string culture) { // Get the type of UnifiedDirectory, we want to skip this in our search results var unifiedDirectoryType = typeof(EPiServer.Web.Hosting.UnifiedDirectory);   var fuzzyQuery = new EPiServer.Search.Queries.Lucene.FuzzyQuery(q, fuzziness); var fuzzyQueryresult = SearchHandler.Instance.GetSearchResults(fuzzyQuery, page, pageSize) .IndexResponseItems .Where(x => (x.Culture.Equals(culture) || string.IsNullOrEmpty(x.Culture)) && !x.ItemType.Contains(unifiedDirectoryType.FullName) ) .ToList();   return fuzzyQueryresult; }
   } }

A view

The view, typed to SearchPageViewModel. As simple as can be.

@using MySite.Business.Core @model MySite.Models.Pages.SearchPageViewModel  

 

@{ //ViewBag.Title = "Index"; Layout = "~/Views/Shared/Masters/_MainMaster.cshtml"; }  

 

@section TitleContent{ }  

 

@section MainContent{ <div id="leftContent"> @*Meny*@ <div id="subMenuHeader"> <h2>@Model.Name&nbsp;</h2> </div> <div id="subMenu"> </div> </div> <div class="middleSpacer"></div>

   <div id="rightContent">

   <div id="searchPage">

   @using (Html.BeginForm("Index", "SearchPage", FormMethod.Post, new { id = "searchForm" })) { <div id="searchBox"> <ul> <li> @Html.LabelFor(m => m.SearchText, new { @class= "searchLabel" }) </li> <li> @Html.TextBoxFor(m => m.SearchText) @Html.ValidationMessageFor(m => m.SearchText) </li> @*<li> Välj språk </li>*@ <li> <input type="Submit" id="searchSubmitMain" value="Sök" /> </li> </ul> </div>   if (!Model.InvalidQuery) { <div id="searchResult"> <h2>Sökresultat</h2> <p>Din sökning gav ca @Model.SearchHits träffar</p>

   <ul> @foreach (var item in Model.SearchResult.Skip((Model.Page - 1) * Model.PageSize).Take(Model.PageSize)) { <li class="searchResultHeader"><a href="@item.GetExternalUrl()"><h3>@item.Title</h3></a></li> <li class="searchResultText">@item.DisplayText.TruncateAtWord(20)</li> <li class="spacer"></li> } </ul>

   <div id="searchPager"> @*@Html.Hidden("page")*@ @Html.HiddenFor(m => m.Page) @Html.HiddenFor(m => m.SearchHits) @Html.HiddenFor(m => m.PageSize) <input type="button" id="prev" value="Föregående" /> <input type="button" id="next" value="Nästa" /> </div> </div> } } </div> </div> }

Javascript eventhandling

Some JS, customized to use prototyping and jQuery.

$(document).ready(function() { function search() { var that = this;

 

   // Control handling that.PreviousControl(); that.NextControl();

 

   // Event handlers $(".searchBoxSmall").focus(function () { if ($(this).val() == "Sök") { $(this).val(''); } });

$(".searchBoxSmall").blur(function () { if ($(this).val().length == 0) { $(this).val('Sök'); } });

 

   // ... previous $("#searchResult #prev").click(function () { that.Paging("prev"); });

 

   // ... next $("#searchResult #next").click(function () { that.Paging("next"); });

 

   // ... snabbsök $("#header #headerTools #toolKit #searchIcon").click(function () { document.forms["searchFormSmall"].submit(); }); }

 

   // Handle next control search.prototype.NextControl = function () { var searchHits = $("#searchResult #SearchHits").val(); var pageSize = parseInt($("#searchResult #PageSize").val()); var next = $("#searchResult #next"); var page = parseInt($("#searchResult #Page").val());

 

   if (!isNumeric(searchHits) || parseInt(searchHits) <= pageSize || (page * pageSize) >= parseInt(searchHits)) { // Hide next.hide(); } else { // Show next.show(); } };

 

   // Handle previous control search.prototype.PreviousControl = function () { var page = $("#searchResult #Page"); var prev = $("#searchResult #prev"); if (!isNumeric(page.val()) || parseInt(page.val()) <= 1) { // Hide prev.hide(); } else { // Show prev.show(); } };

 

   // Handle paging search.prototype.Paging = function (action) {

    var page = $("#searchResult #Page");

   switch (action) { case "prev": // If no value is set to page or page is equal to 1 if (!isNumeric(page.val()) || parseInt(page.val()) <= 1) { page.val(1); } else { page.val(parseInt(page.val()) - 1); } break;

   case "next": // If no value is set to page if (!isNumeric(page.val())) { page.val(2); } else { page.val(parseInt(page.val()) + 1); }   break; }   document.forms["searchForm"].submit(); }; });

Extension methods

Some simple extension methods to keep the code clean in the view.

namespace MySite.Business.Core
{
    public static class ExtensionMethods
    {
	private static readonly ILog Log = LogManager.GetLogger(typeof(ExtensionMethods));   /// <summary>
        /// Truncate strings after n words
        /// </summary>
        /// <param name="input"></param>
        /// <param name="noWords"></param>
        /// <returns></returns>
        public static string TruncateAtWord(this string input, int noWords)
        {
            string output = string.Empty;
            string[] inputArr = input.Split(new[] { ' ' });   if (inputArr.Length <= noWords)
                return input;   if (noWords > 0)
            {
                for (int i = 0; i < noWords; i++)
                {
                    output += inputArr[i] + " ";
                }
                output += "...";
                return output;
            }
            return input;
        }   public static Uri GetExternalUrl(this IndexResponseItem item)
        {
            try
            {
                UrlBuilder url = new UrlBuilder(item.Uri);
                EPiServer.Global.UrlRewriteProvider.ConvertToExternal(url, item, System.Text.Encoding.UTF8);
                return url.Uri;
            }
            catch (Exception ex)
            {
                Log.Error("Can't create external url on the item because ", ex);
                return default(Uri);
            }
        }
    }
}

Expected result

sök1

Jan 07, 2013

Comments

Jan 7, 2013 05:47 PM

:) Nice article, I enjoyed reading it. Thanks for sharing the JS/JQuery code as well.

Jan 7, 2013 06:59 PM

Thanks, I've cleaned up the text and code formatting a bit!

Santosh Achanta
Santosh Achanta Jan 9, 2013 12:34 AM

Good one Eric :)

Linus Ekström
Linus Ekström Jan 10, 2013 09:06 AM

Shouldn't the Union return call be done outside of the loop in the MergeSearchResults-method to ensure that all duplicates are removed?

Jan 10, 2013 10:39 AM

Linus, I did try that. It didn't work as expected and I had a deadline. I'll update this post when I have found a solution. The code posted works though, and it's fast enough :)

Feb 5, 2013 10:22 AM

I have done exactly like your code, what I can see. But I got no hits at all in the SearchHandler.Instance.GetSearchResults call. Does anyone have an idea why? What have I missed?

Feb 5, 2013 10:30 AM

Andreas,
Is your indexing service running?

Have a look in web.config and check the settings in

In example


Most likely it is but what's more imortant to see is if there are any data in your index at all. In the section episerver.search.indexingservice / namedIndexes / namedIndexes there should be a reference to your index. In my case this is the folder C:\EPiServer\Sites\SiteName\AppData\Index

Download the utility Luke and open the index and confirm that your dataindexing is up and running!

Feb 5, 2013 10:36 AM

Thanks for your answer Eric!

I found the answer in this thread. http://social.msdn.microsoft.com/Forums/en-US/sqlmds/thread/80a6eec0-ac9e-4b0c-94b0-46f7b9bd2fa1

I had to install the "HTTP Activation" in Windows 8 and restart the site. Tada, search hits! :)

Great example Eric, thanks a lot for that!

Joshua Folkerts
Joshua Folkerts Feb 22, 2013 03:31 AM

I do have a question Eric, how can I prevent the index from displaying items that are in the recycle bin and pages that are "Containers" / "Templateless". Thanks for the code samples. Always nice to see strong post to bring strength to the community.

Mar 28, 2013 12:22 PM

Hi Joshua,
Did you solve your problem with pages in the wastebasket?

When the editor moves a page to the wastebasket. Should the page automatic been removed from the index or do I have to re-index or overload a delete method to remove the page from the search-index?

Ted
Ted Apr 4, 2013 09:31 AM

Nice post! I've complemented with some info on adding custom data to the index to tweak how content can be searched: http://tedgustaf.com/blog/2013/4/add-custom-fields-to-the-episerver-search-index-with-episerver-7/

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP - OptiGraphExtensions v2.0: Enhanced Search Control with Language Support, Synonym Slots, and Stop Words

Supercharge your Optimizely Graph search experience with powerful new features for multilingual sites and fine-grained search tuning. As search...

Graham Carr | Dec 16, 2025

A day in the life of an Optimizely OMVP - Optimizely Opal: Specialized Agents, Workflows, and Tools Explained

The AI landscape in digital experience platforms has shifted dramatically. At Opticon 2025, Optimizely unveiled the next evolution of Optimizely Op...

Graham Carr | Dec 16, 2025

Optimizely CMS - Learning by Doing: EP09 - Create Hero, Breadcrumb's and Integrate SEO : Demo

  Episode 9  is Live!! The latest installment of my  Learning by Doing: Build Series  on  Optimizely Episode 9 CMS 12  is now available on YouTube!...

Ratish | Dec 15, 2025 |

Building simple Opal tools for product search and content creation

Optimizely Opal tools make it easy for AI agents to call your APIs – in this post we’ll build a small ASP.NET host that exposes two of them: one fo...

Pär Wissmark | Dec 13, 2025 |

CMS Audiences - check all usage

Sometimes you want to check if an Audience from your CMS (former Visitor Group) has been used by which page(and which version of that page) Then yo...

Tuan Anh Hoang | Dec 12, 2025

Data Imports in Optimizely: Part 2 - Query data efficiently

One of the more time consuming parts of an import is looking up data to update. Naively, it is possible to use the PageCriteriaQueryService to quer...

Matt FitzGerald-Chamberlain | Dec 11, 2025 |