From 7f6f9abe2725f0027010ff7e951c78856c2b9da1 Mon Sep 17 00:00:00 2001 From: Mike Williamson <89110166+PBMikeW@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:30:01 +1300 Subject: [PATCH 1/6] Add switch statement and dynamic template for elastic geo_point field --- .../Mappings/GeoLocationModel.cs | 12 ++++++++++++ .../Services/ElasticIndexManager.cs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Mappings/GeoLocationModel.cs diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Mappings/GeoLocationModel.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Mappings/GeoLocationModel.cs new file mode 100644 index 00000000000..eb1254fd585 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Mappings/GeoLocationModel.cs @@ -0,0 +1,12 @@ +using Nest; + +namespace OrchardCore.Search.Elasticsearch.Core.Mappings; + +internal sealed class GeoLocationModel +{ + [Number(Name = "Latitude")] + public string Latitude { get; set; } + + [Number(Name = "Longitude")] + public string Longitude { get; set; } +} diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs index ad5b12d9f3c..c25450bfdab 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs @@ -191,6 +191,18 @@ await _elasticClient.MapAsync(p => p ) ) ); + //DynamicTemplates mapping for Geo fields, the index adds a Location item by default when its a geofield. + await _elasticClient.MapAsync(p => p + .Index(fullIndexName) + .DynamicTemplates(d => d + .DynamicTemplate("*.Location", dyn => dyn + .MatchMappingType("object") + .PathMatch("*" + ".Location") + .Mapping(m => m + .GeoPoint(g => g)) + ) + ) + ); return response.Acknowledged; } @@ -571,6 +583,12 @@ private static Dictionary CreateElasticDocument(DocumentIndex do } } break; + case DocumentIndex.Types.GeoPoint: + if (entry.Value is DocumentIndex.GeoPoint point) + { + AddValue(entries, entry.Name, new GeoLocation((double)point.Latitude, (double)point.Longitude)); + } + break; } } From 29c7f5ec0ed9710ed627726568dd48b1ceead001 Mon Sep 17 00:00:00 2001 From: Mike Williamson <89110166+PBMikeW@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:45:03 +1300 Subject: [PATCH 2/6] Update comment to better reflect where location is being set. --- .../Services/ElasticIndexManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs index c25450bfdab..8735a1b974e 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs @@ -191,7 +191,7 @@ await _elasticClient.MapAsync(p => p ) ) ); - //DynamicTemplates mapping for Geo fields, the index adds a Location item by default when its a geofield. + // DynamicTemplates mapping for Geo fields, the GeoPointFieldIndexHandler adds a Location index by default. await _elasticClient.MapAsync(p => p .Index(fullIndexName) .DynamicTemplates(d => d From 17995f849b336c3945a72e5bf8817e5c6774da29 Mon Sep 17 00:00:00 2001 From: Mike Williamson <89110166+PBMikeW@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:50:15 +1300 Subject: [PATCH 3/6] Remove unused model --- .../Mappings/GeoLocationModel.cs | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Mappings/GeoLocationModel.cs diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Mappings/GeoLocationModel.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Mappings/GeoLocationModel.cs deleted file mode 100644 index eb1254fd585..00000000000 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Mappings/GeoLocationModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Nest; - -namespace OrchardCore.Search.Elasticsearch.Core.Mappings; - -internal sealed class GeoLocationModel -{ - [Number(Name = "Latitude")] - public string Latitude { get; set; } - - [Number(Name = "Longitude")] - public string Longitude { get; set; } -} From 2e34f3c8dc4c0de0f2576cd3b2f154c9e3ebac0a Mon Sep 17 00:00:00 2001 From: Mike Williamson <89110166+PBMikeW@users.noreply.github.com> Date: Fri, 18 Oct 2024 08:04:41 +1300 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Hisham Bin Ateya --- .../Services/ElasticIndexManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs index 8735a1b974e..ac68f0c1f38 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticIndexManager.cs @@ -198,8 +198,7 @@ await _elasticClient.MapAsync(p => p .DynamicTemplate("*.Location", dyn => dyn .MatchMappingType("object") .PathMatch("*" + ".Location") - .Mapping(m => m - .GeoPoint(g => g)) + .Mapping(m => m.GeoPoint(g => g)) ) ) ); @@ -588,6 +587,7 @@ private static Dictionary CreateElasticDocument(DocumentIndex do { AddValue(entries, entry.Name, new GeoLocation((double)point.Latitude, (double)point.Longitude)); } + break; } } From 75626464f6118abff9db677d5d3ee4c1df6668f4 Mon Sep 17 00:00:00 2001 From: Mike Williamson <89110166+PBMikeW@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:20:03 +1300 Subject: [PATCH 5/6] POC Working --- .../Controllers/QueryApiController.cs | 2 + .../OrchardCore.Queries.csproj | 1 + ...ontentQueryOrchardRazorHelperExtensions.cs | 40 ++++++++++------- .../Controllers/ElasticsearchApiController.cs | 2 + .../OrchardCore.Search.Elasticsearch.csproj | 1 + .../Controllers/LuceneApiController.cs | 2 + .../OrchardCore.Search.Lucene.csproj | 1 + .../Filters/ApiExceptionHandlingFilter.cs | 43 +++++++++++++++++++ .../Services/ElasticQueryService.cs | 2 + 9 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Infrastructure/Filters/ApiExceptionHandlingFilter.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Controllers/QueryApiController.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Controllers/QueryApiController.cs index 4268548d15f..86f8eaad398 100644 --- a/src/OrchardCore.Modules/OrchardCore.Queries/Controllers/QueryApiController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Queries/Controllers/QueryApiController.cs @@ -3,12 +3,14 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using OrchardCore.Infrastructure.Filters; namespace OrchardCore.Queries.Controllers; [Route("api/queries")] [ApiController] [Authorize(AuthenticationSchemes = "Api")] +[TypeFilter(typeof(ApiExceptionHandlingFilter))] [IgnoreAntiforgeryToken] [AllowAnonymous] public sealed class QueryApiController : ControllerBase diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/OrchardCore.Queries.csproj b/src/OrchardCore.Modules/OrchardCore.Queries/OrchardCore.Queries.csproj index e803bfa1c80..b531bc51be3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Queries/OrchardCore.Queries.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Queries/OrchardCore.Queries.csproj @@ -23,6 +23,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Razor/ContentQueryOrchardRazorHelperExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Razor/ContentQueryOrchardRazorHelperExtensions.cs index a02df63f330..723ce687d8b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Queries/Razor/ContentQueryOrchardRazorHelperExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Queries/Razor/ContentQueryOrchardRazorHelperExtensions.cs @@ -14,33 +14,41 @@ public static Task> ContentQueryAsync(this IOrchardHelp public static async Task> ContentQueryAsync(this IOrchardHelper orchardHelper, string queryName, IDictionary parameters) { - var results = await orchardHelper.QueryAsync(queryName, parameters); - var contentItems = new List(); - if (results != null) + var contentItems = new List(); + try { - foreach (var result in results) + var results = await orchardHelper.QueryAsync(queryName, parameters); + if (results != null) { - if (result is not ContentItem contentItem) + foreach (var result in results) { - contentItem = null; + if (result is not ContentItem contentItem) + { + contentItem = null; - if (result is JsonObject jObject) + if (result is JsonObject jObject) + { + contentItem = jObject.ToObject(); + } + } + + // If input is a 'JObject' but which not represents a 'ContentItem', + // a 'ContentItem' is still created but with some null properties. + if (contentItem?.ContentItemId == null) { - contentItem = jObject.ToObject(); + continue; } - } - // If input is a 'JObject' but which not represents a 'ContentItem', - // a 'ContentItem' is still created but with some null properties. - if (contentItem?.ContentItemId == null) - { - continue; + contentItems.Add(contentItem); } - - contentItems.Add(contentItem); } } + catch (Exception) + { + return contentItems; + } + return contentItems; } diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/ElasticsearchApiController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/ElasticsearchApiController.cs index db37262efcf..05c7c9ac1b1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/ElasticsearchApiController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/ElasticsearchApiController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OrchardCore.Entities; +using OrchardCore.Infrastructure.Filters; using OrchardCore.Queries; using OrchardCore.Search.Elasticsearch.Core.Services; using OrchardCore.Search.Elasticsearch.Models; @@ -11,6 +12,7 @@ namespace OrchardCore.Search.Elasticsearch; [Route("api/elasticsearch")] [ApiController] +[TypeFilter(typeof(ApiExceptionHandlingFilter))] [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] public sealed class ElasticsearchApiController : ControllerBase { diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/OrchardCore.Search.Elasticsearch.csproj b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/OrchardCore.Search.Elasticsearch.csproj index 3f212daa56e..0cc4adf650e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/OrchardCore.Search.Elasticsearch.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/OrchardCore.Search.Elasticsearch.csproj @@ -17,6 +17,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/LuceneApiController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/LuceneApiController.cs index 413686ee0ac..8d5c3f5f3ab 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/LuceneApiController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/LuceneApiController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OrchardCore.Entities; +using OrchardCore.Infrastructure.Filters; using OrchardCore.Queries; using OrchardCore.Search.Lucene.Model; @@ -10,6 +11,7 @@ namespace OrchardCore.Search.Lucene.Controllers; [Route("api/lucene")] [ApiController] [Authorize(AuthenticationSchemes = "Api")] +[TypeFilter(typeof(ApiExceptionHandlingFilter))] [IgnoreAntiforgeryToken] [AllowAnonymous] public sealed class LuceneApiController : ControllerBase diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/OrchardCore.Search.Lucene.csproj b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/OrchardCore.Search.Lucene.csproj index 08165054031..fefa4b27656 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/OrchardCore.Search.Lucene.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/OrchardCore.Search.Lucene.csproj @@ -27,6 +27,7 @@ + diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Filters/ApiExceptionHandlingFilter.cs b/src/OrchardCore/OrchardCore.Infrastructure/Filters/ApiExceptionHandlingFilter.cs new file mode 100644 index 00000000000..65742cd6cc2 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Infrastructure/Filters/ApiExceptionHandlingFilter.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http.Json; +using Json.More; +namespace OrchardCore.Infrastructure.Filters; + +public class ApiExceptionHandlingFilter : IAsyncExceptionFilter +{ + private static readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + public ApiExceptionHandlingFilter() + { + + } + + public async Task OnExceptionAsync(ExceptionContext context) + { + var response = new ProblemDetails + { + Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1", + Title = "An error occurred while processing your request.", + Status = StatusCodes.Status500InternalServerError, + Detail = context.Exception.Message, + Instance = context.HttpContext.Request.Path + }; + + context.HttpContext.Response.StatusCode = response.Status.Value; + context.Result = new JsonResult(response, _jsonOptions); + context.ExceptionHandled = true; + } +} + diff --git a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs index 543b41e1eec..c5353b4a8a8 100644 --- a/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs +++ b/src/OrchardCore/OrchardCore.Search.Elasticsearch.Core/Services/ElasticQueryService.cs @@ -1,4 +1,5 @@ using System.Text; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Nest; @@ -81,6 +82,7 @@ public async Task SearchAsync(string indexName, string query) catch (Exception ex) { _logger.LogError(ex, "Error while querying elastic with exception: {Message}", ex.Message); + throw; } return elasticTopDocs; From 39661d840230f234f4d292fd57dd9747a063e443 Mon Sep 17 00:00:00 2001 From: Mike Williamson <89110166+PBMikeW@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:09:33 +1300 Subject: [PATCH 6/6] Fix text errors (Async) --- .../Filters/ApiExceptionHandlingFilter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Filters/ApiExceptionHandlingFilter.cs b/src/OrchardCore/OrchardCore.Infrastructure/Filters/ApiExceptionHandlingFilter.cs index 65742cd6cc2..8c7d4fefa9e 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Filters/ApiExceptionHandlingFilter.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Filters/ApiExceptionHandlingFilter.cs @@ -11,7 +11,7 @@ using Json.More; namespace OrchardCore.Infrastructure.Filters; -public class ApiExceptionHandlingFilter : IAsyncExceptionFilter +public class ApiExceptionHandlingFilter : IExceptionFilter { private static readonly JsonSerializerOptions _jsonOptions = new() { @@ -24,7 +24,7 @@ public ApiExceptionHandlingFilter() } - public async Task OnExceptionAsync(ExceptionContext context) + public void OnException(ExceptionContext context) { var response = new ProblemDetails {