Building AEM custom search forms with Query Builder predicates

AEM developers working across Sydney agencies and Melbourne-based digital consultancies have long wrestled with the platform's content retrieval layer. Query Builder sits at the heart of how author and publish instances surface results, yet the way it is exposed to business users often determines whether a project feels intuitive or frustrating. When a content team in Brisbane asks for "find me every campaign page tagged with the spring promotion that was published last quarter," the underlying mechanism that answers that question is almost always a set of well-configured predicates.

Custom search forms built on top of Query Builder close the gap between technical API calls and the everyday needs of marketers, editors, and compliance reviewers. Rather than forcing content authors to learn JCR-SQL2 or hand-craft XPath expressions, a thoughtfully designed form lets them select options from dropdowns, type into date pickers, and check boxes that map directly to predicate parameters. The result is a search experience that feels native to AEM, while still leveraging the same engine that powers the platform's internal queries.

This article walks through the practical work of creating these forms, from predicate selection and JSON configuration through to performance considerations that matter when your repository holds millions of nodes. It draws on patterns shared at CIRCUIT conferences and refined by teams who run AEM at scale across Australian enterprises, government departments, and education institutions.

Mapping predicate types to user expectations

Every custom search form begins with a clear picture of what the user is looking for. Query Builder exposes around fifteen core predicate types, and choosing the right one for each form field is what separates a usable interface from a confusing one. The path predicate limits results to a specific branch of the content tree, the type predicate filters by node type such as cq:Page or dam:Asset, and property predicates like property, hasproperty, and propertyvalue let you match against authored metadata.

When designing for an Australian content team, think about how they describe their work. A marketer in Perth might say "pull up all the landing pages from the campaign folder," while a compliance officer in Canberra could be searching for "every document reviewed in the last ninety days that mentions privacy." Each of those phrases maps cleanly to a combination of predicates: path plus type plus a date range predicate such as daterange or daterelative. The art lies in translating colloquial language into predicate syntax without losing the user's intent.

It helps to sketch the form on paper before touching any configuration. List every search dimension the user might want to combine, then check each one against the predicate catalogue. If a dimension does not map to an existing predicate type, that is usually a signal that you need a custom index in Oak, or that the search requirement is better served by an external reporting tool. Trying to force every business question into Query Builder leads to slow queries and frustrated users.

Building the form structure in Touch UI

The Touch UI in AEM 6.x and AEMaaCS provides Granite UI components that render form fields whose values are aggregated into a single map and handed to the Query Builder servlet. A typical custom search dialog lives under /apps/<your-project>/components/search and contains a cq:dialog node with granite:data configuration pointing to searchtype=fulltext or searchtype=predicate. The latter is what you want for predicate-driven forms, because it tells the framework to expose each input as a predicate parameter rather than running a full-text search.

Within the dialog, drop in the Granite UI components that match your chosen predicates. A granite/ui/components/coral/foundation/form/pathfield works perfectly for path, while granite/ui/components/coral/foundation/form/select with appropriate annotations can drive type or boolean predicates. For date ranges, the granite/ui/components/coral/foundation/form/datepicker pair allows operators such as >=, <=, or between. Make sure each component carries a name attribute that matches the predicate parameter name you intend to use, otherwise the values will be silently dropped.

Naming conventions matter. Australian teams often inherit naming patterns from legacy systems that mixed camelCase, kebab-case, and underscores. Standardise on lowercase, hyphen-separated names that mirror the predicate parameters exactly, so property, property.value, and property.operation all become clear and predictable. This makes the form easier to debug later, particularly when you are reviewing recordings of past CIRCUIT sessions on Touch UI implementation that walked through similar patterns.

Constructing predicate JSON and Java predicates

While form fields generate a map of values, Query Builder expects either a JSON map or a Java PredicateGroup that defines the boolean logic between predicates. The JSON form lives in OSGi configurations or in clientlibs that build the map dynamically, and it looks roughly like { "type": "cq:Page", "path": "/content/campaigns", "1_property": "jcr:content/cq:template", "1_property.value": "/conf/.../templates/promotion", "group.p.or": "true" }. Group numbering lets you build AND-of-OR constructs that are common in real-world content searches.

For more complex logic, building a PredicateGroup in Java gives you type safety and the ability to inject custom predicates. Adobe's Query Builder API exposes Predicate, PredicateConverter, and PredicateEvaluator interfaces, the last of which lets you register a brand-new predicate evaluator with its own parameter names and processing logic. This is the path teams take when they need a predicate that understands a domain-specific concept, such as "campaign expiry within the next fortnight" or "pages missing alt text that fails WCAG AA."

If your search surface area grows beyond a handful of predicates, consider splitting the logic into a dedicated service. Teams running Adobe Managed Services from data centres in Sydney and Melbourne have found that wrapping the predicate construction in an OSGi service keeps the components lean and makes the logic unit-testable. You can then expose the service to both Touch UI components and any headless clients using the same codebase, which is a substantial win for maintenance.

Optimising index coverage and query speed

A well-built form still feels slow if Oak cannot resolve it against an index. Every Query Builder call ultimately becomes a JCR-SQL2 or XPath statement that the Oak query engine evaluates, and without the right indexes it falls back to traversal. The Oak Index Manager at /oak:index lists the existing indexes, and most projects already ship with lucene indexes for nt:base, cq:Page, and dam:Asset. For custom searches you typically need to add a property index or a lucene property index that covers the fields referenced by your predicates.

The rule of thumb is to index anything that appears in a property predicate where the value is not the primary node type. Authored fields such as campaignId, region, or complianceReviewed almost always warrant dedicated lucene indexes with appropriate includedPaths, excludedPaths, and analyzers. Privacy Act obligations around data handling, plus accessibility standards adopted across Australian federal agencies, add extra weight to this advice, because compliance teams will run the same search repeatedly and a slow query blocks their workflow.

Indexes are not free, though. Each lucene index consumes heap and disk, and rebuilding them during a publish rollout can take hours on a large repository. A useful pattern, shared in CIRCUIT recordings covering horizontal clustering and large-scale deployments, is to keep custom search indexes scoped to specific paths under /content rather than the entire repository. The scaling AEM with horizontal clustering discussion on the CIRCUIT site covers how this interacts with shared nothing stores and how to size clusters when index rebuilds run concurrently.

Bringing it together with the final predicate map

The final step is wiring the form values to the predicate map at query time. In a HTL or JSP component, you typically read the request parameters, build the map, and pass it to QueryBuilder.createQuery(PredicateMap, Session). Iterate over the resulting SearchResult to extract hits, then render them through the same Granite UI list components you would use elsewhere. The key is keeping the predicate construction in a single method that both the form submission and any future REST endpoints can share.

Before you ship the form to a wider audience, run it through a suite of representative searches against a production-sized repository clone. Test edge cases like empty values, very broad date ranges, and combinations of predicates that should return zero results. A surprising number of forms behave correctly for the happy path but fail silently when a user clears a checkbox or types an invalid date. Logging the generated predicate map at debug level makes these cases trivial to diagnose.

Once you have a form that returns the right results quickly, share the configuration with the wider team. Post the predicate map, the index definitions, and a short usage guide in your internal wiki. Many Australian AEM practitioners working in federal government and higher education have found that capturing these artefacts in a standard template prevents the same questions from being answered over and over in team channels. If you want to see how other teams have approached similar challenges, the recordings and slides from past CIRCUIT events are still available, and the CIRCUIT conference app makes it easy to browse sessions by topic, speaker, or track on the train ride home from the venue.

For deeper reading on the authoring side of AEM, the article on Touch UI implementation best practices complements this discussion nicely, particularly the sections on dialog design and component reuse. Pair the two together, and you have a solid foundation for any custom search experience you need to build.