Integrating Coveo Search with Adobe Experience Manager (AEM): A Deep Dive
Coveo is an AI powered search and relevance platform that pairs naturally with AEM. Rather than relying on AEM’s built-in query builder for…

Integrating Coveo Search with Adobe Experience Manager (AEM): A Deep Dive
Coveo is an AI powered search and relevance platform that pairs naturally with AEM. Rather than relying on AEM’s built-in query builder for full text search, teams use Coveo to deliver faceted, personalized, and entitlement aware search experiences. This post walks through all the moving parts of a production grade AEM + Coveo integration: OSGi services, token generation, search payloads, sitemap metadata, and the Coveo JSUI frontend layer.
Architecture Overview
A typical AEM + Coveo setup involves two integration modes that often coexist:
-
Direct Coveo REST API integration: AEM backend services call Coveo’s
/rest/search/v2endpoint directly, using a server-side API key. This is ideal for server-rendered pages and batch data lookups (e.g., scheduled jobs that pull product catalog data from Coveo). -
Coveo JSUI (JavaScript UI) integration: The front end initializes Coveo’s JavaScript framework using a short lived search token generated by AEM on every page load. This token is user specific and carries entitlement groups and filter expressions, enabling content gating without exposing your API key to the browser.
Both modes share the same OSGi configuration and token infrastructure.
Part 1: OSGi Configuration
Global Search Settings
Coveo’s core connectivity parameters (endpoint URL, organization ID, API key) live in AEM’s JCR under an /apps/settings/wcm/properties style node. A CoveoSearchGlobalService reads these at activation time:
@Reference
CoveoSearchGlobalService coveoSearchGlobalService;
@Activate
protected void activate() {
coveoSearchEndpoint = coveoSearchGlobalService.getSearchEndpointUrl();
coveoOrganizationId = coveoSearchGlobalService.getOrganizationId();
// coveoApiKey is read separately from a secured JCR node
}
Storing credentials in JCR (encrypted) rather than OSGi config values keeps them out of source control and allows environment-specific overrides without a code deployment.
API Gateway Service Configuration
When Coveo is fronted by an API gateway (e.g., for centralized rate limiting or audit logging), a separate OSGi config provides the gateway credentials:
@ObjectClassDefinition(
name = "Search Gateway Service Configuration",
description = "OAuth2 credentials and endpoints for the API gateway layer"
)
public @interface SearchGatewayServiceConfig {
@AttributeDefinition(name = "Access Token URL", type = AttributeType.STRING)
String accessTokenUrl() default "https://your-gateway.example.com/v1/auth/token";
@AttributeDefinition(name = "Client ID", type = AttributeType.STRING)
String clientId() default ""; // stored encrypted in production
@AttributeDefinition(name = "Client Secret", type = AttributeType.STRING)
String clientSecret() default ""; // stored encrypted in production
@AttributeDefinition(name = "Cache Auth Token", type = AttributeType.BOOLEAN)
boolean useCache4AuthenticationToken() default true;
@AttributeDefinition(name = "Cache Search Token in Session", type = AttributeType.BOOLEAN)
boolean useSession4SearchToken() default true;
@AttributeDefinition(name = "Coveo Get-Content URL", type = AttributeType.STRING)
String coveoGetContentUrl() default "https://your-gateway.example.com/csearch/v1/get-contents";
}
Key points:
1- useCache4AuthenticationToken: stores the OAuth2 bearer token in a JCS cache region to avoid redundant token calls on every request.
2- useSession4SearchToken: stores the user’s Coveo search token in the HTTP session so it is only regenerated when it expires (typically 1 hour).
Search Token Service Configuration
A second config governs the token payload itself:
@ObjectClassDefinition(
name = "Search Token Service",
description = "Controls entitlement groups injected into every Coveo search token"
)
public @interface SearchTokenServiceConfig {
@AttributeDefinition(
name = "Environment-specific entitlement groups",
description = "Groups added to every token from this AEM environment (e.g. env:staging)"
)
String[] environmentSpecificEntitlements() default {"env:prod"};
@AttributeDefinition(
name = "Supported Role Names",
description = "Role names whose presence in the user profile adds a corresponding group to the token"
)
String[] supportedRoles() default {};
}
Part 2: Authentication and Token Flow
OAuth2 Auth Token (API Gateway Access)
If Coveo is called through an API gateway, you first need a bearer token from that gateway using the client credentials grant:
@Component(immediate = true, service = {SearchGatewayService.class})
@Designate(ocd = SearchGatewayServiceConfig.class)
public class SearchGatewayServiceImpl implements SearchGatewayService {
private static final String TOKEN_CACHE_REGION = "gatewaytoken";
private static final String TOKEN_CACHE_KEY = "gateway_auth_token";
private String authorizationHeader;
private SearchGatewayServiceConfig config;
@Activate @Modified
public void activate(SearchGatewayServiceConfig config) {
// Build "Basic Base64(clientId:clientSecret)" header
String clientId = decrypt(config.clientId());
String clientSecret = decrypt(config.clientSecret());
this.authorizationHeader = createBasicAuthHeader(clientId, clientSecret);
this.config = config;
}
@Override
public AuthTokenBean getAuthorizationToken(boolean forceRefresh) {
if (!forceRefresh && config.useCache4AuthenticationToken()) {
AuthTokenBean cached = JcsCacheUtil.getInstance()
.get(TOKEN_CACHE_KEY, TOKEN_CACHE_REGION);
if (cached != null) return cached;
}
RestRequest req = new RestRequest();
req.setUrl(config.accessTokenUrl());
req.setHeader("Content-Type", "application/x-www-form-urlencoded");
req.setHeader("Authorization", authorizationHeader);
req.setParam("grant_type", "client_credentials");
RestResponse resp = new RestHttpClientImpl().executePost(req);
AuthTokenBean token = new Gson().fromJson(
resp.getJSONResponse().toJSONString(), AuthTokenBean.class);
if (token != null && config.useCache4AuthenticationToken()) {
JcsCacheUtil.getInstance().put(TOKEN_CACHE_KEY, token, TOKEN_CACHE_REGION);
}
return token;
}
}
AuthTokenBean is a simple POJO:
public class AuthTokenBean {
@SerializedName("access_token") private String accessToken;
@SerializedName("token_type") private String tokenType;
@SerializedName("expires_in") private String expiresIn;
@SerializedName("ext_expires_in") private String extExpiresIn;
// getters/setters omitted
}
Coveo Search Token (User-Specific, JSUI)
The search token is what the Coveo JavaScript UI uses. It is generated by POSTing to Coveo’s /token endpoint with the user’s identity, entitlement groups, and a filter expression. This is the heart of content gating.
private RestRequest buildSearchTokenRequest(SearchTokenPayload payload) {
RestRequest req = new RestRequest();
req.setUrl(coveoSearchTokenEndpoint + "/token");
req.setHeader("Content-Type", "application/json");
req.setHeader("Authorization", "Bearer " + coveoApiKey); // server-side key
JSONObject body = new JSONObject();
body.put("searchHub", payload.getSearchHub());
body.put("validFor", payload.getValidFor()); // token TTL in seconds
body.put("userIds", toJson(payload.getUserIds()));
body.put("userGroups", toJson(payload.getUserGroups()));
body.put("filter", payload.getFilter());
req.setParamObject(body);
return req;
}
SearchTokenBean carries the result back to the caller and into the HTTP session:
public class SearchTokenBean {
private String token;
private String organizationId;
private String searchHub;
private boolean isLoggedIn;
private boolean isRdcPrivileged; // example: resource-download center membership
private boolean canDownload;
private boolean canBookmark;
private boolean canViewDetails;
private boolean isCompetitor;
private Long expirationTime; // wall-clock ms, set by AEM (not Coveo)
// getters/setters omitted
}
Session caching with a TTL check:
@Override
public SearchTokenBean getSearchToken(HttpRequest request, boolean forceRefresh) {
if (!forceRefresh && config.useSession4SearchToken()) {
Object cached = request.getSession().getAttribute(SEARCH_TOKEN_SESSION_KEY);
if (cached instanceof SearchTokenBean) {
SearchTokenBean bean = (SearchTokenBean) cached;
if (System.currentTimeMillis() < bean.getExpirationTime()) {
return bean; // cache hit
}
}
}
// generate fresh token ...
bean.setExpirationTime(System.currentTimeMillis() + ONE_HOUR_MS);
request.getSession().setAttribute(SEARCH_TOKEN_SESSION_KEY, bean);
return bean;
}
Part 3: Entitlement Groups and Content Gating
The most powerful aspect of AEM + Coveo is the ability to gate content at query time. Instead of filtering results after the fact, you pass user group memberships to Coveo when generating the token. Coveo’s index then enforces visibility rules so users only see documents their groups are allowed to see.
The SearchTokenServiceImpl builds the group set from multiple sources:
private Set<String> buildEntitlementGroups(EntitledUser user) {
Set<String> groups = new HashSet<>();
// Always-present baseline groups
groups.add("acl:public:anonymous");
groups.add("acl:public:all");
groups.addAll(environmentSpecificEntitlements); // e.g. "env:staging"
if (user == null) return groups; // anonymous user stops here
// Registered-user groups
if (user.isPartner()) {
groups.add("program:partner");
groups.add("user:" + user.getEnterpriseId());
}
if (user.isInternal()) {
groups.add("role:internal:all-employees");
if (user.isBlueBadge()) {
groups.add("role:internal:full-time");
}
}
if (user.isCompetitor()) {
groups.add("role:competitor");
}
// ... additional program memberships, distribution roles, etc.
groups.removeIf(StringUtils::isEmpty);
return groups;
}
Partner Filter Expressions
Beyond groups, filter expressions restrict which indexed fields a user can see. For partner portals this is common different partner tiers see different documents:
// In Coveo query syntax, this says:
// "Exclude any document that targets a partner tier UNLESS it matches the user's tier"
private String buildPartnerFilter(String userSpecialty, String userRoleTier) {
String filter = "NOT @partnertarget"; // exclude docs targeting specific partners...
if (StringUtils.isNotEmpty(userSpecialty)) {
// ...unless the user has this specialty
filter += " OR @partnertarget==(\"" + userSpecialty + "\")";
}
if (StringUtils.isNotEmpty(userRoleTier)) {
// ...or this role tier (also include "All Partners" catch-all)
filter += " OR @partnertarget==(\"" + userRoleTier + "\",\"All Partners\")";
}
return filter;
}
This filter is injected into the token payload, so Coveo applies it server-side at query time.
Part 4: Querying Coveo from AEM (Search Service)
The Service Interface
public interface CoveoSearchService {
SearchResults search(SearchInputs inputs, ExecutionContext ctx);
SearchResults search(SearchInputs inputs, ExecutionContext ctx, String searchHub);
}
The Search Payload
CoveoSearchPayload maps directly to Coveo’s REST search request body:
public class CoveoSearchPayload {
private String q; // free-text query
private String aq; // advanced query (field expressions)
private int firstResult; // pagination offset
private int numberOfResults; // page size
private String locale; // e.g. "en-US"
private String timezone; // e.g. "America/Chicago"
private String sortCriteria; // e.g. "@lastmodifieddt descending"
private boolean allowQueriesWithoutKeywords;
private String[] fieldsToInclude; // limit which metadata fields are returned
private Facet[] facets;
private String pipeline;
private String searchHub;
// getters/setters omitted
}
Building the Advanced Query
The AEM service translates Java objects into Coveo’s query expression syntax:
private static String buildAdvancedQuery(FieldCriteria[] criteria, String customQuery) {
StringBuilder sb = new StringBuilder();
if (criteria != null) {
for (FieldCriteria c : criteria) {
switch (c.getOperator()) {
case EQUALS:
sb.append("(@").append(c.getField()).append("==").append(c.getValue()).append(")");
break;
case GREATER_THAN:
sb.append("(@").append(c.getField()).append(">").append(c.getValue()).append(")");
break;
case NOT_EMPTY:
sb.append("(@").append(c.getField()).append(")");
break;
case STARTS_WITH:
sb.append("(@").append(c.getField()).append("=\"").append(c.getValue()).append("\"*)");
break;
// LESSER_THAN, GREATER_OR_EQUALS, LESSER_OR_EQUALS follow the same pattern
}
}
}
if (StringUtils.isNotEmpty(customQuery)) {
sb.append(customQuery);
}
return sb.length() > 0 ? sb.toString() : null;
}
Example 1: find all published technical articles modified in the last 30 days:
FieldCriteria recentFilter = new FieldCriteria(
"lastmodifieddt",
FieldOperator.GREATER_OR_EQUALS,
"2024/01/01"
);
SearchInputs inputs = new SearchInputs();
inputs.setFieldCriteria(new FieldCriteria[]{recentFilter});
inputs.setFieldsToInclude(new String[]{"title", "description", "clickableuri"});
inputs.setNumberOfResults(20);
inputs.setSortCriteria(new SortCriteria(SortStrategy.FIELD, "lastmodifieddt", SortDirection.DESCENDING));
SearchResults results = coveoSearchService.search(inputs, executionContext);
Example 2: Handling Large Result Sets (>1 000 results)
Coveo’s API caps a single response at 1 000 documents. To retrieve more, the service uses cursor based pagination via the sysrowid field:
// First request: sort by sysrowid ascending, include sysrowid in fields
payload.setSortCriteria("@sysrowid ascending");
payload.setFieldsToInclude(new String[]{"title", "description", "sysrowid"});
do {
RestResponse response = restClient.executePost(buildRequest(payload, token));
SearchResults chunk = gson.fromJson(response.getStringResponse(), SearchResults.class);
allResults.addAll(chunk.getResults());
if (chunk.getResults().size() < batchSize) break; // last page
String lastRowId = chunk.getResults()
.get(chunk.getResults().size() - 1).getSysrowid();
// Next request: add a GREATER_THAN filter on sysrowid
payload.setAq("(@sysrowid>" + lastRowId + ")");
} while (allResults.size() < maxResults);
This pattern avoids Coveo’s firstResult offset limit (which also caps at 1 000) and is reliable for batch export scenarios like scheduled jobs that sync Coveo product data to AEM.
Part 5: Sort Criteria
SearchInputs supports named sort presets that translate to Coveo field expressions:
- Relevancy → relevancy
- Newest → @lastmodifieddt descending
- Oldest → @lastmodifieddt ascending
- AtoZ → @title ascending
- ZtoA → @title descending
- Popularity → qre (Query Ranking Expression)
- NoSort → nosort
The sort builder:
private static String buildSortCriteria(SortCriteria criteria) {
if (criteria == null) return null;
switch (criteria.getStrategy()) {
case RELEVANCY: return "relevancy";
case QRE: return "qre";
case NO_SORT: return "nosort";
case FIELD:
if (StringUtils.isEmpty(criteria.getField())) return "relevancy";
return "@" + criteria.getField() + " " +
(criteria.getSortDirection() == SortDirection.ASCENDING ? "ascending" : "descending");
default: return "relevancy";
}
}
Part 6: The Coveo JSUI Filter Blade (Front-end Integration)
The Coveo JavaScript UI framework is initialized from a JSP component that receives configuration from an AEM custom tag. The SearchBean is the bridge between AEM’s Java layer and the JS layer.
SearchBean the Configuration Bridge
public class SearchBean {
private String coveoRestUrl; // Coveo endpoint URL
private String coveoToken; // short-lived user search token
private String searchHub; // named search hub in Coveo
private String searchAnalyticUrl; // UA endpoint
private String pipeline; // Coveo ML pipeline name
private String language;
private String localCode;
private String pageResultCount; // items per page
private String dataLayout; // "grid" | "list" | "table"
private String onloadSortCriteria;
// Facet configuration (left-rail filters)
private List<SearchFacetProperties> searchFacetPropertiesList;
// Column/card configuration (result layout)
private List<SearchResultData> gridSearchResultDataList;
// ... getters/setters
}
SearchFacetProperties Configuring Facets
public class SearchFacetProperties {
private String facetClass; // CSS class / Coveo component class
private String dataField; // Coveo field e.g. "@contenttypes"
private String dataTitle; // Display label
private String dataId; // Unique facet ID (used by Coveo routing)
private String basePath; // For hierarchical facets
private String initialLevel; // Starting depth for hierarchical facets
// getters/setters omitted
}
SearchResultData Configuring Result Columns
public class SearchResultData {
private String dataField; // Coveo field to display
private String dataTitle; // Column header label
private String dateFormat; // For date fields
private String displayResults; // Visibility toggle
private String listFromMulti; // Render multi-value field as a list
// getters/setters omitted
}
SearchTag Injecting Coveo Config via Sling Model
The Sling Model reads global Coveo settings and exposes them to the HTL template. In older AEM projects this was done via a JSP custom tag; the logic is equivalent, only the wiring differs:
public class SearchTag extends BaseComponentTag {
@Override
public void doTag() throws JspException, IOException {
super.doTag();
SearchConfigBean config = new SearchConfigBean();
// These come from a global settings service, not hardcoded
config.setSearchEndpointUrl(getCoveoSearchEndpointUrl());
config.setSearchAnalyticUrl(getCoveoSearchAnalyticsUrl());
config.setSearchOrganizationUrl(getCoveoOrganizationId());
config.setSearchHub(getSettingProperty("searchHub"));
config.setSearchScriptLibrary(getSettingProperty("searchScriptLibrary"));
config.setSearchStyleLibrary(getSettingProperty("searchStyleLibrary"));
pageContext.setAttribute("replatformInfo", config);
}
}
In modern AEM projects using HTL (Sling Templating Language), the SearchBean is exposed through a Sling Model instead of a JSP tag, and the markup is rendered in an .html HTL template. The pattern is equivalent only the templating syntax changes.
he Sling Model adapter:
@Model(adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SearchModel {
@OSGiService
private SearchTokenService searchTokenService;
@OSGiService
private CoveoSearchGlobalService coveoSearchGlobalService;
@ScriptVariable
private Page currentPage;
private SearchBean searchBean;
@PostConstruct
protected void init() {
searchBean = new SearchBean();
searchBean.setCoveoRestUrl(coveoSearchGlobalService.getSearchEndpointUrl());
searchBean.setSearchHub(coveoSearchGlobalService.getSearchHub());
// token generation happens here — keeps it off the template
JSONObject token = searchTokenService.generateSearchTokenForUser(executionContext);
if (token != null) {
searchBean.setCoveoToken(token.get("token").toString());
}
}
public SearchBean getSearchBean() { return searchBean; }
}
The HTL template (search.html):
<sly data-sly-use.model="com.yourcompany.search.models.SearchModel"
data-sly-use.template="core/wcm/components/commons/v1/templates.html" />
<!-- Coveo CSS and JS loaded from the version-controlled CDN URL -->
<link rel="stylesheet" href="${model.searchBean.searchStyleLibrary}" />
<script src="${model.searchBean.searchScriptLibrary}"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
Coveo.SearchEndpoint.configureCloudV2Endpoint(
"${model.searchBean.searchOrganizationId @ context='scriptString'}",
"${model.searchBean.coveoToken @ context='scriptString'}",
"${model.searchBean.coveoRestUrl @ context='scriptString'}"
);
Coveo.init(document.getElementById("search"));
});
</script>
<div id="search"
class="CoveoSearchInterface"
data-search-hub="${model.searchBean.searchHub}"
data-pipeline="${model.searchBean.pipeline}">
<div class="CoveoSearchBox"></div>
<div class="CoveoFacet"
data-sly-list.facet="${model.searchBean.searchFacetPropertiesList}"
data-field="${facet.dataField}"
data-title="${facet.dataTitle}">
</div>
<div class="CoveoResultList"></div>
</div>
Note the @context=’scriptString’ XSS context on values rendered inside <script> blocks. HTL requires this to apply the correct escaping.
Dropdown Data Source Servlet
Authors configure facets through the AEM dialog. A datasource servlet populates the dropdown options by reading from the JCR (component configuration nodes) rather than hardcoding values:
@Component(immediate = true, service = Servlet.class, property = {
"sling.servlet.resourceTypes=your-app/components/coveo-filter-blade",
"sling.servlet.selectors=coveo-facets",
"sling.servlet.extensions=json"
})
public class CoveoFacetDropdownServlet extends SlingSafeMethodsServlet {
@Override
protected void doGet(SlingHttpServletRequest req, SlingHttpServletResponse resp) {
Resource configResource = req.getResourceResolver()
.getResource("/apps/your-app/settings/coveo/facets");
// iterate children, build DataSource, write JSON
}
}
Part 7: Sitemap XML with Coveo Metadata
For content types managed in AEM but not natively crawlable by Coveo (e.g., events stored in Content Fragments), you can generate a custom XML sitemap that includes <coveo:metadata> tags. Coveo’s crawler reads these and adds the fields to the search index.
@Component(immediate = true, service = Servlet.class, property = {
"sling.servlet.paths=/apps/your-app/services/eventsitemap",
"sling.servlet.extensions=xml",
"sling.servlet.methods=GET"
})
public class EventsSitemapServlet extends SlingSafeMethodsServlet {
private static final String XML_HEADER =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<urlset\n" +
" xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n" +
" xmlns:coveo=\"https://www.coveo.com/schemas\">\n";
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/xml; charset=UTF-8");
StringBuilder sitemap = new StringBuilder(XML_HEADER);
sitemap.append(buildEventEntries(request.getResourceResolver()));
sitemap.append("</urlset>");
response.getWriter().println(sitemap);
}
private StringBuilder buildEventEntries(ResourceResolver resolver) {
StringBuilder entries = new StringBuilder();
// iterate Content Fragment nodes under the events root path
// for each event, build a <url> block
for (EventContentFragment event : loadEvents(resolver)) {
entries.append("<url>\n");
entries.append(" <loc>").append(event.getUrl()).append("</loc>\n");
entries.append(" <coveo:metadata>\n");
entries.append(" <title><![CDATA[").append(event.getName()).append("]]></title>\n");
entries.append(" <description><![CDATA[").append(event.getDescription()).append("]]></description>\n");
entries.append(" <eventstartdate>").append(event.getStartDate()).append("</eventstartdate>\n");
entries.append(" <eventenddate>").append(event.getEndDate()).append("</eventenddate>\n");
entries.append(" <timeZone>").append(event.getTimeZone()).append("</timeZone>\n");
entries.append(" </coveo:metadata>\n");
entries.append("</url>\n");
}
return entries;
}
}
The <coveo:metadata> block tells Coveo’s crawler to index custom fields alongside the standard <loc>. You then map these in the Coveo source configuration to Coveo fields like @eventstartdate.
Part 8: Putting It All Together Request Flow
Here is the full request flow for a page that uses Coveo JSUI:
Browser (page request)
│
▼
AEM Dispatcher
│
▼
AEM Publish (JSP rendering)
│
├─► SearchTag.doTag()
│ └─► reads Coveo endpoint/org/hub from global settings (JCR)
│
├─► SearchTokenService.generateSearchTokenForUser(request)
│ ├─► loads EntitledUser from SSO/profile service
│ ├─► builds entitlement groups (public + program memberships + roles)
│ ├─► builds partner filter expression
│ └─► POSTs to Coveo /token endpoint → returns short-lived JWT
│
└─► JSP renders HTML with:
- Coveo JS/CSS from CDN
- <script> bootstrap with organizationId + search token
- Coveo component markup (facets, result list, search box)
│
▼
Browser receives page
│
▼
Coveo JS Framework initializes
│
└─► sends search request to Coveo REST API
Bearer: <search token> ← user identity + groups + filter baked in
Body: { q, aq, facets, ... }
│
▼
Coveo returns results
└─► only documents the user's groups are permitted to see
For server-side search (e.g., a scheduled job or server-rendered content collection):
AEM scheduled job / servlet
│
├─► CoveoSearchService.search(inputs, ctx)
│ ├─► SearchTokenService.generateSearchTokenForUser(ctx) ← gets JWT
│ ├─► CoveoSearchServiceImpl.buildCoveoSearchPayload(inputs)
│ │ ├─► buildAdvancedQuery(fieldCriteria, customQuery)
│ │ └─► buildSortCriteria(sortCriteria)
│ └─► POSTs to Coveo /v2?organizationId=xxx
│ Authorization: Bearer <search token>
│
└─► returns SearchResults → rendered on page or processed in job
Key Takeaways
- Never expose your Coveo API key to the browser: Always use short-lived search tokens generated server-side. The API key only lives on the AEM author/publish tier.
- Cache strategically: Auth tokens are cached in a distributed JCS region (shared across cluster nodes). Search tokens are cached in the HTTP session per user, with TTL enforcement on the AEM side.
- Entitlement groups are your security boundary: Coveo enforces visibility based on the groups in the token. Build your group taxonomy carefully and keep it consistent between what you index and what you grant in tokens.
- Use the “sysrowid” cursor for large exports: Coveo’s “firstResult” offset limit and the 1 000 result page cap mean any batch job retrieving more than 1 000 documents must use cursor based pagination.
- Coveo metadata in sitemaps: is an underused feature that lets you index AEM content fragments and other non HTML assets without a custom Coveo connector.
- OSGi configuration: is the right place for environment-specific settings (endpoint URLs, search hub names, entitlement group names). JCR is the right place for encrypted credentials.
Suggested Project Structure
your-aem-project/
└── core/
└── src/main/java/com/yourcompany/
├── search/
│ ├── constants/
│ │ └── SearchConstants.java # enums, JCR node names
│ ├── beans/
│ │ ├── request/
│ │ │ ├── SearchInputs.java # inputs builder
│ │ │ ├── CoveoSearchPayload.java # REST body
│ │ │ ├── FieldCriteria.java
│ │ │ └── SortCriteria.java
│ │ └── response/
│ │ ├── SearchResults.java
│ │ └── Result.java
│ ├── services/
│ │ ├── CoveoSearchService.java # interface
│ │ ├── SearchTokenService.java # interface
│ │ └── impl/
│ │ ├── CoveoSearchServiceImpl.java
│ │ └── SearchTokenServiceImpl.java
│ └── osgiconfig/
│ ├── SearchTokenServiceConfig.java
│ └── SearchGlobalServiceConfig.java
├── gateway/
│ ├── services/
│ │ ├── SearchGatewayService.java # optional: API gateway layer
│ │ └── impl/
│ │ └── SearchGatewayServiceImpl.java
│ └── osgiconfig/
│ └── SearchGatewayServiceConfig.java
└── ui/
├── beans/
│ ├── SearchBean.java # JSUI config bridge
│ ├── SearchFacetProperties.java
│ └── SearchResultData.java
├── tags/
│ └── SearchTag.java # JSP custom tag
└── servlets/
├── CoveoFacetDropdownServlet.java # AEM dialog datasource
└── EventsSitemapServlet.java # Coveo XML sitemap
Coveo’s combination of token based security, rich query syntax, and a powerful JavaScript UI framework make it an excellent fit for large scale AEM deployments. The patterns described here layered token generation, entitlement based content gating, cursor pagination, and sitemap based indexing have been proven at scale and represent a solid foundation for any team building search on top of AEM.
A Note on “JcsCacheUtil”
Throughout this post, “JcsCacheUtil” is used to cache auth tokens across requests. This is not a standard library class. it is a project specific wrapper around Apache JCS (Java Caching System), an open-source distributed caching framework. If you are implementing this pattern from scratch, you have several alternatives:
- Apache JCS directly: JCS.getInstance(“regionName”)` gives you a CacheAccess object with get/put methods. Suitable if you want the same underlying library.
- Caffeine: a high performance in-memory cache with a clean API and built in TTL support. A common choice for OSGi bundles in modern AEM projects.
- Ehcache: more feature rich, supports off-heap and disk tiers. Better if you need large cache regions.
- AEM’s Sling Commons Cache: available as an OSGi service on AEM, integrates naturally with the container lifecycle.
Whichever you choose, the key design principle remains the same: cache the auth token (long-lived, shared across all users) in a cluster-aware cache region, and cache the search token (short-lived, user-specific) in the HTTP session with a wall clock TTL check on read.
메타데이터
- post_id
- 71ea486ce287
- slug
- integrating-coveo-search-with-adobe-experience-manager-aem-a-deep-dive-71ea486ce287
- url
- https://medium.com/@frannet82/integrating-coveo-search-with-adobe-experience-manager-aem-a-deep-dive-71ea486ce287
- canonical_url
- https://medium.com/@frannet82/integrating-coveo-search-with-adobe-experience-manager-aem-a-deep-dive-71ea486ce287
- author_url
- https://medium.com/@frannet82
- status
- ok
- fetched_at
- 2026-06-10 08:17:25