Skip to content

Commit

Permalink
Search options builder for search adapter (#52)
Browse files Browse the repository at this point in the history
* Completed build and tests

* Fixed unused async in test

* Added additional test conditions in CognitiveSearchServiceAdapterTests vased on PR feedback

---------

Co-authored-by: Spencer O'HEGARTY <[email protected]>
  • Loading branch information
spanersoraferty and spencerohegartyDfE authored Oct 1, 2024
1 parent aa30e30 commit 72ebfde
Show file tree
Hide file tree
Showing 8 changed files with 454 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.Usecase;

namespace Dfe.Data.SearchPrototype.Infrastructure.Builders
{
/// <summary>
/// Provides an abstraction on which to establish the behaviour
/// used to build configured <see cref="SearchOptions" /> instances.
/// </summary>
public interface ISearchOptionsBuilder
{
/// <summary>
/// Sets the number of search items to retrieve.
/// </summary>
/// <param name="size">
/// The number of results to return as specified.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
ISearchOptionsBuilder WithSize(int? size);

/// <summary>
/// Sets the mode of search to invoke, i.e. All or Any.
/// </summary>
/// <param name="searchMode">
/// The mode of search to invoke, i.e. any search terms may match (Any),
/// or all search terms must match (All).
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
ISearchOptionsBuilder WithSearchMode(SearchMode searchMode);

/// <summary>
/// Sets the option to include the total search results count in the search response.
/// </summary>
/// <param name="includeTotalCount">
/// The boolean value used to instruct the total count to be added to the response, or otherwise.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
ISearchOptionsBuilder WithIncludeTotalCount(bool? includeTotalCount);

/// <summary>
/// Sets the fields on which to establish the search.
/// </summary>
/// <param name="searchFields">
/// List of fields over which to specify the search.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
ISearchOptionsBuilder WithSearchFields(IList<string>? searchFields);

/// <summary>
/// Sets the facets to include in the search response.
/// </summary>
/// <param name="facets">
/// List of facets to include in the search response.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
ISearchOptionsBuilder WithFacets(IList<string>? facets);

/// <summary>
/// Sets the filters on which to establish the search.
/// </summary>
/// <param name="filters">
/// List of filters on which to establish the search.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
ISearchOptionsBuilder WithFilters(IList<FilterRequest>? filters);

/// <summary>
/// Builds the configured instance of the <see cref="SearchOptions"/> type requested.
/// </summary>
/// <returns>
/// The fully configured (built) <see cref="SearchOptions"/> instance.
/// </returns>
SearchOptions Build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using Dfe.Data.Common.Infrastructure.CognitiveSearch.Filtering;
using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.Usecase;

namespace Dfe.Data.SearchPrototype.Infrastructure.Builders
{
/// <summary>
/// Provides a concrete implementation of the <see cref="ISearchOptionsBuilder"/> abstraction,
/// which establishes a configured <see cref="SearchOptions" /> instance which conforms to the prescribed behaviour.
/// </summary>
public sealed class SearchOptionsBuilder : ISearchOptionsBuilder
{
private readonly SearchOptions _searchOptions;
private readonly ISearchFilterExpressionsBuilder _searchFilterExpressionsBuilder;

private SearchMode? _searchMode;
private int? _size;
private bool? _includeTotalCount;
private IList<string>? _searchFields;
private IList<string>? _facets;
private IList<FilterRequest>? _filters;

/// <summary>
/// The following <see cref="ISearchFilterExpressionsBuilder"/> dependency provides the
/// behaviour on which to generate fully configured, search filter string expressions based
/// on the provisioned request, the complete implementation of which is defined in the IOC container.
/// </summary>
/// <param name="searchFilterExpressionsBuilder">
/// Builds the search filter expression required by Azure AI Search
/// </param>
public SearchOptionsBuilder(ISearchFilterExpressionsBuilder searchFilterExpressionsBuilder)
{
_searchFilterExpressionsBuilder = searchFilterExpressionsBuilder;
_searchOptions = new SearchOptions();
}

/// <summary>
/// Sets the number of search items to retrieve.
/// </summary>
/// <param name="size">
/// The number of results to return as specified.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
public ISearchOptionsBuilder WithSize(int? size)
{
_size = size;
return this;
}

/// <summary>
/// Sets the mode of search to invoke, i.e. All or Any.
/// </summary>
/// <param name="searchMode">
/// The mode of search to invoke, i.e. any search terms may match (Any),
/// or all search terms must match (All).
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
public ISearchOptionsBuilder WithSearchMode(SearchMode searchMode)
{
_searchMode = searchMode;
return this;
}

/// <summary>
/// Sets the option to include the total search results count in the search response.
/// </summary>
/// <param name="includeTotalCount">
/// The boolean value used to instruct the total count to be added to the response, or otherwise.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
public ISearchOptionsBuilder WithIncludeTotalCount(bool? includeTotalCount)
{
_includeTotalCount = includeTotalCount;
return this;
}

/// <summary>
/// Sets the fields on which to establish the search.
/// </summary>
/// <param name="searchFields">
/// List of fields over which to specify the search.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
public ISearchOptionsBuilder WithSearchFields(IList<string>? searchFields)
{
_searchFields = searchFields;
return this;
}

/// <summary>
/// Sets the facets to include in the search response.
/// </summary>
/// <param name="facets">
/// List of facets to include in the search response.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
public ISearchOptionsBuilder WithFacets(IList<string>? facets)
{
_facets = facets;
return this;
}

/// <summary>
/// Sets the filters on which to establish the search.
/// </summary>
/// <param name="filters">
/// List of filters on which to establish the search.
/// </param>
/// <returns>
/// The updated builder instance.
/// </returns>
public ISearchOptionsBuilder WithFilters(IList<FilterRequest>? filters)
{
_filters = filters;
return this;
}

/// <summary>
/// Builds the configured instance of the <see cref="SearchOptions"/> type requested.
/// </summary>
/// <returns>
/// The fully configured (built) <see cref="SearchOptions"/> instance.
/// </returns>
public SearchOptions Build()
{
_searchOptions.SearchMode = _searchMode;
_searchOptions.Size = _size;
_searchOptions.IncludeTotalCount = _includeTotalCount;
_searchFields?.ToList().ForEach(_searchOptions.SearchFields.Add);
_facets?.ToList().ForEach(_searchOptions.Facets.Add);

if (_filters?.Count > 0)
{
_searchOptions.Filter =
_searchFilterExpressionsBuilder.BuildSearchFilterExpressions(
_filters.Select(filterRequest =>
new SearchFilterRequest(filterRequest.FilterName, filterRequest.FilterValues)));
}

return _searchOptions;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using Dfe.Data.Common.Infrastructure.CognitiveSearch.Filtering;
using Dfe.Data.Common.Infrastructure.CognitiveSearch.SearchByKeyword;
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Builders;
using Dfe.Data.SearchPrototype.Infrastructure.Options;
using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.ServiceAdapters;
using Dfe.Data.SearchPrototype.SearchForEstablishments.Models;
Expand All @@ -19,10 +19,10 @@ namespace Dfe.Data.SearchPrototype.Infrastructure;
public sealed class CognitiveSearchServiceAdapter<TSearchResult> : ISearchServiceAdapter where TSearchResult : class
{
private readonly ISearchByKeywordService _searchByKeywordService;
private readonly IMapper<Pageable<AzureModels.SearchResult<TSearchResult>>, EstablishmentResults> _searchResultMapper;
private readonly IMapper<Pageable<SearchResult<TSearchResult>>, EstablishmentResults> _searchResultMapper;
private readonly IMapper<Dictionary<string, IList<AzureModels.FacetResult>>, EstablishmentFacets> _facetsMapper;
private readonly AzureSearchOptions _azureSearchOptions;
private readonly ISearchFilterExpressionsBuilder _searchFilterExpressionsBuilder;
private readonly ISearchOptionsBuilder _searchOptionsBuilder;

/// <summary>
/// The following dependencies include the core cognitive search service definition,
Expand All @@ -40,22 +40,22 @@ public sealed class CognitiveSearchServiceAdapter<TSearchResult> : ISearchServic
/// <param name="facetsMapper">
/// Maps the raw Azure search response to the required <see cref="EstablishmentFacets"/>
/// </param>
/// <param name="searchFilterExpressionsBuilder">
/// Builds the search filter expression required by Azure AI Search
/// <param name="searchOptionsBuilder">
/// Builds the search options by Azure AI Search
/// </param>
public CognitiveSearchServiceAdapter(
ISearchByKeywordService searchByKeywordService,
IOptions<AzureSearchOptions> azureSearchOptions,
IMapper<Pageable<AzureModels.SearchResult<TSearchResult>>, EstablishmentResults> searchResultMapper,
IMapper<Pageable<SearchResult<TSearchResult>>, EstablishmentResults> searchResultMapper,
IMapper<Dictionary<string, IList<AzureModels.FacetResult>>, EstablishmentFacets> facetsMapper,
ISearchFilterExpressionsBuilder searchFilterExpressionsBuilder)
ISearchOptionsBuilder searchOptionsBuilder)
{
ArgumentNullException.ThrowIfNull(azureSearchOptions.Value);
_azureSearchOptions = azureSearchOptions.Value;
_searchByKeywordService = searchByKeywordService;
_searchResultMapper = searchResultMapper;
_facetsMapper = facetsMapper;
_searchFilterExpressionsBuilder = searchFilterExpressionsBuilder;
_searchOptionsBuilder = searchOptionsBuilder;
}

/// <summary>
Expand All @@ -78,25 +78,15 @@ public CognitiveSearchServiceAdapter(
/// </exception>
public async Task<SearchResults> SearchAsync(SearchServiceAdapterRequest searchServiceAdapterRequest)
{
SearchOptions searchOptions = new()
{
SearchMode = (SearchMode)_azureSearchOptions.SearchMode,
Size = _azureSearchOptions.Size,
IncludeTotalCount = _azureSearchOptions.IncludeTotalCount,
};

searchServiceAdapterRequest.SearchFields?.ToList()
.ForEach(searchOptions.SearchFields.Add);

searchServiceAdapterRequest.Facets?.ToList()
.ForEach(searchOptions.Facets.Add);

if (searchServiceAdapterRequest.SearchFilterRequests?.Count > 0)
{
searchOptions.Filter = _searchFilterExpressionsBuilder.BuildSearchFilterExpressions(
searchServiceAdapterRequest.SearchFilterRequests
.Select(filterRequest => new SearchFilterRequest(filterRequest.FilterName, filterRequest.FilterValues)));
}
SearchOptions searchOptions =
_searchOptionsBuilder
.WithSearchMode((SearchMode)_azureSearchOptions.SearchMode)
.WithSize(_azureSearchOptions.Size)
.WithIncludeTotalCount(_azureSearchOptions.IncludeTotalCount)
.WithSearchFields(searchServiceAdapterRequest.SearchFields)
.WithFacets(searchServiceAdapterRequest.Facets)
.WithFilters(searchServiceAdapterRequest.SearchFilterRequests)
.Build();

Response<SearchResults<TSearchResult>> searchResults =
await _searchByKeywordService.SearchAsync<TSearchResult>(
Expand Down
2 changes: 2 additions & 0 deletions Dfe.Data.SearchPrototype/Infrastructure/CompositionRoot.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Azure;
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.Common.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Builders;
using Dfe.Data.SearchPrototype.Infrastructure.Mappers;
using Dfe.Data.SearchPrototype.Infrastructure.Options;
using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.ServiceAdapters;
Expand Down Expand Up @@ -45,6 +46,7 @@ public static void AddCognitiveSearchAdaptorServices(this IServiceCollection ser
.Bind(settings));

services.AddScoped(typeof(ISearchServiceAdapter), typeof(CognitiveSearchServiceAdapter<DataTransferObjects.Establishment>));
services.AddScoped<ISearchOptionsBuilder, SearchOptionsBuilder>();
services.AddSingleton(typeof(IMapper<Pageable<SearchResult<DataTransferObjects.Establishment>>, EstablishmentResults>), typeof(PageableSearchResultsToEstablishmentResultsMapper));
services.AddSingleton<IMapper<Dictionary<string, IList<Azure.Search.Documents.Models.FacetResult>>, EstablishmentFacets>, AzureFacetResultToEstablishmentFacetsMapper>();
services.AddSingleton<IMapper<DataTransferObjects.Establishment, Address>, AzureSearchResultToAddressMapper>();
Expand Down
Loading

0 comments on commit 72ebfde

Please sign in to comment.