For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/aem/content-and-data/query-builder.md.

In-depth guide to AEM QueryBuilder predicates, pagination, indexing, and optimization.

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

Tình huốngDùng
Input từ user (servlet, request param)Query Builder — sanitize/escape an toàn
Query cố định, đơn giản trong coderesourceResolver.findResources() (SQL2/XPath)

Hai API truy cập JCR

APIMô tảKhi dùng
Sling Resource APICấp cao, làm việc với Resource/ResourceResolver, portable, testableMặc định, idiomatic trong AEM
JCR Node API (javax.jcr)Cấp thấp, làm việc trực tiếp Node/Session/PropertyChỉ khi Sling không expose: versioning, ordering, workspace ops

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

  1. Viết query trong Debugger
  2. 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
  3. Paste vào Explain Query (Tools → Operations → Diagnosis → Explain Query)
  4. Xem query plan → index nào được dùng
  5. 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

LoạiKhi nào dùng
Lucene (async)Property search, fulltext — phổ biến nhất
Property (sync)Unique constraint, tìm theo 1 property đơn giản
TraversalFallback khi không có index — tránh ở production

Dùng đúng type để tận dụng OOTB index

Query vềDùng typeIndex
Pagescq:PagecqPageLucene
DAM Assetsdam:AssetdamAssetLucene
Genericnt:unstructuredKhông có — traversal

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

Công cụURL
Index ManagerTools → Operations → Diagnosis → Index Manager
Query PerformanceTools → Operations → Diagnosis → Query Performance
Explain QueryTools → Operations → Diagnosis → Explain Query
Oak Index JSONhttp://localhost:4502/oak:index.tidy.-1.json
Reindex (JMX)http://localhost:4502/system/console/jmx → IndexStats

9. Best Practices & Lỗi thường gặp

Bảng tổng hợp

RuleĐúngSai
Giới hạn pathpath=/content/mysiteKhông có path
Type cụ thểtype=cq:Pagetype=nt:unstructured
Paginationp.limit=20, p.offset=0p.limit=-1 trong servlet
Result lớnp.guessTotal=trueresult.getTotalMatches()
Property conproperty=jcr:content/titleQuery trực tiếp trên node cha
IndexTạo custom index cho property query thường xuyênĐể traversal
DebugDùng Debugger trước khi codeViết code rồi mới test
ResourceResolverĐóng bằng try-with-resourcesĐể leak
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

  • path đặt càng hẹp càng tốt
  • type chỉ định node type cụ thể
  • p.limit hợp lý (10–100)
  • p.guessTotal=true thay vì đếm chính xác
  • Explain Query → xác nhận query hit index
  • Không còn WARN traversal trong error.log
  • Property custom đã có Oak index
  • Batch operations dùng pagination, không dùng p.limit=-1
  • Query nằm trong service, không chạy trực tiếp trong component

Tham khảo