Bboss——高性能ElasticSearch客户端实践总结

388 阅读14分钟

由于工作中需要用到ElasticSearch,而现有的ES客户端五花八门,于是从前辈那里了解到了一款不算热门的客户端工具,之前没有听说过Bboss,为避免遗忘,这里总结了一篇文档来记录本次的学习开发过程。

Bboss介绍

官方说明:bboss是一款高性能elasticsearch ORM开发库, 以类似于mybatis管理和配置sql的方式,采用xml文件管理elasticsearch的dsl脚本,在dsl脚本中可以使用变量、dsl片段、foreach循环、逻辑判断、注释;支持在线修改、自动热加载dsl配置文件,开发和调试非常方便。

简单来说:Bboss就是Elasticsearch的Java客户端。

es和os区别:Elasticsearch是由Elastic公司维护和开发的,OpenSearch由亚马逊AWS公司创建和维护,它基于Elasticsearch的开源代码进行了修改和优化,主要是为了适应云环境下的场景和需求,总的来说,Elasticsearch和OpenSearch很相似,Elasticsearch拥有一个庞大的社区,有很多的插件和工具可供使用,社区支持也比较好。而OpenSearch的社区相对小一些,但随着OpenSearch的推出,也会逐渐增长

es常见的java客户端

  • Bboss(国内开源社区自主开发)
  • Spring Data Elasticsearch(Spring框架提供)
  • REST High Level Client(Elastic官方提供)

bboss在性能表现上优于Rest High Level Client,因为它采用了连接池技术、异步IO操作和多线程等技术来提高请求响应速度和吞吐量。

另外,我最看重的一点就是兼容性很好。无需考虑es和springboot的版本,这是其他es客户端无法做到的。

bbossElasticsearchspring boot
all1.x1.x,2.x,3.x
all2.x1.x,2.x,3.x
all5.x1.x,2.x,3.x
all6.x1.x,2.x,3.x
all7.x1.x,2.x,3.x
all8.x1.x,2.x,3.x

jdk兼容性:jdk 1.8+

如果喜欢直接使用query dsl(es的官方语言),但是又不想在代码里面编写冗长的dsl拼接串的话,可以考虑采用 bboss。

bboss整合springboot

参考 esdoc.bbossgroups.com/#/spring-bo…

导入依赖

<!-- 核心依赖 -->
<dependency>
  <groupId>com.bbossgroups.plugins</groupId>
  <artifactId>bboss-elasticsearch-spring-boot-starter</artifactId>
  <version>7.0.2</version>
</dependency>
<!-- 其他非必须依赖 -->
<dependency>
  <groupId>com.bbossgroups.plugins</groupId>
  <artifactId>bboss-datatran-jdbc</artifactId>
  <version>7.0.2</version>
</dependency>
<dependency>
  <groupId>com.bbossgroups.plugins</groupId>
  <artifactId>bboss-elasticsearch-rest-jdbc</artifactId>
  <version>7.0.2</version>
</dependency>
<dependency>
  <groupId>com.bbossgroups.plugins</groupId>
  <artifactId>bboss-elasticsearch-rest-entity</artifactId>
  <version>7.0.2</version>
</dependency>

application.properties

#es的地址
spring.elasticsearch.bboss.elasticsearch.rest.hostNames=10.21.20.168:9200
#es的用户名和密码
spring.elasticsearch.bboss.elasticUser=elastic
spring.elasticsearch.bboss.elasticPassword=changeme
#在控制台输出脚本调试开关showTemplate,false关闭,true打开,同时log4j至少是info级别(DSL脚本调试日志开关)
spring.elasticsearch.bboss.elasticsearch.showTemplate=true

创建客户端实例

@Autowired
private BBossESStarter bbossESStarter;

//获取加载读取dsl xml配置文件的api接口实例,可以在代码里面直接通过dsl配置名称引用dsl即可, single instance multithreaded security
ClientInterface configClientUtil = bbossESStarter.getConfigRestClient("esmapper/xxxxx.xml");
//获取不需要读取dsl xml配置文件的api接口实例,可以在代码里面直接编写dsl, single instance multi-thread security
ClientInterface clientUtil = bbossESStarter.getRestClient();   

或者

// 通过ElasticSearchHelper工具直接创建api接口实例
ClientInterface clientUtil = ElasticSearchHelper.getConfigRestClientUtil("esmapper/xxxxx.xml");
ClientInterface clientUtil1 = ElasticSearchHelper.getRestClientUtil();

使用示例

索引相关

判断索引和类型是否存在

existIndice

ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
// 只判断索引是否存在
boolean exist = clientUtil.existIndice("hotfix");
System.out.println("hotfix索引是否存在:" + exist);
// 判断索引和类型是否同时存在
boolean indiceType = clientUtil.existIndiceType("hotfix", "doc");
System.out.println("索引hotfix类型doc是否存在:" + indiceType);

删除索引

dropIndice

ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
boolean existed = clientUtil.existIndice("hotfix1");
if (existed){
    String dropIndice = clientUtil.dropIndice("hotfix1");
    System.out.println("删除索引返回结果"+dropIndice);
}
// 控制台输出
删除索引返回结果{
  "acknowledged" : true
}

创建索引

createIndiceMapping

ClientInterface clientUtil = ElasticSearchHelper.getConfigRestClientUtil("esmapper/xxx.xml");
clientUtil.createIndiceMapping("demo",// 索引名
        "createDemoIndice");// 索引映射DSL脚本名称,在esmapper/xxx.xml中定义createDemoIndice
String demoIndice = clientUtil.getIndice("demo");
System.out.println("获取索引返回的结果:" + demoIndice);

// 控制台输出
获取索引返回的结果:{
  "demo2" : {
    "mappings" : {
      "demo2" : { }
    }
  }
}
<properties>
    <property name="createDemoIndice">
        <![CDATA[
            {
                "mappings": {
                    "demo2": {
                    }
                }
            }
        ]]>
    </property>
</properties>

文档相关

创建文档

addDocument

简单的创建文档不指定id
HashMap<String, String> docData = new HashMap<>();
docData.put("name", "andy");
docData.put("age", "18");
docData.put("address", "beijing");
ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
String response = clientUtil.addDocument("demo2", // 索引名
                                         "test", // 类型
                                         docData, // 文档数据 可以是map也可以是对象
                                         "refresh=true");// 强制刷新
System.out.println("增加文档返回结果:" + response);

// 控制台输出
增加文档返回结果:{"_index":"demo2","_type":"test","_id":"AYkfpje0WzjIuz7qoiNV","_version":1,"result":"created",
    "forced_refresh":true,"_shards":{"total":2,"successful":1,"failed":0},"created":true}
创建文档并指定id的三中方式
通过@ESId指定文档id
@Data
public class Demo {
    @ESId
    private String demoId;
    private String name;
    private String age;
    private String address;
}

Demo docData = new Demo();
docData.setDemoId("id_1001");
docData.setName("jordon");
docData.setAge("25");
docData.setAddress("shanghai");
ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
String response = clientUtil.addDocument("demo2", // 索引名
        "test", // 类型
        docData, // 文档数据 可以是map也可以是对象
        "refresh=true");// 强制刷新
System.out.println("增加文档返回结果:" + response);

// 控制台输出
增加文档返回结果:{"_index":"demo2","_type":"test","_id":"id_1001","_version":1,"result":"created",
    "forced_refresh":true,"_shards":{"total":2,"successful":1,"failed":0},"created":true}

通过postman查看这两条文档

curl --location --request GET 'http://localhost:9200/demo2/_search' \
--header 'Content-Type: application/json' \
--data '{
      "query": {
          "match_all":{}
      }
}'
{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 2,
        "max_score": 1.0,
        "hits": [
            {
                "_index": "demo2",
                "_type": "test",
                "_id": "id_1001",
                "_score": 1.0,
                "_source": {
                    "demoId": "id_1001",
                    "name": "jordon",
                    "age": "25",
                    "address": "shanghai"
                }
            },
            {
                "_index": "demo2",
                "_type": "test",
                "_id": "AYkfpje0WzjIuz7qoiNV",
                "_score": 1.0,
                "_source": {
                    "address": "beijing",
                    "name": "andy",
                    "age": "18"
                }
            }
        ]
    }
}

可以看到通过@ESId指定文档id后文档字段中也多了一个名为demoId的字段,值和文档id相同。假如我不想要字段中多一个demoId字段,只想单纯的指定文档id,可以下面的方式

通过addDocumentWithId指定文档id
HashMap<String, String> docData = new HashMap<>();
docData.put("name", "luce");
docData.put("age", "18");
docData.put("address", "beijing");
ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
String response = clientUtil.addDocumentWithId("demo2", // 索引名
        "test", // 类型
        docData, // 文档数据 可以是map也可以是对象
        "id_1002");// 文档id
System.out.println("增加文档返回结果: "+response);

// 控制台输出
增加文档返回结果: {"_index":"demo2","_type":"test","_id":"id_1002","_version":1,"result":"created",
    "_shards":{"total":2,"successful":1,"failed":0},"created":true}

通过这种方式确实指定了文档id,而且也没有多余字段,但是没有指定刷新策略,如果我还想指定刷新策略怎么办呢?

通过addDocument同时指定文档id和刷新策略
HashMap<String, String> docData = new HashMap<>();
docData.put("name", "lisha");
docData.put("age", "18");
docData.put("address", "zhengzhou");
ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
String response = clientUtil.addDocument("demo2", // 索引名
        "test", // 类型
        docData, // 文档数据 可以是map也可以是对象
        "id_1003",// 文档id
        "refresh=true");// 强制刷新
System.out.println("增加文档返回结果: "+response);

// 控制台输出
增加文档返回结果: {"_index":"demo2","_type":"test","_id":"id_1003","_version":1,"result":"created","forced_refresh":true,
    "_shards":{"total":2,"successful":1,"failed":0},"created":true}

refresh=true解释:

在Elasticsearch中,文档的写入是异步的,即写入操作成功后,文档不一定会立即显示在搜索结果中。这是因为Elasticsearch使用了一种叫做“刷新(refresh)”的机制,即将写入的文档放在一个缓存中,而不是立即写入磁盘。只有当缓存中的文档数量达到一定阈值或者一定时间间隔后,Elasticsearch才会将缓存中的文档刷新到磁盘,并使其在搜索结果中可见。

当您在使用BBoss进行文档写入操作时,可以设置refresh参数来控制文档的刷新时间。当refresh=true时,表示写入操作成功后,BBoss会立即执行一次刷新操作,使得写入的文档可以立即在搜索结果中可见。而当refresh=false时,表示写入操作成功后,只有当缓存中的文档数量达到一定阈值或者一定时间间隔后,Elasticsearch才会将缓存中的文档刷新到磁盘,并使其在搜索结果中可见。因此,设置refresh=true可以使得写入操作的结果更加及时地反映在搜索结果中,但也会增加系统的负载和延迟。

批量创建文档

addDateDocuments

ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
List<Demo> demos = new ArrayList<>();
Demo demo = new Demo();
demo.setAge("18");
demo.setName("name1");
demo.setAddress("tianjin");
demos.add(demo);

demo = new Demo();
demo.setAge("10");
demo.setName("name2");
demo.setAddress("chongqing");
demos.add(demo);

//批量创建文档
String response = clientUtil.addDateDocuments("demo",//索引表
                                              "demo",//索引类型
                                              demos);

获取文档

getDocument

ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
Map document = clientUtil.getDocument("demo2", // 索引名
        "test", // 类型
        "AYkfpje0WzjIuz7qoiNV", // 文档id 创建文档时ES自动生成的
        Map.class); // 结果类型 可以是map或对象来接收
System.out.println("获取文档:"+document);

// 控制台输出
获取文档:{address=beijing, name=andy, age=18}

ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
Demo document = clientUtil.getDocument("demo2", // 索引名
        "test", // 类型
        "id_1001", // 文档id 创建文档时ES自动生成的
        Demo.class); // 结果类型 可以是map或对象来接收
System.out.println("获取文档:"+document);

// 控制台输出
获取文档:Demo(demoId=id_1001, name=jordon, age=25, address=shanghai)

修改文档

getDocument

ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
Demo document = clientUtil.getDocument("demo2", // 索引名
        "test", // 类型
        "id_1001", // 文档id 创建文档时ES自动生成的
        Demo.class); // 结果类型 可以是map或对象来接收
System.out.println("获取文档:"+document);
document.setName("jordon_modified");
document.setAddress("shanghai_modified");
String response = clientUtil.addDocument("demo2",
        "test",
        document,
        "refresh=true");
System.out.println("修改文档返回结果:" + response);

// 控制台输出
修改文档返回结果:{"_index":"demo2","_type":"test","_id":"id_1001","_version":2,"result":"updated","forced_refresh":true,
    "_shards":{"total":2,"successful":1,"failed":0},"created":false}

删除文档

deleteDocument

ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
String response = clientUtil.deleteDocument("demo2",
        "test",
        "AYkfumOWWzjIuz7qoiNX");
System.out.println("删除文档返回结果:" + response);

// 控制台输出
删除文档返回结果:{"found":true,"_index":"demo2","_type":"test","_id":"AYkfumOWWzjIuz7qoiNX","_version":2,"result":"deleted",
    "_shards":{"total":2,"successful":1,"failed":0}}

deleteDocuments

ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
String response = clientUtil.deleteDocuments("demo2",
        "test",
        new String[]{"id_1001", "AYkfpje0WzjIuz7qoiNV"});
System.out.println("删除文档返回结果:" + response);

// 控制台输出
删除文档返回结果:{"took":14,"errors":false,
    "items":[{"delete":{"found":true,"_index":"demo2","_type":"test","_id":"id_1001","_version":3,"result":"deleted",
    "_shards":{"total":2,"successful":1,"failed":0},"status":200}},
          {"delete":{"found":true,"_index":"demo2","_type":"test","_id":"AYkfpje0WzjIuz7qoiNV","_version":2,"result":"deleted",
              "_shards":{"total":2,"successful":1,"failed":0},"status":200}}]}

ES搜索

以hotfix搜索为示例

代码

public ESDatas<HotfixEntity> searchHotfixList(HotfixQuery queryParam) {
    // 调试打开dsl打印,线上关闭
    HostDiscoverUtil.swithShowdsl(true);
    ClientInterface clientUtil = ElasticSearchHelper.getConfigRestClientUtil("esmapper/hotfix-manage.xml");
    ESDatas<HotfixEntity> searched = null;
    try {
        searched = clientUtil.searchList("hotfix/_search",
                "queryServiceByCondition",
                queryParam,
                HotfixEntity.class);
    } catch (ElasticSearchException e) {
        log.error("es search hotfix documents error", e);
        return new ESDatas();
    }
    return searched;
}

esmapper/hotfix-manage.xml

<properties>
    <property name="queryServiceByCondition">
        <![CDATA[
            {
                "query": {
                        "bool": {
                            "must": [
                                {
                                    "match_phrase": {
                                        "product": #[product]
                                    }
                                }
                                #if($keyword && !$keyword.equals(""))
                                ,
                                {
                                    "multi_match": {
                                        "query": #[keyword],
                                        "fields": ["version", "app_package", "download_path", "fix_problem", "update_command", "release_date", "contact_person"]
                                    }
                                }
                                #end
                                #if($version && !$version.equals("") && !$version.equals("all"))
                                ,
                                {
                                    "match_phrase": {
                                        "version": #[version]
                                    }
                                }
                                #end
                                #if($contactPersonList)
                                ,
                                {
                                    "bool": {
                                        "should": [
                                            #foreach($contactPerson in $contactPersonList)
                                            {
                                                "match_phrase": {
                                                    "contact_person": #[contactPersonList[$velocityCount]]
                                                }
                                            }
                                            #if($foreach.hasNext),#end
                                            #end
                                        ]
                                    }
                                }
                                #end
                            ]
                            ,
                            "filter": [
                                 #if($startTime && $endTime)
                                 {"range": {
                                        "release_timestamp": {
                                            "gte": #[startTime],
                                            "lte": #[endTime]
                                        }
                                    }
                                 },
                                 #end
                                 {
                                    "match_all": {}
                                 }
                            ]
                        }
                },
                "sort": [
                    #if($keyword && !$keyword.equals(""))
                    {
                        "_score": {
                            "order": "desc"
                        }
                    },
                    #end
                    {
                        "release_timestamp": {
                            "order": "desc"
                        }
                    }
                ],
                "highlight": {
                    "fields": {
                        "fix_problem": {},
                        "update_command": {},
                        "download_path": {}
                    },
                    "pre_tags": "<span style='background-color: mediumspringgreen;filter: brightness(80%) contrast(120%)'>",
                    "post_tags": "</span>"
                },
                "size": 10000
            }
        ]]>
    </property>
</properties>

{
  "query": {
    "bool": {
      "must": [
        {
          "match_phrase": {
            "product": "chatbot"
          }
        },
        {
          "multi_match": {
            "query": "数据看板",
            "fields": [
              "version",
              "app_package",
              "download_path",
              "fix_problem",
              "update_command",
              "release_date",
              "contact_person"
            ]
          }
        },
        {
          "match_phrase": {
            "version": "5.6.0"
          }
        },
        {
          "bool": {
            "should": [
              {
                "match_phrase": {
                  "contact_person": "届远"
                }
              },
              {
                "match_phrase": {
                  "contact_person": "克霖"
                }
              },
              {
                "match_phrase": {
                  "contact_person": "嗨伦"
                }
              }
            ]
          }
        }
      ],
      "filter": [
        {
          "range": {
            "release_timestamp": {
              "gte": 1685548800000,
              "lte": 1688140799000
            }
          }
        },
        {
          "match_all": {}
        }
      ]
    }
  },
  "sort": [
    {
      "_score": {
        "order": "desc"
      }
    },
    {
      "release_timestamp": {
        "order": "desc"
      }
    }
  ],
  "highlight": {
    "fields": {
      "fix_problem": {},
      "update_command": {},
      "download_path": {}
    },
    "pre_tags": "<span style='background-color: mediumspringgreen;filter: brightness(80%) contrast(120%)'>",
    "post_tags": "</span>"
  },
  "size": 10000
}
作用

在XML中,可能包含一些特殊字符,例如“<”,“>”和“&”,这些特殊字符必须进行转义,否则会被解析器误认为是标记符号,导致解析错误,为了避免这种情况,可以使用CDATA块将包含特殊字符的文本数据进行包装,告诉解析器这段文本数据不需要进行解析和转义。注意:CDATA中不能再包含"]]>",如果非要包含"]]>",则可以通过拆分的方式,如下:

<![CDATA[ This is some text with ]]]]><![CDATA[>]]>
velocity语法
变量

脚本中变量定义语法有两种:#[xxx],xxx,大部分情况下用#[xxx],因为#[xxx]类型</font>**<font style="color:rgb(44, 62, 80);">变量值中包含的可能破坏dsl json语法结构的特殊字符(例如回车符号,换行符号,注释符号等),框架会自动进行转义处理(有点类似SQL注入);</font>**<font style="color:rgb(52, 73, 94);">xxx类型变量直接输出原始值(特殊字符不做转移处理)。当然,一些特殊情况需要用xxxxxx,xxx类型变量可以用于if/else和foreach循环控制变量,而#[xxx]不可以。建议:在dsl拼接中采用#[xxx]替代xxx模式变量,在foreachif/else语法中使用xxx模式变量,在foreach和if/else语法中使用xxx。变量来源:1.自己在脚本中定义;2.代码中通过参数传过来;3.有特殊作用的内置变量,如$velocityCount

“#”

使用"#"用来标识Velocity的关键字,包括#set、#if 、#else、#end、#foreach、#end、#include、#parse、#macro等;

“$”

使用""用来标识Velocity的变量,比如:"用来标识Velocity的变量,比如:name,$msg等;

“!”

变量名前加上!,比如:!name,表示如果name有值将显示name的值,如果不存在就会显示为空白。一般推荐使用。例如:当找不到username的时候,name,表示如果name有值将显示name的值,如果不存在就会显示为空白。一般推荐使用。例如:当找不到username的时候,username返回字符串"username",而username",而!username返回空字符串""。!namename和!name

声明变量

#set用来定义变量,用法:#set(name="andy"),变量以name = "andy"),变量以开头,第一个字符必须是字母,可以包含字母,数字,连字符(-)下划线(_),等号右边的内容如果出现以开始的字符串时,将做变量的替换,例如:</font>#set(hello ="hello name")<fontstyle="color:rgb(51,51,51);">这个等式将会给name"),<font style="color:rgb(51, 51, 51);">这个等式将会给hello赋值为"hello andy"

条件判断

条件判断用#if关键字,和java里的if用法相似

#if(condition)
	逻辑处理
#end

#if(condition)
	逻辑处理
#else
	逻辑处理
#end

#if(condition)
	逻辑处理
#elseif()
	逻辑处理
#else
	逻辑处理
#end
判断List集合datas不为空并且datas的size大于0

#if($datas && $datas.size()> 0)
  #foreach($bb in $datas)
  
  #end
#end
变量值逻辑判断
#if($name) 
#end
#if(!$name)
#end
#if($name && !$name.equals(""))
#end
#if($name > 0)
#end
#if($datas && $datas.size() > 0)
#end

逻辑判断还可以包含各种组合 && || 操作

循环

更多复杂案例参考:esdoc.bbossgroups.com/#/developme…

foreach循环内置循环变量:$velocityCount,用来获取当前循环到的元素的索引值,不需要从外部传入,可以直接使用,在循环内部可以通过

#[collection[velocityCount]]拿到指定下标的元素,如果元素是一个对象,而我想拿到对象的某个属性值,可以用#[collection[velocityCount]->fieldname],如果是复杂的对象呢?莫急莫急,后面有专门的总结【#[xxx]变量的格式】。

注意:bboss框架的velocityCount0开始</font><fontstyle="color:rgb(51,51,51);">计数</font><fontstyle="color:rgb(52,73,94);">的,而</font><fontstyle="color:rgb(51,51,51);">Velocity模板引擎的velocityCount是0开始</font><font style="color:rgb(51, 51, 51);">计数</font><font style="color:rgb(52, 73, 94);">的,而</font><font style="color:rgb(51, 51, 51);">Velocity模板引擎的velocityCount是从1开始计数的。也就是说bboss的模板引擎虽然大部分和Velocity模板引擎是一样的,但细微的一些地方上还是有不同的。

#foreach($item in $collection)
逻辑处理
#end
{
  "bool": {
    "should": [
      #foreach($contact in $contactList)
      {
        "match_phrase": {
          "contact": #[contactList[$velocityCount]]
        }
      }
      #if($foreach.hasNext),#end
      #end
    ]
  }
}
等价于
{
  "bool": {
    "should": [
      #foreach($contact in $contactList)
      #if($velocityCount > 0),#end
      {
        "match_phrase": {
          "contact": #[contactList[$velocityCount]]
        }
      }
      #end
    ]
  }
}

另外注意:这里面循环内的每一项元素并不是用contact来获取,而是通过#[contactList[velocityCount]]方式来获取,这有什么区别呢?

例如:假设$contactList={"张三", "李四", "王五"},

$contact来获取,那么代码解析后的结果是

{
    "bool": {
        "should": [
            {
                "match_phrase": {
                    "contact": 张三
                }
            },
            {
                "match_phrase": {
                    "contact": 李四
                }
            },
            {
                "match_phrase": {
                    "contact": 王五
                }
            }
        ]
    }
}

#[contactList[$velocityCount]]来获取,那么代码解析后的结果是

{
    "bool": {
        "should": [
            {
                "match_phrase": {
                    "contact": "张三"
                }
            },
            {
                "match_phrase": {
                    "contact": "李四"
                }
            },
            {
                "match_phrase": {
                    "contact": "王五"
                }
            }
        ]
    }
}

因此,如果是数字类型那么用哪种方式获取元素都是可以的,但是如果是字符串类型,只能用

#[contactList[$velocityCount]]来获取。

那么假如我就想用contact来获取字符串类型的数据呢?也不是不行,可以通过"contact来获取字符串类型的数据呢?也不是不行,可以通过"contact"的方式,因为$contact是值的替换

{
  "bool": {
    "should": [
      #foreach($contact in $contactList)
      #if($velocityCount > 0),#end
      {
        "match_phrase": {
          "contact": "$contact"
        }
      }
      #end
    ]
  }
}

如果变量$contact为数字类型,值为100,那么替换后得到"100",数字100被当成String处理了,这种情况下可能会出现不可预知的问题

再次总结:建议在dsl拼接中采用#[xxx]替代xxx模式变量,在foreachif/else语法中使用xxx模式变量,在foreach和if/else语法中使用xxx.

#[xxx]变量的格式
  1. #[aaa->bb] 如果aaa是一个bean对象,这个变量格式表示了对aaa对象的bb属性的引用,如果aaa是一个map对象,这个变量格式表示了对aaa对象的key为bb的元素值引用
  2. #[aaa[key]] 引用map对象aaa中key所对应的value数据,引用map元素的等价方法#[aaa->key]
  3. #[aaa[0]] (一维数组中的第一个元素,或者list中的第一个元素,具体取决于aaa变量是一个数组还是list对象)
  4. #[aaa[0][1]](二维数组中的第一维度的第二个元素,或者list中的第一个元素的第二个子元素,具体取决于aaa变量是每一维度是数组还是list对象)
  5. #[aaa[key][0]] 引用map对象aaa中key所对应的value的第一个元素,取决于value的类型是数组还是list,等价引用方法#[aaa->key[0]]

以上就是全部的类型,每种类型可以任意组合

#[aaa->bb[0]] aaa对象的bb属性的第一个元素,bb属性类型是数组或List

#[aaa[0]->bb[0]] 泛型为aaa的list的第一个元素的bb属性的第一个元素,bb属性类型是数组或List

#[aaa[0]->bb[0]->fff] #[aaa[0]->bb[0]->cc[keyname]] #[aaa[0]->bb->cc[keyname]] 等等

#[xxx]变量属性

更多参考 esdoc.bbossgroups.com/#/developme…

可以在#[]变量中指定escapeCount, serialJson, quoted,lpad,rpad,escape,esEncode,dateformat/locale/timezone属性,属性和变量名称用逗号分隔:

#[变量名,quoted=false,lpad=xxx,rpad=ddd]

#[变量名,quoted=false,lpad=xxx|3,rpad=ddd|4]

#[dynamicPriceTemplate->goodName,escapeCount=2]

#[dynamicPriceTemplate->rules[$velocityCount],serialJson=true]

#[testVar,serialJson=true]

这里只做少部分的属性的解释说明:

  1. quoted: boolean类型,控制是否为字符串变量和日期变量串两头添加"号,true添加,false不加,默认为true,一般在不需要自动加"号的情况下使用;

例如:

"asdfaf#[application,quoted=false]s"
变量application的值为testweb,解析后的效果如下:
"asdfaftestwebs"

"asdfaf#[application,quoted=true]s"
变量application的值为testweb,解析后的效果如下:
"asdfaf"testweb"s"

这里也解释了循环那块为什么用#[xxx]可以得到正确的字符串
  1. lpad、rpad:在通过lpad(左边追加)和rpad(右边追加)给变量值两头追加字符串,同时可以通过 | 指定一个数字,表示追加多少次

例如:

"#[application,quoted=false,lpad=#]s"
变量的值为testweb,解析后的效果如下:
"#testwebs"
"ddd#[application,quoted=false,lpad=#|2,rpad=#|3]s"
变量的值为testweb,解析后的效果如下:
"ddd##testweb###s"
  1. serialJson:boolean类型,通过属性serialJson指示框架直接将对象序列化为json数据;
  2. escapeCount: int类型,在脚本中,含有特殊字符的goodName需要转义2次;
$xxx变量

通过循环那块的介绍已经大概了解了xxx变量作为一个普通字符串或数字类型的特点,假如xxx变量作为一个普通字符串或数字类型的特点,假如xxx是一个对象呢?

变量如果是一个对象,可以通过以下方式引用对象中定义的属性:

$customer.Address
$purchase.Total
或者
$customer.getAddress()
$purchase.getTotal()

变量如果是List或者Array数组,size长度获取和为空判断方法:

$myarray.isEmpty()

$myarray.size()

如何修改$xxx的值呢?和声明变量时一样

变量定义
#set( $hasParam = false )

然后在dsl其他地方可以修改变量的值
#set( $hasParam = true )
总结#[xxx]和$xxx两种模式变量的区别
  	#[xxx]自动对变量值中的特殊字符进行转义处理,而$xxx不会进行处理直接输出原始值。

		#[xxx]自动在String类型变量两边加上双引号"",如果不需要双引号,就写成这样:#[xxx,quoted=false]。

		#[xxx]自动对日期类型变量值采用Utc标准时间格式(yyyy-MM-dd'T'HH\:mm\:ss.SSS'Z')进行格式化
处理,而$xxx不会进行处理直接输出原始值,因此如果$xxx模式变量参数中包含有特殊字符或者是日期类型,
请在程序中自行处理好。

		$xxx可用于逻辑判断、循环处理语句,#[xxx]不能用于逻辑判断、循环处理语句。

		$xxx变量参数值拼接到dsl中需要特别注意,如果变量的值不确定,变化频繁,在高并发的场景下会导
致严重的性能问题;$xxx用在foreach和if/else语法中不会存在这个问题。
@{pianduan}-片段变量使用

@{xxx}类型变量用于在query dsl中引用脚本片段。很多的dsl脚本会包含一些公共内容,比如查询条件,聚合操作脚本等等,可以把这些公共部分抽取出来定义成dsl片段;另外,一些复杂的搜索聚合查询的dsl脚本很长,由很多比较通用独立的部分组成,这样也可以将独立部分剥离形成片段,这样dsl的结构更加清晰,更加易于维护。作用相当于mybatis的xml中的 。

片段定义一定要定义在引用片段的dsl脚本前面。

<properties>
	<property name="column">
  	<![CDATA[
							 "title": {
                    "type": "text",
                    "index": true
                },
                "version": {
                    "type": "text",
                    "index": true
                },
                "fix_problem": {
                    "type": "text",
                    "index": true
                }
    ]]>
	</property>
	<property name="searchColumn">
		<![CDATA[
      {
          "mappings": {
              "hotfix": {
                  "properties": {
                    @{column}
                   }
              }
          }
      }
    ]]>
	</property>
</properties>

片段变量只是一个占位符,在系统第一次加载配置文件时候,直接被column对应的片段内容替换,片段定义中同样可以引用其他片段。

也可以引用其他文件中定义的dsl片段(外部片段引用bboss 5.5.6才支持),例如:

<properties>
	<property name="pianduan">
  	<![CDATA[
							 "title": {
                    "type": "text",
                    "index": true
                },
                "version": {
                    "type": "text",
                    "index": true
                },
                "fix_problem": {
                    "type": "text",
                    "index": true
                }
    ]]>
	</property>
</properties>
<properties>
  <property name="outPianduanRef"
                templateFile="esmapper/pianduan.xml"
                templateName="pianduan"/>

  <property name="testoutPianduan">
          <![CDATA[
            {
                "mappings": {
                    "hotfix": {
                        "properties": {
                          @{outPianduanRef}
                         }
                    }
                }
            }
          ]]>
  </property>
</properties>
DSL注释

单行注释用 ##

##注释内容

多行注释用#* 和 *#包起来

#*
注释内容
注释内容
*#

注释使用的示例:

<properties>
<!-- 这里是普通注释 -->
  <property>
    <![CDATA[  
      ## 这里是DSL注释 
    ]]>
  </property>
</properties>

至此,日常使用bboss开发ES已经满足基本的使用场景了,更多进阶教程见bboss官网