Episode 11 - Part 1

Umbraco 13 Tutorial - Episode 11 Part 1 - Searching with Examine

Links

Support from me and my employers ClerksWell

https://www.clerkswell.com/contact/

 

GitHub Repo

🐙 GitHub

https://github.com/prjseal/Umbraco-13-Series/

📝 Guest Book

https://github.com/prjseal/Umbraco-13-Series/issues/1

 

Get Help

💬 Discord

https://discord.gg/umbraco

🗣️ Facebook Group

https://www.facebook.com/groups/umbracowebdevs

🐘 Mastodon

https://umbracocommunity.social/

🗨️ Umbraco Forum

https://our.umbraco.com/forum/

☕Buy me a coffee

https://codeshare.co.uk/coffee

Code

RegisterServicesComposer.cs

using Freelancer.Services;

using Umbraco.Cms.Core.Composing;

namespace Freelancer.Composers;

public class RegisterServicesComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.Services.AddTransient<ISearchService, SearchService>();
    }
}

RegisterServicesComposer.cs

SearchPageController.cs

using Freelancer.Models.ContentModels;
using Freelancer.Models.Search;
using Freelancer.Models.ViewModels;
using Freelancer.Services;

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;

using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Common.Controllers;

namespace Freelancer.Controllers.Render;

public class SearchPageController(
    ILogger<RenderController> logger,
    ICompositeViewEngine compositeViewEngine,
    IUmbracoContextAccessor umbracoContextAccessor,
    IHttpContextAccessor httpContextAccessor,
    ISearchService searchService)
        : RenderController(logger, compositeViewEngine, umbracoContextAccessor)
{
    public override IActionResult Index()
    {
        var httpContext = httpContextAccessor.HttpContext;
        var query = httpContext?.Request.Query["query"];
        var page = httpContext?.Request.Query["page"];

        if (CurrentPage == null) return BadRequest();

        var pageSize = 10;

        var searchRequest = new SearchRequestModel(query, page, pageSize);

        var searchResponse = searchService.Search(searchRequest);

        var pagination = new PaginationViewModel
        {
            TotalResults = searchResponse.TotalResultCount,
            TotalPages = (int)Math.Ceiling((double)(searchResponse.TotalResultCount / searchRequest.PageSize)),
            ResultsPerPage = searchRequest.PageSize,
            CurrentPage = searchRequest.Page
        };

        var model = new SearchPageContentModel(CurrentPage)
        {
            SearchRequest = searchRequest,
            SearchResponse = searchResponse,
            Pagination = pagination
        };

        return CurrentTemplate(model);
    }
}

SearchPageController.cs

SearchPageContentModel.cs

using Freelancer.Models.Search;
using Freelancer.Models.ViewModels;

namespace Freelancer.Models.ContentModels;

public class SearchPageContentModel(IPublishedContent? content) : ContentModel(content)
{
    public SearchRequestModel? SearchRequest { get; set; }
    public SearchResponseModel? SearchResponse { get; set; }
    public PaginationViewModel Pagination { get; set; }
}

SearchPageContentModel.cs

SearchRequestModel.cs

namespace Freelancer.Models.Search;

public class SearchRequestModel
{
    public string? Query { get; set; }
    public int Page { get; set; }
    public int PageSize { get; set; }
    public int Skip => Page > 1 ? (Page - 1) * PageSize : 0;

    public SearchRequestModel(string? query, string? page, int pageSize)
    {
        Query = query;

        if (int.TryParse(page, out int pageNumber) && pageNumber > 0)
        {
            Page = pageNumber;
        }

        PageSize = pageSize;
    }
}

SearchRequestModel.cs

SearchResponseModel.cs

using Examine;

namespace Freelancer.Models.Search;

public class SearchResponseModel
{
    public bool HasResults => TotalResultCount > 0;
    public string? Query { get; set; }
    public long TotalResultCount { get; set; }
    public IEnumerable<ISearchResult>? SearchResults { get; set; }

    public SearchResponseModel() { }

    public SearchResponseModel(string? query, long totalResultCount, IEnumerable<ISearchResult>? searchResults)
    {
        Query = query;
        TotalResultCount = totalResultCount;
        SearchResults = searchResults;
    }
}

SearchResponseModel.cs

PaginationViewModel.cs

namespace Freelancer.Models.ViewModels;

public class PaginationViewModel
{
    public long TotalResults { get; set; }
    public int ResultsPerPage { get; set; }
    public int TotalPages { get; set; }
    public string? PaginationUrlFormat { get; set; }
    public int CurrentPage { get; set; }
}

PaginationViewModel

ISearchService.cs

using Freelancer.Models.Search;

namespace Freelancer.Services;

public interface ISearchService
{
    public SearchResponseModel Search(SearchRequestModel searchRequest);
}

ISearchService

SearchService.cs

using Examine;
using Examine.Search;

using Freelancer.Models.Search;

using Lucene.Net.Analysis.Core;

using StackExchange.Profiling.Internal;

using Umbraco.Cms.Infrastructure.Examine;

using UmbracoConstants = Umbraco.Cms.Core.Constants;

namespace Freelancer.Services;

public class SearchService : ISearchService
{
    private readonly IExamineManager _examineManager;
    private readonly string[] _docTypesToExclude =
        [PageTags.ModelTypeAlias, PageTag.ModelTypeAlias, SiteSettings.ModelTypeAlias, ReusableContentRepository.ModelTypeAlias, ReusableContent.ModelTypeAlias];

    public SearchService(IExamineManager examineManager)
    {
        _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager));

    }

    public SearchResponseModel Search(SearchRequestModel searchRequest)
    {
        if (searchRequest == null || !_examineManager.TryGetIndex(UmbracoConstants.UmbracoIndexes.ExternalIndexName, out IIndex? index))
        {
            return new SearchResponseModel();
        }

        IBooleanOperation? query = index.Searcher.CreateQuery(IndexTypes.Content)
            .GroupedNot(["umbracoNaviHide"], ["1"])
            .And().GroupedNot(["__NodeTypeAlias"], _docTypesToExclude); //fixed it for you

        string[]? terms = !string.IsNullOrWhiteSpace(searchRequest.Query)
            ? searchRequest.Query.Split(" ", StringSplitOptions.RemoveEmptyEntries)
            .Where(x => !StopAnalyzer.ENGLISH_STOP_WORDS_SET.Contains(x.ToLower()) && x.Length > 2).ToArray() : null;

        if (terms != null && terms.Length > 0)
        {
            query!.And().Group(q => q
                .GroupedOr(["nodeName"], terms), BooleanOperation.Or);
        }

        ISearchResults? pageOfResults = query.Execute(new QueryOptions(searchRequest.Skip, searchRequest.PageSize));

        return new SearchResponseModel(searchRequest.Query, pageOfResults.TotalItemCount, pageOfResults);
    }
}

SearchService.cs

SearchPage.cshtml

@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<SearchPageContentModel>

@{
    Layout = "Main.cshtml";
}

<header class="masthead bg-primary text-white text-center">
    <div class="container d-flex align-items-center flex-column">
        <h1 class="masthead-heading text-uppercase mb-0">@Model.Content.Name</h1>
    </div>
</header>

<div class="container my-5">
    <div class="row">
        <div class="col-lg-3 col-12 mx-auto">
            @Model.SearchRequest.Query
        </div>
        <div class="col-lg-9 col-12 mx-auto">
            @if(Model.SearchResponse.HasResults)
            {
                <ul>
                    @foreach(var item in Model.SearchResponse.SearchResults)
                    {
                        var contentItem = Umbraco.Content(item.Id);
                        <li>
                            <a href="@contentItem.Url()">@contentItem.Name</a>
                        </li>
                    }
                </ul>
            }
        </div>
    </div>
</div>

SearchPage.cshtml

_ViewImports.cshtml

@using Freelancer.Extensions
@using Freelancer.Models.ContentModels
@using Umbraco.Cms.Core.Services
@using Umbraco.Extensions
@using Umbraco.Cms.Web.Common.PublishedModels;
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@using Umbraco.Cms.Web.Common.Views
@using Umbraco.Cms.Core.Models.Blocks
@using Umbraco.Cms.Core.Models.PublishedContent
@using Microsoft.AspNetCore.Html
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Smidge
@addTagHelper *, Our.Umbraco.TagHelpers
@inject Smidge.SmidgeHelper SmidgeHelper
@using Slimsy.Enums;
@addTagHelper *, Slimsy
@inject Slimsy.Services.SlimsyService SlimsyService
@using Slimsy.Extensions

_ViewImports.cshtml