Skip to content

Commit

Permalink
feat: or-2038 add sorting on beheer zoeken
Browse files Browse the repository at this point in the history
  • Loading branch information
QuintenGreenstack committed Jan 23, 2024
1 parent 4daee6d commit 300d863
Show file tree
Hide file tree
Showing 22 changed files with 751 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,24 @@ public static IServiceCollection AddElasticSearch(
var elasticClient = (IServiceProvider serviceProvider)
=> CreateElasticClient(elasticSearchOptions, serviceProvider.GetRequiredService<ILogger<ElasticClient>>());

services.AddMappingsForVerenigingZoek(elasticSearchOptions.Indices!.Verenigingen!);

services.AddSingleton(sp => elasticClient(sp));
services.AddSingleton<IElasticClient>(serviceProvider => serviceProvider.GetRequiredService<ElasticClient>());

return services;
}

private static IServiceCollection AddMappingsForVerenigingZoek(this IServiceCollection services, string indexName)
=> services.AddSingleton(
serviceProvider => serviceProvider
.GetMappingFor<VerenigingZoekDocument>()
.Indices[indexName]
.Mappings);

private static GetMappingResponse GetMappingFor<T>(this IServiceProvider serviceProvider) where T : class
=> serviceProvider.GetRequiredService<ElasticClient>().Indices.GetMapping<T>();

private static ElasticClient CreateElasticClient(ElasticSearchOptionsSection elasticSearchOptions, ILogger logger)
{
var settings = new ConnectionSettings(new Uri(elasticSearchOptions.Uri!))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace AssociationRegistry.Admin.Api.Verenigingen.Search.Exceptions;

using Be.Vlaanderen.Basisregisters.AggregateSource;
using System;

[Serializable]
public class ZoekOpdrachtBevatOnbekendeSorteerVelden : DomainException
{
public ZoekOpdrachtBevatOnbekendeSorteerVelden(string onbekendVeld) :
base(string.Format(ExceptionMessages.ZoekOpdrachtBevatOnbekendeSorteerVelden, onbekendVeld))
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace AssociationRegistry.Admin.Api.Verenigingen.Search.Exceptions;

using Be.Vlaanderen.Basisregisters.AggregateSource;
using System;

[Serializable]
public class ZoekOpdrachtWasIncorrect : DomainException
{
public ZoekOpdrachtWasIncorrect() : base(ExceptionMessages.ZoekOpdrachtWasIncorrect)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ namespace AssociationRegistry.Admin.Api.Verenigingen.Search;
using Be.Vlaanderen.Basisregisters.Api;
using Be.Vlaanderen.Basisregisters.Api.Exceptions;
using Examples;
using Exceptions;
using FluentValidation;
using Infrastructure;
using Infrastructure.Swagger.Annotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Nest;
using RequestModels;
using ResponseModels;
using Schema.Search;
using Swashbuckle.AspNetCore.Filters;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ProblemDetails = Be.Vlaanderen.Basisregisters.BasicApiProblem.ProblemDetails;
Expand All @@ -27,11 +31,19 @@ public class SearchVerenigingenController : ApiController
{
private readonly ElasticClient _elasticClient;
private readonly SearchVerenigingenResponseMapper _responseMapper;
private readonly TypeMapping _typeMapping;

public SearchVerenigingenController(ElasticClient elasticClient, SearchVerenigingenResponseMapper responseMapper)
private static readonly Func<SortDescriptor<VerenigingZoekDocument>, SortDescriptor<VerenigingZoekDocument>> DefaultSort =
x => x.Descending(v => v.VCode);

public SearchVerenigingenController(
ElasticClient elasticClient,
SearchVerenigingenResponseMapper responseMapper,
TypeMapping typeMapping)
{
_elasticClient = elasticClient;
_responseMapper = responseMapper;
_typeMapping = typeMapping;
}

/// <summary>
Expand All @@ -58,6 +70,32 @@ public SearchVerenigingenController(ElasticClient elasticClient, SearchVerenigin
/// - `q=...&amp;offset=30&amp;limit=30`
/// Er kan enkel gepagineerd worden binnen de eerste 1000 resultaten.
/// Dit betekent dat de som van limit en offset nooit meer kan bedragen dan 1000.
///
/// ### Sortering
///
/// Standaard wordt aflopend gesorteerd op vCode.
/// Wil je een eigen sortering meegeven, kan je gebruik maken van `sort=veldNaam`.
/// - Zonder `sort` parameter wordt standaard aflopend gesorteerd op `vCode`.
/// - `sort=naam` sorteert oplopend op `naam`.
/// - `sort=-naam` sorteert aflopend op `naam`.
///
/// Om te zoeken op een genest veld, beschrijf je het pad naar het veld.
/// - `sort=type.code`
///
/// Om te sorteren op meerdere velden, combineer je de verschillende velden gescheiden door een komma.
/// - `sort=type.code,-naam`
///
/// De volgende velden worden ondersteund voor gebruik bij het sorteren:
/// - `vCode`
/// - `type.code`
/// - `type.beschrijving`
/// - `roepnaam`
/// - `naam`
/// - `korteNaam`
/// - `doelgroep.minimumleeftijd`
/// - `doelgroep.maximumleeftijd`
///
/// Het gedrag van het sorteren op andere velden kan niet gegarandeerd worden.
/// </remarks>
/// <param name="q">De querystring</param>
/// <param name="paginationQueryParams">De paginatie parameters</param>
Expand All @@ -76,29 +114,49 @@ public SearchVerenigingenController(ElasticClient elasticClient, SearchVerenigin
[ProducesJson]
public async Task<IActionResult> Zoeken(
[FromQuery] string? q,
[FromQuery] string? sort,
[FromQuery] PaginationQueryParams paginationQueryParams,
[FromServices] IValidator<PaginationQueryParams> validator,
[FromServices] ILogger<SearchVerenigingenController> logger,
CancellationToken cancellationToken)
{
await validator.ValidateAndThrowAsync(paginationQueryParams, cancellationToken);
q ??= "*";

var searchResponse = await Search(_elasticClient, q, paginationQueryParams);
var searchResponse = await Search(_elasticClient, q, sort, paginationQueryParams,_typeMapping);

if (searchResponse.ApiCall.HttpStatusCode == 400)
return MapBadRequest(logger, searchResponse);

var response = _responseMapper.ToSearchVereningenResponse(searchResponse, paginationQueryParams, q);

return Ok(response);
}

private IActionResult MapBadRequest(ILogger logger, ISearchResponse<VerenigingZoekDocument> searchResponse)
{
var match = Regex.Match(searchResponse.ServerError.Error.RootCause.First().Reason,
pattern: @"No mapping found for \[(.*).keyword\] in order to sort on");

logger.LogError(searchResponse.OriginalException, message: "Fout bij het aanroepen van ElasticSearch");

if (match.Success)
throw new ZoekOpdrachtBevatOnbekendeSorteerVelden(match.Groups[1].Value);

throw new ZoekOpdrachtWasIncorrect();
}

private static async Task<ISearchResponse<VerenigingZoekDocument>> Search(
IElasticClient client,
string q,
PaginationQueryParams paginationQueryParams)
string? sort,
PaginationQueryParams paginationQueryParams,
TypeMapping typemapping)
=> await client.SearchAsync<VerenigingZoekDocument>(
s => s
.From(paginationQueryParams.Offset)
.Size(paginationQueryParams.Limit)
.Sort(x => x.Descending(v => v.VCode))
.ParseSort(sort, DefaultSort, typemapping)
.Query(query => query
.Bool(boolQueryDescriptor =>
boolQueryDescriptor.Must(MatchWithQuery(q))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace AssociationRegistry.Admin.Api.Verenigingen.Search;

using Nest;
using System;
using System.Linq;

public static class SearchVerenigingenExtensions
{
public static SearchDescriptor<T> ParseSort<T>(
this SearchDescriptor<T> source,
string? sort,
Func<SortDescriptor<T>, SortDescriptor<T>> defaultSort,
TypeMapping mapping) where T : class
{
if (string.IsNullOrWhiteSpace(sort))
return source.Sort(defaultSort);

return source.Sort(_ => SortDescriptor<T>(sort, mapping).ThenBy(defaultSort));
}

private static SortDescriptor<T> SortDescriptor<T>(string sort, TypeMapping mapping) where T : class
{
var sortParts = sort.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim())
.ToArray();

var sortDescriptor = new SortDescriptor<T>();

foreach (var sortPart in sortParts)
{
var descending = sortPart.StartsWith("-");
var part = descending ? sortPart.Substring(1) : sortPart;
var isKeyword = IsKeyword(mapping, part);
sortDescriptor.Field($"{part}{(isKeyword ? "" : ".keyword")}", descending ? SortOrder.Descending : SortOrder.Ascending);
}

return sortDescriptor;
}

private static bool IsKeyword(ITypeMapping mapping, string field)
=> InspectPropertyType(mapping.Properties, field.Split('.'), currentIndex: 0) == "keyword";

private static string InspectPropertyType(IProperties properties, string[] pathSegments, int currentIndex)
{
if (currentIndex < pathSegments.Length && properties.ContainsKey(pathSegments[currentIndex]))
{
var currentProperty = properties[pathSegments[currentIndex]];

if (currentIndex == pathSegments.Length - 1)
// We've reached the desired property
return currentProperty.Type;

if (currentProperty is ObjectProperty objectProperty)
// We need to delve deeper into the object properties
return InspectPropertyType(objectProperty.Properties, pathSegments, currentIndex + 1);
}

// The desired property or path wasn't found
return null;
}

private static SortDescriptor<T> ThenBy<T>(this SortDescriptor<T> first, Func<SortDescriptor<T>, SortDescriptor<T>> second)
where T : class
=> second(first);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private static Vereniging Map(VerenigingZoekDocument verenigingZoekDocument, App
=> new()
{
VCode = verenigingZoekDocument.VCode,
Verenigingstype = Map(verenigingZoekDocument.Type),
Verenigingstype = Map(verenigingZoekDocument.Verenigingstype),
Naam = verenigingZoekDocument.Naam,
Roepnaam = verenigingZoekDocument.Roepnaam,
KorteNaam = verenigingZoekDocument.KorteNaam,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task Handle(EventEnvelope<FeitelijkeVerenigingWerdGeregistreerd> me
new VerenigingZoekDocument
{
VCode = message.Data.VCode,
Type = new VerenigingZoekDocument.VerenigingsType
Verenigingstype = new VerenigingZoekDocument.VerenigingsType
{
Code = Verenigingstype.FeitelijkeVereniging.Code,
Beschrijving = Verenigingstype.FeitelijkeVereniging.Naam,
Expand Down Expand Up @@ -50,7 +50,7 @@ public async Task Handle(EventEnvelope<VerenigingMetRechtspersoonlijkheidWerdGer
new VerenigingZoekDocument
{
VCode = message.Data.VCode,
Type = new VerenigingZoekDocument.VerenigingsType
Verenigingstype = new VerenigingZoekDocument.VerenigingsType
{
Code = Verenigingstype.Parse(message.Data.Rechtsvorm).Code,
Beschrijving = Verenigingstype.Parse(message.Data.Rechtsvorm).Naam,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace AssociationRegistry.Public.Schema.Search;

using Nest;

public static class PropertyDescriptorExtensions
{
public static TDescriptor WithKeyword<TDescriptor, TInterface, T>(
this CorePropertyDescriptorBase<TDescriptor, TInterface, T> source,
string? normalizer = null)
where TDescriptor : CorePropertyDescriptorBase<TDescriptor, TInterface, T>, TInterface
where TInterface : class, ICoreProperty
where T : class
{
return source.Fields(x =>
x.Keyword(
y =>
normalizer is null
? y.Name("keyword")
: y.Name("keyword").Normalizer(normalizer)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace AssociationRegistry.Admin.Schema.Search;
public class VerenigingZoekDocument
{
public string VCode { get; set; } = null!;
public VerenigingsType Type { get; set; } = null!;
public VerenigingsType Verenigingstype { get; set; } = null!;
public string Naam { get; set; } = null!;
public string Roepnaam { get; set; } = null!;
public string KorteNaam { get; set; } = null!;
Expand Down
Loading

0 comments on commit 300d863

Please sign in to comment.