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);
        }
Example #5
0
        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);
        }
Example #12
0
        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);
        }
Example #13
0
        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());
        }
Example #14
0
        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);
        }
Example #15
0
        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);
        }
Example #16
0
        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"));
        }
Example #18
0
        public async Task <ActionResult <ProductIndexedSearchResult> > SearchProducts([FromBody] ProductIndexedSearchCriteria criteria)
        {
            criteria.ObjectType = KnownDocumentTypes.Product;
            var result = await _productIndexedSearchService.SearchAsync(criteria);

            return(Ok(result));
        }