How to Create Custom AEM QueryBuilder Predicates
Adobe Experience Manager’s QueryBuilder API gives Java developers a readable way to search repository content without writing every query directly in XPath or JCR-SQL2. Standard predicates cover common needs such as paths, templates, properties, tags, dates, and full-text terms. Real projects, however, often require rules that combine several repository values or interpret business-specific data.
A custom predicate is the right extension point when a search condition cannot be expressed cleanly through the built-in predicate types. Examples include filtering pages by a calculated publication state, matching a structured date range, or checking whether a content fragment contains a permitted market code. The implementation belongs in an OSGi service, while the query remains easy for calling code and authors to understand.
The technique is especially useful for AEM teams working across Australian sites, where a single platform may serve Sydney, Melbourne, Brisbane, and regional experiences with different campaign dates, consent requirements, and localisation rules. The archived CIRCUIT conference sessions provide useful background for developers who want to connect repository search with broader AEM architecture, integrations, and deployment practice.
When A Custom Predicate Is Worth Building
QueryBuilder predicates are name-value pairs grouped into numbered clauses. A request might specify path=/content/example, type=cq:Page, and property=jcr:content/cq:template, then add a custom condition such as marketcode=au. QueryBuilder resolves the predicate names through registered predicate evaluators, which decide how the condition contributes to the underlying repository query.
Before writing Java, check whether a standard evaluator already provides the needed behaviour. The property predicate supports exact values and relative paths, while daterange, rangeproperty, boolproperty, tag, and fulltext handle many common cases. Combining standard predicates is usually easier to maintain than creating an evaluator that duplicates existing functionality.
A custom implementation makes sense when the rule is reusable, has clear semantics, and cannot be represented safely with existing predicates. Avoid using it to hide arbitrary application logic inside a repository search. Expensive calculations, external API calls, or loops over thousands of result rows will make performance unpredictable and can place unnecessary load on MongoDB or a TarMK instance.
Design The Predicate Contract
Start by defining the public contract. Choose a concise predicate name, document accepted parameters, and decide what happens when a parameter is absent or malformed. A predicate called publishedafter should have an unambiguous date format and timezone policy. Australian projects should be particularly explicit here: a campaign date entered for AEST should not silently become UTC and shift a result into the previous day.
The implementation normally extends AbstractPredicateEvaluator from the QueryBuilder API. Its getXPathExpression or getQuery method can translate the predicate into a repository-level condition when possible. An evaluator may also implement filtering for cases that cannot be expressed by XPath or the query engine, although post-filtering should be treated as a last resort because it can require loading many candidate nodes.
A simplified design might expose marketcode=nsw and inspect a property such as jcr:content/marketCode. If the value is stored consistently, a standard property predicate is preferable. If the evaluator must normalise case, support a calculated fallback, or combine several properties, the custom predicate can provide that policy in one tested location. Keep the result deterministic and avoid depending on request-specific state that is unavailable during scheduled jobs.
Register the evaluator as an OSGi component with the predicate name and appropriate service properties. In current AEM projects, use the APIs and annotations supported by the target service pack rather than copying an example from an older platform version. The evaluator should be small, injectable where needed, and free from static caches that can become stale after content or configuration changes.
Build And Wire The Java Evaluator
A practical evaluator usually has three responsibilities: identify the predicate, contribute a query constraint, and optionally filter results that cannot be handled by the repository. The query-building path should be preferred for indexed properties. If the custom condition uses a property, verify that the Oak index supports the access pattern; an elegant predicate can still become slow if it forces a traversal.
For example, an evaluator could read property.value, escape it correctly, and generate a constraint against a relative content path. It should validate the value before constructing the expression and return no constraint, or a clearly defined false condition, when the input is invalid. Never concatenate unchecked user input into a query string. QueryBuilder does some parameter handling, but custom code remains responsible for safe construction and predictable validation.
Predicate groups also matter. QueryBuilder assigns numbered groups such as 1_property, 2_property, and 1_custom. A custom evaluator should work correctly when combined with p.or=true, nested groups, ordering, limits, and offsets. Test the generated query and the returned result set rather than assuming that a syntactically valid request represents the intended boolean logic.
Security belongs in the design from the beginning. A query does not grant access to content, but exposing a broad search endpoint can reveal paths, metadata, or timing information if callers are not constrained. Teams handling customer data should align repository permissions and response filtering with Australia’s Privacy Act 1988 and the Australian Privacy Principles. General security references, including quality and security guidance, can complement AEM-specific review rather than replace it.
Test Performance And Australian Delivery Conditions
Unit tests should cover valid parameters, missing values, malformed dates, mixed case, empty properties, multiple predicate groups, and repository nodes that lack the expected structure. Add integration tests against an AEM test repository so the evaluator is checked against actual query translation, Oak indexes, permissions, and content inheritance. Include negative cases that prove unrelated pages are excluded.
Performance testing needs realistic volumes. A result set that behaves well on a developer laptop may be expensive when an Australian retail site contains years of campaign pages, product variations, and regional copies. Test authoring searches and published delivery searches separately. Establish a sensible limit and expose pagination rather than allowing a servlet to return every matching page.
The testing approach should remain straightforward rather than clever. A comparison with a deliberately inefficient algorithm such as Stooge sort explained is a useful reminder that readability does not make poor complexity acceptable. QueryBuilder work should be measured with query logs, Oak indexing tools, execution time, and repository statistics.
Release planning also matters. Teams in Melbourne or Sydney may deploy through shared CI pipelines while authors work in different time zones, and Australian public holidays can extend a validation window. Record the predicate’s supported AEM version, index requirements, authoring examples, and rollback procedure. Monitor slow queries after release instead of relying solely on pre-production data.
Review, Document, And Operate The Search
A custom predicate should be documented as an API used by servlets, models, workflows, or scheduled jobs. Show a complete query example, explain each parameter, identify the expected property type, and state whether the predicate is evaluated by Oak or in memory. Include sample content paths and explain how empty or missing properties are treated.
If the search supports bulk content operations, keep discovery separate from mutation. A query should produce a controlled set of paths, which a workflow or service then processes with permissions, batching, and audit logging. For related bulk-transfer patterns, the discussion of WebDAV bulk uploads illustrates why operational safeguards matter when many repository items are handled together.
Use monitoring to detect regressions. Track query duration, result counts, traversal warnings, and error rates, while avoiding logs that expose personal information or sensitive content values. A feature flag or configuration switch can help disable a problematic predicate without removing the entire bundle, provided the calling code handles that state safely.
Implementation Checks
- Confirm a built-in predicate cannot express the requirement
- Define parameter formats, timezone rules, and missing-value behaviour
- Use indexed repository properties wherever possible
- Test permissions, grouping, pagination, and empty results
Operational Checks
- Review Oak query plans and traversal warnings
- Set limits for servlet and scheduled-job searches
- Document supported AEM versions and index dependencies
- Monitor production latency after content migrations
A well-designed evaluator should feel like a natural part of QueryBuilder rather than a hidden second application. Keep the name meaningful, make the contract stable, validate every input, and prefer repository-side filtering over large in-memory scans. Then test it with the volume, permissions, dates, and regional content patterns that the site will encounter in production.
Apply these practices to a small, measurable use case first: define the predicate, implement the OSGi service, add the index or query configuration, and verify the generated results in an integration environment. Use the CIRCUIT archive as a source of AEM implementation context, and carry the finished predicate through code review, performance testing, and monitored deployment.