Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 60 additions & 2 deletions LearningHub.Nhs.WebUI/Controllers/Api/SearchController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace LearningHub.Nhs.WebUI.Controllers.Api
namespace LearningHub.Nhs.WebUI.Controllers.Api
{
using System;
using System.Linq;
Expand All @@ -8,6 +8,7 @@
using LearningHub.Nhs.WebUI.Helpers;
using LearningHub.Nhs.WebUI.Interfaces;
using LearningHub.Nhs.WebUI.Models;
using LearningHub.Nhs.WebUI.Models.Search;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -20,14 +21,17 @@
public class SearchController : ControllerBase
{
private readonly ISearchService searchService;
private readonly ISearchTelemetryService searchTelemetryService;

/// <summary>
/// Initializes a new instance of the <see cref="SearchController"/> class.
/// </summary>
/// <param name="searchService">Resource service.</param>
public SearchController(ISearchService searchService)
/// <param name="searchTelemetryService">Search telemetry service.</param>
public SearchController(ISearchService searchService, ISearchTelemetryService searchTelemetryService)
{
this.searchService = searchService;
this.searchTelemetryService = searchTelemetryService;
}

/// <summary>
Expand Down Expand Up @@ -145,5 +149,59 @@ public async Task<IActionResult> RecordClickedCatalogueSearchResult(SearchAction

return this.Ok(await this.searchService.CreateCatalogueSearchActionAsync(searchActionCatalogueModel));
}

/// <summary>
/// Records search result click telemetry for Azure Search observability.
/// </summary>
/// <param name="model">The click telemetry payload.</param>
/// <returns>An <see cref="IActionResult"/>.</returns>
[HttpPost("RecordResultClickTelemetry")]
public async Task<IActionResult> RecordResultClickTelemetry(SearchResultClickTelemetryModel model)
{
if (model == null || string.IsNullOrWhiteSpace(model.ResultUrl))
{
return this.BadRequest();
}

await this.searchTelemetryService.RecordResultClickTelemetryAsync(model);

return this.Ok();
}

/// <summary>
/// Records search executed telemetry for zero-result rate analysis.
/// </summary>
/// <param name="model">The search executed telemetry payload.</param>
/// <returns>An <see cref="IActionResult"/>.</returns>
// [HttpPost("RecordSearchExecutedTelemetry")]
// public async Task<IActionResult> RecordSearchExecutedTelemetry(SearchExecutedTelemetryModel model)
// {
// if (model == null || string.IsNullOrWhiteSpace(model.QueryText))
// {
// return this.BadRequest();
// }

// await this.searchTelemetryService.RecordSearchExecutedFromApiAsync(model);

// return this.Ok();
// }

/// <summary>
/// Records search facet applied telemetry for facet usage analysis.
/// </summary>
/// <param name="model">The facet applied telemetry payload.</param>
/// <returns>An <see cref="IActionResult"/>.</returns>
[HttpPost("RecordFacetAppliedTelemetry")]
public async Task<IActionResult> RecordFacetAppliedTelemetry(SearchFacetAppliedTelemetryModel model)
{
if (model == null || string.IsNullOrWhiteSpace(model.FacetField))
{
return this.BadRequest();
}

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);

return this.Ok();
}
}
}
208 changes: 207 additions & 1 deletion LearningHub.Nhs.WebUI/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ namespace LearningHub.Nhs.WebUI.Controllers
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using LearningHub.Nhs.Models.Extensions;
using LearningHub.Nhs.Models.Search;
using LearningHub.Nhs.Models.Search.SearchClick;
using LearningHub.Nhs.WebUI.Filters;
Expand All @@ -31,6 +33,7 @@ public class SearchController : BaseController
private readonly ISearchService searchService;
private readonly IFileService fileService;
private readonly IFeatureManager featureManager;
private readonly ISearchTelemetryService searchTelemetryService;

/// <summary>
/// Initializes a new instance of the <see cref="SearchController"/> class.
Expand All @@ -43,6 +46,7 @@ public class SearchController : BaseController
/// <param name="fileService">The fileService.</param>
/// <param name="featureManager"> The Feature flag manager.</param>
/// <param name="moodleBridgeApiService">moodleBridgeApiService.</param>
/// <param name="searchTelemetryService">Search telemetry service.</param>
public SearchController(
IHttpClientFactory httpClientFactory,
IWebHostEnvironment hostingEnvironment,
Expand All @@ -51,12 +55,14 @@ public SearchController(
ILogger<SearchController> logger,
IFileService fileService,
IMoodleBridgeApiService moodleBridgeApiService,
IFeatureManager featureManager)
IFeatureManager featureManager,
ISearchTelemetryService searchTelemetryService)
: base(hostingEnvironment, httpClientFactory, logger, moodleBridgeApiService, settings.Value)
{
this.searchService = searchService;
this.fileService = fileService;
this.featureManager = featureManager;
this.searchTelemetryService = searchTelemetryService;
}

/// <summary>
Expand All @@ -77,6 +83,8 @@ public async Task<IActionResult> Index(SearchRequestViewModel search, bool noSor
var azureSearchEnabled = Task.Run(() => this.featureManager.IsEnabledAsync(FeatureFlags.AzureSearch)).Result;
SearchResultViewModel searchResult = new SearchResultViewModel();

var stopwatch = Stopwatch.StartNew();

if (azureSearchEnabled)
{
searchResult = await this.searchService.PerformSearch(this.User, search);
Expand All @@ -86,6 +94,8 @@ public async Task<IActionResult> Index(SearchRequestViewModel search, bool noSor
searchResult = await this.searchService.PerformSearchInFindwise(this.User, search);
}

stopwatch.Stop();

if (search.SearchId == 0 && searchResult.ResourceSearchResult != null)
{
var searchId = await this.searchService.RegisterSearchEventsAsync(
Expand All @@ -99,6 +109,9 @@ public async Task<IActionResult> Index(SearchRequestViewModel search, bool noSor
{
searchResult.CatalogueSearchResult.SearchId = searchId;
}

// Record SearchExecutedTelemetry for zero-result rate analysis
await this.searchTelemetryService.RecordSearchExecutedAsync(search, searchResult, this.User.Identity.GetCurrentUserId(), stopwatch.ElapsedMilliseconds);
}

if (filterApplied)
Expand Down Expand Up @@ -177,6 +190,9 @@ public async Task<IActionResult> IndexPost([FromQuery] SearchRequestViewModel se
return await this.Index(search, noSortFilterError: true);
}

// Record facet telemetry when filters are changed
await this.RecordFacetChangesAsync(search, filterUpdated, newFilters, existingFilters, resourceAccessLevelFilterUpdated, resourceAccessLevelId, search.ResourceAccessLevelId, filterProviderUpdated, newProviderFilters, existingProviderFilters, filterResourceCollectionUpdated, newResourceCollectionFilter, existingResourceCollectionFilter);

if (search.ResourcePageIndex > 0 && (filterUpdated || resourceAccessLevelFilterUpdated || filterProviderUpdated || filterResourceCollectionUpdated))
{
search.ResourcePageIndex = null;
Expand Down Expand Up @@ -435,5 +451,195 @@ public IActionResult RecordAutoSuggestionClick(string term, string url, string c
this.searchService.SendAutoSuggestionClickActionAsync(clickPayloadModel);
return this.Redirect(url);
}

/// <summary>
/// Records facet changes when filters are applied via the Apply button.
/// </summary>
/// <param name="search">The current search request.</param>
/// <param name="filterUpdated">Whether resource type filters were updated.</param>
/// <param name="newFilters">The new resource type filters.</param>
/// <param name="existingFilters">The existing resource type filters.</param>
/// <param name="resourceAccessLevelFilterUpdated">Whether resource access level filter was updated.</param>
/// <param name="newAccessLevelId">The new resource access level filter id.</param>
/// <param name="existingAccessLevelId">The existing resource access level filter id.</param>
/// <param name="filterProviderUpdated">Whether provider filters were updated.</param>
/// <param name="newProviderFilters">The new provider filters.</param>
/// <param name="existingProviderFilters">The existing provider filters.</param>
/// <param name="filterResourceCollectionUpdated">Whether resource collection filters were updated.</param>
/// <param name="newResourceCollectionFilter">The new resource collection filters.</param>
/// <param name="existingResourceCollectionFilter">The existing resource collection filters.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
private async Task RecordFacetChangesAsync(
SearchRequestViewModel search,
bool filterUpdated,
IOrderedEnumerable<string> newFilters,
IOrderedEnumerable<string> existingFilters,
bool resourceAccessLevelFilterUpdated,
int? newAccessLevelId,
int? existingAccessLevelId,
bool filterProviderUpdated,
IOrderedEnumerable<string> newProviderFilters,
IOrderedEnumerable<string> existingProviderFilters,
bool filterResourceCollectionUpdated,
IOrderedEnumerable<string> newResourceCollectionFilter,
IOrderedEnumerable<string> existingResourceCollectionFilter)
{
var correlationId = search.SearchId.ToString();
var sessionId = search.GroupId ?? string.Empty;
var queryText = search.Term ?? string.Empty;

// Record resource type filter changes
if (filterUpdated)
{
var addedFilters = newFilters.Except(existingFilters);
var removedFilters = existingFilters.Except(newFilters);

foreach (var filter in addedFilters)
{
var model = new SearchFacetAppliedTelemetryModel
{
CorrelationId = correlationId,
SessionId = sessionId,
QueryText = queryText,
QueryMode = "standard",
FacetField = "ResourceType",
FacetValue = filter,
FacetAction = "applied",
};

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);
}

foreach (var filter in removedFilters)
{
var model = new SearchFacetAppliedTelemetryModel
{
CorrelationId = correlationId,
SessionId = sessionId,
QueryText = queryText,
QueryMode = "standard",
FacetField = "ResourceType",
FacetValue = filter,
FacetAction = "removed",
};

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);
}
}

// Record resource access level filter changes
if (resourceAccessLevelFilterUpdated)
{
if (existingAccessLevelId.HasValue && existingAccessLevelId > 0)
{
var model = new SearchFacetAppliedTelemetryModel
{
CorrelationId = correlationId,
SessionId = sessionId,
QueryText = queryText,
QueryMode = "standard",
FacetField = "AudienceAccessLevel",
FacetValue = existingAccessLevelId.ToString(),
FacetAction = "removed",
};

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);
}

if (newAccessLevelId.HasValue && newAccessLevelId > 0)
{
var model = new SearchFacetAppliedTelemetryModel
{
CorrelationId = correlationId,
SessionId = sessionId,
QueryText = queryText,
QueryMode = "standard",
FacetField = "AudienceAccessLevel",
FacetValue = newAccessLevelId.ToString(),
FacetAction = "applied",
};

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);
}
}

// Record provider filter changes
if (filterProviderUpdated)
{
var addedProviders = newProviderFilters.Except(existingProviderFilters);
var removedProviders = existingProviderFilters.Except(newProviderFilters);

foreach (var provider in addedProviders)
{
var model = new SearchFacetAppliedTelemetryModel
{
CorrelationId = correlationId,
SessionId = sessionId,
QueryText = queryText,
QueryMode = "standard",
FacetField = "Provider",
FacetValue = provider,
FacetAction = "applied",
};

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);
}

foreach (var provider in removedProviders)
{
var model = new SearchFacetAppliedTelemetryModel
{
CorrelationId = correlationId,
SessionId = sessionId,
QueryText = queryText,
QueryMode = "standard",
FacetField = "Provider",
FacetValue = provider,
FacetAction = "removed",
};

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);
}
}

// Record resource collection filter changes
if (filterResourceCollectionUpdated)
{
var addedCollections = newResourceCollectionFilter.Except(existingResourceCollectionFilter);
var removedCollections = existingResourceCollectionFilter.Except(newResourceCollectionFilter);

foreach (var collection in addedCollections)
{
var model = new SearchFacetAppliedTelemetryModel
{
CorrelationId = correlationId,
SessionId = sessionId,
QueryText = queryText,
QueryMode = "standard",
FacetField = "ResourceCollection",
FacetValue = collection,
FacetAction = "applied",
};

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);
}

foreach (var collection in removedCollections)
{
var model = new SearchFacetAppliedTelemetryModel
{
CorrelationId = correlationId,
SessionId = sessionId,
QueryText = queryText,
QueryMode = "standard",
FacetField = "ResourceCollection",
FacetValue = collection,
FacetAction = "removed",
};

await this.searchTelemetryService.RecordFacetAppliedTelemetryAsync(model);
}
}
}
}
}
Loading
Loading