BEYABLE Product Ranking API - SAP Commerce Cloud Integration Guide
Overview
This guide provides step-by-step instructions for integrating the BEYABLE Product Ranking API with SAP Commerce Cloud (formerly Hybris). The integration enables AI-powered product ranking to optimize your product listings across category pages, search results, and recommendation slots.
Table of Contents
- Prerequisites
- Architecture Overview
- Extension Structure
- Installation
- Configuration
- Implementation
- CronJob Configuration
- Frontend Integration
- Testing
- Deployment
- Monitoring & Troubleshooting
- Performance Optimization
Prerequisites
System Requirements
- SAP Commerce Cloud 1905 or higher (Hybris 6.x also supported with modifications)
- Administrator access to Backoffice and HAC (Hybris Administration Console)
- Java 11+ (Java 8 for older versions)
- Apache Maven 3.6+
- Knowledge of Spring Framework and Hybris platform
Required Information from BEYABLE
Obtain the following from your BEYABLE account manager:
- Account ID: Your 32-character UUID (without hyphens)
- Subscription Key: Your API authentication key
- Ranking Rule ID(s): UUID for each ranking rule
- Segmented Ranking ID(s): UUID for segmented rankings (if applicable)
- Site/Locale Mapping: Tenant configuration for your SAP Commerce sites
SAP Commerce Knowledge Required
- Understanding of SAP Commerce extensions and build process
- Experience with Backoffice configuration
- Familiarity with Flexible Search and models
- Knowledge of CronJobs and service layer
- Understanding of Solr search configuration
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ BEYABLE Platform │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ Ranking Rules & │ │ Product Ranking API │ │
│ │ Segment Config │───────>│ api.eu1.beyable.com │ │
│ └──────────────────┘ └──────────────────────────────┘ │
└───────────────────────────────────────┬─────────────────────────┘
│ CSV Response
│ (ProductId, Position)
▼
┌─────────────────────────────────────────────────────────────────┐
│ SAP Commerce Cloud (Hybris) │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ CronJob: Sync │ │ Model: │ │
│ │ Rankings │───────>│ BeyableRankingModel │ │
│ │ (Every 2hrs) │ │ (Store rankings) │ │
│ └──────────────────┘ └──────────────────────────────┘ │
│ │ │
│ ┌──────────────────┐ │ │
│ │ Shopper visits │ │ │
│ │ Category/Search │ ▼ │
│ └────────┬─────────┘ ┌──────────────────────────────┐ │
│ │ │ Service/DAO: │ │
│ │ │ - Read segment cookie │ │
│ └─────────────────>│ - Apply rankings │ │
│ │ - Sort products │ │
│ └──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Optimized Product Listing │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Integration Flow:
- BEYABLE Cookie: BEYABLE Platform sets visitor segment cookie
- Scheduled CronJob: SAP Commerce job fetches rankings from BEYABLE API (every 1-4 hours)
- Data Storage: Rankings stored in SAP Commerce database via FlexibleSearch
- Runtime Application: Services read segment, apply rankings to product searches
- Frontend Display: Optimized product listings displayed to shoppers
Extension Structure
beyableranking/
├── acceleratoraddon/
│ └── web/
│ ├── src/
│ │ └── com/beyable/ranking/addon/
│ │ └── controllers/
│ │ └── BeyableRankingPageController.java
│ └── webroot/
│ ├── WEB-INF/
│ │ ├── tags/
│ │ │ └── beyable/
│ │ │ └── tracking.tag
│ │ └── views/
│ │ └── responsive/
│ │ └── pages/
│ └── _ui/
│ └── responsive/
│ └── common/
│ └── js/
│ └── beyable-tracking.js
├── resources/
│ ├── beyableranking-items.xml
│ ├── beyableranking-spring.xml
│ ├── beyableranking-beans.xml
│ └── localization/
│ └── beyableranking-locales_en.properties
├── src/
│ └── com/beyable/ranking/
│ ├── constants/
│ │ └── BeyablerankingConstants.java
│ ├── dao/
│ │ ├── BeyableRankingDao.java
│ │ └── impl/
│ │ └── DefaultBeyableRankingDao.java
│ ├── service/
│ │ ├── BeyableRankingService.java
│ │ ├── BeyableApiService.java
│ │ └── impl/
│ │ ├── DefaultBeyableRankingService.java
│ │ └── DefaultBeyableApiService.java
│ ├── cronjob/
│ │ └── BeyableRankingSyncJob.java
│ ├── populators/
│ │ └── BeyableProductSearchPagePopulator.java
│ ├── strategies/
│ │ └── BeyableProductSortStrategy.java
│ └── facade/
│ └── impl/
│ └── BeyableProductSearchFacade.java
├── testsrc/
│ └── com/beyable/ranking/
│ └── service/
│ └── BeyableRankingServiceTest.java
├── extensioninfo.xml
├── external-dependencies.xml
├── project.properties
└── buildcallbacks.xml
Installation
Step 1: Create Extension
Create the extension directory structure:
cd hybris/bin/custom
mkdir -p beyableranking
cd beyableranking
Step 2: Extension Info
Create extensioninfo.xml:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<extensioninfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="extensioninfo.xsd">
<extension abstractclassprefix="Generated"
classprefix="Beyableranking"
name="beyableranking"
usemaven="true">
<requires-extension name="commerceservices"/>
<requires-extension name="commercefacades"/>
<requires-extension name="acceleratorstorefrontcommons"/>
<requires-extension name="basecommerce"/>
<requires-extension name="processing"/>
<coremodule generated="true"
manager="com.beyable.ranking.jalo.BeyablerankingManager"
packageroot="com.beyable.ranking"/>
<webmodule jspcompile="false" webroot="/beyableranking"/>
<meta key="backoffice-module" value="true"/>
</extension>
</extensioninfo>
Step 3: Project Properties
Create project.properties:
# Extension Information
beyableranking.key=beyableranking
# Build Configuration
beyableranking.global.dir=${ext.beyableranking.path}
beyableranking.build.dir=${ext.beyableranking.path}/classes
beyableranking.jar.file=${ext.beyableranking.path}/bin/beyablerankingserver.jar
# Dependencies
beyableranking.dependencies=commerceservices,commercefacades,acceleratorstorefrontcommons
Step 4: External Dependencies
Create external-dependencies.xml:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<external-dependencies>
<extensions>
<!-- Apache HTTP Client for API calls -->
<extension name="httpcore" version="4.4.14"/>
<extension name="httpclient" version="4.5.13"/>
<!-- CSV Parsing -->
<extension name="commons-csv" version="1.9.0"/>
<!-- JSON Parsing (optional) -->
<extension name="gson" version="2.8.9"/>
</extensions>
</external-dependencies>
Step 5: Add to Local Extensions
Edit hybris/config/localextensions.xml:
<hybrisconfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="resources/schemas/extensions.xsd">
<extensions>
<!-- Other extensions... -->
<extension name="beyableranking" />
</extensions>
</hybrisconfig>
Configuration
Step 1: Items Definition
Create resources/beyableranking-items.xml:
<?xml version="1.0" encoding="UTF-8"?>
<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="items.xsd">
<atomictypes>
<atomictype class="java.lang.String" autocreate="true" generate="false"/>
<atomictype class="java.lang.Integer" autocreate="true" generate="false"/>
<atomictype class="java.util.Date" autocreate="true" generate="false"/>
</atomictypes>
<itemtypes>
<!-- BEYABLE Ranking Item Type -->
<itemtype code="BeyableRanking"
extends="GenericItem"
autocreate="true"
generate="true"
jaloclass="com.beyable.ranking.jalo.BeyableRanking">
<deployment table="beyableranking" typecode="10500"/>
<attributes>
<!-- Product Reference -->
<attribute qualifier="product" type="Product">
<persistence type="property"/>
<modifiers read="true" write="true" search="true" optional="false"/>
</attribute>
<!-- Ranking Position -->
<attribute qualifier="position" type="java.lang.Integer">
<persistence type="property"/>
<modifiers read="true" write="true" search="true" optional="false"/>
</attribute>
<!-- A/B Test Group -->
<attribute qualifier="abTestGroup" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" search="true" optional="true"/>
</attribute>
<!-- Visitor Segment -->
<attribute qualifier="segmentId" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" search="true" optional="true"/>
</attribute>
<!-- Rule ID -->
<attribute qualifier="ruleId" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" search="true" optional="false"/>
</attribute>
<!-- Base Site -->
<attribute qualifier="baseSite" type="BaseSite">
<persistence type="property"/>
<modifiers read="true" write="true" search="true" optional="false"/>
</attribute>
<!-- Last Sync Date -->
<attribute qualifier="lastSyncDate" type="java.util.Date">
<persistence type="property"/>
<modifiers read="true" write="true" search="true" optional="true"/>
</attribute>
</attributes>
<indexes>
<index name="productRankingIdx">
<key attribute="product"/>
<key attribute="baseSite"/>
<key attribute="segmentId"/>
</index>
<index name="ruleSegmentIdx">
<key attribute="ruleId"/>
<key attribute="segmentId"/>
<key attribute="baseSite"/>
</index>
<index name="positionIdx">
<key attribute="position"/>
</index>
</indexes>
</itemtype>
<!-- BEYABLE Configuration Item Type -->
<itemtype code="BeyableConfig"
extends="GenericItem"
autocreate="true"
generate="true"
jaloclass="com.beyable.ranking.jalo.BeyableConfig">
<deployment table="beyableconfig" typecode="10501"/>
<attributes>
<!-- Base Site Reference -->
<attribute qualifier="baseSite" type="BaseSite">
<persistence type="property"/>
<modifiers read="true" write="true" search="true" optional="false" unique="true"/>
</attribute>
<!-- API Configuration -->
<attribute qualifier="accountId" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" optional="false"/>
</attribute>
<attribute qualifier="subscriptionKey" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" optional="false" encrypted="true"/>
</attribute>
<attribute qualifier="apiBaseUrl" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" optional="true"/>
<defaultvalue>"https://api.eu1.beyable.com"</defaultvalue>
</attribute>
<!-- Ranking Rules -->
<attribute qualifier="defaultRankingId" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" optional="false"/>
</attribute>
<attribute qualifier="segmentedRankingId" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" optional="true"/>
</attribute>
<attribute qualifier="tenantId" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" optional="true"/>
</attribute>
<!-- Feature Flags -->
<attribute qualifier="enabled" type="java.lang.Boolean">
<persistence type="property"/>
<modifiers read="true" write="true" optional="false"/>
<defaultvalue>java.lang.Boolean.FALSE</defaultvalue>
</attribute>
<attribute qualifier="useSegmentation" type="java.lang.Boolean">
<persistence type="property"/>
<modifiers read="true" write="true" optional="false"/>
<defaultvalue>java.lang.Boolean.FALSE</defaultvalue>
</attribute>
<attribute qualifier="applyToCategories" type="java.lang.Boolean">
<persistence type="property"/>
<modifiers read="true" write="true" optional="false"/>
<defaultvalue>java.lang.Boolean.TRUE</defaultvalue>
</attribute>
<attribute qualifier="applyToSearch" type="java.lang.Boolean">
<persistence type="property"/>
<modifiers read="true" write="true" optional="false"/>
<defaultvalue>java.lang.Boolean.TRUE</defaultvalue>
</attribute>
<!-- Cookie Configuration -->
<attribute qualifier="segmentCookieName" type="java.lang.String">
<persistence type="property"/>
<modifiers read="true" write="true" optional="true"/>
<defaultvalue>"b_segment"</defaultvalue>
</attribute>
<!-- Sync Configuration -->
<attribute qualifier="syncEnabled" type="java.lang.Boolean">
<persistence type="property"/>
<modifiers read="true" write="true" optional="false"/>
<defaultvalue>java.lang.Boolean.TRUE</defaultvalue>
</attribute>
<attribute qualifier="apiTimeout" type="java.lang.Integer">
<persistence type="property"/>
<modifiers read="true" write="true" optional="true"/>
<defaultvalue>java.lang.Integer.valueOf(30000)</defaultvalue>
</attribute>
<!-- Debug -->
<attribute qualifier="debugLogging" type="java.lang.Boolean">
<persistence type="property"/>
<modifiers read="true" write="true" optional="false"/>
<defaultvalue>java.lang.Boolean.FALSE</defaultvalue>
</attribute>
</attributes>
</itemtype>
</itemtypes>
</items>
Step 2: Spring Configuration
Create resources/beyableranking-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<!-- Constants -->
<bean id="beyablerankingConstants"
class="com.beyable.ranking.constants.BeyablerankingConstants"/>
<!-- DAO Layer -->
<bean id="beyableRankingDao"
class="com.beyable.ranking.dao.impl.DefaultBeyableRankingDao">
<property name="flexibleSearchService" ref="flexibleSearchService"/>
<property name="modelService" ref="modelService"/>
</bean>
<!-- Service Layer -->
<bean id="beyableApiService"
class="com.beyable.ranking.service.impl.DefaultBeyableApiService">
<property name="configurationService" ref="configurationService"/>
</bean>
<bean id="beyableRankingService"
class="com.beyable.ranking.service.impl.DefaultBeyableRankingService">
<property name="beyableRankingDao" ref="beyableRankingDao"/>
<property name="beyableApiService" ref="beyableApiService"/>
<property name="modelService" ref="modelService"/>
<property name="baseSiteService" ref="baseSiteService"/>
<property name="productService" ref="productService"/>
</bean>
<!-- CronJob -->
<bean id="beyableRankingSyncJob"
class="com.beyable.ranking.cronjob.BeyableRankingSyncJob"
parent="abstractJobPerformable">
<property name="beyableRankingService" ref="beyableRankingService"/>
<property name="baseSiteService" ref="baseSiteService"/>
</bean>
<!-- Product Search Strategy -->
<bean id="beyableProductSortStrategy"
class="com.beyable.ranking.strategies.BeyableProductSortStrategy">
<property name="beyableRankingService" ref="beyableRankingService"/>
</bean>
<!-- HTTP Client Configuration -->
<bean id="beyableHttpClient"
class="org.apache.http.impl.client.HttpClients"
factory-method="createDefault"/>
</beans>
Step 3: Build the Extension
cd hybris/bin/platform
. ./setantenv.sh
ant clean all
Step 4: Update System
ant updatesystem
Or via HAC:
- Navigate to: Platform > Update
- Click Update button
- Wait for completion
Implementation
Step 1: Constants
Create src/com/beyable/ranking/constants/BeyablerankingConstants.java:
package com.beyable.ranking.constants;
public final class BeyablerankingConstants extends GeneratedBeyablerankingConstants
{
public static final String EXTENSIONNAME = "beyableranking";
// Cookie Names
public static final String DEFAULT_SEGMENT_COOKIE = "b_segment";
// API Endpoints
public static final String API_ENDPOINT_PRODUCT_RANKING = "/productranking/{accountId}/{rankingId}";
public static final String API_ENDPOINT_PRODUCT_RANKING_TENANT = "/productranking/{accountId}/{rankingId}/{tenant}";
public static final String API_ENDPOINT_SEGMENTED_RANKING = "/segmented-productranking/{accountId}/{rankingId}/{segmentId}";
public static final String API_ENDPOINT_SEGMENTED_RANKING_TENANT = "/segmented-productranking/{accountId}/{rankingId}/{segmentId}/{tenant}";
// HTTP Headers
public static final String HEADER_SUBSCRIPTION_KEY = "Subscription-Key";
public static final String HEADER_ACCEPT = "Accept";
public static final String HEADER_ACCEPT_CSV = "text/csv";
// Default Values
public static final String DEFAULT_API_BASE_URL = "https://api.eu1.beyable.com";
public static final int DEFAULT_API_TIMEOUT = 30000;
private BeyablerankingConstants()
{
// Private constructor to prevent instantiation
}
}
Step 2: DAO Layer
Create src/com/beyable/ranking/dao/BeyableRankingDao.java:
package com.beyable.ranking.dao;
import com.beyable.ranking.model.BeyableRankingModel;
import de.hybris.platform.basecommerce.model.site.BaseSiteModel;
import de.hybris.platform.core.model.product.ProductModel;
import java.util.List;
public interface BeyableRankingDao
{
/**
* Find ranking for a specific product
*/
BeyableRankingModel findRanking(ProductModel product, String ruleId, String segmentId, BaseSiteModel baseSite);
/**
* Find all rankings for a rule and segment
*/
List<BeyableRankingModel> findAllRankings(String ruleId, String segmentId, BaseSiteModel baseSite);
/**
* Clear all rankings for a rule and segment
*/
void clearRankings(String ruleId, String segmentId, BaseSiteModel baseSite);
/**
* Save ranking
*/
void saveRanking(BeyableRankingModel ranking);
/**
* Save rankings in batch
*/
void saveRankings(List<BeyableRankingModel> rankings);
}
Create src/com/beyable/ranking/dao/impl/DefaultBeyableRankingDao.java:
package com.beyable.ranking.dao.impl;
import com.beyable.ranking.dao.BeyableRankingDao;
import com.beyable.ranking.model.BeyableRankingModel;
import de.hybris.platform.basecommerce.model.site.BaseSiteModel;
import de.hybris.platform.core.model.product.ProductModel;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.search.FlexibleSearchQuery;
import de.hybris.platform.servicelayer.search.FlexibleSearchService;
import de.hybris.platform.servicelayer.search.SearchResult;
import org.apache.log4j.Logger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DefaultBeyableRankingDao implements BeyableRankingDao
{
private static final Logger LOG = Logger.getLogger(DefaultBeyableRankingDao.class);
private FlexibleSearchService flexibleSearchService;
private ModelService modelService;
@Override
public BeyableRankingModel findRanking(ProductModel product, String ruleId, String segmentId, BaseSiteModel baseSite)
{
final StringBuilder query = new StringBuilder();
query.append("SELECT {pk} FROM {BeyableRanking} WHERE ");
query.append("{product} = ?product AND ");
query.append("{ruleId} = ?ruleId AND ");
query.append("{baseSite} = ?baseSite");
final Map<String, Object> params = new HashMap<>();
params.put("product", product);
params.put("ruleId", ruleId);
params.put("baseSite", baseSite);
if (segmentId != null)
{
query.append(" AND {segmentId} = ?segmentId");
params.put("segmentId", segmentId);
}
else
{
query.append(" AND {segmentId} IS NULL");
}
final FlexibleSearchQuery fsQuery = new FlexibleSearchQuery(query.toString());
fsQuery.addQueryParameters(params);
try
{
final SearchResult<BeyableRankingModel> result = flexibleSearchService.search(fsQuery);
return result.getResult().isEmpty() ? null : result.getResult().get(0);
}
catch (Exception e)
{
LOG.error("Error finding ranking: " + e.getMessage(), e);
return null;
}
}
@Override
public List<BeyableRankingModel> findAllRankings(String ruleId, String segmentId, BaseSiteModel baseSite)
{
final StringBuilder query = new StringBuilder();
query.append("SELECT {pk} FROM {BeyableRanking} WHERE ");
query.append("{ruleId} = ?ruleId AND ");
query.append("{baseSite} = ?baseSite");
final Map<String, Object> params = new HashMap<>();
params.put("ruleId", ruleId);
params.put("baseSite", baseSite);
if (segmentId != null)
{
query.append(" AND {segmentId} = ?segmentId");
params.put("segmentId", segmentId);
}
else
{
query.append(" AND {segmentId} IS NULL");
}
query.append(" ORDER BY {position} ASC");
final FlexibleSearchQuery fsQuery = new FlexibleSearchQuery(query.toString());
fsQuery.addQueryParameters(params);
try
{
final SearchResult<BeyableRankingModel> result = flexibleSearchService.search(fsQuery);
return result.getResult();
}
catch (Exception e)
{
LOG.error("Error finding rankings: " + e.getMessage(), e);
return List.of();
}
}
@Override
public void clearRankings(String ruleId, String segmentId, BaseSiteModel baseSite)
{
final List<BeyableRankingModel> rankings = findAllRankings(ruleId, segmentId, baseSite);
if (!rankings.isEmpty())
{
modelService.removeAll(rankings);
LOG.info(String.format("Cleared %d rankings for rule %s, segment %s",
rankings.size(), ruleId, segmentId));
}
}
@Override
public void saveRanking(BeyableRankingModel ranking)
{
modelService.save(ranking);
}
@Override
public void saveRankings(List<BeyableRankingModel> rankings)
{
modelService.saveAll(rankings);
}
// Getters and Setters
public void setFlexibleSearchService(FlexibleSearchService flexibleSearchService)
{
this.flexibleSearchService = flexibleSearchService;
}
public void setModelService(ModelService modelService)
{
this.modelService = modelService;
}
}
Step 3: API Service
Create src/com/beyable/ranking/service/BeyableApiService.java:
package com.beyable.ranking.service;
import com.beyable.ranking.dto.BeyableRankingData;
import com.beyable.ranking.model.BeyableConfigModel;
import java.util.List;
public interface BeyableApiService
{
/**
* Fetch rankings from BEYABLE API
*/
List<BeyableRankingData> fetchRankings(BeyableConfigModel config, String ruleId, String segmentId);
}
Create src/com/beyable/ranking/service/impl/DefaultBeyableApiService.java:
package com.beyable.ranking.service.impl;
import com.beyable.ranking.constants.BeyablerankingConstants;
import com.beyable.ranking.dto.BeyableRankingData;
import com.beyable.ranking.model.BeyableConfigModel;
import com.beyable.ranking.service.BeyableApiService;
import de.hybris.platform.servicelayer.config.ConfigurationService;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
public class DefaultBeyableApiService implements BeyableApiService
{
private static final Logger LOG = Logger.getLogger(DefaultBeyableApiService.class);
private ConfigurationService configurationService;
@Override
public List<BeyableRankingData> fetchRankings(BeyableConfigModel config, String ruleId, String segmentId)
{
final String endpoint = buildEndpoint(config, ruleId, segmentId);
final String url = config.getApiBaseUrl() + endpoint;
if (config.getDebugLogging())
{
LOG.debug("BEYABLE API Request: " + url);
}
try (CloseableHttpClient httpClient = HttpClients.createDefault())
{
final HttpGet request = new HttpGet(url);
request.setHeader(BeyablerankingConstants.HEADER_SUBSCRIPTION_KEY, config.getSubscriptionKey());
request.setHeader(BeyablerankingConstants.HEADER_ACCEPT, BeyablerankingConstants.HEADER_ACCEPT_CSV);
final HttpResponse response = httpClient.execute(request);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200)
{
LOG.error("BEYABLE API Error: Status " + statusCode);
throw new IOException("API returned status " + statusCode);
}
final String csvContent = EntityUtils.toString(response.getEntity());
final List<BeyableRankingData> rankings = parseCSV(csvContent);
if (config.getDebugLogging())
{
LOG.debug("BEYABLE API Response: " + rankings.size() + " rankings retrieved");
}
return rankings;
}
catch (Exception e)
{
LOG.error("Error fetching rankings from BEYABLE API: " + e.getMessage(), e);
throw new RuntimeException("Failed to fetch rankings", e);
}
}
private String buildEndpoint(BeyableConfigModel config, String ruleId, String segmentId)
{
final String accountId = config.getAccountId();
final String tenantId = config.getTenantId();
String endpoint;
if (segmentId != null && tenantId != null)
{
endpoint = BeyablerankingConstants.API_ENDPOINT_SEGMENTED_RANKING_TENANT
.replace("{accountId}", accountId)
.replace("{rankingId}", ruleId)
.replace("{segmentId}", segmentId)
.replace("{tenant}", tenantId);
}
else if (segmentId != null)
{
endpoint = BeyablerankingConstants.API_ENDPOINT_SEGMENTED_RANKING
.replace("{accountId}", accountId)
.replace("{rankingId}", ruleId)
.replace("{segmentId}", segmentId);
}
else if (tenantId != null)
{
endpoint = BeyablerankingConstants.API_ENDPOINT_PRODUCT_RANKING_TENANT
.replace("{accountId}", accountId)
.replace("{rankingId}", ruleId)
.replace("{tenant}", tenantId);
}
else
{
endpoint = BeyablerankingConstants.API_ENDPOINT_PRODUCT_RANKING
.replace("{accountId}", accountId)
.replace("{rankingId}", ruleId);
}
return endpoint;
}
private List<BeyableRankingData> parseCSV(String csvContent) throws IOException
{
final List<BeyableRankingData> rankings = new ArrayList<>();
try (CSVParser parser = CSVParser.parse(new StringReader(csvContent),
CSVFormat.DEFAULT.withFirstRecordAsHeader()))
{
for (CSVRecord record : parser)
{
final BeyableRankingData data = new BeyableRankingData();
data.setProductId(record.get("ProductId"));
data.setAbTestGroup(record.get("abTestGroup"));
data.setPosition(Integer.parseInt(record.get("Position")));
rankings.add(data);
}
}
return rankings;
}
// Getters and Setters
public void setConfigurationService(ConfigurationService configurationService)
{
this.configurationService = configurationService;
}
}
Step 4: Ranking Service
Create src/com/beyable/ranking/service/BeyableRankingService.java:
package com.beyable.ranking.service;
import com.beyable.ranking.model.BeyableConfigModel;
import de.hybris.platform.basecommerce.model.site.BaseSiteModel;
import de.hybris.platform.core.model.product.ProductModel;
import java.util.List;
public interface BeyableRankingService
{
/**
* Sync rankings from BEYABLE API
*/
int syncRankings(BaseSiteModel baseSite);
/**
* Get configuration for base site
*/
BeyableConfigModel getConfiguration(BaseSiteModel baseSite);
/**
* Apply rankings to product list
*/
List<ProductModel> applyRankings(List<ProductModel> products, String segmentId, BaseSiteModel baseSite);
/**
* Get visitor segment from cookies
*/
String getVisitorSegment();
/**
* Check if BEYABLE is enabled for site
*/
boolean isEnabled(BaseSiteModel baseSite);
}
Create src/com/beyable/ranking/service/impl/DefaultBeyableRankingService.java:
package com.beyable.ranking.service.impl;
import com.beyable.ranking.constants.BeyablerankingConstants;
import com.beyable.ranking.dao.BeyableRankingDao;
import com.beyable.ranking.dto.BeyableRankingData;
import com.beyable.ranking.model.BeyableConfigModel;
import com.beyable.ranking.model.BeyableRankingModel;
import com.beyable.ranking.service.BeyableApiService;
import com.beyable.ranking.service.BeyableRankingService;
import de.hybris.platform.basecommerce.model.site.BaseSiteModel;
import de.hybris.platform.core.model.product.ProductModel;
import de.hybris.platform.product.ProductService;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.site.BaseSiteService;
import org.apache.log4j.Logger;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;
public class DefaultBeyableRankingService implements BeyableRankingService
{
private static final Logger LOG = Logger.getLogger(DefaultBeyableRankingService.class);
private BeyableRankingDao beyableRankingDao;
private BeyableApiService beyableApiService;
private ModelService modelService;
private BaseSiteService baseSiteService;
private ProductService productService;
@Override
public int syncRankings(BaseSiteModel baseSite)
{
final BeyableConfigModel config = getConfiguration(baseSite);
if (config == null || !config.getEnabled() || !config.getSyncEnabled())
{
LOG.warn("BEYABLE sync is disabled for site: " + baseSite.getUid());
return 0;
}
LOG.info("Starting BEYABLE ranking sync for site: " + baseSite.getUid());
int totalSynced = 0;
try
{
// Sync default rankings
final String defaultRankingId = config.getDefaultRankingId();
if (defaultRankingId != null)
{
totalSynced += syncRankingsForRule(config, defaultRankingId, null, baseSite);
}
// Sync segmented rankings if enabled
if (config.getUseSegmentation() && config.getSegmentedRankingId() != null)
{
// Note: You might want to sync multiple segments
// For now, we'll just sync the primary segmented rule
final List<String> segments = getSegmentsToSync();
for (String segmentId : segments)
{
totalSynced += syncRankingsForRule(config, config.getSegmentedRankingId(), segmentId, baseSite);
}
}
LOG.info("BEYABLE sync completed. Total rankings synced: " + totalSynced);
}
catch (Exception e)
{
LOG.error("Error during BEYABLE sync: " + e.getMessage(), e);
throw new RuntimeException("Sync failed", e);
}
return totalSynced;
}
private int syncRankingsForRule(BeyableConfigModel config, String ruleId, String segmentId, BaseSiteModel baseSite)
{
LOG.info(String.format("Syncing rankings for rule: %s, segment: %s", ruleId, segmentId != null ? segmentId : "default"));
try
{
// Fetch rankings from API
final List<BeyableRankingData> rankingData = beyableApiService.fetchRankings(config, ruleId, segmentId);
LOG.info("Fetched " + rankingData.size() + " rankings from BEYABLE API");
// Clear old rankings
beyableRankingDao.clearRankings(ruleId, segmentId, baseSite);
// Store new rankings
final List<BeyableRankingModel> rankings = new ArrayList<>();
final Date now = new Date();
for (BeyableRankingData data : rankingData)
{
try
{
final ProductModel product = productService.getProductForCode(data.getProductId());
final BeyableRankingModel ranking = modelService.create(BeyableRankingModel.class);
ranking.setProduct(product);
ranking.setPosition(data.getPosition());
ranking.setAbTestGroup(data.getAbTestGroup());
ranking.setSegmentId(segmentId);
ranking.setRuleId(ruleId);
ranking.setBaseSite(baseSite);
ranking.setLastSyncDate(now);
rankings.add(ranking);
}
catch (Exception e)
{
LOG.warn("Product not found: " + data.getProductId());
}
}
// Batch save
beyableRankingDao.saveRankings(rankings);
LOG.info("Stored " + rankings.size() + " rankings");
return rankings.size();
}
catch (Exception e)
{
LOG.error("Error syncing rankings for rule " + ruleId + ": " + e.getMessage(), e);
return 0;
}
}
private List<String> getSegmentsToSync()
{
// Customize based on your segmentation strategy
return Arrays.asList("premium", "standard", "vip");
}
@Override
public BeyableConfigModel getConfiguration(BaseSiteModel baseSite)
{
final String query = "SELECT {pk} FROM {BeyableConfig} WHERE {baseSite} = ?baseSite";
final Map<String, Object> params = new HashMap<>();
params.put("baseSite", baseSite);
try
{
return flexibleSearchService.searchUnique(new FlexibleSearchQuery(query, params));
}
catch (Exception e)
{
LOG.warn("No BEYABLE configuration found for site: " + baseSite.getUid());
return null;
}
}
@Override
public List<ProductModel> applyRankings(List<ProductModel> products, String segmentId, BaseSiteModel baseSite)
{
final BeyableConfigModel config = getConfiguration(baseSite);
if (config == null || !config.getEnabled())
{
return products;
}
// Get rankings
final String ruleId = (segmentId != null && config.getSegmentedRankingId() != null)
? config.getSegmentedRankingId()
: config.getDefaultRankingId();
final List<BeyableRankingModel> rankings = beyableRankingDao.findAllRankings(ruleId, segmentId, baseSite);
if (rankings.isEmpty())
{
return products;
}
// Create ranking map
final Map<String, Integer> rankingMap = new HashMap<>();
for (BeyableRankingModel ranking : rankings)
{
rankingMap.put(ranking.getProduct().getCode(), ranking.getPosition());
}
// Split products into ranked and unranked
final List<ProductModel> rankedProducts = new ArrayList<>();
final List<ProductModel> unrankedProducts = new ArrayList<>();
for (ProductModel product : products)
{
if (rankingMap.containsKey(product.getCode()))
{
rankedProducts.add(product);
}
else
{
unrankedProducts.add(product);
}
}
// Sort ranked products by position
rankedProducts.sort(Comparator.comparingInt(p -> rankingMap.get(p.getCode())));
// Combine: ranked first, then unranked
final List<ProductModel> result = new ArrayList<>(rankedProducts);
result.addAll(unrankedProducts);
if (config.getDebugLogging())
{
LOG.debug(String.format("Applied rankings: %d ranked, %d unranked",
rankedProducts.size(), unrankedProducts.size()));
}
return result;
}
@Override
public String getVisitorSegment()
{
final BaseSiteModel currentSite = baseSiteService.getCurrentBaseSite();
final BeyableConfigModel config = getConfiguration(currentSite);
if (config == null || !config.getUseSegmentation())
{
return null;
}
final ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null)
{
final HttpServletRequest request = attributes.getRequest();
final Cookie[] cookies = request.getCookies();
if (cookies != null)
{
final String cookieName = config.getSegmentCookieName() != null
? config.getSegmentCookieName()
: BeyablerankingConstants.DEFAULT_SEGMENT_COOKIE;
for (Cookie cookie : cookies)
{
if (cookieName.equals(cookie.getName()))
{
return cookie.getValue();
}
}
}
}
return null;
}
@Override
public boolean isEnabled(BaseSiteModel baseSite)
{
final BeyableConfigModel config = getConfiguration(baseSite);
return config != null && config.getEnabled();
}
// Getters and Setters
public void setBeyableRankingDao(BeyableRankingDao beyableRankingDao)
{
this.beyableRankingDao = beyableRankingDao;
}
public void setBeyableApiService(BeyableApiService beyableApiService)
{
this.beyableApiService = beyableApiService;
}
public void setModelService(ModelService modelService)
{
this.modelService = modelService;
}
public void setBaseSiteService(BaseSiteService baseSiteService)
{
this.baseSiteService = baseSiteService;
}
public void setProductService(ProductService productService)
{
this.productService = productService;
}
}
Step 5: DTO Class
Create src/com/beyable/ranking/dto/BeyableRankingData.java:
package com.beyable.ranking.dto;
public class BeyableRankingData
{
private String productId;
private String abTestGroup;
private Integer position;
// Getters and Setters
public String getProductId()
{
return productId;
}
public void setProductId(String productId)
{
this.productId = productId;
}
public String getAbTestGroup()
{
return abTestGroup;
}
public void setAbTestGroup(String abTestGroup)
{
this.abTestGroup = abTestGroup;
}
public Integer getPosition()
{
return position;
}
public void setPosition(Integer position)
{
this.position = position;
}
}
CronJob Configuration
Step 1: CronJob Implementation
Create src/com/beyable/ranking/cronjob/BeyableRankingSyncJob.java:
package com.beyable.ranking.cronjob.
;
import com.beyable.ranking.service.BeyableRankingService;
import de.hybris.platform.basecommerce.model.site.BaseSiteModel;
import de.hybris.platform.cronjob.enums.CronJobResult;
import de.hybris.platform.cronjob.enums.CronJobStatus;
import de.hybris.platform.cronjob.model.CronJobModel;
import de.hybris.platform.servicelayer.cronjob.AbstractJobPerformable;
import de.hybris.platform.servicelayer.cronjob.PerformResult;
import de.hybris.platform.site.BaseSiteService;
import org.apache.log4j.Logger;
import java.util.Collection;
public class BeyableRankingSyncJob extends AbstractJobPerformable<CronJobModel>
{
private static final Logger LOG = Logger.getLogger(BeyableRankingSyncJob.class);
private BeyableRankingService beyableRankingService;
private BaseSiteService baseSiteService;
@Override
public PerformResult perform(CronJobModel cronJob)
{
LOG.info("Starting BEYABLE ranking synchronization");
try
{
final Collection<BaseSiteModel> baseSites = baseSiteService.getAllBaseSites();
int totalSynced = 0;
for (BaseSiteModel baseSite : baseSites)
{
if (beyableRankingService.isEnabled(baseSite))
{
LOG.info("Syncing rankings for site: " + baseSite.getUid());
totalSynced += beyableRankingService.syncRankings(baseSite);
}
}
LOG.info("BEYABLE sync completed successfully. Total rankings synced: " + totalSynced);
return new PerformResult(CronJobResult.SUCCESS, CronJobStatus.FINISHED);
}
catch (Exception e)
{
LOG.error("Error during BEYABLE sync: " + e.getMessage(), e);
return new PerformResult(CronJobResult.ERROR, CronJobStatus.ABORTED);
}
}
// Getters and Setters
public void setBeyableRankingService(BeyableRankingService beyableRankingService)
{
this.beyableRankingService = beyableRankingService;
}
public void setBaseSiteService(BaseSiteService baseSiteService)
{
this.baseSiteService = baseSiteService;
}
}
Step 2: Create CronJob via Backoffice
- Navigate to System > Background Processes > Cronjobs
- Click "+" to create new cronjob
- Configure:
- Code:
beyableRankingSyncJob - Job: Select
beyableRankingSyncJob - Session Language: en
- Code:
- Click Save
Step 3: Create Trigger
- In the cronjob, go to Time Schedule tab
- Click "+" to add trigger
- Configure:
- Active: Yes
- Cron Expression:
0 0 */2 * * ?(every 2 hours)
- Click Save
Step 4: Alternative: ImpEx Script
Create resources/impex/essentialdata-beyable-cronjobs.impex:
# BEYABLE Ranking Sync CronJob
INSERT_UPDATE ServicelayerJob; code[unique=true]; springId
; beyableRankingSyncJob ; beyableRankingSyncJob
INSERT_UPDATE CronJob; code[unique=true]; job(code); sessionLanguage(isocode)
; beyableRankingSyncCronJob ; beyableRankingSyncJob ; en
INSERT_UPDATE Trigger; cronjob(code)[unique=true]; cronExpression
; beyableRankingSyncCronJob ; 0 0 */2 * * ?
Load via HAC:
- Navigate to Console > Impex Import
- Paste the ImpEx
- Click Import content
Frontend Integration
Step 1: Product Search Populator
Create src/com/beyable/ranking/populators/BeyableProductSearchPagePopulator.java:
package com.beyable.ranking.populators;
import com.beyable.ranking.service.BeyableRankingService;
import de.hybris.platform.commercefacades.product.data.ProductData;
import de.hybris.platform.commercefacades.search.data.SearchStateData;
import de.hybris.platform.commerceservices.search.facetdata.ProductSearchPageData;
import de.hybris.platform.converters.Populator;
import de.hybris.platform.core.model.product.ProductModel;
import de.hybris.platform.product.ProductService;
import de.hybris.platform.servicelayer.dto.converter.ConversionException;
import de.hybris.platform.site.BaseSiteService;
import org.apache.log4j.Logger;
import java.util.ArrayList;
import java.util.List;
public class BeyableProductSearchPagePopulator implements Populator<ProductSearchPageData<SearchStateData, ProductData>, ProductSearchPageData<SearchStateData, ProductData>>
{
private static final Logger LOG = Logger.getLogger(BeyableProductSearchPagePopulator.class);
private BeyableRankingService beyableRankingService;
private ProductService productService;
private BaseSiteService baseSiteService;
@Override
public void populate(ProductSearchPageData<SearchStateData, ProductData> source,
ProductSearchPageData<SearchStateData, ProductData> target) throws ConversionException
{
if (!beyableRankingService.isEnabled(baseSiteService.getCurrentBaseSite()))
{
return;
}
final String segmentId = beyableRankingService.getVisitorSegment();
final List<ProductData> products = source.getResults();
if (products == null || products.isEmpty())
{
return;
}
try
{
// Convert ProductData to ProductModel
final List<ProductModel> productModels = new ArrayList<>();
for (ProductData productData : products)
{
final ProductModel product = productService.getProductForCode(productData.getCode());
productModels.add(product);
}
// Apply rankings
final List<ProductModel> rankedProducts = beyableRankingService.applyRankings(
productModels,
segmentId,
baseSiteService.getCurrentBaseSite()
);
// Convert back to ProductData and update target
// Note: This is a simplified approach. In production, you'd need proper conversion
final List<ProductData> rankedProductData = new ArrayList<>();
for (ProductModel productModel : rankedProducts)
{
for (ProductData productData : products)
{
if (productData.getCode().equals(productModel.getCode()))
{
rankedProductData.add(productData);
break;
}
}
}
target.setResults(rankedProductData);
if (LOG.isDebugEnabled())
{
LOG.debug("BEYABLE rankings applied to " + rankedProductData.size() + " products");
}
}
catch (Exception e)
{
LOG.error("Error applying BEYABLE rankings: " + e.getMessage(), e);
// Don't fail the search, just log the error
}
}
// Getters and Setters
public void setBeyableRankingService(BeyableRankingService beyableRankingService)
{
this.beyableRankingService = beyableRankingService;
}
public void setProductService(ProductService productService)
{
this.productService = productService;
}
public void setBaseSiteService(BaseSiteService baseSiteService)
{
this.baseSiteService = baseSiteService;
}
}
Register the populator in beyableranking-spring.xml:
<!-- Add to beyableranking-spring.xml -->
<bean id="beyableProductSearchPagePopulator"
class="com.beyable.ranking.populators.BeyableProductSearchPagePopulator">
<property name="beyableRankingService" ref="beyableRankingService"/>
<property name="productService" ref="productService"/>
<property name="baseSiteService" ref="baseSiteService"/>
</bean>
<!-- Modify commerceSearchPageDataConverter to include our populator -->
<bean parent="modifyPopulatorList">
<property name="list" ref="commerceSearchPageDataConverter"/>
<property name="add" ref="beyableProductSearchPagePopulator"/>
</bean>
Step 2: Tracking Tag
Create acceleratoraddon/web/webroot/WEB-INF/tags/beyable/tracking.tag:
<%@ tag body-content="empty" trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%-- BEYABLE Tracking Script --%>
<c:if test="${beyableEnabled}">
<script type="text/javascript">
// BEYABLE integration - segment detection
(function() {
var segmentCookieName = '${beyableSegmentCookieName}';
// Helper to read cookie
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// Log segment for debugging (if debug mode is enabled)
var segment = getCookie(segmentCookieName);
if (segment && console && console.log) {
console.log('BEYABLE Segment:', segment);
}
})();
</script>
</c:if>
Include in your footer:
<%@ taglib prefix="beyable" tagdir="/WEB-INF/tags/beyable" %>
<!-- Add to footer template -->
<beyable:tracking/>
Testing
Step 1: Configuration Test
Create Configuration via ImpEx:
# Create BEYABLE Configuration
INSERT_UPDATE BeyableConfig; baseSite(uid)[unique=true]; accountId; subscriptionKey; apiBaseUrl; defaultRankingId; enabled
; electronics ; YOUR_ACCOUNT_ID ; YOUR_SUBSCRIPTION_KEY ; https://api.eu1.beyable.com ; YOUR_RANKING_ID ; true
# Update with additional settings
UPDATE BeyableConfig; baseSite(uid)[unique=true]; useSegmentation; applyToCategories; applyToSearch; segmentCookieName
; electronics ; true ; true ; true ; b_segment
Load via HAC:
- Console > Impex Import
- Paste the ImpEx
- Click Import content
Step 2: Manual CronJob Test
Via Backoffice:
- Navigate to System > Background Processes > Cronjobs
- Find
beyableRankingSyncCronJob - Click Start
- Monitor logs in
hybris/log/tomcat/console.log
Via HAC:
- Navigate to Monitoring > Cronjobs
- Find
beyableRankingSyncCronJob - Click Run
Step 3: Verify Data Storage
Via Flexible Search (HAC):
SELECT {pk}, {product}, {position}, {ruleId}, {segmentId}
FROM {BeyableRanking}
Via Backoffice:
- Navigate to System > Types
- Search for
BeyableRanking - View instances
Step 4: Frontend Test
Clear Cache:
- HAC > Platform > Cache > Clear All
Test Category Page:
- Visit a category page
- Verify products are ordered by BEYABLE rankings
- Use browser dev tools to check segment cookie
Test Search:
- Perform a product search
- Verify ranked results
Step 5: Segment Test
Set Test Cookie (Browser Console):
document.cookie = "b_segment=premium; path=/";
location.reload();
Clear Cookie:
document.cookie = "b_segment=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/";
Deployment
Production Deployment Checklist
Pre-Deployment
Code Review:
- Review all custom code
- Ensure error handling is robust
- Verify logging is appropriate
Backup:
- Export type system (HAC > Platform > Type System)
- Backup database
- Document current configuration
Build:
cd hybris/bin/platform
. ./setantenv.sh
ant clean all
Deployment Steps
Stop Server:
./hybrisserver.sh stopDeploy Code:
- Copy extension to
hybris/bin/custom/ - Update
localextensions.xml
- Copy extension to
Update System:
ant updatesystemStart Server:
./hybrisserver.sh startInitialize Configuration:
- Load initial ImpEx (configuration, cronjob)
- Via HAC or Backoffice
Schedule CronJob:
- Configure trigger
- Run initial sync
Clear Caches:
- HAC > Platform > Cache > Clear All
- HAC > Monitoring > JVM > Clear Caches
Post-Deployment
Verify:
- Check storefront functionality
- Verify rankings are applied
- Test segment detection
- Review logs for errors
Monitor:
- Monitor cronjob execution
- Check database record counts
- Monitor page performance
- Track error rates
Documentation:
- Document configuration
- Create runbook
- Train support team
Monitoring & Troubleshooting
Log Files
Monitor these log files:
Application Logs:
hybris/log/tomcat/console.log- Contains all application logs
Cronjob Logs:
- View in Backoffice: System > Background Processes > Cronjobs > Logs
- Or HAC: Monitoring > Cronjobs
Common Issues
1. API Authentication Failures
Symptom: HTTP 401 errors in logs
Solution:
- Verify Subscription-Key in BeyableConfig
- Test API manually:
curl -H "Subscription-Key: YOUR_KEY" \
"https://api.eu1.beyable.com/productranking/ACCOUNT_ID/RANKING_ID" - Check configuration in Backoffice
2. Rankings Not Applied
Symptom: Products not sorted by BEYABLE ranking
Solution:
- Verify BEYABLE is enabled in configuration
- Check database for ranking records:
SELECT COUNT(*) FROM {BeyableRanking} - Verify cronjob execution in Backoffice
- Enable debug logging in configuration
- Clear all caches (HAC > Platform > Cache)
3. CronJob Failures
Symptom: Cronjob shows error status
Solution:
- Review cronjob logs in Backoffice
- Check application logs for stack traces
- Verify API credentials
- Test API connection manually
- Check for network/firewall issues
4. Performance Issues
Symptom: Slow category/search pages
Solution:
- Enable caching for search results
- Index database properly (rankings table)
- Reduce ranking object count
- Consider Solr integration for large catalogs
- Monitor query performance
5. Segment Not Detected
Symptom: Segmented rankings not working
Solution:
- Verify cookie name matches configuration
- Check browser cookies in dev tools
- Test with manual cookie
- Verify tracking tag is loaded
- Check cookie domain settings
Debug Commands
Test Service via Groovy Script (HAC):
import com.beyable.ranking.service.BeyableRankingService
import de.hybris.platform.site.BaseSiteService
def beyableRankingService = spring.getBean("beyableRankingService", BeyableRankingService.class)
def baseSiteService = spring.getBean("baseSiteService", BaseSiteService.class)
def baseSite = baseSiteService.getBaseSiteForUID("electronics")
def config = beyableRankingService.getConfiguration(baseSite)
println "Config found: " + (config != null)
println "Enabled: " + (config != null ? config.enabled : "N/A")
println "Account ID: " + (config != null ? config.accountId : "N/A")
// Test sync
def result = beyableRankingService.syncRankings(baseSite)
println "Synced: " + result + " rankings"
Performance Optimization
1. Database Indexing
Ensure proper indexes (defined in items.xml):
<indexes>
<index name="productRankingIdx">
<key attribute="product"/>
<key attribute="baseSite"/>
<key attribute="segmentId"/>
</index>
</indexes>
2. Caching Strategy
Implement caching in service layer:
@Cacheable(value = "beyableRankingsCache", key = "#ruleId + '_' + #segmentId + '_' + #baseSite.uid")
public List<BeyableRankingModel> findAllRankings(String ruleId, String segmentId, BaseSiteModel baseSite)
{
// Implementation
}
Configure in Spring:
<cache:annotation-driven/>
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
<property name="name" value="beyableRankingsCache"/>
</bean>
</set>
</property>
</bean>
3. Reduce Object Count
Only sync rankings for active products:
private List<BeyableRankingData> filterActiveProducts(List<BeyableRankingData> rankings)
{
return rankings.stream()
.filter(data -> {
try {
ProductModel product = productService.getProductForCode(data.getProductId());
return product != null && !product.getVariantType().equals(VariantType.valueOf("VARIANT"));
} catch (Exception e) {
return false;
}
})
.collect(Collectors.toList());
}
4. Async Processing
For large catalogs, consider async processing:
@Async
public CompletableFuture<Integer> syncRankingsAsync(BaseSiteModel baseSite)
{
return CompletableFuture.completedFuture(syncRankings(baseSite));
}
5. Solr Integration
For optimal performance with large catalogs, integrate with Solr:
- Add ranking position to Solr schema
- Index rankings during sync
- Use Solr sorting instead of post-processing
Appendix
A. API Endpoint Reference
| Endpoint | Use Case |
|---|---|
/productranking/{accountId}/{rankingId} | Default tenant, no segmentation |
/productranking/{accountId}/{rankingId}/{tenant} | Specific tenant, no segmentation |
/segmented-productranking/{accountId}/{rankingId}/{segmentId} | Default tenant, with segmentation |
/segmented-productranking/{accountId}/{rankingId}/{segmentId}/{tenant} | Full configuration |
B. Backoffice Navigation
| Task | Navigation Path |
|---|---|
| Configure BEYABLE | System > Types > BeyableConfig |
| View Rankings | System > Types > BeyableRanking |
| Manage CronJobs | System > Background Processes > Cronjobs |
| View Logs | System > Background Processes > Logs |
C. Troubleshooting Checklist
- BEYABLE enabled in configuration
- Account ID and Subscription Key configured
- Ranking rule IDs configured
- Extension added to localextensions.xml
- System updated (ant updatesystem)
- CronJob created and scheduled
- Database records created
- Rankings applied to product listings
- Logs free of errors
- Segment cookie detected (if using segmentation)
Support
For additional assistance:
- Extension Issues: Check application logs and HAC
- BEYABLE API Issues: Contact your BEYABLE account manager
- SAP Commerce Issues: Refer to SAP Commerce documentation or SAP support