diff --git a/src/SImpl.SearchModule.Abstraction/Queries/IMultiSearchQuery.cs b/src/SImpl.SearchModule.Abstraction/Queries/IMultiSearchQuery.cs new file mode 100644 index 0000000..53c9dbd --- /dev/null +++ b/src/SImpl.SearchModule.Abstraction/Queries/IMultiSearchQuery.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using SImpl.CQRS.Queries; +using SImpl.SearchModule.Abstraction.Models; +using SImpl.SearchModule.Abstraction.Results; + +namespace SImpl.SearchModule.Abstraction.Queries +{ + public interface IMultiSearchQuery : IQuery + { + public IDictionary> Queries { get; set; } + } +} \ No newline at end of file diff --git a/src/SImpl.SearchModule.Abstraction/Queries/IMultiSearchQueryResult.cs b/src/SImpl.SearchModule.Abstraction/Queries/IMultiSearchQueryResult.cs new file mode 100644 index 0000000..87728ae --- /dev/null +++ b/src/SImpl.SearchModule.Abstraction/Queries/IMultiSearchQueryResult.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using SImpl.SearchModule.Abstraction.Results; + +namespace SImpl.SearchModule.Abstraction.Queries +{ + public class MultiSearchQueryResult : IMultiSearchQueryResult + { + public IDictionary Results { get; set; } + } + public interface IMultiSearchQueryResult + { + public IDictionary Results { get; set; } + } +} \ No newline at end of file diff --git a/src/SImpl.SearchModule.Abstraction/Queries/ISearchQuery.cs b/src/SImpl.SearchModule.Abstraction/Queries/ISearchQuery.cs index cd51e09..09b144a 100644 --- a/src/SImpl.SearchModule.Abstraction/Queries/ISearchQuery.cs +++ b/src/SImpl.SearchModule.Abstraction/Queries/ISearchQuery.cs @@ -8,19 +8,19 @@ namespace SImpl.SearchModule.Abstraction.Queries { public interface ISearchQuery : IDictionary, ICreatableSearchQuery, IQuery, ISearchQuery { - public int Page { get; set; } - public int PageSize { get; set; } public List SortOrder { get; set; } public List FacetQueries { get; set; } public List HighlightQueries { get; set; } public IDictionary PostFilterQuery { get; set; } - public string Index { get; set; } public DateTime? PreviewAt { get; set; } void Add(Occurance queryOccurance, ISearchSubQuery booleanQueryQuery); } public interface ISearchQuery { - + public int Page { get; set; } + public int PageSize { get; set; } + public string Index { get; set; } + } } \ No newline at end of file diff --git a/src/SImpl.SearchModule.Abstraction/Queries/MultiSearchQuery.cs b/src/SImpl.SearchModule.Abstraction/Queries/MultiSearchQuery.cs new file mode 100644 index 0000000..3b92bdf --- /dev/null +++ b/src/SImpl.SearchModule.Abstraction/Queries/MultiSearchQuery.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using SImpl.CQRS.Queries; +using SImpl.SearchModule.Abstraction.Models; + +namespace SImpl.SearchModule.Abstraction.Queries +{ + public class MultiSearchQuery : IMultiSearchQuery, IQuery + { + public IDictionary> Queries { get; set; } + } +} \ No newline at end of file diff --git a/src/SImpl.SearchModule.ElasticSearch/Application/QueryHandlers/ElasticSearchMultiSearchQueryHandler.cs b/src/SImpl.SearchModule.ElasticSearch/Application/QueryHandlers/ElasticSearchMultiSearchQueryHandler.cs new file mode 100644 index 0000000..9801186 --- /dev/null +++ b/src/SImpl.SearchModule.ElasticSearch/Application/QueryHandlers/ElasticSearchMultiSearchQueryHandler.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Nest; +using SImpl.CQRS.Queries; +using SImpl.SearchModule.Abstraction.Handlers; +using SImpl.SearchModule.Abstraction.Models; +using SImpl.SearchModule.Abstraction.Queries; +using SImpl.SearchModule.Abstraction.Results; +using SImpl.SearchModule.ElasticSearch.Application.Services; +using SImpl.SearchModule.ElasticSearch.Application.Services.MultiSearch; +using SImpl.SearchModule.ElasticSearch.Application.Services.Result; +using SImpl.SearchModule.ElasticSearch.Configuration; +using SImpl.SearchModule.ElasticSearch.Models; +using IAggregation = SImpl.SearchModule.Abstraction.Models.IAggregation; +using IBucket = Nest.IBucket; + +namespace SImpl.SearchModule.ElasticSearch.Application.QueryHandlers +{ + public class ElasticSearchMultiSearchQueryHandler : IQueryHandler + { + private IElasticSearchMultiQueryTranslatorService _translatorService; + private readonly IElasticClient _client; + private readonly ILogger _logger; + private readonly ElasticSearchConfiguration _configuration; + private readonly IEnumerable _collection; + + public ElasticSearchMultiSearchQueryHandler(IElasticSearchMultiQueryTranslatorService translatorService, + IElasticClient client, + ILogger logger, ElasticSearchConfiguration configuration, + IEnumerable collection) + { + _translatorService = translatorService; + _client = client; + _logger = logger; + _configuration = configuration; + _collection = collection; + } + + public async Task HandleAsync(MultiSearchQuery query) + { + MultiSearchDescriptor searchDescriptor = _translatorService.Translate(query); + + MultiSearchResponse result = + await _client.MultiSearchAsync(searchDescriptor); + if (_configuration.UseDebugStream) + { + _logger.LogInformation(result.DebugInformation); + _logger.LogInformation(searchDescriptor.ToString()); + } + + var resultModel = new MultiSearchQueryResult() + { + Results = new Dictionary() + }; + foreach (var singularQuery in query.Queries) + { + var searchResponse = result.GetResponse(singularQuery.Key); + if (searchResponse.IsValid) + { + resultModel.Results.Add(singularQuery.Key, new SimplQueryResult() + { + SearchModels = searchResponse.Documents.Select(ElasticSearchModelMapper.Map).ToList(), + Aggregations = TranslateAggregations(searchResponse.Aggregations), + Pagination = new Pagination() + { + CurrentPage = singularQuery.Value.Page, + PageSize = singularQuery.Value.PageSize, + Total = searchResponse.Total, + TotalNumberOfPages = + (int)Math.Ceiling((searchResponse.Total / (double)singularQuery.Value.PageSize)) + }, + HighLighter = TranslateHighLighter(searchResponse.Hits) + }); + } + } + + return resultModel; + } + + + private HighLighter TranslateHighLighter(IReadOnlyCollection> resultHits) + { + var highLghter = new HighLighter(); + foreach (var hit in resultHits) + { + foreach (var highlightHit in hit.Highlight) + { + highLghter.Add(highlightHit.Key, highlightHit.Value.Select(x => x as object).ToList()); + } + } + + return highLghter; + } + + private List TranslateAggregations(AggregateDictionary resultAggregations) + { + var list = new List(); + + foreach (var aggregation in resultAggregations) + { + var aggregationModel = TranslateAggregate(aggregation); + list.Add(aggregationModel); + } + + return list; + } + + //todo: figure out if we can simplify stuff there + private IAggregation TranslateAggregate(KeyValuePair aggregation) + { + var type = aggregation.Value.GetType(); + var handlerType = + typeof(IAggregationTranslationService<>).MakeGenericType(type); + var translator = + _collection.FirstOrDefault(x => x.GetType().GetInterfaces().Any(x => x == handlerType)); + var aggregationModel = translator.Translate(aggregation); + aggregationModel.NestedAggregation = TranslateSubAggregate(aggregation.Value); + return aggregationModel; + } + + private List TranslateSubAggregate(IAggregate aggregationValue) + { + var list = new List(); + //todo: add other options of buckets in future + switch (aggregationValue) + { + case BucketAggregate keyedBucket: + list.AddRange(keyedBucket.Items.Select(x => x as KeyedBucket) + .SelectMany(x => x.Select(y => TranslateAggregate(y)))); + break; + case SingleBucketAggregate singleBucketAggregate: + list.AddRange(singleBucketAggregate.Select(y => TranslateAggregate(y))); + break; + } + + return list; + } + } +} \ No newline at end of file diff --git a/src/SImpl.SearchModule.ElasticSearch/Application/Services/MultiSearch/ElasticSearchMultiQueryTranslatorService.cs b/src/SImpl.SearchModule.ElasticSearch/Application/Services/MultiSearch/ElasticSearchMultiQueryTranslatorService.cs new file mode 100644 index 0000000..c6291a9 --- /dev/null +++ b/src/SImpl.SearchModule.ElasticSearch/Application/Services/MultiSearch/ElasticSearchMultiQueryTranslatorService.cs @@ -0,0 +1,30 @@ +using Nest; +using SImpl.SearchModule.Abstraction.Models; +using SImpl.SearchModule.Abstraction.Queries; +using SImpl.SearchModule.ElasticSearch.Models; + +namespace SImpl.SearchModule.ElasticSearch.Application.Services.MultiSearch +{ + public class ElasticSearchMultiQueryTranslatorService : IElasticSearchMultiQueryTranslatorService + { + private readonly IElasticSearchQueryTranslatorService _elasticSearchQueryTranslatorService; + + public ElasticSearchMultiQueryTranslatorService(IElasticSearchQueryTranslatorService elasticSearchQueryTranslatorService) + { + _elasticSearchQueryTranslatorService = elasticSearchQueryTranslatorService; + } + + public MultiSearchDescriptor Translate(MultiSearchQuery query) + { + var descriptor = new MultiSearchDescriptor(); + foreach (var keyedQuery in query.Queries) + { + descriptor.Search(keyedQuery.Key, + n => _elasticSearchQueryTranslatorService.Translate(keyedQuery.Value)); + + } + + return descriptor; + } + } +} \ No newline at end of file diff --git a/src/SImpl.SearchModule.ElasticSearch/Application/Services/MultiSearch/IElasticSearchMultiQueryTranslatorService.cs b/src/SImpl.SearchModule.ElasticSearch/Application/Services/MultiSearch/IElasticSearchMultiQueryTranslatorService.cs new file mode 100644 index 0000000..b4b5697 --- /dev/null +++ b/src/SImpl.SearchModule.ElasticSearch/Application/Services/MultiSearch/IElasticSearchMultiQueryTranslatorService.cs @@ -0,0 +1,10 @@ +using Nest; +using SImpl.SearchModule.Abstraction.Queries; + +namespace SImpl.SearchModule.ElasticSearch.Application.Services.MultiSearch +{ + public interface IElasticSearchMultiQueryTranslatorService + { + MultiSearchDescriptor Translate(MultiSearchQuery query); + } +} \ No newline at end of file diff --git a/src/SImpl.SearchModule.ElasticSearch/ElasticSearchModule.cs b/src/SImpl.SearchModule.ElasticSearch/ElasticSearchModule.cs index 0e115a2..78354a8 100644 --- a/src/SImpl.SearchModule.ElasticSearch/ElasticSearchModule.cs +++ b/src/SImpl.SearchModule.ElasticSearch/ElasticSearchModule.cs @@ -13,6 +13,7 @@ using SImpl.SearchModule.Abstraction.Queries; using SImpl.SearchModule.Core.Application.Services; using SImpl.SearchModule.ElasticSearch.Application.Services; +using SImpl.SearchModule.ElasticSearch.Application.Services.MultiSearch; using SImpl.SearchModule.ElasticSearch.Application.Services.Result; using SImpl.SearchModule.ElasticSearch.Application.Services.SubQueries; using SImpl.SearchModule.ElasticSearch.Configuration; @@ -45,6 +46,7 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(typeof(IElasticSearchClientFactory), typeof(ElasticSearchClientFactory)); services.AddTransient(typeof(IElasticClient), provider => provider.GetService().CreateClient()); services.AddTransient(); + services.AddTransient(); services.AddTransient(typeof(IIndexingService), Config.SearchService); services.Scan(s => diff --git a/src/SImpl.SearchModule.FluentApi/Configuration/FluentApiSearchQueryCreator.cs b/src/SImpl.SearchModule.FluentApi/Configuration/FluentApiSearchQueryCreator.cs index 66df692..e4923b4 100644 --- a/src/SImpl.SearchModule.FluentApi/Configuration/FluentApiSearchQueryCreator.cs +++ b/src/SImpl.SearchModule.FluentApi/Configuration/FluentApiSearchQueryCreator.cs @@ -5,6 +5,45 @@ namespace SImpl.SearchModule.FluentApi.Configuration { + public class FluentApiMultiSearchQueryCreator : IFluentApiMultiSearchQueryCreator + { + private IMultiSearchQuery _baseQuery { get; set; } + + public FluentApiMultiSearchQueryCreator() + { + _baseQuery = new MultiSearchQuery(); + } + public IMultiSearchQuery Compile() + { + return _baseQuery; + } + public FluentApiMultiSearchQueryCreator CreateSearchQuery(string key, SearchQuery searchQuery,Action configurator ) + { + var baseQuery = new QueryCreatorConfigurator(searchQuery); + var query = new FluentApiSearchQueryCreator(baseQuery.Query) + .CreateSearchQuery(queryConfigurator => configurator.Invoke(new FluentQueryConfigurator(baseQuery.Query))); + _baseQuery.Queries.Add(key,query ); + return this; + } + public FluentApiMultiSearchQueryCreator CreateSearchQuery(string key,Action searchQuery,Action configurator ) + { + var baseQuery = new QueryCreatorConfigurator(new SearchQuery()); + searchQuery.Invoke(baseQuery); + var query = new FluentApiSearchQueryCreator(baseQuery.Query) + .CreateSearchQuery(queryConfigurator => configurator.Invoke(new FluentQueryConfigurator(baseQuery.Query))); + _baseQuery.Queries.Add(key,query ); + return this; + } + } + + public interface IFluentApiMultiSearchQueryCreator + { + FluentApiMultiSearchQueryCreator CreateSearchQuery(string key, Action searchQuery, + Action configurator); + + IMultiSearchQuery Compile(); + } + public class FluentApiSearchQueryCreator : IFluentApiSearchQueryCreator { private ISearchQuery _baseQuery { get; set; } diff --git a/src/Simpl.SearchModule.TestApi/Controllers/SearchApiTest.cs b/src/Simpl.SearchModule.TestApi/Controllers/SearchApiTest.cs index 6871111..b4e1097 100644 --- a/src/Simpl.SearchModule.TestApi/Controllers/SearchApiTest.cs +++ b/src/Simpl.SearchModule.TestApi/Controllers/SearchApiTest.cs @@ -35,6 +35,19 @@ public async Task Get(SearchRequest request) { try { + var multiSearchQuery = new FluentApiMultiSearchQueryCreator().CreateSearchQuery("test", f => + { + f.WithIndex("test") + }, query => + { + query.CreateAggregationQuery(f => + { + + f.CreateFilterQuery(); + f.CreateTermQuery(x=>x.WithName().WithField()) + }) + query.CreateBoolQuery() + }); var query = new FluentApiSearchQueryCreator(new SearchQuery() { Index = "headlesssearchindex",