Context-based search in SAP Commerce Cloud

Ncremental
11 min readMay 29, 2023

SAP Commerce Cloud’s Search and Navigation Module comes with multiple APIs that allow you to customize what and how values are indexed and searched in Solr documents. One of them is Value Provider API. SAP Commerce instances often implement ValueResolver (or deprecated FieldValueProvider) interface to index values that are not included out of the box. What is not so often implemented is the QualifierProvider interface. With the ValueResolver implementations, one could configure a QualifierProvider instance.

<bean id="customValueResolver" class="some.package.CustomValueResolver">
<property name="qualifierProvider" ref="someQualifierProvider"/>
</bean>

This interface is useful when you want to index and search values based on simple data from the current user’s context with relatively low development effort (compared to say, integrating with external providers such as Context-Driven Services. We will look at how it can be utilized to provide context based search.

Qualifier Provider

QualifierProvider interface helps qualify (or decorate) Solr fields, and is used during both indexing and searching to append qualifier to the Solr field names. SAP Commerce ships with two QualifierProvider implementations:

  • LanguageQualifierProvider
  • CurrencyQualifierProvider

These are used for values that may be different for different language and currency contexts. For example, since product names could be localized, they are indexed in Solr fields having language ISO appended such as name_text_en (English) and name_text_de (German), rather than just name_text. The LanaguageQualifierProvider in this case helps index product names for all available languages during indexing and determine the right field name based on the current user's session language during searching.

User Price Group Based Pricing

Problem

The two out-of-the-box QualifierProvider implementations cover a lot of context-based searching use cases, but there are certainly other scenarios that need another kind of a QualifierProvider. One scenario is User Price Group based pricing.

Having different prices for different currencies is supported out of the box. Having different prices for different user groups is also supported out of the box but only partially. When a Price Row is set up with a Customer Price List (i.e. User Price Group), while the Product Details Page displays the expected User Price Group based price, the Solr-search based pages — Product Listing Page and Product Search Page — do not. This results in contradicting prices between pages and could confuse customers.

There is more than one way to support User Price Group based pricing on Solr-search based pages. While determining the best way would depend on many factors, such as traffic volume, architecture, etc, one easy and not-so-complicated way is implementing a custom QualifierProvider. Keep in mind also that other search related features such as faceting, sorting and pagination must stay intact.

In this article, I will share an example of UserPriceGroup oriented QualifierProvider.

Solution

The code snippets in this section are meant to serve as simple examples only and by no means complete or produciton-grade. They need to be modified and adapted to meet your own needs. The examples were written for SAP Commerce version 2105.0 and may not be compatible with other versions.

Value Provider API

Start by inspecting the QualifierProvider interface to learn about its specifications. This is an excerpt from the SAP Help Portal:

Java Interface QualifierProvider

 /**
* This interface provides support for different types of qualifiers.
*/
public interface QualifierProvider
{
/**
* Returns all the supported types by this provider.
*
* @return the supported types
*/
Set<Class<?>> getSupportedTypes();

/**
* Checks if qualifiers can be applied/used with the indexed property passed as parameter.
*
* @param indexedProperty
* - the indexed property
*
* @return {@code true} if qualifiers can be used, {@code false} otherwise
*/
boolean canApply(IndexedProperty indexedProperty);

/**
* Returns all the possible qualifiers for a given index configuration and indexed type.
*
* @param facetSearchConfig
* - the facet search configuration
* @param indexedType
* - the indexed type
*
* @return the available qualifiers
*/
Collection<Qualifier> getAvailableQualifiers(FacetSearchConfig facetSearchConfig, IndexedType indexedType);

/**
* Applies the qualifier passed as parameter. This normally consists in setting some attributes on the session, e.g.
* by calling a service to set the current session language, currency, etc.
*
* @param qualifier
* - the {@link Qualifier} to be applied
*/
void applyQualifier(Qualifier qualifier);

/**
* Returns the current qualifier. This normally consists in getting some attributes from the session and creating the
* corresponding qualifier.
*
* @return the current qualifier
*/
Qualifier getCurrentQualifier();
}

You may also analyze the AbstractValueResolver class to learn more about how it utilizes the QualifierProvider interface in depth.

Java Class AbstractValueResolver

package de.hybris.platform.solrfacetsearch.provider.impl;

import de.hybris.platform.core.model.ItemModel;
import de.hybris.platform.jalo.JaloSession;
import de.hybris.platform.servicelayer.session.Session;
import de.hybris.platform.servicelayer.session.SessionService;
import de.hybris.platform.servicelayer.util.ServicesUtil;
import de.hybris.platform.solrfacetsearch.config.FacetSearchConfig;
import de.hybris.platform.solrfacetsearch.config.IndexedProperty;
import de.hybris.platform.solrfacetsearch.config.IndexedType;
import de.hybris.platform.solrfacetsearch.config.exceptions.FieldValueProviderException;
import de.hybris.platform.solrfacetsearch.indexer.IndexerBatchContext;
import de.hybris.platform.solrfacetsearch.indexer.spi.InputDocument;
import de.hybris.platform.solrfacetsearch.provider.Qualifier;
import de.hybris.platform.solrfacetsearch.provider.QualifierProvider;
import de.hybris.platform.solrfacetsearch.provider.QualifierProviderAware;
import de.hybris.platform.solrfacetsearch.provider.ValueFilter;
import de.hybris.platform.solrfacetsearch.provider.ValueResolver;
import java.util.Collection;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Required;

public abstract class AbstractValueResolver<T extends ItemModel, M, Q> implements ValueResolver<T>, QualifierProviderAware {
private SessionService sessionService;
private QualifierProvider qualifierProvider;
private Collection<ValueFilter> valueFilters;

public AbstractValueResolver() {
}

public SessionService getSessionService() {
return this.sessionService;
}

@Required
public void setSessionService(SessionService sessionService) {
this.sessionService = sessionService;
}

public QualifierProvider getQualifierProvider() {
return this.qualifierProvider;
}

@Required
public void setQualifierProvider(QualifierProvider qualifierProvider) {
this.qualifierProvider = qualifierProvider;
}

public Collection<ValueFilter> getValueFilters() {
return this.valueFilters;
}

public void setValueFilters(Collection<ValueFilter> valueFilters) {
this.valueFilters = valueFilters;
}

public void resolve(InputDocument document, IndexerBatchContext batchContext, Collection<IndexedProperty> indexedProperties, T model) throws FieldValueProviderException {
ServicesUtil.validateParameterNotNull("model", "model instance is null");

try {
this.createLocalSessionContext();
this.doResolve(document, batchContext, indexedProperties, model);
} finally {
this.removeLocalSessionContext();
}

}

protected void doResolve(InputDocument document, IndexerBatchContext batchContext, Collection<IndexedProperty> indexedProperties, T model) throws FieldValueProviderException {
M data = this.loadData(batchContext, indexedProperties, model);
AbstractValueResolver.ValueResolverContext<M, Q> resolverContext = new AbstractValueResolver.ValueResolverContext<>();
resolverContext.setData(data);

for(IndexedProperty indexedProperty : indexedProperties) {
if (!this.qualifierProvider.canApply(indexedProperty)) {
this.addFieldValues(document, batchContext, indexedProperty, model, resolverContext);
}
}

FacetSearchConfig facetSearchConfig = batchContext.getFacetSearchConfig();
IndexedType indexedType = batchContext.getIndexedType();

for(Qualifier qualifier : this.qualifierProvider.getAvailableQualifiers(facetSearchConfig, indexedType)) {
this.qualifierProvider.applyQualifier(qualifier);
String fieldQualifier = qualifier.toFieldQualifier();
Q qualifierData = this.loadQualifierData(batchContext, indexedProperties, model, qualifier);
resolverContext.setQualifier(qualifier);
resolverContext.setFieldQualifier(fieldQualifier);
resolverContext.setQualifierData(qualifierData);

for(IndexedProperty indexedProperty : indexedProperties) {
if (this.qualifierProvider.canApply(indexedProperty)) {
this.addFieldValues(document, batchContext, indexedProperty, model, resolverContext);
}
}
}

}

protected M loadData(IndexerBatchContext batchContext, Collection<IndexedProperty> indexedProperties, T model) throws FieldValueProviderException {
return null;
}

protected Q loadQualifierData(IndexerBatchContext batchContext, Collection<IndexedProperty> indexedProperties, T model, Qualifier qualifier) throws FieldValueProviderException {
return null;
}

protected abstract void addFieldValues(
InputDocument var1, IndexerBatchContext var2, IndexedProperty var3, T var4, AbstractValueResolver.ValueResolverContext<M, Q> var5
) throws FieldValueProviderException;

protected Object filterFieldValue(IndexerBatchContext batchContext, IndexedProperty indexedProperty, Object value) {
Object filedValue = value;
if (this.valueFilters != null && !this.valueFilters.isEmpty()) {
for(ValueFilter valueFilter : this.valueFilters) {
filedValue = valueFilter.doFilter(batchContext, indexedProperty, filedValue);
}
}

return filedValue;
}

protected boolean addFieldValue(InputDocument document, IndexerBatchContext batchContext, IndexedProperty indexedProperty, Object value, String qualifier) throws FieldValueProviderException {
boolean isString = value instanceof String;
if (isString && StringUtils.isBlank((String)value)) {
return false;
} else {
document.addField(indexedProperty, value, qualifier);
return true;
}
}

protected boolean filterAndAddFieldValues(
InputDocument document, IndexerBatchContext batchContext, IndexedProperty indexedProperty, Object value, String qualifier
) throws FieldValueProviderException {
boolean hasValue = false;
if (value != null) {
Object fieldValue = this.filterFieldValue(batchContext, indexedProperty, value);
if (fieldValue instanceof Collection) {
for(Object singleValue : (Collection)fieldValue) {
hasValue |= this.addFieldValue(document, batchContext, indexedProperty, singleValue, qualifier);
}
} else {
hasValue = this.addFieldValue(document, batchContext, indexedProperty, fieldValue, qualifier);
}
}

return hasValue;
}

protected void createLocalSessionContext() {
Session session = this.sessionService.getCurrentSession();
JaloSession jaloSession = (JaloSession)this.sessionService.getRawSession(session);
jaloSession.createLocalSessionContext();
}

protected void removeLocalSessionContext() {
Session session = this.sessionService.getCurrentSession();
JaloSession jaloSession = (JaloSession)this.sessionService.getRawSession(session);
jaloSession.removeLocalSessionContext();
}

protected static final class ValueResolverContext<T, U> {
private T data;
private U qualifierData;
private Qualifier qualifier;
private String fieldQualifier;

protected ValueResolverContext() {
}

public T getData() {
return this.data;
}

public void setData(T data) {
this.data = data;
}

public U getQualifierData() {
return this.qualifierData;
}

public void setQualifierData(U qualifierData) {
this.qualifierData = qualifierData;
}

public Qualifier getQualifier() {
return this.qualifier;
}

public void setQualifier(Qualifier qualifier) {
this.qualifier = qualifier;
}

public String getFieldQualifier() {
return this.fieldQualifier;
}

public void setFieldQualifier(String fieldQualifier) {
this.fieldQualifier = fieldQualifier;
}
}
}

Then let’s look at one of the out-of-the-box implementations CurrencyQualifierProvider. We could model our custom QualifierProvider after this class.

Java Class CurrencyQualifierProvider

package de.hybris.platform.solrfacetsearch.provider.impl;

import de.hybris.platform.core.model.c2l.CurrencyModel;
import de.hybris.platform.servicelayer.i18n.CommonI18NService;
import de.hybris.platform.solrfacetsearch.config.FacetSearchConfig;
import de.hybris.platform.solrfacetsearch.config.IndexedProperty;
import de.hybris.platform.solrfacetsearch.config.IndexedType;
import de.hybris.platform.solrfacetsearch.provider.Qualifier;
import de.hybris.platform.solrfacetsearch.provider.QualifierProvider;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.lang3.builder.EqualsBuilder;

public class CurrencyQualifierProvider implements QualifierProvider {
private CommonI18NService commonI18NService;
private final Set<Class<?>> supportedTypes = Collections.singleton(CurrencyModel.class);

public CurrencyQualifierProvider() {
}

public CommonI18NService getCommonI18NService() {
return this.commonI18NService;
}

public void setCommonI18NService(CommonI18NService commonI18NService) {
this.commonI18NService = commonI18NService;
}

public Set<Class<?>> getSupportedTypes() {
return this.supportedTypes;
}

public Collection<Qualifier> getAvailableQualifiers(FacetSearchConfig facetSearchConfig, IndexedType indexedType) {
Objects.requireNonNull(facetSearchConfig, "facetSearchConfig is null");
Objects.requireNonNull(indexedType, "indexedType is null");
List<Qualifier> qualifiers = new ArrayList();

for(CurrencyModel currency : facetSearchConfig.getIndexConfig().getCurrencies()) {
Qualifier qualifier = new CurrencyQualifierProvider.CurrencyQualifier(currency);
qualifiers.add(qualifier);
}

return Collections.unmodifiableList(qualifiers);
}

public boolean canApply(IndexedProperty indexedProperty) {
Objects.requireNonNull(indexedProperty, "indexedProperty is null");
return indexedProperty.isCurrency();
}

public void applyQualifier(Qualifier qualifier) {
Objects.requireNonNull(qualifier, "qualifier is null");
if (!(qualifier instanceof CurrencyQualifierProvider.CurrencyQualifier)) {
throw new IllegalArgumentException("provided qualifier is not of expected type");
} else {
this.commonI18NService.setCurrentCurrency(((CurrencyQualifierProvider.CurrencyQualifier)qualifier).getCurrency());
}
}

public Qualifier getCurrentQualifier() {
CurrencyModel currency = this.commonI18NService.getCurrentCurrency();
return currency == null ? null : new CurrencyQualifierProvider.CurrencyQualifier(currency);
}

protected static class CurrencyQualifier implements Qualifier {
private final CurrencyModel currency;

public CurrencyQualifier(CurrencyModel currency) {
Objects.requireNonNull(currency, "currency is null");
this.currency = currency;
}

public CurrencyModel getCurrency() {
return this.currency;
}

public <U> U getValueForType(Class<U> type) {
Objects.requireNonNull(type, "type is null");
if (Objects.equals(type, CurrencyModel.class)) {
return (U)this.currency;
} else {
throw new IllegalArgumentException("type not supported");
}
}

public String toFieldQualifier() {
return this.currency.getIsocode();
}

public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj != null && this.getClass() == obj.getClass()) {
CurrencyQualifierProvider.CurrencyQualifier that = (CurrencyQualifierProvider.CurrencyQualifier)obj;
return new EqualsBuilder().append(this.currency, that.currency).isEquals();
} else {
return false;
}
}

public int hashCode() {
return this.currency.hashCode();
}
}
}

The above two Java classes are recreated from respective .class files found in hybris/bin/modules/search-and-navigation/solrfacetsearch/bin/solrfacetsearchserver.jar by Quiltflower.

We can see that the CurrencyQualifierProvider operates around the condition returned by the IndexedProperty#isCurrency method. This is effectively a getter method for the persisted property field SolrIndexedPropertyModel#CURRENCY. Following this pattern, we can add a persisted property userPriceGroup to the SolrIndexedProperty item type that is accessible by IndexedProperty#isUserPriceGroup method so that only certain Indexed Properties are decorated by User Price Group.

User Price Group property

Item Type SolrIndexedProperty

<itemtype code="SolrIndexedProperty" autocreate="false" generate="false">
<attributes>
<attribute qualifier="userPriceGroup" type="boolean">
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>

Java Bean IndexedProperty

<bean class="de.hybris.platform.solrfacetsearch.config.IndexedProperty">
<property name="userPriceGroup" type="boolean"/>
</bean>

Java Class UserPriceGroupIndexedPropertyPopulator

package io.ncremental.example;

import de.hybris.platform.converters.Populator;
import de.hybris.platform.solrfacetsearch.config.IndexedProperty;
import de.hybris.platform.solrfacetsearch.model.config.SolrIndexedPropertyModel;

public class UserGroupIndexedPropertyPopulator implements Populator<SolrIndexedPropertyModel, IndexedProperty> {
public void populate(final SolrIndexedPropertyModel source, final IndexedProperty target) {
target.setUserPriceGroup(source.isUserPriceGroup());
}
}

Spring Bean userPriceGroupIndexedPropertyPopulator

<bean id="userPriceGroupIndexedPropertyPopulator" class="io.ncremental.example.UserGroupIndexedPropertyPopulator">
<!-- set bean properties -->
</bean>
<bean parent="modifyPopulatorList">
<property name="list" ref="indexedPropertyConverter"/>
<property name="add" ref="userPriceGroupIndexedPropertyPopulator"/>
</bean>

Implementation of QualifierProvider

We are ready to create a custom QualifierProvider implementation named UserPriceGroupQualifierProvider. In fact, since we want to customize the price component, which is a currency-dependent component, it would be reasonable for our custom class to extend the CurrencyQualifierProvider and inherit as much of it as possible.

Java Class UserPriceGroupQualifierProvider

package io.ncremental.example;

import de.hybris.platform.core.model.user.CustomerModel;
import de.hybris.platform.core.model.user.UserModel;
import de.hybris.platform.enumeration.EnumerationService;
import de.hybris.platform.europe1.enums.UserPriceGroup;
import de.hybris.platform.order.exceptions.CalculationException;
import de.hybris.platform.order.strategies.calculation.impl.internal.PricingCustomizationDetector;
import de.hybris.platform.order.strategies.calculation.pdt.impl.PDTEnumGroupsHelper;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.session.SessionService;
import de.hybris.platform.servicelayer.type.TypeService;
import de.hybris.platform.servicelayer.user.UserService;
import de.hybris.platform.solrfacetsearch.config.FacetSearchConfig;
import de.hybris.platform.solrfacetsearch.config.IndexedProperty;
import de.hybris.platform.solrfacetsearch.config.IndexedType;
import de.hybris.platform.solrfacetsearch.provider.Qualifier;
import de.hybris.platform.solrfacetsearch.provider.impl.CurrencyQualifierProvider;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;

import static de.hybris.platform.core.model.user.UserGroupModel.USERPRICEGROUP;
import static de.hybris.platform.core.model.user.UserModel.EUROPE1PRICEFACTORY_UPG;

public class UserPriceGroupQualifierProvider extends CurrencyQualifierProvider {
// ideally, set the value from the property `solr.indexedproperty.forbidden.char`
private static final String DELIM = "_";
private SessionService sessionService;
private PricingCustomizationDetector pricingCustomizationDetector;
private UserService userService;
private TypeService typeService;
private ModelService modelService;
private EnumerationService enumerationService;
private PDTEnumGroupsHelper pdtEnumGroupsHelper;

@Override
public Set<Class<?>> getSupportedTypes() {
final Set<Class<?>> types = new HashSet<>();
types.add(UserPriceGroup.class);
types.addAll(super.getSupportedTypes());
return types;
}

@Override
public Collection<Qualifier> getAvailableQualifiers(
final FacetSearchConfig facetSearchConfig, final IndexedType indexedType) {
final Collection<Qualifier> currencyQualifiers =
super.getAvailableQualifiers(facetSearchConfig, indexedType);
final List<Qualifier> qualifiers = new ArrayList<>(currencyQualifiers);
final Collection<UserPriceGroup> upgs =
enumerationService.getEnumerationValues(UserPriceGroup.class);
currencyQualifiers.forEach(
cq ->
upgs.forEach(
ug ->
qualifiers.add(
new UserPriceGroupQualifierProvider.UserPriceGroupQualifier(
ug, (CurrencyQualifier) cq))));
return Collections.unmodifiableList(qualifiers);
}

@Override
public boolean canApply(final IndexedProperty indexedProperty) {
return super.canApply(indexedProperty) && indexedProperty.isUserPriceGroup();
}

@Override
public void applyQualifier(final Qualifier qualifier) {
super.applyQualifier(qualifier);
if (qualifier instanceof UserPriceGroupQualifierProvider.UserPriceGroupQualifier) {
final UserPriceGroup upg =
((UserPriceGroupQualifierProvider.UserPriceGroupQualifier) qualifier).getUpg();
final boolean jalo = pricingCustomizationDetector.useJalo();
sessionService.setAttribute(USERPRICEGROUP, upg);
sessionService.setAttribute(
EUROPE1PRICEFACTORY_UPG,
jalo && upg != null
? modelService.getSource(typeService.getEnumerationValue(upg))
: upg);
}
}

@Override
public Qualifier getCurrentQualifier() {
final Qualifier cq = super.getCurrentQualifier();
return cq == null
? null
: new UserPriceGroupQualifierProvider.UserPriceGroupQualifier(
getCurrentUserPriceGroup(), (CurrencyQualifier) cq);
}

private UserPriceGroup getCurrentUserPriceGroup() {
UserPriceGroup upg = sessionService.getAttribute(USERPRICEGROUP);
if (upg == null) {
final UserModel user = userService.getCurrentUser();
if (user instanceof CustomerModel) {
try {
upg = pdtEnumGroupsHelper.getUPG(user);
} catch (final CalculationException e) {
// Handle exception
}
}
}
return upg;
}

public static class UserPriceGroupQualifier extends CurrencyQualifier {
private final UserPriceGroup upg;

public UserPriceGroupQualifier(
final UserPriceGroup upg,
final CurrencyQualifier cq) {
super(cq.getCurrency());
this.upg = upg;
}

@Override
public <U> U getValueForType(final Class<U> type) {
try {
return super.getValueForType(type);
} catch (final IllegalArgumentException e) {
if (Objects.equals(type, UserPriceGroup.class)) {
return (U) upg;
} else {
throw e;
}
}
}

@Override
public String toFieldQualifier() {
return toFieldQualifier(upg, getCurrency().getIsocode());
}

public static String toFieldQualifier(final UserPriceGroup upg, final String currency) {
final Collection<String> q = new ArrayList<>();
q.add(upg == null ? null : StringUtils.trim(upg.getCode()));
q.add(currency);
return String.join(DELIM, q).toLowerCase(Locale.ROOT).replaceAll("\\s+", "-_-");
}

public UserPriceGroup getUpg() {
return upg;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}

if (o == null || getClass() != o.getClass()) {
return false;
}

final UserPriceGroupQualifier that = (UserPriceGroupQualifier) o;
return new EqualsBuilder().appendSuper(super.equals(o)).append(upg, that.upg).isEquals();
}

@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).appendSuper(super.hashCode()).append(upg).toHashCode();
}
}
}

Finally, we create a Spring Bean for this class, and supply it as a qualifierProvider to a ProductPricesValueResolver bean that is used as a Property Value Provider for an Indexed Property such as priceValue. Do not forget to set this property's userPriceGroup flag to true.

Spring Beans userPriceGroupQualifierProvider and userPriceGroupProductPricesValueResolver

<bean id="userPriceGroupQualifierProvider" class="io.ncremental.example.UserPriceGroupQualifierProvider"
parent="currencyQualifierProvider">
<!-- set bean properties -->
</bean>

<bean id="userPriceGroupProductPricesValueResolver" parent="productPricesValueResolver">
<property name="qualifierProvider" ref="userPriceGroupQualifierProvider" />
</bean>

Indexed Property priceValue

Ranged Indexed Property

You may encounter the exception

Cannot get range for property [propertyName] with value [propertyValue]

when using the userPriceGroupProductPricesValueResolver with ranged index properties such as price.

Indexed Property price

To resolve this, we need to populate ValueRangeSets with User Price Group decorated field qualifiers as keys.

Java Class UserPriceGroupIndexedPropertyPopulator

if (source.isUserPriceGroup()) {
final Map<String, ValueRangeSet> rangeSets = target.getValueRangeSets();
// currency rangeSets should be populated already by OOTB class `DefaultIndexedPropertyPopulator`
if (!MapUtils.isEmpty(rangeSets)) {
final Collection<UserPriceGroup> upgs = enumerationService.getEnumerationValues(UserPriceGroup.class);
final Map<String, ValueRangeSet> upgRangeSets = new HashMap<>(rangeSets);
rangeSets.forEach(
(key, value) ->
upgs.forEach(
upg -> upgRangeSets.put(
UserPriceGroupQualifierProvider.UserPriceGroupQualifier.toFieldQualifier(upg, key), value)));
target.setValueRangeSets(upgRangeSets);
}
}

Last but not least, when a new User Price Group is created during application runtime, the above exception may occur again. This is because of the Solr Configuration Caching. Therefore, we shall invalidate the cache when a new User Price Group is created. This can be done by providing the item type code of UserPriceGroup to the FacetSearchConfigInvalidationTypeSet. Since UserPriceGroup is not an itemtype but an enumtype, we provide the type code for the EnumerationValue.

Spring Bean enumValueFacetSearchConfigInvalidationTypeSet

<bean id="enumValueFacetSearchConfigInvalidationTypeSet"
class="de.hybris.platform.solrfacetsearch.config.cache.FacetSearchConfigInvalidationTypeSet">
<constructor-arg>
<set>
<!-- EnumerationValue -->
<value>91</value>
</set>
</constructor-arg>
</bean>

Conclusion

In this article, I have illustrated how a custom QualifierProvider can be used to support User Price Group based prices on product listing and search pages. Similar approaches could be taken to support other context-based attributes on Solr-search driven pages such as User Discount Group based discounts.

Value Provider API

Start by inspecting the QualifierProvider interface to learn about its specifications. This is an excerpt from the SAP Help Portal:

Java Interface QualifierProvider

--

--