public async Task <IList <IBrowseFilter> > GetBrowseFiltersAsync(ProductIndexedSearchCriteria criteria) { if (criteria == null) { throw new ArgumentNullException(nameof(criteria)); } var aggregations = (await GetAllAggregations(criteria))?.AsQueryable(); // Check allowed aggregations if (criteria.IncludeAggregations != null) { aggregations = aggregations?.Where(f => criteria.IncludeAggregations.Contains(f.Key, StringComparer.OrdinalIgnoreCase)); } // Check forbidden aggregations if (criteria.ExcludeAggregations != null) { aggregations = aggregations?.Where(f => !criteria.ExcludeAggregations.Contains(f.Key, StringComparer.OrdinalIgnoreCase)); } var result = aggregations ?.Where(f => !(f is PriceRangeFilter) || ((PriceRangeFilter)f).Currency.EqualsInvariant(criteria.Currency)) .ToList(); return(result); }
protected virtual IList <IFilter> GetPermanentFilters(ProductIndexedSearchCriteria criteria) { var result = new List <IFilter>(); if (!string.IsNullOrEmpty(criteria.Keyword)) { var parseResult = _searchPhraseParser.Parse(criteria.Keyword); criteria.Keyword = parseResult.Keyword; result.AddRange(parseResult.Filters); } if (criteria.ObjectIds != null) { result.Add(new IdsFilter { Values = criteria.ObjectIds }); } if (!string.IsNullOrEmpty(criteria.CatalogId)) { result.Add(FiltersHelper.CreateTermFilter("catalog", criteria.CatalogId.ToLowerInvariant())); } result.Add(FiltersHelper.CreateOutlineFilter(criteria)); if (criteria.StartDateFrom.HasValue) { result.Add(FiltersHelper.CreateDateRangeFilter("startdate", criteria.StartDateFrom, criteria.StartDate, false, true)); } if (criteria.EndDate.HasValue) { result.Add(FiltersHelper.CreateDateRangeFilter("enddate", criteria.EndDate, null, false, false)); } if (!criteria.ClassTypes.IsNullOrEmpty()) { result.Add(FiltersHelper.CreateTermFilter("__type", criteria.ClassTypes)); } if (!criteria.WithHidden) { result.Add(FiltersHelper.CreateTermFilter("status", "visible")); } if (criteria.PriceRange != null) { var range = criteria.PriceRange; result.Add(FiltersHelper.CreatePriceRangeFilter(criteria.Currency, criteria.Pricelists, range.Lower, range.Upper, range.IncludeLower, range.IncludeUpper)); } if (criteria.GeoDistanceFilter != null) { result.Add(criteria.GeoDistanceFilter); } return(result); }
protected virtual Task <IList <IBrowseFilter> > GetAllAggregations(ProductIndexedSearchCriteria criteria) { if (criteria == null) { throw new ArgumentNullException(nameof(criteria)); } return(GetStoreAggregationsAsync(criteria.StoreId)); }
protected virtual IList <SortingField> GetSorting(ProductIndexedSearchCriteria criteria) { var result = new List <SortingField>(); var priorityFields = criteria.GetPriorityFields(); foreach (var sortInfo in criteria.SortInfos) { var sortingField = new SortingField(); if (sortInfo is GeoSortInfo geoSortInfo) { sortingField = new GeoDistanceSortingField { Location = geoSortInfo.GeoPoint }; } sortingField.FieldName = sortInfo.SortColumn.ToLowerInvariant(); sortingField.IsDescending = sortInfo.SortDirection == SortDirection.Descending; switch (sortingField.FieldName) { case "price": if (!criteria.Pricelists.IsNullOrEmpty()) { result.AddRange( criteria.Pricelists.Select(priceList => new SortingField($"price_{criteria.Currency}_{priceList}".ToLowerInvariant(), sortingField.IsDescending))); } else { result.Add(new SortingField($"price_{criteria.Currency}".ToLowerInvariant(), sortingField.IsDescending)); } break; case "priority": result.AddRange(priorityFields.Select(priorityField => new SortingField(priorityField, sortingField.IsDescending))); break; case "name": case "title": result.Add(new SortingField("name", sortingField.IsDescending)); break; default: result.Add(sortingField); break; } } if (!result.Any()) { result.AddRange(priorityFields.Select(priorityField => new SortingField(priorityField, true))); result.Add(new SortingField("__sort")); } return(result); }
public virtual async Task <FiltersContainer> GetTermFiltersAsync(ProductIndexedSearchCriteria criteria) { var result = new FiltersContainer(); var terms = criteria.GetTerms(); if (terms.Any()) { var browseFilters = await _browseFilterService.GetBrowseFiltersAsync(criteria); var filtersAndValues = browseFilters ?.Select(f => new { Filter = f, Values = f.GetValues() }) .ToList(); foreach (var term in terms) { var browseFilter = browseFilters?.SingleOrDefault(x => x.Key.EqualsInvariant(term.Key)); // Handle special filter term with a key = "tags", it contains just values and we need to determine which filter to use if (browseFilter == null && term.Key == "tags") { foreach (var termValue in term.Values) { // Try to find filter by value var filterAndValues = filtersAndValues?.FirstOrDefault(x => x.Values?.Any(v => v.Id.Equals(termValue)) == true); if (filterAndValues != null) { var filter = ConvertBrowseFilter(filterAndValues.Filter, term.Values, criteria); result.PermanentFilters.Add(filter); } else { // Unknown term values should produce empty result result.PermanentFilters.Add(new IdsFilter { Values = new[] { string.Empty } }); } } } else if (browseFilter != null) // Predefined filter { var filter = ConvertBrowseFilter(browseFilter, term.Values, criteria); result.RemovableFilters.Add(new KeyValuePair <string, IFilter>(browseFilter.Key, filter)); } else // Custom term { var filter = FiltersHelper.CreateTermFilter(term.Key, term.Values); result.PermanentFilters.Add(filter); } } } return(result); }
protected virtual async Task <FiltersContainer> GetAllFiltersAsync(ProductIndexedSearchCriteria criteria) { var permanentFilters = GetPermanentFilters(criteria); var termFilters = await _termFilterBuilder.GetTermFiltersAsync(criteria); var result = new FiltersContainer { PermanentFilters = permanentFilters.Concat(termFilters.PermanentFilters).ToList(), RemovableFilters = termFilters.RemovableFilters, }; return(result); }
public async Task <IList <AggregationRequest> > GetAggregationRequestsAsync(ProductIndexedSearchCriteria criteria, FiltersContainer allFilters) { var result = new List <AggregationRequest>(); var browseFilters = await _browseFilterService.GetBrowseFiltersAsync(criteria); if (browseFilters != null) { foreach (var filter in browseFilters) { var existingFilters = allFilters.GetFiltersExceptSpecified(filter.Key); var attributeFilter = filter as AttributeFilter; var priceRangeFilter = filter as PriceRangeFilter; var rangeFilter = filter as RangeFilter; AggregationRequest aggregationRequest = null; IList <AggregationRequest> aggregationRequests = null; if (attributeFilter != null) { aggregationRequest = GetAttributeFilterAggregationRequest(attributeFilter, existingFilters); } else if (rangeFilter != null) { aggregationRequests = GetRangeFilterAggregationRequests(rangeFilter, existingFilters); } else if (priceRangeFilter != null && priceRangeFilter.Currency.EqualsInvariant(criteria.Currency)) { aggregationRequests = GetPriceRangeFilterAggregationRequests(priceRangeFilter, criteria, existingFilters); } if (aggregationRequest != null) { result.Add(aggregationRequest); } if (aggregationRequests != null) { result.AddRange(aggregationRequests.Where(f => f != null)); } } } return(result); }
public async Task TestSimpleAggregations(string terms, string expectedFilter1, string expectedFilter2, string expectedFilter3, string expectedFilter4) { var criteria = new ProductIndexedSearchCriteria { Terms = terms?.Split(';'), }; var allFilters = await GetTermFilterBuilder().GetTermFiltersAsync(criteria); var requests = await GetAggregationRequestBuilder().GetAggregationRequestsAsync(criteria, allFilters); Assert.Equal(4, requests.Count); Assert.IsType <TermAggregationRequest>(requests[0]); var request = (TermAggregationRequest)requests[0]; Assert.Equal("Brand", request.FieldName); Assert.Equal(expectedFilter1, request.Filter?.ToString()); Assert.Null(request.Id); Assert.Equal(5, request.Size); Assert.Null(request.Values); Assert.IsType <TermAggregationRequest>(requests[1]); request = (TermAggregationRequest)requests[1]; Assert.Equal("Color", request.FieldName); Assert.Equal(expectedFilter2, request.Filter?.ToString()); Assert.Null(request.Id); Assert.Null(request.Size); Assert.Equal("Red,Green,Blue", string.Join(",", request.Values)); Assert.IsType <TermAggregationRequest>(requests[2]); request = (TermAggregationRequest)requests[2]; Assert.Null(request.FieldName); Assert.Equal(expectedFilter3, request.Filter?.ToString()); Assert.Equal("Size-size1", request.Id); Assert.Null(request.Size); Assert.Null(request.Values); Assert.IsType <TermAggregationRequest>(requests[3]); request = (TermAggregationRequest)requests[3]; Assert.Null(request.FieldName); Assert.Equal(expectedFilter4, request.Filter?.ToString()); Assert.Equal("Size-size2", request.Id); Assert.Null(request.Size); Assert.Null(request.Values); }
[InlineData("Size:unknown", "", "ID:")] // Predefined range term with unknown value produces a filter which will not return any document public async Task TestSimpleTerms(string terms, string expectedPermanentFilters, string expectedRemovableFilters) { var criteria = new ProductIndexedSearchCriteria { Terms = terms?.Split(';'), }; var termFilterBuilder = GetTermFilterBuilder(); var filters = await termFilterBuilder.GetTermFiltersAsync(criteria); var permanentFilters = string.Join(";", filters.PermanentFilters.Select(f => f.ToString())); Assert.Equal(expectedPermanentFilters, permanentFilters); var removableFilters = string.Join(";", filters.RemovableFilters.Select(kvp => kvp.Value.ToString())); Assert.Equal(expectedRemovableFilters, removableFilters); }
public async Task TestPriceRangeFilter(string lower, string upper, string currency, string priceLists, string expectedFilter) { var criteria = new ProductIndexedSearchCriteria { PriceRange = new NumericRange { Lower = ParseDecimal(lower), Upper = ParseDecimal(upper), }, Currency = currency, Pricelists = priceLists?.Split(';'), }; var termFilterBuilder = GetSearchRequestBuilder(); var searchRequest = await termFilterBuilder.BuildRequestAsync(criteria); var priceFilter = (searchRequest.Filter as AndFilter)?.ChildFilters.Last().ToString(); Assert.Equal(expectedFilter, priceFilter); }
public virtual async Task <SearchProductResponse> Handle(SearchProductQuery request, CancellationToken cancellationToken) { var allStoreCurrencies = await _storeCurrencyResolver.GetAllStoreCurrenciesAsync(request.StoreId, request.CultureName); var currency = await _storeCurrencyResolver.GetStoreCurrencyAsync(request.CurrencyCode, request.StoreId, request.CultureName); var store = await _storeService.GetByIdAsync(request.StoreId); var responseGroup = EnumUtility.SafeParse(request.GetResponseGroup(), ExpProductResponseGroup.None); var builder = new IndexSearchRequestBuilder() .WithFuzzy(request.Fuzzy, request.FuzzyLevel) .ParseFilters(_phraseParser, request.Filter) .WithSearchPhrase(request.Query) .WithPaging(request.Skip, request.Take) .AddObjectIds(request.ObjectIds) .AddSorting(request.Sort) .WithIncludeFields(IndexFieldsMapper.MapToIndexIncludes(request.IncludeFields).ToArray()); if (request.ObjectIds.IsNullOrEmpty()) { //filter products only the store catalog and visibility status when search builder.AddTerms(new[] { "status:visible" });//Only visible, exclude variations from search result builder.AddTerms(new[] { $"__outline:{store.Catalog}" }); } //Use predefined facets for store if the facet filter expression is not set if (responseGroup.HasFlag(ExpProductResponseGroup.LoadFacets)) { var predefinedAggregations = await _aggregationConverter.GetAggregationRequestsAsync(new ProductIndexedSearchCriteria { StoreId = request.StoreId, Currency = request.CurrencyCode, }, new FiltersContainer()); builder.ParseFacets(_phraseParser, request.Facet, predefinedAggregations) .ApplyMultiSelectFacetSearch(); } var searchRequest = builder.Build(); var searchResult = await _searchProvider.SearchAsync(KnownDocumentTypes.Product, searchRequest); var criteria = new ProductIndexedSearchCriteria { StoreId = request.StoreId, Currency = request.CurrencyCode, }; //TODO: move later to own implementation //Call the catalog aggregation converter service to convert AggregationResponse to proper Aggregation type (term, range, filter) var resultAggregations = await _aggregationConverter.ConvertAggregationsAsync(searchResult.Aggregations, criteria); searchRequest.SetAppliedAggregations(resultAggregations); var products = searchResult.Documents?.Select(x => _mapper.Map <ExpProduct>(x)).ToList() ?? new List <ExpProduct>(); var result = new SearchProductResponse { Query = request, AllStoreCurrencies = allStoreCurrencies, Currency = currency, Store = store, Results = products, Facets = resultAggregations?.Select(x => _mapper.Map <FacetResult>(x)).ToList(), TotalCount = (int)searchResult.TotalCount }; await _pipeline.Execute(result); return(result); }
protected virtual IList <AggregationRequest> GetPriceRangeFilterAggregationRequests(PriceRangeFilter priceRangeFilter, ProductIndexedSearchCriteria criteria, IList <IFilter> existingFilters) { var result = priceRangeFilter.Values?.Select(v => GetPriceRangeFilterValueAggregationRequest(priceRangeFilter, v, existingFilters, criteria.Pricelists)).ToList(); return(result); }
public virtual async Task <Aggregation[]> ConvertAggregationsAsync(IList <AggregationResponse> aggregationResponses, ProductIndexedSearchCriteria criteria) { var result = new List <Aggregation>(); var browseFilters = await _browseFilterService.GetBrowseFiltersAsync(criteria); if (browseFilters != null && aggregationResponses?.Any() == true) { foreach (var filter in browseFilters) { Aggregation aggregation = null; switch (filter) { case AttributeFilter attributeFilter: aggregation = GetAttributeAggregation(attributeFilter, aggregationResponses); break; case RangeFilter rangeFilter: aggregation = GetRangeAggregation(rangeFilter, aggregationResponses); break; case PriceRangeFilter priceRangeFilter: aggregation = GetPriceRangeAggregation(priceRangeFilter, aggregationResponses); break; } if (aggregation?.Items?.Any() == true) { result.Add(aggregation); } } } // Add localized labels for names and values if (result.Any()) { await AddLabelsAsync(result, criteria.CatalogId); } return(result.ToArray()); }
protected virtual IFilter ConvertBrowseFilter(IBrowseFilter filter, IList <string> valueIds, ProductIndexedSearchCriteria criteria) { IFilter result = null; if (filter != null && valueIds != null) { var attributeFilter = filter as AttributeFilter; var rangeFilter = filter as BrowseFilters.RangeFilter; var priceRangeFilter = filter as PriceRangeFilter; if (attributeFilter != null) { result = ConvertAttributeFilter(attributeFilter, valueIds); } else if (rangeFilter != null) { result = ConvertRangeFilter(rangeFilter, valueIds); } else if (priceRangeFilter != null) { result = ConvertPriceRangeFilter(priceRangeFilter, valueIds, criteria); } } return(result); }
protected virtual IFilter ConvertPriceRangeFilter(PriceRangeFilter priceRangeFilter, IList <string> valueIds, ProductIndexedSearchCriteria criteria) { IFilter result = null; if (string.IsNullOrEmpty(criteria.Currency) || priceRangeFilter.Currency.EqualsInvariant(criteria.Currency)) { var knownValues = priceRangeFilter.Values ?.Where(v => valueIds.Contains(v.Id, StringComparer.OrdinalIgnoreCase)) .ToArray(); if (knownValues != null && knownValues.Any()) { var filters = knownValues .Select(v => FiltersHelper.CreatePriceRangeFilter(priceRangeFilter.Currency, criteria.Pricelists, v.Lower, v.Upper, v.IncludeLower, v.IncludeUpper)) .Where(f => f != null) .ToList(); result = filters.Or(); } else { // Unknown term values should produce empty result result = new IdsFilter { Values = new[] { string.Empty } }; } } return(result); }
public virtual async Task <SearchProductResponse> Handle(SearchProductQuery request, CancellationToken cancellationToken) { var allStoreCurrencies = await _storeCurrencyResolver.GetAllStoreCurrenciesAsync(request.StoreId, request.CultureName); var currency = await _storeCurrencyResolver.GetStoreCurrencyAsync(request.CurrencyCode, request.StoreId, request.CultureName); var store = await _storeService.GetByIdAsync(request.StoreId); var responseGroup = EnumUtility.SafeParse(request.GetResponseGroup(), ExpProductResponseGroup.None); var builder = new IndexSearchRequestBuilder() .WithCurrency(currency.Code) .WithFuzzy(request.Fuzzy, request.FuzzyLevel) .ParseFilters(_phraseParser, request.Filter) .WithSearchPhrase(request.Query) .WithPaging(request.Skip, request.Take) .AddObjectIds(request.ObjectIds) .AddSorting(request.Sort) .WithIncludeFields(IndexFieldsMapper.MapToIndexIncludes(request.IncludeFields).ToArray()); if (request.ObjectIds.IsNullOrEmpty()) { AddDefaultTerms(builder, store.Catalog); } var criteria = new ProductIndexedSearchCriteria { StoreId = request.StoreId, Currency = request.CurrencyCode ?? store.DefaultCurrency, LanguageCode = store.Languages.Contains(request.CultureName) ? request.CultureName : store.DefaultLanguage, CatalogId = store.Catalog }; //Use predefined facets for store if the facet filter expression is not set if (responseGroup.HasFlag(ExpProductResponseGroup.LoadFacets)) { var predefinedAggregations = await _aggregationConverter.GetAggregationRequestsAsync(criteria, new FiltersContainer()); builder.WithCultureName(criteria.LanguageCode); builder.ParseFacets(_phraseParser, request.Facet, predefinedAggregations) .ApplyMultiSelectFacetSearch(); } var searchRequest = builder.Build(); var searchResult = await _searchProvider.SearchAsync(KnownDocumentTypes.Product, searchRequest); var resultAggregations = await ConvertResultAggregations(criteria, searchRequest, searchResult); searchRequest.SetAppliedAggregations(resultAggregations.ToArray()); var products = searchResult.Documents?.Select(x => _mapper.Map <ExpProduct>(x)).ToList() ?? new List <ExpProduct>(); var result = new SearchProductResponse { Query = request, AllStoreCurrencies = allStoreCurrencies, Currency = currency, Store = store, Results = products, Facets = resultAggregations?.ApplyLanguageSpecificFacetResult(criteria.LanguageCode) .Select(x => _mapper.Map <FacetResult>(x, options => { options.Items["cultureName"] = criteria.LanguageCode; })).ToList(), TotalCount = (int)searchResult.TotalCount }; await _pipeline.Execute(result); return(result); async Task <Aggregation[]> ConvertResultAggregations(ProductIndexedSearchCriteria criteria, SearchRequest searchRequest, SearchResponse searchResult) { // Preconvert resulting aggregations to be properly understandable by catalog module var preconvertedAggregations = new List <AggregationResponse>(); //Remember term facet ids to distinguish the resulting aggregations are range or term var termsInRequest = new List <string>(searchRequest.Aggregations.Where(x => x is TermAggregationRequest).Select(x => x.Id ?? x.FieldName)); foreach (var aggregation in searchResult.Aggregations) { if (!termsInRequest.Contains(aggregation.Id)) { // There we'll go converting range facet result var fieldName = new Regex(@"^(?<fieldName>[A-Za-z0-9]+)(-.+)*$", RegexOptions.IgnoreCase).Match(aggregation.Id).Groups["fieldName"].Value; if (!fieldName.IsNullOrEmpty()) { preconvertedAggregations.AddRange(aggregation.Values.Select(x => { var matchId = new Regex(@"^(?<left>[0-9*]+)-(?<right>[0-9*]+)$", RegexOptions.IgnoreCase).Match(x.Id); var left = matchId.Groups["left"].Value; var right = matchId.Groups["right"].Value; x.Id = left == "*" ? $@"under-{right}" : x.Id; x.Id = right == "*" ? $@"over-{left}" : x.Id; return(new AggregationResponse() { Id = $@"{fieldName}-{x.Id}", Values = new List <AggregationResponseValue> { x } }); } )); } } else { // This is term aggregation, should skip converting and put resulting aggregation as is preconvertedAggregations.Add(aggregation); } } //Call the catalog aggregation converter service to convert AggregationResponse to proper Aggregation type (term, range, filter) return(await _aggregationConverter.ConvertAggregationsAsync(preconvertedAggregations, criteria)); } }
public async Task <ActionResult <ProductIndexedSearchResult> > SearchProducts([FromBody] ProductIndexedSearchCriteria criteria) { criteria.ObjectType = KnownDocumentTypes.Product; var result = await _productIndexedSearchService.SearchAsync(criteria); //It is a important to return serialized data by such way. Instead you have a slow response time for large outputs //https://github.com/dotnet/aspnetcore/issues/19646 return(Content(JsonConvert.SerializeObject(result, _jsonOptions.SerializerSettings), "application/json")); }
public async Task <ActionResult <ProductIndexedSearchResult> > SearchProducts([FromBody] ProductIndexedSearchCriteria criteria) { criteria.ObjectType = KnownDocumentTypes.Product; var result = await _productIndexedSearchService.SearchAsync(criteria); return(Ok(result)); }