AEM Query Builder
Note này gom toàn bộ về Query Builder trong AEM 6.5 on-premise: nền tảng JCR/Oak, predicates, logic groups, pagination, indexing, best practices và code mẫu production. Trọng tâm là Query Builder, bỏ qua JCR-SQL2 thuần.
1. Nền tảng: JCR, Oak và cách query hoạt động
JCR (Java Content Repository) là kho lưu trữ dạng cây phân cấp — mỗi node có primaryType, properties và child nodes. Mọi thứ trong AEM đều là node:
/content/mysite/en/about
├── jcr:primaryType = "cq:Page"
└── jcr:content
├── jcr:title = "About Us"
└── sling:resourceType = "mysite/components/page"
Query Builder là lớp API do Adobe phát triển nằm trên đỉnh Oak query engine. Nó nhận một map key-value (predicates), dịch sang XPath, rồi Oak thực thi. Từ AEM 6.0 (Jackrabbit lên Oak), XPath và JCR-SQL bị deprecated nên Oak chuyển tiếp XPath sang JCR-SQL2 bên dưới.
QueryBuilder predicates → XPath → Oak engine (JCR-SQL2) → Oak Index (hoặc traversal)
Khi nào dùng Query Builder vs findResources
Hai API truy cập JCR
2. Hai cách sử dụng Query Builder
REST API
GET http://localhost:4502/bin/querybuilder.json?
type=cq:Page
&path=/content/mysite
&property=jcr:content/jcr:title
&property.value=About
&p.limit=10
Java API — 3 cách truyền predicates
@Reference
private QueryBuilder queryBuilder;
// Cách 1: HashMap (phổ biến nhất, đơn giản nhất)
Map<String, String> map = new HashMap<>();
map.put("path", "/content/mysite");
map.put("type", "cq:Page");
map.put("p.limit", "10");
map.put("p.guessTotal", "true");
Query query = queryBuilder.createQuery(PredicateGroup.create(map), session);
// Cách 2: PredicateGroup API (cho query phức tạp, build động)
PredicateGroup group = new PredicateGroup();
group.add(new Predicate("mypath", "path").set("path", "/content/mysite"));
group.add(new Predicate("mytype", "type").set("type", "cq:Page"));
Query query2 = queryBuilder.createQuery(group, session);
// Cách 3: Từ HTTP request parameters (dùng trong servlet)
Session session = request.getResourceResolver().adaptTo(Session.class);
PredicateGroup root = PredicateGroup.create(request.getParameterMap());
Query query3 = queryBuilder.createQuery(root, session);
// Lấy kết quả — chuẩn cho mọi cách
SearchResult result = query.getResult();
for (Hit hit : result.getHits()) {
Resource resource = hit.getResource(); // null-check bắt buộc
String path = hit.getPath();
}
Tip
Dùng Query Builder (thay vì findResources) khi cần sanitize input từ user, vì nó xử lý escaping an toàn hơn.
3. Predicates quan trọng
path — Giới hạn phạm vi tìm kiếm
path=/content/mysite/en
path.exact=true # chỉ match đúng path đó, không bao gồm con
path.flat=true # chỉ tìm direct children
path.self=true # bao gồm cả node gốc trong kết quả
Warning
Luôn đặt path càng hẹp càng tốt. Không có path = quét toàn bộ /content.
type — Lọc theo node type
type=cq:Page # trang AEM
type=dam:Asset # asset trong DAM
type=nt:file # file node
type=nt:unstructured # generic — tránh dùng, không có index tốt
Warning
type bao gồm cả subtypes. Dùng dam:Asset thay vì nt:unstructured để Oak chọn đúng index (damAssetLucene), tránh traversal warning.
property — Lọc theo property
# Exact match
property=jcr:content/jcr:title
property.value=About Us
# Chứa chuỗi (LIKE)
property.operation=like
property.value=%About%
# Kiểm tra tồn tại
property.operation=exists
property.value=true
# Multi-value: OR (mặc định)
property=jcr:content/cq:tags
property.1_value=cat/politics
property.2_value=cat/technology
# Multi-value: AND
property.and=true
property.1_value=tag1
property.2_value=tag2
# Độ sâu tìm kiếm
property.depth=2 # tìm trong 2 levels con bên dưới
Các operation: equals (default), unequals, like, not, exists.
daterange — Khoảng thời gian tuyệt đối
daterange.property=jcr:content/jcr:created
daterange.lowerBound=2025-01-01
daterange.lowerOperation=>=
daterange.upperBound=2025-01-31T23:59:59
daterange.upperOperation=<=
Format: ISO8601 (YYYY-MM-DDTHH:mm:ss, có thể kèm offset +07:00) hoặc POSIX timestamp.
relativedaterange — Khoảng thời gian tương đối
# Content sửa trong 7 ngày qua
relativedaterange.property=jcr:content/cq:lastModified
relativedaterange.lowerBound=-7d
# upperBound mặc định = now; upperBound=5d = 5 ngày tới
Syntax: 1s 2m 3h 4d 5w 6M 7y. Prefix - = quá khứ, không dấu = tương lai.
nodename — Lọc theo tên node
type=nt:file
nodename=*.jar
Wildcard: * (0+ ký tự), ? (1 ký tự), [abc] (ký tự trong tập hợp).
fulltext — Tìm kiếm toàn văn
fulltext=Adobe Experience Manager
fulltext.relPath=jcr:content # chỉ tìm trong jcr:content
Dùng Lucene index, cần fulltext index được cấu hình.
boolproperty — Property boolean
boolproperty=jcr:content/hideInNav
boolproperty.value=true
false match cả node không có property đó.
tag và tagid — Tìm theo tag
# Theo tag ID
tagid=properties:orientation/landscape
tagid.property=jcr:content/cq:tags
# Theo tag title path
tag=properties:orientation/landscape
4. Logic Groups: p.or và p.and
Mặc định tất cả predicates được AND với nhau. Dùng group cho điều kiện phức tạp.
OR trong một nhóm
# (title = "About") OR (navTitle = "About")
group.p.or=true
group.1_property=jcr:content/jcr:title
group.1_property.value=About
group.2_property=jcr:content/navTitle
group.2_property.value=About
Nested groups với numeric prefix
// (category=politics OR category=technology) AND status=published
map.put("path", "/content/newssite/articles");
map.put("type", "cq:Page");
// Group 1: OR categories
map.put("1_group.p.or", "true");
map.put("1_group.1_property", "jcr:content/articleCategory");
map.put("1_group.1_property.value", "politics");
map.put("1_group.2_property", "jcr:content/articleCategory");
map.put("1_group.2_property.value", "technology");
// Group 2: AND status
map.put("2_group.property", "jcr:content/articleStatus");
map.put("2_group.property.value", "published");
Phủ định nhóm
group.p.not=true
group.property=jcr:content/hideInNav
group.property.value=true
Warning
Không dùng trùng numeric prefix cho hai predicate khác nhau trong cùng một query (vd hai cái 1_).
5. Phân trang và kiểm soát kết quả
p.offset=0 # bắt đầu từ kết quả thứ N
p.limit=20 # số kết quả mỗi trang
p.guessTotal=true # ước tính tổng, tránh đếm chính xác
params.put("p.offset", "0");
params.put("p.limit", "20");
params.put("p.guessTotal", "true");
SearchResult result = query.getResult();
// result.getHits() → danh sách kết quả (tối đa 20)
// result.hasMore() → còn kết quả tiếp theo không
// result.getTotalMatches() → ước tính tổng (với guessTotal)
Kiểm soát output
p.hits=full # tất cả properties
p.hits=selective # chỉ properties chỉ định
p.properties=jcr:title sling:resourceType # dùng với selective
p.nodedepth=2 # include child nodes đến depth N
6. Sorting
orderby=@jcr:content/cq:lastModified
orderby.sort=desc # hoặc asc (default)
orderby.case=ignore # case-insensitive (AEM 6.2+)
# Sort theo nhiều field
1_orderby=@jcr:content/jcr:title
2_orderby=@jcr:content/cq:lastModified
2_orderby.sort=desc
7. Query Builder Debugger
Công cụ thiết yếu khi phát triển:
http://localhost:4502/libs/cq/search/content/querydebug.html
Cho phép: nhập predicates dạng text, chạy query, xem kết quả JSON, và lấy XPath tương đương.
Workflow debug chuẩn
- Viết query trong Debugger
- Copy XPath được sinh ra, ví dụ:
/jcr:root/content/newssite/articles//element(*, cq:Page)
[jcr:content/@articleCategory = 'politics']
order by jcr:content/@jcr:lastModified descending
- Paste vào Explain Query (
Tools → Operations → Diagnosis → Explain Query)
- Xem query plan → index nào được dùng
- Nếu thấy "no index found" → cần tạo Oak index
Lấy XPath qua logging
Felix Console → Sling Log Support
Logger: com.day.cq.search.impl.builder.QueryImpl
Level: DEBUG
XPath query: /jcr:root/content//element(*, cq:Page)[jcr:contains(jcr:content, "AEM")]
8. Oak Indexing (On-Premise)
Oak không index mặc định
Khác Jackrabbit 2, Oak không tự index properties. Query không hit index → Oak traverse toàn bộ repository, cực chậm và nguy hiểm.
*WARN* Traversed 1000 nodes with filter Filter(query=select ...)
consider creating an index or changing the query
Từ AEM 6.3, traversal đạt 100,000 nodes thì query fail và throw exception. Nếu thật sự cần query trả nhiều hơn, tăng giới hạn qua JVM:
-Doak.queryLimitInMemory=500000 -Doak.queryLimitReads=100000
Các loại index AEM 6.5
Dùng đúng type để tận dụng OOTB index
Tạo custom Lucene index
Cấu trúc node:
/oak:index/myCustomIndex
├── jcr:primaryType = oak:QueryIndexDefinition
├── type = lucene
├── async = async
├── fulltextEnabled = false
└── indexRules
└── cq:PageContent
└── properties
└── myProperty
├── name = myProperty
└── propertyIndex = true
Hoặc khai báo trong project (ui.apps/.../oak:index/.../.content.xml):
<jcr:root
jcr:primaryType="oak:QueryIndexDefinition"
type="lucene"
async="async"
reindex="{Boolean}false">
<indexRules jcr:primaryType="nt:unstructured">
<cq:Page jcr:primaryType="nt:unstructured">
<properties jcr:primaryType="nt:unstructured">
<articleCategory
jcr:primaryType="nt:unstructured"
name="jcr:content/articleCategory"
propertyIndex="{Boolean}true"/>
<publishDate
jcr:primaryType="nt:unstructured"
name="jcr:content/publishDate"
propertyIndex="{Boolean}true"
ordered="Date"/>
</properties>
</cq:Page>
</indexRules>
</jcr:root>
Sau khi tạo/deploy, set reindex=true (hoặc qua JMX IndexStats) để trigger re-indexing.
Warning
Lucene index chạy async — kết quả có thể chưa reflect thay đổi vừa write. Không dùng cho use case cần strong consistency.
Công cụ quản lý index
9. Best Practices & Lỗi thường gặp
Bảng tổng hợp
Warning
Query là một trong các operation tốn kém nhất trong AEM. Tránh chạy query trong component — nhiều query mỗi lần render page sẽ giảm performance nghiêm trọng. Đưa vào service + cache nếu cần.
Lỗi thường gặp
1. Query trả về jcr:content thay vì cq:Page
# Sai: trả về jcr:content nodes
type=cq:PageContent
property=cq:template
property.value=/conf/mysite/settings/wcm/templates/mytemplate
# Đúng: trả về cq:Page nodes
type=cq:Page
property=jcr:content/cq:template
property.value=/conf/mysite/settings/wcm/templates/mytemplate
2. Numeric prefix trùng nhau
# SAI: cùng prefix "1_" cho hai predicate khác nhau
1_property=jcr:title
1_property.value=Test
1_fulltext=Test # conflict!
# ĐÚNG
1_property=jcr:title
1_property.value=Test
2_fulltext=Test
3. type=nt:unstructured cho query lớn → trả hầu như mọi node, gây traversal. Dùng type cụ thể.
4. OR query thiếu index → mỗi nhánh p.or=true cần index riêng, nếu không hiệu năng rất kém.
10. Ví dụ thực tế (predicates)
Tìm trang dùng template cụ thể
type=cq:Page
path=/content/mysite
property=jcr:content/cq:template
property.value=/conf/mysite/settings/wcm/templates/article
orderby=@jcr:content/cq:lastModified
orderby.sort=desc
p.limit=20
p.guessTotal=true
Tìm asset upload trong 30 ngày qua
type=dam:Asset
path=/content/dam/mysite
relativedaterange.property=jcr:content/metadata/jcr:lastModified
relativedaterange.lowerBound=-30d
p.limit=50
p.guessTotal=true
Tìm trang trong nhiều path (OR)
type=cq:Page
fulltext=campaign
group.p.or=true
group.1_path=/content/mysite/en
group.2_path=/content/mysite/de
p.limit=20
Tìm trang bị ẩn trong navigation
type=cq:Page
path=/content/mysite
boolproperty=jcr:content/hideInNav
boolproperty.value=true
p.limit=100
p.guessTotal=true
Full-text search trên page content và tags
type=cq:Page
path=/content/mysite
group.p.or=true
group.1_fulltext=keyword
group.1_fulltext.relPath=jcr:content
group.2_fulltext=keyword
group.2_fulltext.relPath=jcr:content/@cq:tags
p.limit=20
p.guessTotal=true
11. Code mẫu production: ArticleQueryService
Đóng gói query vào OSGi service, tách khỏi component. executeQuery xử lý session null, null-check hit, và bắt lỗi tập trung.
package com.mysite.core.services.impl;
import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import com.mysite.core.services.ArticleQueryService;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.util.*;
@Component(service = ArticleQueryService.class)
public class ArticleQueryServiceImpl implements ArticleQueryService {
private static final Logger LOG = LoggerFactory.getLogger(ArticleQueryServiceImpl.class);
private static final String ARTICLES_ROOT = "/content/newssite/articles";
@Reference
private QueryBuilder queryBuilder;
@Override
public List<Resource> getLatestArticles(ResourceResolver resolver, int limit) {
Map<String, String> map = new HashMap<>();
map.put("path", ARTICLES_ROOT);
map.put("type", "cq:Page");
map.put("property", "jcr:content/articleStatus");
map.put("property.value", "published");
map.put("orderby", "@jcr:content/publishDate");
map.put("orderby.sort", "desc");
map.put("p.limit", String.valueOf(limit));
map.put("p.guessTotal", "true");
return executeQuery(resolver, map);
}
@Override
public List<Resource> getArticlesByCategory(ResourceResolver resolver,
String category, int limit) {
Map<String, String> map = new HashMap<>();
map.put("path", ARTICLES_ROOT);
map.put("type", "cq:Page");
map.put("property", "jcr:content/articleCategory");
map.put("property.value", category);
map.put("orderby", "@jcr:content/publishDate");
map.put("orderby.sort", "desc");
map.put("p.limit", String.valueOf(limit));
map.put("p.guessTotal", "true");
return executeQuery(resolver, map);
}
@Override
public List<Resource> searchArticles(ResourceResolver resolver,
String keyword, int limit, int offset) {
Map<String, String> map = new HashMap<>();
map.put("path", ARTICLES_ROOT);
map.put("type", "cq:Page");
map.put("fulltext", keyword);
map.put("fulltext.relPath", "jcr:content");
map.put("orderby", "@jcr:score");
map.put("orderby.sort", "desc");
map.put("p.limit", String.valueOf(limit));
map.put("p.offset", String.valueOf(offset));
map.put("p.guessTotal", "true");
return executeQuery(resolver, map);
}
@Override
public List<Resource> getRecentArticles(ResourceResolver resolver,
int daysBack, int limit) {
Map<String, String> map = new HashMap<>();
map.put("path", ARTICLES_ROOT);
map.put("type", "cq:Page");
map.put("relativedaterange.property", "jcr:content/publishDate");
map.put("relativedaterange.lowerBound", "-" + daysBack + "d");
map.put("orderby", "@jcr:content/publishDate");
map.put("orderby.sort", "desc");
map.put("p.limit", String.valueOf(limit));
return executeQuery(resolver, map);
}
private List<Resource> executeQuery(ResourceResolver resolver,
Map<String, String> predicates) {
Session session = resolver.adaptTo(Session.class);
if (session == null) {
LOG.error("Cannot adapt ResourceResolver to Session");
return Collections.emptyList();
}
List<Resource> results = new ArrayList<>();
try {
Query query = queryBuilder.createQuery(
PredicateGroup.create(predicates), session);
SearchResult result = query.getResult();
for (Hit hit : result.getHits()) {
try {
Resource resource = hit.getResource();
if (resource != null) {
results.add(resource);
}
} catch (RepositoryException e) {
LOG.warn("Error getting resource from hit: {}", e.getMessage());
}
}
} catch (Exception e) {
LOG.error("Query execution failed: {}", e.getMessage(), e);
}
return results;
}
}
12. Checklist trước khi deploy query lên production
Tham khảo