1、golang访问es常用包
目前主流的包有两个,一个是官方提供的:
github.com/elastic/go-elasticsearch/v7
V7版本的es可以使用上述官方提供的包,如果是V8版本,使用如下的包:
github.com/elastic/go-elasticsearch/v8
第二个是第三方包,目前最流行的为:
https://github.com/olivere/elastic/v7
但是,从github上的记录看,当前最新版本是22年3月份发布的,已经两年多没有更新了。
从我个人的使用来说,两个包各有千秋,从开发效率上来说,我比较喜欢使用第三方包,尤其是在构造dsl上,提供了api,和查询无缝衔接,代码非常之简洁,逻辑清晰,官方包偏底层,dsl需要开发者自行构建,不是很方便,但是在性能上更胜一筹,如果对性能要求较高的,还是推荐使用官方包进行开发。
2、为什么单独开发一个构建dsl的库
在开发数仓平台之初,我们选用的es包为官方包,开发中发现这个包没有提供标准的api来构建查询语句,只能是开发者自行构建,由于自动驾驶数据的庞大,而且检索条件多种多样,dsl查询语言构建起来非常之繁琐和复杂,代码可读性极差。
在开发一些临时行脚本过程中,决定使用一下三方库:https://github.com/olivere/elastic/v7,也是看重了其提供标准构建dsl的api,使用起来非常的方便,之后使用这个包开发了各种各样的小脚本工具,开发过程也非常顺手,而且代码的可读性也很好,因此后来就想把之前开发的数仓平台检索部分全部更换为第三方包,很不幸,由于自动驾驶数据的检索量非常之巨大,在性能上第三方包和官方包很难打平,性能相对较差,因此不得不终止了替换的想法。
到这里,对两个包的使用上算是比较熟悉了,而且两者的优势、劣势也算是基本摸清楚,因此就有了取两个包的优势于一体,初步的想法就是将olivere包构建dsl的技术方案拿过来,和官方包相结合,说干就干,因此就有了这个单独开发一个构建dsl包的想法(基于olivere的实现方案)。
3、dsl构建包的实现方案
构建dsl的包地址为:https://github.com/liupengh3c/esbuilder,目前只实现了项目上开发所需要的一些查询,有需要的同学可以直接使用,如果没能满足你的需求,期待与大家共建(可以在博客评论说出你的需求)以解决更多同学面临的问题。
3.1 实现原理
目前项目上主要使用到的是bool查询,bool查询可以嵌套must、must not、filter等,dsl本质上就是一个json,因此首先定义个interface,这个接口,就是根据查询条件构建一个map,这个map就是最后生成json的数据,看代码会更加清晰一点:
type query interface {
// Build returns the map query request.
Build() (interface{}, error)
}
以term查询为例,首先定义term查询结构体:
type termQuery struct {
name string // Name of the field
value interface{} // Value of the field
boost *float64 // Boost
caseInsensitive *bool // 是否区分大小写
queryName string
}
关于term查询的详细信息,可以查看文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/query-dsl-term-query.html
build 函数的实现如下(这个函数非常重要,需要根据es官方对该查询的说明进行实现):
func (q *termQuery) Build() (interface{}, error) {
source := make(map[string]interface{})
tq := make(map[string]interface{})
source["term"] = tq
if q.boost == nil && q.caseInsensitive == nil && q.queryName == "" {
tq[q.name] = q.value
} else {
subQ := make(map[string]interface{})
subQ["value"] = q.value
if q.boost != nil {
subQ["boost"] = *q.boost
}
if q.caseInsensitive != nil {
subQ["case_insensitive"] = *q.caseInsensitive
}
if q.queryName != "" {
subQ["_name"] = q.queryName
}
tq[q.name] = subQ
}
return source, nil
}
该函数的实现详细来说,就是根据官方文档,构建对应的map,之后在上层(dsl哪一层)实现一个json化的函数即可,这样就能够根据查询条件实现对应dsl语句json字符串的输出,有了dsl,就可以直接调用es官方的包执行查询操作了。
3.2 term查询具体实现
term查询的dsl语句一般为:
{
"query": {
"bool": {
"filter": [
{
"term": {
"tag_name.keyword": "pnc_send"
}
}
]
}
}
}
以上整个dsl,对应代码中的dsl结构:
type dsl struct {
QueryDsl query `json:"query"`
Source []string `json:"_source,omitempty"`
Size int64 `json:"size,omitempty"`
}
dsl实现了一个重要的reciver函数buildjson:
func (dsl *dsl) BuildJson() string {
var json = jsoniter.ConfigCompatibleWithStandardLibrary
query, _ := dsl.QueryDsl.Build() // 构建map
mapDsl := map[string]any{
"query": query,
}
if dsl.Size > 0 {
mapDsl["size"] = dsl.Size
}
if len(dsl.Source) > 0 {
mapDsl["_source"] = dsl.Source
}
strDsl, _ := json.MarshalToString(mapDsl)
return strDsl
}
QueryDsl对应的就是上面的query查询语句,定义如下:
// For more details, see:
// https://www.elastic.co/guide/en/elasticsearch/reference/7.10/query-dsl-bool-query.html
type boolQuery struct {
mustItems []query
mustNotItems []query
filterItems []query
shouldItems []query
minimumShouldMatch string
boost *float64
}
bool查询的build函数实现,构建一个map:
func (q *boolQuery) Build() (interface{}, error) {
query := make(map[string]interface{})
boolClause := make(map[string]interface{})
query["bool"] = boolClause
// must
if len(q.mustItems) == 1 {
src, err := q.mustItems[0].Build()
if err != nil {
return nil, err
}
boolClause["must"] = src
} else if len(q.mustItems) > 1 {
var clauses []interface{}
for _, subQuery := range q.mustItems {
src, err := subQuery.Build()
if err != nil {
return nil, err
}
clauses = append(clauses, src)
}
boolClause["must"] = clauses
}
// must_not
if len(q.mustNotItems) == 1 {
src, err := q.mustNotItems[0].Build()
if err != nil {
return nil, err
}
boolClause["must_not"] = src
} else if len(q.mustNotItems) > 1 {
var clauses []interface{}
for _, subQuery := range q.mustNotItems {
src, err := subQuery.Build()
if err != nil {
return nil, err
}
clauses = append(clauses, src)
}
boolClause["must_not"] = clauses
}
// filter
if len(q.filterItems) == 1 {
src, err := q.filterItems[0].Build()
if err != nil {
return nil, err
}
boolClause["filter"] = src
} else if len(q.filterItems) > 1 {
var clauses []interface{}
for _, subQuery := range q.filterItems {
src, err := subQuery.Build()
if err != nil {
return nil, err
}
clauses = append(clauses, src)
}
boolClause["filter"] = clauses
}
// should
if len(q.shouldItems) == 1 {
src, err := q.shouldItems[0].Build()
if err != nil {
return nil, err
}
boolClause["should"] = src
} else if len(q.shouldItems) > 1 {
var clauses []interface{}
for _, subQuery := range q.shouldItems {
src, err := subQuery.Build()
if err != nil {
return nil, err
}
clauses = append(clauses, src)
}
boolClause["should"] = clauses
}
if q.boost != nil {
boolClause["boost"] = *q.boost
}
if q.minimumShouldMatch != "" {
boolClause["minimum_should_match"] = q.minimumShouldMatch
}
return query, nil
}
一般情况下,如果只需要根据某个字段的值来过滤文档,而不关心相关性得分,为了提升查询性能,我们会使用过滤器,将查询条件放在filter过滤器下。
3.3 使用示例
假如有以下结构的文档:
{
"car_id": "CJH133",
"start_time": 1730119229,
"end_time": "1730119235",
"tag_name": "pnc",
"tag_value": "xxxx",
"hardwares": {
"regin": "bj_haidian",
"hd_version": "v1.0"
},
"additional_info": {
"obs_id": "xxxx",
"position": "xxxx"
}
}
我们需求是通过car_id:CJH133以及包含时间段:1730119229--1730119232,对应的dsl语言应该是:
GET /new_tag_202410/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"car_id.keyword": "CJH133"
}
},
{
"range": {
"start_time": {
"lte": 1730119229
}
}
},
{
"range": {
"end_time": {
"gte": 1730119232
}
}
}
]
}
}
}
对应构建如上dsl的代码如下:
package main
import (
"fmt"
"github.com/liupengh3c/esbuilder"
)
func main() {
boolQuery := esbuilder.NewBoolQuery()
boolQuery.Filter(esbuilder.NewTermQuery("car_id.keyword", "CJH133"))
boolQuery.Filter(esbuilder.NewRangeQuery("start_time").Lte(1730119229))
boolQuery.Filter(esbuilder.NewRangeQuery("end_time").Gte(1730119232))
dsl, _ := boolQuery.BuildJson()
fmt.Println(dsl)
}
代码运行结果如下:
{
"query": {
"bool": {
"filter": [
{
"term": {
"car_id.keyword": "CJH133"
}
},
{
"range": {
"start_time": {
"lte": 1730119229
}
}
},
{
"range": {
"end_time": {
"gte": 1730119232
}
}
}
]
}
}
}
4、共建
该库目前只是实现了简单的dsl构建,欢迎各位程序员同学一起共同建设,项目地址:
github.com/liupengh3c/esbuilder
希望该包能够帮助到需要的同学。
如果在使用过程中发现任何问题,欢迎关注公众号【码农夜读】沟通交流。