# ES 使用规范

> 基于 `sieiot-es-plus-starter`（ES 8.x），使用静态链式 API 操作 Elasticsearch。

---

## 一、静态链式 API
通过 `Es.chain*` 无需注入 Service 直接操作 ES，适用于所有业务场景。

### 实体类定义 （可不定义）
```java
@Data
@EsIndex(index = "sys_user")
public class SysUser {
    @EsId
    private Long id;

    @EsField(type = EsFieldType.KEYWORD)
    private String username;

    private String nickName;
    private Integer lockState;

    @EsField(type = EsFieldType.NESTED)
    private SysRole sysRole;
}
```
### 不定义实体类时或者实体类无@EsIndex(index = "sys_user")时
当实体对象xxx不存在索引注解时，所有的查询都需要手动指定索引。

```java
Es.chainLambdaQuery(Map.class).index("xxxx")
Es.chainLambdaUpdate(Map.class).index("xxxx")
```


### 查询

```java
// 精确查询
EsResponse<SysUser> resp = Es.chainLambdaQuery(SysUser.class)
    .must()
    .term(SysUser::getUsername, "admin")
    .ge(SysUser::getAge, 18)
    .list();
List<SysUser> list = resp.getList();

// 全文检索 + 排序 + 分页（≤10000 条场景）
EsResponse<SysUser> resp = Es.chainLambdaQuery(SysUser.class)
    .must()
    .match(SysUser::getContent, "关键词")
    .sortByDesc(SysUser::getCreateTime)
    .searchPage(1, 10);

// 布尔组合查询：(age>=18) AND (username=admin OR username=hzh) AND (deleted!=true)
EsResponse<SysUser> resp = Es.chainLambdaQuery(SysUser.class)
    .must()
    .ge(SysUser::getAge, 18)
    .must(w -> w.should()
        .term(SysUser::getUsername, "admin")
        .term(SysUser::getUsername, "hzh"))
    .must(w -> w.mustNot()
        .term(SysUser::getDeleted, true))
    .list();

// 动态条件（参数为 null 时忽略该条件）
EsResponse<SysUser> resp = Es.chainLambdaQuery(SysUser.class)
    .must()
    .term(username != null, SysUser::getUsername, username)
    .ge(minAge != null, SysUser::getAge, minAge)
    .searchPage(1, 10);

// 嵌套查询
EsResponse<SysUser> resp = Es.chainLambdaQuery(SysUser.class)
    .must()
    .nestedQuery(SysUser::getRoles, SysRole.class, w ->
        w.must().term(SysRole::getState, true))
    .list();

// 指定自定义索引
EsResponse<SysUser> resp = Es.chainLambdaQuery(SysUser.class)
    .index("sys_user_2024")
    .term(SysUser::getUsername, "admin")
    .list();

// 按 ID 查询
SysUser user = Es.chainLambdaQuery(SysUser.class)
    ._id("123")
    .search()
    .getOne();
```

### 聚合

```java
EsResponse<SysUser> resp = Es.chainLambdaQuery(SysUser.class)
    .must()
    .terms(SysUser::getStatus, 1, 2)
    .esLambdaAggWrapper()
    .terms(SysUser::getUsername, a -> a.size(1000))
    .subAggregation(t -> t.count(SysUser::getId))
    .subAggregation(t -> t.avg(SysUser::getAge))
    .search();

EsAggregationsResponse<SysUser> agg = resp.getEsAggregationsResponse();
Map<String, Long> termsMap = agg.getTermsAsMap(SysUser::getUsername);
```

### 写入 / 更新 / 删除

```java
// 保存（有实体类）
Es.chainLambdaUpdate(SysUser.class).save(user);

// 批量保存
Es.chainLambdaUpdate(SysUser.class).saveBatch(userList);

// 条件删除
Es.chainLambdaUpdate(SysUser.class)
    .term(SysUser::getUsername, "admin")
    .remove();

// 无实体类，直接操作 Map
Map<String, Object> data = new HashMap<>();
data.put("id", "123");
data.put("username", "admin");
Es.chainUpdate(Map.class).index("sys_user").save(data);
```



---

## 二、深分页（`EsPaginationUtil`）

ES 默认 `from + size` 分页有 `search-size`（默认 5000）上限。**超过 5000 条的分页必须使用 `EsPaginationUtil`**，其基于 `search_after` 游标实现无限深分页。

> **原理**：首页/下一页/上一页/末页四种操作通过游标（`sortValues`）定位，前端需保存并回传 `prevSortValues` / `nextSortValues`。

### 使用方式

```java
@Autowired
private EsPaginationUtil esPaginationUtil;

public EsPaginationVo<Map<String, Object>> queryPage(EsPaginationPo<?> po, Query query) throws Exception {
    // 排序字段（必须包含唯一性字段，如 id，否则游标会不稳定）
    List<EsOrderBean> orderBeans = new ArrayList<>();
    EsOrderBean order1 = new EsOrderBean();
    order1.setField("createTime");
    order1.setSort(SortOrder.Desc);
    orderBeans.add(order1);
    EsOrderBean order2 = new EsOrderBean();   // 唯一性保障
    order2.setField("id");
    order2.setSort(SortOrder.Asc);
    orderBeans.add(order2);

    return esPaginationUtil.searchPageDataWithAfter(
        po.getPageAction(),   // HOME / NEXT / PREV / LAST
        "sys_user",           // 索引名
        query,                // ES Query 对象（用原生 API 构建）
        po.getFields(),       // 返回字段，逗号分隔，null 返回全部
        po.getPageIndex(),    // 当前页码
        po.getPageRows(),     // 每页条数
        orderBeans,
        po.getNextSort(),     // 下一页游标（前端回传）
        po.getPrevSort()      // 上一页游标（前端回传）
    );
}
```

### 请求参数（`EsPaginationPo`）

| 字段 | 类型 | 说明 |
|------|------|------|
| `pageAction` | `EsPageAction` | 操作类型：`HOME`首页、`NEXT`下一页、`PREV`上一页、`LAST`末页 |
| `pageIndex` | `Integer` | 当前页码（默认 1） |
| `pageRows` | `Integer` | 每页条数（默认 10） |
| `fields` | `String` | 返回字段，逗号分隔，null 返回全部 |
| `nextSort` | `List<Object>` | 下一页游标，前端从上次响应的 `nextSortValues` 回传 |
| `prevSort` | `List<Object>` | 上一页游标，前端从上次响应的 `prevSortValues` 回传 |

### 响应结果（`EsPaginationVo`）

| 字段 | 类型 | 说明 |
|------|------|------|
| `curIndex` | `Integer` | 当前页码 |
| `pageSize` | `Integer` | 每页大小 |
| `pagesCount` | `Integer` | 总页数 |
| `count` | `Long` | 总记录数 |
| `data` | `List<T>` | 当前页数据 |
| `nextSortValues` | `List<Object>` | 下一页游标（前端翻页时回传） |
| `prevSortValues` | `List<Object>` | 上一页游标（前端翻页时回传） |

### 交互流程

```
首次请求：pageAction=HOME，nextSort=null，prevSort=null
↓ 响应返回 nextSortValues=[v1,v2]，prevSortValues=[v3,v4]

下一页：pageAction=NEXT，nextSort=[v1,v2]（上次的 nextSortValues）
上一页：pageAction=PREV，prevSort=[v3,v4]（上次的 prevSortValues）
末页：  pageAction=LAST，nextSort=null，prevSort=null
```

---

## 三、常用查询方法速查

| 方法 | 含义 | 适用类型 |
|------|------|------|
| `term(field, value)` | 精确匹配 | KEYWORD、数值、布尔 |
| `terms(field, v1, v2...)` | 多值匹配（IN） | KEYWORD |
| `match(field, value)` | 全文检索 | TEXT |
| `ge/le/gt/lt(field, value)` | 范围查询 | 数值、日期 |
| `between(field, from, to)` | 区间查询 | 数值、日期 |
| `_id(id)` | 按文档 ID 查询 | - |
| `sortByDesc/sortByAsc(field)` | 排序 | - |
| `searchPage(page, size)` | 分页（≤5000） | - |
| `list()` | 查询全部（≤5000） | - |
| `count()` | 统计数量 | - |

---

## 四、禁止事项

- **禁止**对 `TEXT` 类型字段使用 `term` 精确查询（应使用 `match`）
- **禁止**在循环中单条调用 `save()`，批量操作必须用 `saveBatch()`
- **禁止**不加任何查询条件执行 `remove()`（全量删除）
- **禁止**数据量超过 5000 时使用 `searchPage()` 或 `list()`，必须改用 `EsPaginationUtil`
- **禁止**深分页排序字段不包含唯一性字段（如 `id`），否则游标不稳定导致数据重复或遗漏
