Sample Code

Sample code for the live site search implementation

After referencing the Bolster assemblies in your .NET project, you will use the Search method in the BolsterSearchService class in the Roundedcube.Kentico.Bolster.Search namespace to retrieve search results.

Some other necessary classes are:

  • Roundedcube.Bolster.Lucene.Models.SearchResults
    • encapsulates the search results from a query
  • Roundedcube.Bolster.Kentico.Search.Models.SearchRequest
    • wraps relevant information for the search query
    • is the only parameter for the BolsterSearchService.Search method
  • Roundedcube.Bolster.Kentico.Search.Models.SearchFilter
    • models the user selected filters

 

Some sample code is included here to help you get started.

 

Create the Component

SearchIndexDropDownComponent.cs

This code implements a dropdown form control that allows selection of a search index in the search results widget properties.

using CMS.Search;
using CMS.SiteProvider;
using DancingGoat.Search.Forms;
using Kentico.Forms.Web.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;


[assembly: RegisterFormComponent("SearchIndexDropDownComponent", typeof(SearchIndexDropDownComponent), "Search Index Dropdown Component")]
namespace DancingGoat.Search.Forms
{
    public class SearchIndexDropDownComponent : SelectorFormComponent
    {
        public const string IDENTIFIER = "SearchIndexDropDownComponent";

        protected override IEnumerable GetItems()
        {
            var props = this.Properties;
            List searchIndices = SearchIndexSiteInfoProvider.GetSiteIndexes(SiteContext.CurrentSiteID).ToList();
            SearchIndexInfo noIndex = new SearchIndexInfo { IndexDisplayName = "None", IndexID = 0 };
            searchIndices.Insert(0, noIndex);
            foreach (var searchIndex in searchIndices)
            {
                var listItem = new SelectListItem() { Text = searchIndex.IndexDisplayName, Value = searchIndex.IndexID.ToString() };
                yield return listItem;
            }
        }
    }
}

 

_SearchIndexDropDownComponent.cshtml

Front end code for the search index dropdown control.

@using Kentico.Forms.Web.Mvc
@using DancingGoat.Search.Forms

@model SearchIndexDropDownComponent

@{
    var htmlAttributes = ViewData.GetEditorHtmlAttributes();
}

@Html.DropDownListFor(x => x.SelectedValue, Model.Items, null, htmlAttributes)

Setup the Widget Properties

SearchResultsWidgetProperties.cs

This class defines widget properties for the Search Results widget.

using DancingGoat.Search.Forms;
using Kentico.Forms.Web.Mvc;
using Kentico.PageBuilder.Web.Mvc;
using Roundedcube.Bolster.Kentico.Search;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace DancingGoat.Search.Models.PageBuilder
{
    public class SearchResultsWidgetProperties : IWidgetProperties
    {
        [EditingComponent(SearchIndexDropDownComponent.IDENTIFIER, Label = "Search Index")]
        public string SearchIndex { get; set; }

        [EditingComponent(IntInputComponent.IDENTIFIER, Label = "Page Size")]
        public int PageSize { get; set; }

        [EditingComponent(DropDownComponent.IDENTIFIER, Label = "SearchMode", DefaultValue = "allwordsorsynonyms")]
        [EditingComponentProperty(nameof(DropDownProperties.DataSource), BolsterSearchService.EXACTPHRASE_SEARCHMODE + ";Exact Phrase\r\n" + BolsterSearchService.ANYWORDS_SEARCHMODE + ";Any word\r\n" + BolsterSearchService.ALLWORDS_SEARCHMODE + ";All words\r\n" + BolsterSearchService.ANYWORDSSYNONYMS_SEARCHMODE + ";Any word or related terms\r\n" + BolsterSearchService.ALLWORDSSYNONYMS_SEARCHMODE + ";All words or related terms")]
        public string SearchMode { get; set; }

        [EditingComponent(TextAreaComponent.IDENTIFIER, Label = "No Results Message")]
        public string NoResultsMessage { get; set; }
    }
}

Create the View Model

SearchResultsViewModel.cs

using DancingGoat.Search.Models.PageBuilder;
using Roundedcube.Bolster.Lucene.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using static Roundedcube.Bolster.Lucene.Models.SearchResults;

namespace DancingGoat.Search.Models
{
    public class SearchResultsViewModel
    {
        public List SearchResults { get; set; }
        public List Facets { get; set; }
        public int Page { get; set; }
        public int Total { get; set; }
        public int StartPosition { get; set; }
        public int EndPosition { get; set; }
        public SearchResultsWidgetProperties Properties { get; set; }
        public int IndexID { get; set; }
        public string SearchTerms { get; set; }
    }
}

Create a Controller

SearchResultsWidgetController.cs

This code implements the search results widget. It calls the BolsterSearchService.Search() method, and provides an action that builds a collection of SearchFilter objects that are used for facet search.

using DancingGoat.Search.Controllers.Widgets;
using DancingGoat.Search.Models;
using DancingGoat.Search.Models.PageBuilder;
using Kentico.PageBuilder.Web.Mvc;
using Roundedcube.Bolster.Kentico.Search;
using Roundedcube.Bolster.Kentico.Search.Models;
using Roundedcube.Bolster.Lucene.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using static Roundedcube.Bolster.Lucene.Models.SearchResults;

[assembly: RegisterWidget("DancingGoat.General.SearchResults", typeof(SearchResultsWidgetController), "Bolster Search Results")]
namespace DancingGoat.Search.Controllers.Widgets
{
    public class SearchResultsWidgetController : WidgetController
    {
        public ActionResult Index()
        {
            SearchResultsWidgetProperties props = GetProperties();
            int page = 1;
            int pageSize = props.PageSize;

            SearchResultsViewModel vm = new SearchResultsViewModel { Page = page, SearchResults = new List(), Total = 0, StartPosition = 0, EndPosition = 0 };

            if (Request.QueryString["page"] != null && !String.IsNullOrEmpty(Request.QueryString["page"]))
                page = int.Parse(Request.QueryString["page"]);

            string searchText = String.Empty;
            if (Request.QueryString["q"] != null)
            {
                searchText = Request.QueryString["q"];
                vm.SearchTerms = searchText;
            }

            List searchFilters = new List();
            if (Request.QueryString["f"] != null)
            {
                string facetParams = Request.QueryString["f"];
                if (!String.IsNullOrEmpty(facetParams))
                {
                    string[] facetDimensionsArray = facetParams.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (string facetDimension in facetDimensionsArray)
                    {
                        SearchFilter searchFilter = new SearchFilter();

                        string[] facetDimensionParts = facetDimension.Split(':');
                        if (facetDimensionParts.Count() == 2)
                        {
                            searchFilter.DimensionUrlParameter = facetDimensionParts[0];
                            searchFilter.SelectedValues = facetDimensionParts[1].Split(',').ToList();
                        }

                        searchFilters.Add(searchFilter);
                    }
                }
            }

            BolsterSearchService searchService = new BolsterSearchService();
            SearchResults results = null;
            if (props.SearchIndex != "0" && !String.IsNullOrEmpty(searchText))
            {
                SearchRequest searchRequest = new SearchRequest { SearchIndexID = int.Parse(props.SearchIndex), PageNumber = page, PageSize = pageSize, SearchMode = BolsterSearchMode.AnyWordWithSynonyms, SearchQuery = searchText, SearchFilters = searchFilters, Culture = HttpContext.Request.UserLanguages[0] };
                results = searchService.Search(searchRequest);
            }

            if (results != null)
            {
                vm.Total = results.ResultsTotalCount;
                vm.StartPosition = results.ResultsTotalCount == 0 ? 0 : results.StartPosition;
                vm.EndPosition = results.EndPosition;
                vm.SearchResults = results.Results;
                vm.Facets = results.Facets;
                vm.Page = page;
            }
            vm.Properties = props;

            if (!String.IsNullOrEmpty(props.SearchIndex))
                vm.IndexID = int.Parse(props.SearchIndex);

            return PartialView("~/Views/Shared/Widgets/SearchResults.cshtml", vm);
        }

public ActionResult FilterResults(string dimension, string value, string facetParams, bool removeFilter, string searchTerms) { List searchFilters = new List(); bool facetDimensionExistsInParams = false; //construct the search filters using the facet parameters that passed into this action if (!String.IsNullOrEmpty(facetParams)) { string[] facetDimensionsArray = facetParams.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries); foreach (string facetDimension in facetDimensionsArray) { SearchFilter searchFilter = new SearchFilter(); string[] facetDimensionParts = facetDimension.Split(':'); if (facetDimensionParts.Count() == 2) { searchFilter.DimensionUrlParameter = facetDimensionParts[0]; searchFilter.SelectedValues = facetDimensionParts[1].Split(',').ToList(); if (facetDimensionParts[0] == dimension) { facetDimensionExistsInParams = true; //removal of filter if (searchFilter.SelectedValues.Contains(value) && removeFilter) searchFilter.SelectedValues.Remove(value); //addition of filter, must add to list of values in an existing dimension else if (!searchFilter.SelectedValues.Contains(value) && !removeFilter) searchFilter.SelectedValues.Add(value); } } if (searchFilter.SelectedValues.Count > 0) searchFilters.Add(searchFilter); } } if ((String.IsNullOrEmpty(facetParams) || !facetDimensionExistsInParams) && !removeFilter) { SearchFilter searchFilter = new SearchFilter { DimensionUrlParameter = dimension, SelectedValues = new List { value } }; searchFilters.Add(searchFilter); } //rebuild the facet URL parameter with the changes string newFacetParams = String.Empty; StringBuilder newFacetParamsBuilder = new StringBuilder(); foreach (SearchFilter filter in searchFilters) { if (newFacetParamsBuilder.Length != 0) newFacetParamsBuilder.Append("|"); newFacetParamsBuilder.Append(filter.DimensionUrlParameter + ":"); int facetValueCounter = 1; foreach (string facetValue in filter.SelectedValues) { newFacetParamsBuilder.Append(facetValue); if (facetValueCounter < filter.SelectedValues.Count) newFacetParamsBuilder.Append(","); facetValueCounter++; } } newFacetParams = newFacetParamsBuilder.ToString(); Response.Redirect("~/en-us/landingpage/bolster-search?q=" + searchTerms + "&f=" + newFacetParams); return null; } } }

Show the Results

SearchResults.cshtml

Front end code for the search results widget.


@using DancingGoat.Search.Models


@model SearchResultsViewModel

<input type="hidden" id="hidSearchIndexID" value="@Model.IndexID" />
<input type="hidden" id="hidMaxNAutosuggest" value="@Model.MaxNAutosuggestions" />



<div class="container">
    <div class="row">
        <div class="col-md-7">
            <div>
                @if (!String.IsNullOrEmpty(Model.SearchTerms))
                {
                    <p class="uk-text-bold">Displaying @Model.StartPosition - @Model.EndPosition of @Model.Total results.</p>

                    foreach (var result in Model.SearchResults)
                    {
                        <div class="search-result-item row">
                            @if (!String.IsNullOrEmpty(result.Image))
                            {
                                <div class="col-xs-4 col-sm-2">
                                    <img class="img-responsive" src="@(result.Image.StartsWith("/")?Url.Content("~" + result.Image):result.Image)" />
                                </div>
                            }
                            <div class="@(!String.IsNullOrEmpty(result.Image)?"col-sm-9 col-sm-offset-1":"col-sm-12")">
                                <span style="font-weight:bold; font-size: 1.5rem"><a style="text-decoration:none" href="@(result.Link.StartsWith("/")?Url.Content("~" + result.Link):result.Link)">@result.Title</a></span><br />
                                <p class="margin-vertical-0">
                                    @result.Summary
                                </p>
                            </div>

                        </div>
                    }

                    if (Model.Total == 0)
                    {
                        <p>@Html.Raw(Model.Properties.NoResultsMessage)</p>
                    }
                }
            </div>
            <div class="search-results-pagination">
                <ul>
                    @{
                        var prevpageqs = HttpUtility.ParseQueryString(Request.QueryString.ToString());
                        var toppageqs = HttpUtility.ParseQueryString(Request.QueryString.ToString());
                        var nextpageqs = HttpUtility.ParseQueryString(Request.QueryString.ToString());
                        prevpageqs["page"] = (Model.Page - 1).ToString();
                        nextpageqs["page"] = (Model.Page + 1).ToString();
                        toppageqs.Remove("page");

                        bool showNextPageNumbers = Model.Page * Model.Properties.PageSize < Model.Total;
                        int endNumber = ((Model.Page * Model.Properties.PageSize) + Model.Properties.PageSize) > Model.Total ? Model.Total : ((Model.Page * Model.Properties.PageSize) + Model.Properties.PageSize);

                    }
                    @if (Model.Page > 1)
                    {
                        <li><a href="@(Request.Path + "?" + prevpageqs)">Previous</a></li>
                    }
                    @if (Model.Page != 1)
                    {
                        <li><a href="@(Request.Path + "?" + toppageqs)">Top Page</a></li>
                    }
                    @if (Model.Page * Model.Properties.PageSize < Model.Total)
                    {
                        <li><a href="@(Request.Path + "?" + nextpageqs)">Next @(showNextPageNumbers ? "(" + ((Model.Page * Model.Properties.PageSize) + 1) + " - " + endNumber + ")" : "")</a></li>
                    }
                </ul>
            </div>

        </div>
        <div class="col-md-4 col-md-offset-1">
            @if (Model.Facets.Count > 0)
            {
                <h3>Refine results</h3>
            }

            @{ 
                var qs = HttpUtility.ParseQueryString(Request.QueryString.ToString());
                string facetParameter = qs["f"];
                string searchTerms = qs["q"];
                string page = qs["page"];
            }
            @foreach (var facet in Model.Facets)
            {
                <h4>@facet.Name</h4>

                int valueCounter = 0;
                <ul style="list-style:none">
                @foreach (var value in facet.Values)
                {
                    <li>
                        @if (facet.Selected.Contains(value.Label))
                        {
                            <input type="checkbox" name="@("chk" + facet.UrlParameter)" id="@("chk" + facet.UrlParameter + valueCounter.ToString())" value="@value.Label" checked="checked" onchange="searchWithFilter(event, '@facet.UrlParameter', '@value.Label')" />
                        }
                        else
                        {
                            <input type="checkbox" name="@("chk" + facet.UrlParameter)" id="@("chk" + facet.UrlParameter + valueCounter.ToString())" value="@value.Label" onchange="searchWithFilter(event, '@facet.UrlParameter', '@value.Label')" />
                        }
                        <label for="@("chk" + facet.UrlParameter + valueCounter.ToString())">@value.Label (@value.Value)</label>
                    </li>
                    valueCounter++;
                }
                </ul>
            }
        </div>
    </div>
</div>

<script>
    function searchWithFilter(e, dimension, valueLabel) {
        var facetParameter = "@facetParameter";

        var url = '@Url.Action("FilterResults", "SearchResultsWidget")' + '?dimension=' + dimension + '&value=' + valueLabel + '&facetParams=' + facetParameter + '&removeFilter=' + (!e.target.checked) + '&searchTerms=@searchTerms';
        window.location.href = url;
    }
</script>