Skip to content

Commit

Permalink
Cl/facets (#44)
Browse files Browse the repository at this point in the history
* added facets to models and some test refactoring

* facet faker and failing test for mapper

* Fixes to searchByKeywordResponse

* encapsulate facet results and establishments results into class for use by Adapter and mapper

* refactor mappers

* FacetResult count is long not int

* pass facets from search service out as part of use-case response

* Add integration tests for AdapterAndMapper

* fix warnings
  • Loading branch information
CathLass authored Sep 6, 2024
1 parent d50d8a7 commit 267915e
Show file tree
Hide file tree
Showing 24 changed files with 696 additions and 167 deletions.
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

0 comments on commit 267915e

Please sign in to comment.