Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cl/facets #44

Merged
merged 18 commits into from
Sep 6, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using Dfe.Data.Common.Infrastructure.CognitiveSearch.SearchByKeyword;
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Options;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

using AzureModels = Azure.Search.Documents.Models;

namespace Dfe.Data.SearchPrototype.Infrastructure;

/// <summary>
Expand All @@ -17,7 +18,8 @@ public sealed class CognitiveSearchServiceAdapter<TSearchResult> : ISearchServic
{
private readonly ISearchByKeywordService _searchByKeywordService;
private readonly ISearchOptionsFactory _searchOptionsFactory;
private readonly IMapper<Pageable<SearchResult<TSearchResult>>, EstablishmentResults> _searchResponseMapper;
private readonly IMapper<Pageable<AzureModels.SearchResult<TSearchResult>>, EstablishmentResults> _searchResultMapper;
private readonly IMapper<Dictionary<string, IList<Azure.Search.Documents.Models.FacetResult>>, EstablishmentFacets> _facetsMapper;

/// <summary>
/// The following dependencies include the core cognitive search service definition,
Expand All @@ -29,17 +31,22 @@ public sealed class CognitiveSearchServiceAdapter<TSearchResult> : ISearchServic
/// <param name="searchOptionsFactory">
/// Factory class definition for prescribing the requested search options (by collection context).
/// </param>
/// <param name="searchResponseMapper">
/// Maps the raw azure search response to the required "T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Establishments"
/// <param name="searchResultMapper">
/// Maps the raw Azure search response to the required <see cref="EstablishmentResults"/>
/// </param>
/// <param name="facetsMapper">
/// Maps the the raw Azure search response to the required <see cref="EstablishmentFacets"/>
/// </param>
public CognitiveSearchServiceAdapter(
ISearchByKeywordService searchByKeywordService,
ISearchOptionsFactory searchOptionsFactory,
IMapper<Pageable<SearchResult<TSearchResult>>, EstablishmentResults> searchResponseMapper)
IMapper<Pageable<AzureModels.SearchResult<TSearchResult>>, EstablishmentResults> searchResultMapper,
IMapper<Dictionary<string, IList<AzureModels.FacetResult>>, EstablishmentFacets> facetsMapper)
{
_searchOptionsFactory = searchOptionsFactory;
_searchByKeywordService = searchByKeywordService;
_searchResponseMapper = searchResponseMapper;
_searchResultMapper = searchResultMapper;
_facetsMapper = facetsMapper;
}

/// <summary>
Expand All @@ -62,14 +69,14 @@ public CognitiveSearchServiceAdapter(
/// Exception thrown if the data cannot be mapped
/// </exception>

public async Task<EstablishmentResults> SearchAsync(SearchContext searchContext)
public async Task<SearchResults> SearchAsync(SearchContext searchContext)
{
SearchOptions searchOptions =
_searchOptionsFactory.GetSearchOptions(searchContext.TargetCollection) ??
throw new ApplicationException(
$"Search options cannot be derived for {searchContext.TargetCollection}.");

Response<SearchResults<TSearchResult>> searchResults =
Response<AzureModels.SearchResults<TSearchResult>> searchResults =
await _searchByKeywordService.SearchAsync<TSearchResult>(
searchContext.SearchKeyword,
searchContext.TargetCollection,
Expand All @@ -79,6 +86,14 @@ await _searchByKeywordService.SearchAsync<TSearchResult>(
throw new ApplicationException(
$"Unable to derive search results based on input {searchContext.SearchKeyword}.");

return _searchResponseMapper.MapFrom(searchResults.Value.GetResults());
var results = new SearchResults()
{
Establishments = _searchResultMapper.MapFrom(searchResults.Value.GetResults()),
Facets = searchResults.Value.Facets != null
? _facetsMapper.MapFrom(searchResults.Value.Facets.ToDictionary<string, IList<AzureModels.FacetResult>>())
: null
};

return results;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;

namespace Dfe.Data.SearchPrototype.Infrastructure.Mappers;

using AzureFacetResult = Azure.Search.Documents.Models.FacetResult;

/// <summary>
/// Maps from an Azure facet result to a collection of
/// T:Dfe.Data.SearchPrototype.SearchForEstablishments.Models.EstablishmentFacet
/// </summary>
public class AzureFacetResultToEstablishmentFacetsMapper : IMapper<Dictionary<string, IList<AzureFacetResult>>, EstablishmentFacets>
{
/// <summary>
/// Map from an Azure facet result to a collection of
/// T:Dfe.Data.SearchPrototype.SearchForEstablishments.Models.EstablishmentFacet
/// </summary>
/// <param name="facetResult">The Azure facet result</param>
/// <returns></returns>
public EstablishmentFacets MapFrom(Dictionary<string, IList<AzureFacetResult>> facetResult)
{
var establishmentFacets = new List<EstablishmentFacet>();

foreach (var facetCategory in facetResult.Where(facet => facet.Value != null))
{
var values = facetCategory.Value.Select(f => new FacetResult((string)f.Value, f.Count)).ToList();
var establishmentFacet = new EstablishmentFacet(facetCategory.Key, values);

establishmentFacets.Add(establishmentFacet);
}
return new EstablishmentFacets (establishmentFacets);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public AzureSearchResultToEstablishmentMapper(
/// </exception>
public SearchForEstablishments.Models.Establishment MapFrom(Establishment input)
{
// TODO - only throw for really essential stuff
ArgumentException.ThrowIfNullOrEmpty(input.id, nameof(input.id));
ArgumentException.ThrowIfNullOrEmpty(input.ESTABLISHMENTNAME, nameof(input.ESTABLISHMENTNAME));
ArgumentException.ThrowIfNullOrEmpty(input.TYPEOFESTABLISHMENTNAME, nameof(input.TYPEOFESTABLISHMENTNAME));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Azure;
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Options;
using Dfe.Data.SearchPrototype.Infrastructure.Tests.TestDoubles;
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;
using FluentAssertions;
using Xunit;

namespace Dfe.Data.SearchPrototype.Infrastructure.Tests;

public sealed class CognitiveSearchServiceAdapterAndMapperTests
{
private ISearchOptionsFactory _mockSearchOptionsFactory;
private IMapper<Pageable<SearchResult<Establishment>>, EstablishmentResults> _searchResponseMapper;
private IMapper<Dictionary<string, IList<Azure.Search.Documents.Models.FacetResult>>, EstablishmentFacets> _facetsMapper;

public CognitiveSearchServiceAdapterAndMapperTests()
{
_mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockSearchOptionsFactory();
_searchResponseMapper = new PageableSearchResultsToEstablishmentResultsMapper(
new AzureSearchResultToEstablishmentMapper(
new AzureSearchResultToAddressMapper()));
_facetsMapper = new AzureFacetResultToEstablishmentFacetsMapper();
}

[Fact]
public async Task Search_WithValidSearchContext_ReturnsResults()
{
// arrange
var establishmentSearchResults = new SearchResultFakeBuilder()
.WithSearchResults()
.Create();
var facetResults = new FacetsResultsFakeBuilder()
.WithAutoGeneratedFacets()
.Create();
var mockService = new SearchServiceMockBuilder()
.WithSearchOptions("SearchKeyword", "TargetCollection")
.WithSearchResults(establishmentSearchResults)
.WithFacets(facetResults)
.Create();

ISearchServiceAdapter cognitiveSearchServiceAdapter = new CognitiveSearchServiceAdapter<Establishment>(
mockService,
_mockSearchOptionsFactory,
_searchResponseMapper,
_facetsMapper);

// act
SearchResults? response =
await cognitiveSearchServiceAdapter.SearchAsync(
new SearchContext(
searchKeyword: "SearchKeyword",
targetCollection: "TargetCollection"));

// assert
response.Should().NotBeNull();
response.Establishments.Should().NotBeNull();
response.Establishments!.Establishments.Count().Should().Be(establishmentSearchResults.Count);
response.Facets.Should().NotBeNull();
response.Facets!.Facets.Count().Should().Be(facetResults.Count());
}

[Fact]
public async Task Search_WithNoFacetsReturned_ReturnsNullFacets()
{
// arrange
var mockService = new SearchServiceMockBuilder()
.WithSearchOptions("SearchKeyword", "TargetCollection")
.WithSearchResults(
new SearchResultFakeBuilder()
.WithSearchResults()
.Create())
.Create();
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockSearchOptionsFactory();

ISearchServiceAdapter cognitiveSearchServiceAdapter = new CognitiveSearchServiceAdapter<Establishment>(
mockService,
mockSearchOptionsFactory,
_searchResponseMapper,
_facetsMapper);

// act
SearchResults? response =
await cognitiveSearchServiceAdapter.SearchAsync(
new SearchContext(
searchKeyword: "SearchKeyword",
targetCollection: "TargetCollection"));

// assert
response.Should().NotBeNull();
response.Facets.Should().BeNull();
}

[Fact]
public async Task Search_WithNoResultsReturned_ReturnsEmptyResults()
{
// arrange
var mockService = new SearchServiceMockBuilder()
.WithSearchOptions("SearchKeyword", "TargetCollection")
.WithSearchResults(
new SearchResultFakeBuilder()
.WithEmptySearchResult()
.Create())
.Create();

ISearchServiceAdapter cognitiveSearchServiceAdapter = new CognitiveSearchServiceAdapter<Establishment>(
mockService,
_mockSearchOptionsFactory,
_searchResponseMapper,
_facetsMapper);

// act.
var response = await cognitiveSearchServiceAdapter.SearchAsync(new SearchContext(
searchKeyword: "SearchKeyword",
targetCollection: "TargetCollection"));

// assert
response.Should().NotBeNull();
response.Establishments.Should().NotBeNull();
response.Establishments!.Establishments.Should().BeEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using Dfe.Data.SearchPrototype.SearchForEstablishments;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;
using FluentAssertions;
using Moq;
using Xunit;

namespace Dfe.Data.SearchPrototype.Infrastructure.Tests;
Expand All @@ -17,51 +16,27 @@ public sealed class CognitiveSearchServiceAdapterTests
private static CognitiveSearchServiceAdapter<Establishment> CreateServiceAdapterWith(
ISearchByKeywordService searchByKeywordService,
ISearchOptionsFactory searchOptionsFactory,
IMapper<Pageable<SearchResult<Establishment>>, EstablishmentResults> searchResponseMapper
IMapper<Pageable<SearchResult<Establishment>>, EstablishmentResults> searchResponseMapper,
IMapper<Dictionary<string, IList<Azure.Search.Documents.Models.FacetResult>>, EstablishmentFacets> facetsMapper
) =>
new(searchByKeywordService, searchOptionsFactory, searchResponseMapper);

[Fact]
public async Task Search_WithValidSearchContext_ReturnsConfiguredResults()
{
// arrange
var mockService = SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection");
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockSearchOptionsFactory();
var mockMapper = PageableSearchResultsToEstablishmentResultsMapperTestDouble.MockFor(new EstablishmentResults());

ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
mockService,
mockSearchOptionsFactory,
mockMapper);

// act
EstablishmentResults? response =
await cognitiveSearchServiceAdapter.SearchAsync(
new SearchContext(
searchKeyword: "SearchKeyword",
targetCollection: "TargetCollection"));

// assert
response.Establishments.Should().NotBeNull();
Mock.Get(mockService).Verify(SearchServiceTestDouble.SearchRequest("SearchKeyword", "TargetCollection"),Times.Once());
Mock.Get(mockSearchOptionsFactory).Verify(SearchOptionsFactoryTestDouble.SearchOption(), Times.Once());
Mock.Get(mockMapper).Verify(PageableSearchResultsToEstablishmentResultsMapperTestDouble.MapFrom(), Times.Once());
}
new(searchByKeywordService, searchOptionsFactory, searchResponseMapper, facetsMapper);

[Fact]
public Task Search_WithNoSearchOptions_ThrowsApplicationException()
{
var mockService = SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection");
var mockServiceBuilder = new SearchServiceMockBuilder();
var mockService = mockServiceBuilder.MockSearchService("SearchKeyword", "TargetCollection");
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockForNoOptions();
var mockMapper = PageableSearchResultsToEstablishmentResultsMapperTestDouble.MockFor(new EstablishmentResults());
var mockEstablishmentResultsMapper = PageableSearchResultsToEstablishmentResultsMapperTestDouble.DefaultMock();
var mockFacetsMapper = AzureFacetResultToEstablishmentFacetsMapperTestDouble.DefaultMock();

// arrange
ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
mockService,
mockSearchOptionsFactory,
mockMapper);
mockEstablishmentResultsMapper,
mockFacetsMapper);

// act.
return cognitiveSearchServiceAdapter
Expand All @@ -75,42 +50,21 @@ await serviceAdapter.SearchAsync(
.WithMessage("Search options cannot be derived for TargetCollection.");
}

[Fact]
public async Task Search_WithNoResultsReturned_ReturnsEmptyResults()
{
// arrange
var mockService = SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection");
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockSearchOptionsFactory();
var mockMapper = PageableSearchResultsToEstablishmentResultsMapperTestDouble.MockFor(new EstablishmentResults());

ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
mockService,
mockSearchOptionsFactory,
mockMapper);

// act.
var response = await cognitiveSearchServiceAdapter.SearchAsync(new SearchContext(
searchKeyword: "SearchKeyword",
targetCollection: "TargetCollection"));

// assert
response.Establishments.Should().BeEmpty();
}

[Fact]
public Task Search_MapperThrowsException_ExceptionPassesThrough()
{
// arrange
var mockService = SearchServiceTestDouble.MockSearchService("SearchKeyword", "TargetCollection");
var mockService = new SearchServiceMockBuilder().MockSearchService("SearchKeyword", "TargetCollection");
var mockSearchOptionsFactory = SearchOptionsFactoryTestDouble.MockSearchOptionsFactory();
var mockMapper = PageableSearchResultsToEstablishmentResultsMapperTestDouble.MockMapperThrowingArgumentException();
var mockEstablishmentResultsMapper = PageableSearchResultsToEstablishmentResultsMapperTestDouble.MockMapperThrowingArgumentException();
var mockFacetsMapper = AzureFacetResultToEstablishmentFacetsMapperTestDouble.DefaultMock();

ISearchServiceAdapter cognitiveSearchServiceAdapter =
CreateServiceAdapterWith(
mockService,
mockSearchOptionsFactory,
mockMapper);
mockEstablishmentResultsMapper,
mockFacetsMapper);

// act, assert.
return cognitiveSearchServiceAdapter
Expand Down
Loading
Loading