# paoding-rose-elasticsearch
**Repository Path**: fusheng_zhang/paoding-rose-elasticsearch
## Basic Information
- **Project Name**: paoding-rose-elasticsearch
- **Description**: 简化es的操作语法,支持sql查询es。支持动态sql 等等,具体请查看文档
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 1
- **Created**: 2022-02-24
- **Last Updated**: 2025-04-03
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# es version
```json
{
"name": "node-1",
"cluster_name": "elasticsearch",
"cluster_uuid": "44_cwRu_T9--yqHRXMcuSQ",
"version": {
"number": "7.4.2",
"build_flavor": "default",
"build_type": "tar",
"build_hash": "2f90bbf7b93631e52bafb59b3b049cb44ec25e96",
"build_date": "2019-10-28T20:40:44.881551Z",
"build_snapshot": false,
"lucene_version": "8.2.0",
"minimum_wire_compatibility_version": "6.8.0",
"minimum_index_compatibility_version": "6.0.0-beta1"
},
"tagline": "You Know, for Search"
}
```
# start
## 引入依赖
```xml
cn.zhangfusheng
paoding-rose-elasticsearch
1.0
```
```yaml
spring:
elasticsearch:
uris:
- http://192.168.18.200:9201
```
## 启动类添加注解[EnablePaodingRoseElasticSearch]
* EnablePaodingRoseElasticSearch属性描述
* value/scanReposityPackages: 要扫描的 cn.zhangfusheng.elasticsearch.repository.ElasticSearchRepository 的实现类的包
* mybatisXmlScanPath: mybatis xml的扫描路径,默认为classpath*:mapper/**/*.xml
* mode: 暂无作用
```java
package com.paoding.rose.elasticsearch.demo2;
import cn.zhangfusheng.elasticsearch.annotation.EnablePaodingRoseElasticSearch;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author fusheng.zhang
* @date 2022-03-02 11:44:13
*/
@SpringBootApplication
@EnablePaodingRoseElasticSearch
public class Demo2StartApplication {
public static void main(String[] args) {
SpringApplication.run(Demo2StartApplication.class, args);
}
}
```
# create index and mapping
## index 和 mapping的描述
* index描述 [IndexDiscription]
* value:索引名称,如果为空,则使用类名的全小写格式
* version: 索引版本号,建议从1开始,依次增加
* upgradeVersion: 索引升级编号,同version一起控制索引升级/更新/数据迁移
* createMappingBefore: 创建mapping前执行
* createMappingEnd: 创建mapping后执行
* mapping字段描述 [FieldMapping]
* primaryId: 每一个mapping必须配置 FieldMapping.primaryId=true的字段,且只能存在一个
* routing: 描述字段为routing标识
* ignore: 忽略该字段
* type: 字段类型,对应es文档的字段类型,如果未配置,则按照默认解析,详情查看 AnalysisMapping.getEsType
* geoPoints: FieldType.Geo_Point类型的补充
* alias: FieldType.Alias类型的补充
* patameters: 字段属性配置,对应es文档https://www.elastic.co/guide/en/elasticsearch/reference/7.4/mapping-params.html
* 测试demo TbCompany 包含了绝大部分的用户,具体使用可以参考
```java
package com.paoding.rose.elasticsearch.demo.model;
import cn.zhangfusheng.elasticsearch.annotation.document.IndexDescription;
import cn.zhangfusheng.elasticsearch.annotation.document.field.FieldMapping;
import cn.zhangfusheng.elasticsearch.annotation.document.field.MappingParameters;
import cn.zhangfusheng.elasticsearch.annotation.document.field.parameters.Alias;
import cn.zhangfusheng.elasticsearch.annotation.document.field.parameters.GeoPoint;
import cn.zhangfusheng.elasticsearch.annotation.document.field.parameters.IndexPrefixes;
import cn.zhangfusheng.elasticsearch.constant.enumeration.FieldType;
import cn.zhangfusheng.elasticsearch.util.date.enumeration.DateFormat;
import com.paoding.rose.elasticsearch.demo.cycle.CreateCompanyMappingAround;
import com.paoding.rose.elasticsearch.demo.model.vo.CompanyField;
import com.paoding.rose.elasticsearch.demo.model.vo.CompanyUser;
import com.paoding.rose.elasticsearch.demo.model.vo.DateRange;
import com.paoding.rose.elasticsearch.demo.model.vo.IntegerRangeDetail;
import com.paoding.rose.elasticsearch.demo.model.vo.TagsField;
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
/**
* @author fusheng.zhang
* @date 2022-02-28 13:37:11
*/
@Data
@Accessors(chain = true)
@IndexDescription( // index: tbcompany_v1
value = "tbcompany", // 索引名称,默认为全小写的类名
version = 1, // 索引版本号,
upgradeVersion = "2022-05-10 20:14:30",// 索引更新(创建.升级.更新)编号
createMappingBefore = CreateCompanyMappingAround.class, // 创建索引前执行
createMappingEnd = CreateCompanyMappingAround.class // 创建索引后执行
)
public class TbCompany {
/**
* 主键,必须使用primaryId=true,标注字段为主键
*/
@FieldMapping(primaryId = true)
private String rowId;
/**
* 利用 patameters 配置文档字段的属性,
* 具体查看es文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.4/mapping-params.html
*/
@FieldMapping(mappingParameters = @MappingParameters(index_prefixes = @IndexPrefixes))
private String createUserName;
/**
* analyzer.copy_to.fields 的 Mapping Patameters的使用
*/
@FieldMapping(mappingParameters = {
@MappingParameters(analyzer = "keyword", copy_to = {"searchName", "searchField"}, fields = CompanyField.class)
})
private String companyName;
@FieldMapping(mappingParameters = @MappingParameters(copy_to = {"searchName", "searchField"}))
private String addressName;
@FieldMapping(mappingParameters = @MappingParameters(analyzer = "whitespace"))
private String searchName;
@FieldMapping(mappingParameters = @MappingParameters(analyzer = "whitespace"))
private String searchField;
private Integer sort;
@FieldMapping(routing = true)
private Integer group;
/**
* 地理 geo_point 字段的配置
*/
@FieldMapping(type = FieldType.Geo_Point, geoPoint = @GeoPoint)
private String location;
/**
* arrays 类型字段的配置使用
*/
@FieldMapping(type = FieldType.Arrays, mappingParameters = @MappingParameters(properties = CompanyUser.class))
private List companyUsers;
/**
* object 类型字段的使用
*/
@FieldMapping(type = FieldType.Object, mappingParameters = @MappingParameters(properties = CompanyUser.class))
private CompanyUser companyUser;
/**
* Nested 字段类型
*/
@FieldMapping(type = FieldType.Nested)
private CompanyUser companyUserNested;
/**
* Alias 字段类型
*/
@FieldMapping(type = FieldType.Alias, alias = @Alias(path = "companyUser.userName"))
private String companyUserNameAlias;
@FieldMapping(mappingParameters = @MappingParameters(fields = TagsField.class))
private List tags;
/**
* range 字段类型
*/
@FieldMapping(type = FieldType.Integer_Range)
private IntegerRangeDetail integerRangeDetail;
/**
* date_range 字段类型
*/
@FieldMapping(type = FieldType.Date_Range,
mappingParameters = @MappingParameters(format = "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"))
private DateRange dateRange;
private LocalDateTime defaultTime;
private Date defaultDate;
/**
* 时间格式化
*/
@FieldMapping(mappingParameters = @MappingParameters(format = DateFormat.YYYY_MM_DD_HH_MM_SS))
private LocalDateTime updateTime;
@FieldMapping(mappingParameters = @MappingParameters(format = DateFormat.YYYY_MM_DD_HH_MM_SS))
private Date createTime;
}
```
## 创建 index 和 mapping
* ElasticSearchRepository<>,接口必须继承该接口
```java
public interface TbCompanyRepository extends ElasticSearchRepository {
}
```
## 启动项目 观察日志
* 项目首次启动时,会创建版本控制的index和mapping
* index: transfer_v1
* mapping
```json lines
{
"properties": {
"rowId": {
"type": "keyword"
},
"transfer": {
"type": "boolean"
},
"indexName": {
"type": "text"
},
"upIndexName": {
"type": "text"
},
"className": {
"type": "keyword"
},
"version": {
"type": "integer"
},
"upgradeVersion": {
"type": "text"
},
"transferVersion": {
"type": "keyword"
},
"desc": {
"type": "text"
},
"createTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
```
* 创建 Entity 对应的index和mapping [entity对应的 Repository 必须被使用(注入)]
* indexName:tbcompany_v1,version:1
* mapping
```json lines
{
"properties": {
"rowId": {
"type": "keyword"
},
"createUserName": {
"type": "text",
"index_prefixes": {
"max_chars": 5,
"min_chars": 2
}
},
"companyName": {
"type": "text",
"analyzer": "keyword",
"copy_to": [
"searchName",
"searchField"
],
"fields": {
"one": {
"type": "text",
"analyzer": "whitespace"
},
"two": {
"type": "text",
"analyzer": "whitespace"
}
}
},
"addressName": {
"type": "text",
"copy_to": [
"searchName",
"searchField"
]
},
"searchName": {
"type": "text",
"analyzer": "whitespace"
},
"searchField": {
"type": "text",
"analyzer": "whitespace"
},
"sort": {
"type": "integer"
},
"group": {
"type": "text"
},
"location": {
"type": "geo_point"
},
"companyUsers": {
"properties": {
"userName": {
"type": "text"
},
"age": {
"type": "integer"
}
}
},
"companyUser": {
"properties": {
"userName": {
"type": "text"
},
"age": {
"type": "integer"
}
}
},
"companyUserNested": {
"type": "nested"
},
"companyUserNameAlias": {
"type": "alias",
"path": "companyUser.userName"
},
"tags": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"integerRangeDetail": {
"type": "integer_range"
},
"dateRange": {
"type": "date_range",
"format": "uuuu-MM-dd'T'HH:mm:ss.SSS'Z'||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"defaultTime": {
"type": "date"
},
"defaultDate": {
"type": "date"
},
"updateTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"createTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
```
# ElasticSearchRepository
## 默认方法使用
| 方法名 | 描述 | 参数 |
|-------------|-----------------|---------------------|
| insert | 新建一条数据 | Repository对应的Entity |
| updateById | 更新一条数据 | Repository对应的Entity |
| deleteById | 删除一条数据 | 主键 |
| findById | 根据主键查询 | 主键 |
| findOne | 根据查询条件只能查询到一条数据 | 可以自定义查询条件 |
| findAll | 查询全部数据 | 可以自定义查询条件 |
| findForPage | 分页查询 | 可以自定义查询条件,已经分页参数 |
## 注解的使用
* DslSearch注解 : 可通过该注解控制解析的语句
* DslSortOrder注解: 排序配置
## 个别类
* EsRange 范围数据查询使用
* GeoDistance 地图数据查询
# jpa/动态sql/mybatis 查询
## jpa 查询
> 不需要注解标注,具体用法可查看 JpaCompanyRepository
* 普通查询
* 支持jpa的方法名查询语法
* 自定义关键字查询
* 例如 模糊查询 like,es的模糊查询有 match/match_phrase 等,我们可以通过注解[JpaSearch]指定查询字段使用的关键字
* between and 查询
* 范围查询: 对应字段对应的参数必须是 [JpaBetween]
* 分页查询 [详见分页查询]
* 明确无查询条件的查询,建议使用ElasticSearchRepository.findForPage方法,
* 查询全部数据
* 查询全部数据默认最多查询到 1W 条数据,可以通过注解ElasticSearchConfig.trackTotalHits=true,查询全部数据
* 明确无查询条件的查询,建议使用ElasticSearchRepository.findAll();
## mybatis 查询
> DslWithMybatis注解标注
### 参数名
#### 默认参数名的使用
> Repository方法的参数列表未配置参数别名时,mybatis的xml文件获取参数名的格式为 arg_(index),index为参数的顺序,从1开始
```
@DslWithMybatis(id = "searchById", nameSpace = "com.paoding.rose.elasticsearch.demo.model.TbCompany")
TbCompany searchById(String rowId);
arg_1 对应参数的 rowId
```
```es
cn.zhangfusheng.elasticsearch.template.TemplateMybatisApi : dynamic dsl:{"bool": {"must": [ {"term": {"rowId": {"value": "wrzoSIdZQUmCuCnbyJDWdw"}}} ]}}
cn.zhangfusheng.elasticsearch.template.TemplateMybatisApi : result dsl:{"bool": {"must": [ {"term": {"rowId": {"value": "wrzoSIdZQUmCuCnbyJDWdw"}}} ]}}
cn.zhangfusheng.elasticsearch.template.ElasticSearchTemplateApi : index:[tbcompany_v1],routing:null,queryJson:{"size":10000,"query":{"wrapper":{"query":"eyJib29sIjogeyJtdXN0IjogWyB7InRlcm0iOiB7InJvd0lkIjogeyJ2YWx1ZSI6ICJ3cnpvU0lkWlFVbUN1Q25ieUpEV2R3In19fSBdfX0="}},"track_total_hits":-1}
cn.zhangfusheng.elasticsearch.template.ElasticSearchTemplateApi : index:[tbcompany_v1],routing:null,queryJson:{"from":0,"size":1000,"query":{"wrapper":{"query":"eyJib29sIjogeyJtdXN0IjogWyB7InRlcm0iOiB7InJvd0lkIjogeyJ2YWx1ZSI6ICJ3cnpvU0lkWlFVbUN1Q25ieUpEV2R3In19fSBdfX0="}},"track_total_hits":-1}
```
#### 自定义参数名
> 使用 DslParams 注解自定义参数名
```
@DslWithMybatis(id = "queryById", nameSpace = "com.paoding.rose.elasticsearch.demo.model.TbCompany")
TbCompany queryById(@DslParams("rowId") String rowId);
```
```es
> rowId 不为空
cn.zhangfusheng.elasticsearch.template.TemplateMybatisApi : dynamic dsl:{"bool": {"must": [ {"term": {"rowId": {"value": "wrzoSIdZQUmCuCnbyJDWdw"}}} ]}}
cn.zhangfusheng.elasticsearch.template.TemplateMybatisApi : result dsl:{"bool": {"must": [ {"term": {"rowId": {"value": "wrzoSIdZQUmCuCnbyJDWdw"}}} ]}}
cn.zhangfusheng.elasticsearch.template.ElasticSearchTemplateApi : index:[tbcompany_v1],routing:null,queryJson:{"size":10000,"query":{"wrapper":{"query":"eyJib29sIjogeyJtdXN0IjogWyB7InRlcm0iOiB7InJvd0lkIjogeyJ2YWx1ZSI6ICJ3cnpvU0lkWlFVbUN1Q25ieUpEV2R3In19fSBdfX0="}},"track_total_hits":-1}
> rowId 为空
cn.zhangfusheng.elasticsearch.template.TemplateMybatisApi : dynamic dsl:{"bool": {"must": [ ]}}
cn.zhangfusheng.elasticsearch.template.TemplateMybatisApi : result dsl:{"bool": {"must": [ ]}}
cn.zhangfusheng.elasticsearch.template.ElasticSearchTemplateApi : index:[tbcompany_v1],routing:null,queryJson:{"size":10000,"query":{"wrapper":{"query":"eyJib29sIjogeyJtdXN0IjogWyBdfX0="}},"track_total_hits":-1}
```
### 符号(#)($)的注意事项
* 使用(#)
> 使用(#)时,[#(arg_1)]的前后可以加也可以不加双引号
```
# 假设第一个参数为 张三
xml:{"value":#{arg_1}},解析结果:{"value":"张三"}
xml:{"value":"#{arg_1}"},解析结果:{"value":"张三"}
```
* 使用($)
> 使用($)时,[$(arg_1)]的前后必须加双引号,如果数据类型为int doublue float 等,也可以不加双引号
```
# 假设第一个参数为 张三
xml:{"value":${arg_1}},解析结果:{"value":张三}
xml:{"value":"${arg_1}"},解析结果:{"value":"张三"}
```
* 使用(#)($)的demo和日志
```
@DslWithMybatis(id = "searchByIdInOrId", nameSpace = "com.paoding.rose.elasticsearch.demo.model.TbCompany")
List searchByIdInOrId(@DslParams("rowIds") List rowIds, @DslParams("rowId") String rowId);
cn.zhangfusheng.elasticsearch.template.TemplateMybatisApi : dynamic dsl:{"bool": { "should": [ {"terms": {"rowId": [ "CQXY0OibSXqrz-zsHU39Xg" , "O-m-W07qTM-dlDmWbP84AQ" ]}} ,{"term": {"rowId": {"value": ?}}} ]}}
cn.zhangfusheng.elasticsearch.template.TemplateMybatisApi : result dsl:{"bool": { "should": [ {"terms": {"rowId": [ "CQXY0OibSXqrz-zsHU39Xg" , "O-m-W07qTM-dlDmWbP84AQ" ]}} ,{"term": {"rowId": {"value": "wrzoSIdZQUmCuCnbyJDWdw"}}} ]}}
cn.zhangfusheng.elasticsearch.template.ElasticSearchTemplateApi : index:[tbcompany_v1],routing:null,queryJson:{"size":10000,"query":{"wrapper":{"query":"eyJib29sIjogeyAic2hvdWxkIjogWyB7InRlcm1zIjogeyJyb3dJZCI6IFsgIkNRWFkwT2liU1hxcnotenNIVTM5WGciICwgIk8tbS1XMDdxVE0tZGxEbVdiUDg0QVEiIF19fSAseyJ0ZXJtIjogeyJyb3dJZCI6IHsidmFsdWUiOiAid3J6b1NJZFpRVW1DdUNuYnlKRFdkdyJ9fX0gXX19"}},"track_total_hits":-1}
```
## 动态sql 查询
> 必须用 DslWithSql 进行标注,主要语法文档 https://github.com/iamazy/elasticsearch-sql
* 参数列表获取规则
* 使用 DslParams 注解标定参数名,可通过(:参数名)取值
* 参数为实体: 可通过(:参数名.属性)取值
* 未标定参数名,参数名为 (:arg_[index]) index:为参数顺序,从1开始
* 冒号(:)
* 表示这是一个变量
* $ 表达式
* 使用($)从常量内读取值
```java
import cn.zhangfusheng.elasticsearch.repository.ElasticSearchRepository;
public interface Demo extends ElasticSearchRepository {
static final String INDEX_NAME = "tb_company";
/**
* 解析结果为 select * from tb_company
* @return
*/
@DslWithSql("select * from $INDEX_NAME")
List searchAll();
}
```
* (##)双#表达式
* 表示后面的变量作字符串连接
```java
import cn.zhangfusheng.elasticsearch.annotation.dsl.DslParams;
public interface Demo extends ElasticSearchRepository {
/**
* 假设 tableName=company
* 解析结果为 select * from tb_company
* @param tableName
* @return
*/
@DslWithSql("select * from tb_##(:tableName)")
List searchAll(@DslParams("tableName") String tableName);
}
```
* (#if) 表达式
```
@DslWithSql(value = "select * from $INDEX_NAME", conditions = {
@SqlCondition("#if(:rowIds != null){rowId in (:rowIds)}")
})
List findByIdIn(@DslParams("rowIds") List rowId);
cn.zhangfusheng.elasticsearch.template.TemplateDynamicSqlApi : result sql:select * from tb_company where rowId in ('CQXY0OibSXqrz-zsHU39Xg','O-m-W07qTM-dlDmWbP84AQ')
cn.zhangfusheng.elasticsearch.template.ElasticSearchTemplateApi : index:[tbcompany_v1],routing:null,queryJson:{"from":0,"size":10000,"query":{"terms":{"rowId":["CQXY0OibSXqrz-zsHU39Xg","O-m-W07qTM-dlDmWbP84AQ"],"boost":1.0}},"_source":{"includes":[],"excludes":[]},"track_total_hits":-1}
cn.zhangfusheng.elasticsearch.template.ElasticSearchTemplateApi : index:[tbcompany_v1],routing:null,queryJson:{"from":0,"size":1000,"query":{"terms":{"rowId":["CQXY0OibSXqrz-zsHU39Xg","O-m-W07qTM-dlDmWbP84AQ"],"boost":1.0}},"_source":{"includes":[],"excludes":[]},"track_total_hits":-1}
```
* (#for)表达式
* 注: 可以通过:_index获取当前索引,从1开始
```java
import cn.zhangfusheng.elasticsearch.repository.ElasticSearchRepository;
public interface Demo extends ElasticSearchRepository {
@DslWithSql(value = "select * from $INDEX_NAME", conditions = {
@SqlCondition("#for(tbCompany in :tbCompanies){#if(:_index == 1){rowId = :tbCompany.rowId}}")
})
List testFor(@DslParams("tbCompanies") List tbCompanies);
}
```
# 动态字符串解析
## 动态sql解析
> 利用表达式表述条件,解析表达式生成最终的sql,并转成查询的dsl语句
## 动态query dsl解析
> 同mybatis的xml语法类似
## 动态script解析
# 特殊用法
## 分页查询
> 方法添加类型为 PageRequest的参数, 且返回值为 PageResponse,自动识别为分页查询
* 注:
* 默认只查询前1W条的分页数据,如果想获取超过1W的总条数,可添加注解ElasticSearchConfig.trackTotalHits=true,返回正确的总条数
* 前1W条数据可以正常的跳页查询,超过1W条数据后,可以通过PageRequest.searchAfter参数,继续向后查询.前1W条数据也可以使用searchAfter查询
* 支持超过1W的跳页查询逻辑
## 多索引查询以及返回值配置
> 使用 DSLIndex 注解,配置value属性,执行要查询的索引对应的Entity,配置returnType属性,控制返回值
> 多个索引对应的多个类内相同的字段类型必须一致
## 动态sql/mybatis的方法的参数特点
> 可通过 DslParams.value 配置参数的别名
* 动态sql
* 未配置参数别名,参数的获取格式为 (:arg_[index]) index从1开始
* 配置参数别名,则格式为 (:别名)
* mybatis
* 为配置参数别名,参数的获取格式为 (${arg_[index]}) index从1开始
* 配置别名,则用别名获取 (${别名})
* 如果参数为Object,可以通过点(.)的方式获取 (${别名}.id)
## 索引升级
> 当文档发生变化时,索引需要进行升级操作.即IndexDiscription注解相关配置的调整.
* mapping删除字段时
* 无需进行任何处理
* mapping增加字段时
* 调整 IndexDiscription.upgradeVersion,项目启动后会自动调整Mapping
* 未调整IndexDiscription.upgradeVersion,在进行数据修改时,es会自动根据数据进行文档字段的配置(不建议)
* mapping 调整某个字段类型
* 必须调整 IndexDiscription.version,生成新版本的index和mapping
* 调整version后,系统会自动的进行数据迁移
## 数据迁移
> 将数据从某个索引迁移至当前索引.数据迁移提供两种方式,请查看枚举类 TransferType
* 当IndexDiscription.version发生变化时会自动触发数据迁移
* 通过调整 IndexTransfer.upgradeVersion的值触发数据迁移
## requestOptions
> 自定义 request options
```java
import cn.zhangfusheng.elasticsearch.request.PaodingRequestOptions;
import org.elasticsearch.client.RequestOptions;
// 新建类 实现 PaodingRequestOptions
public class ProjectRequestOptions implements PaodingRequestOptions {
@Override
public RequestOptions options() {
// 返回自定义的 requestOptions
return null;
}
@Override
public String requestOptions() {
// 返回对应的唯一标识
return null;
}
}
// 注入到spring 容器内,使用时可通过注解 ElasticSearchConfig.requestOptions 指定唯一标识,进而使用对应的requestOptions
```