A standalone AEM bundle that extracts content from cq:Page nodes — including referenced Content Fragments —
and applies the cq:Searchable mixin with search properties, enabling fulltext and semantic search via
the cqSiteSearch Oak Elasticsearch index.
Search properties are written directly to the original /content nodes.
A lightweight mirror at /var/indexing/expanded-pages is maintained for operational tracking
(dirty flags, Content Fragment backreferences).
all/pom.xml<repositories>
<repository>
<id>oak-indexing</id>
<url>https://oak-indexing.github.io/maven-repository/</url>
<snapshots><enabled>true</enabled></snapshots>
<releases><enabled>true</enabled></releases>
</repository>
</repositories>
all/pom.xmlIn the <embeddeds> block of filevault-package-maven-plugin:
<embedded>
<groupId>com.adobe.aem.indexing</groupId>
<artifactId>expanded-page-index</artifactId>
<version>1.5.6</version>
<target>/apps/your-packages/application/install</target>
</embedded>
ui.config module
The bundle embeds its own repoinit and service user mapping via Sling-Initial-Content, but in AEM Cloud Service
there is a known ordering risk where the bundle can activate before the JCR Installer feeds those configs to ConfigAdmin.
To guarantee reliable startup, add these two files to your project's ui.config module:
ui.config/.../osgiconfig/config/org.apache.sling.jcr.repoinit.RepositoryInitializer~expanded-page-index.cfg.json
{
"scripts": [
"create path (sling:Folder) /var/indexing/expanded-pages/content\n\ncreate service user expanded-page-index-service with forced path system/cq:services/expanded-page-index\n\nset principal ACL for expanded-page-index-service\n allow jcr:read,jcr:modifyProperties,jcr:nodeTypeManagement on /content\n allow jcr:all on /var\n allow jcr:read on /oak:index\nend"
]
}
ui.config/.../osgiconfig/config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~expanded-page-index.cfg.json
{
"user.mapping": [
"com.adobe.aem.indexing.expand:expanded-page-index=[expanded-page-index-service]"
]
}
The bundle requires two Oak indexes before it activates. These are deployed by Adobe into your AEM Cloud Service environment:
cqPageMirror-1-custom-1 — tracks cq:PageMirror nodes (dirty flags, move detection)cqFragmentMirror-1-custom-1 — tracks cq:FragmentMirror nodes (CF backreferences)cqSiteSearch-1-custom-1 Elasticsearch index is required for search queries to work, but is not a condition for the bundle to activate.
The bundle writes the following properties to each cq:Page node under /content:
| Property | Description |
|---|---|
cq:searchTitle | Page title |
cq:searchDescription | Page description |
cq:searchContent | Extracted page body (multi-value, HTML stripped) |
SELECT [cq:searchTitle], [cq:searchDescription], [jcr:path]
FROM [cq:Searchable]
WHERE ISDESCENDANTNODE('/content/mysite/us/en')
AND contains(*, 'search terms')
OPTION (INDEX TAG cqSiteSearch)
The scheduler runs every 30 seconds by default. To change the interval, configure OSGi PID com.adobe.aem.indexing.expand.scheduler.ExpandedPageIndexScheduler:
| Property | Default | Description |
|---|---|---|
scheduler.expression | 0/30 * * * * ? | Cron expression |
scheduler.concurrent | false | Prevent overlapping runs |
The bundle itself is configured via OSGi PID com.adobe.aem.indexing.expand.config.ExpandedPageIndexConfig:
| Property | Default | Description |
|---|---|---|
referencePropertyPattern |
fragmentPath |
Regex (full match) applied to property names on cq:Page subnodes to identify Content Fragment references. Adjust when your components use a non-standard property name (e.g. cfArticleReference, cf.*Reference). |
contentPropertiesPropertyName |
elementNames |
Name of the sibling property that lists which fragment fields to extract (source-driven / WKND style). Set to empty to disable this strategy. |
contentPropertyPattern |
(empty) | Regex (full match) applied to property names inside the fragment's jcr:content/data/master node. All matching properties are extracted as body text (pattern-driven / Telekom style). Leave empty to disable. |
Two strategies control how text is pulled out of referenced Content Fragments. They run independently and can be used simultaneously.
elementNames) that explicitly lists which fragment fields to include.
Use this when your components already declare which fields they render.
contentPropertyPattern) is matched against all property names inside the fragment's data/master node;
every matching property is extracted. Use this when components do not declare field names, or to extract fields from many different CF models uniformly.
Example config using both strategies together:
{
"referencePropertyPattern": "cf.*Reference",
"contentPropertiesPropertyName": "elementNames",
"contentPropertyPattern": "body|text|description"
}
The mirror at /var/indexing/expanded-pages holds operational state only — dirty flags and CF backreferences.
It is not used for search.
/var/indexing/expanded-pages/
├── state/
│ └── lastRun (timestamp of last successful extraction run)
└── content/ (mirrors /content)
└── mysite/
└── us/
└── en/
└── page/ (cq:PageMirror)
├── path = "/content/mysite/us/en/page"
├── dirty = false
└── fragmentReferences = ["/content/dam/fragments/..."]
The bundle exposes a JMX MBean for operational tasks.
Navigate to: http://localhost:4502/system/console/jmx → com.adobe.aem.indexing.expand → ExpandedPageIndexService
| Operation | Description |
|---|---|
runExtraction() | Process pages changed since the last run |
rebuildMirror() | Delete the mirror and rebuild from scratch |
clearMirror() | Delete the mirror without rebuilding |
extractionService unsatisfied
The ExpandedPageIndexScheduler depends on ExpandedPageIndexService.
If the service component is unsatisfied, check it directly in the OSGi console and look for any unsatisfied references there.
The service user mapping was not processed before the bundle activated.
Add the ServiceUserMapperImpl.amended config to your ui.config module as described in
Quickstart step 3.
Cause: Oak indexes not yet deployed.
Check: Browse to /oak:index/cqPageMirror-1-custom-1 in CRXDE. If missing, the indexes have not been deployed yet.
The bundle logs "Oak indexes not yet available" each scheduler cycle until they exist.
Look for "Content extraction service activated" in error.log. If absent, the service user
expanded-page-index-service may not have been created by the bundle's repoinit script.
Check for repoinit errors at startup.
cq:Searchable mixin is present on target nodes (CRXDE — check a /content page node)elastic-async lane is running — check IndexStats for elastic-async in the JMX consolecqSiteSearch-1-custom-1 exists at /oak:index/cqSiteSearch-1-custom-1 and has type=elasticsearchOPTION (INDEX TAG cqSiteSearch) is present in your querylastRun timestamp at /var/indexing/expanded-pages/state — it should update every 30 secondserror.log for "Previous extraction still running" — a run is taking longer than the scheduler interval
By default, cq:Searchable properties are written to the original /content nodes (ORIGINAL mode).
An alternative MIRROR mode writes them to /var/indexing/expanded-pages instead, keeping /content untouched.
To enable it, add an OSGi config to your ui.config:
ui.config/.../osgiconfig/config/com.adobe.aem.indexing.expand.config.ExpandedPageIndexConfig.cfg.json
{
"searchMode": "MIRROR"
}
In MIRROR mode, adjust your search query to target /var/indexing/expanded-pages instead of /content.
If you have any issues or questions, please open a ticket.