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
🗣️ Facebook Group
https://www.facebook.com/groups/umbracowebdevs
🐘 Mastodon
https://umbracocommunity.social/
🗨️ Umbraco Forum
https://our.umbraco.com/forum/
☕Buy me a 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>();
}
}
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);
}
}
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; }
}
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;
}
}
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;
}
}
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; }
}
ISearchService.cs
using Freelancer.Models.Search;
namespace Freelancer.Services;
public interface ISearchService
{
public SearchResponseModel Search(SearchRequestModel searchRequest);
}
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);
}
}
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>
_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