elasticsearch查询语言DSL构建包使用及实现原理(golang)

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

希望该包能够帮助到需要的同学。

如果在使用过程中发现任何问题,欢迎关注公众号【码农夜读】沟通交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值