> 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.

# 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ống                             | Dùng                                            |
| -------------------------------------- | ----------------------------------------------- |
| Input từ user (servlet, request param) | **Query Builder** — sanitize/escape an toàn     |
| Query cố định, đơn giản trong code     | `resourceResolver.findResources()` (SQL2/XPath) |

### Hai API truy cập JCR

| API                            | Mô tả                                                                   | Khi dùng                                                        |
| ------------------------------ | ----------------------------------------------------------------------- | --------------------------------------------------------------- |
| **Sling Resource API**         | Cấp cao, làm việc với `Resource`/`ResourceResolver`, portable, testable | Mặc định, idiomatic trong AEM                                   |
| **JCR Node API** (`javax.jcr`) | Cấp thấp, làm việc trực tiếp `Node`/`Session`/`Property`                | Chỉ 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

```java
@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

```properties
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

```properties
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

```properties
# 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

```properties
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

```properties
# 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

```properties
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

```properties
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

```properties
boolproperty=jcr:content/hideInNav
boolproperty.value=true
```

`false` match cả node không có property đó.

### `tag` và `tagid` — Tìm theo tag

```properties
# 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

```properties
# (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

```java
// (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

```properties
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ả

```properties
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
```

```java
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

```properties
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

```properties
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ụ:
   ```text
   /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ại                | Khi 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  |
| **Traversal**       | Fallback khi không có index — tránh ở production |

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

| Query về   | Dùng type         | Index                |
| ---------- | ----------------- | -------------------- |
| Pages      | `cq:Page`         | `cqPageLucene`       |
| DAM Assets | `dam:Asset`       | `damAssetLucene`     |
| Generic    | `nt:unstructured` | Khô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`):

```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 Manager     | `Tools → Operations → Diagnosis → Index Manager`          |
| Query Performance | `Tools → Operations → Diagnosis → Query Performance`      |
| Explain Query     | `Tools → Operations → Diagnosis → Explain Query`          |
| Oak Index JSON    | `http://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             | Đúng                                             | Sai                           |
| ---------------- | ------------------------------------------------ | ----------------------------- |
| Giới hạn path    | `path=/content/mysite`                           | Không có path                 |
| Type cụ thể      | `type=cq:Page`                                   | `type=nt:unstructured`        |
| Pagination       | `p.limit=20`, `p.offset=0`                       | `p.limit=-1` trong servlet    |
| Result lớn       | `p.guessTotal=true`                              | `result.getTotalMatches()`    |
| Property con     | `property=jcr:content/title`                     | Query trực tiếp trên node cha |
| Index            | Tạo custom index cho property query thường xuyên | Để traversal                  |
| Debug            | Dùng Debugger trước khi code                     | Viế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`**

```properties
# 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**

```properties
# 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ể

```properties
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

```properties
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)

```properties
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

```properties
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

```properties
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.

```java
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

- [AEM 6.5 Query Builder API](https://experienceleague.adobe.com/en/docs/experience-manager-65/content/implementing/developing/platform/query-builder/querybuilder-api)
- [Query Builder Predicate Reference](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/developing/full-stack/search/query-builder-predicates)
- [Oak Queries and Indexing (AEM 6.5)](https://experienceleague.adobe.com/en/docs/experience-manager-65/content/implementing/deploying/deploying/queries-and-indexing)
- [JCR Query Cheatsheet (PDF)](https://experienceleague.adobe.com/docs/experience-manager-65/assets/JCR_query_cheatsheet-v1.1.pdf)
