elasticsearch中如何实现多字段聚合统计?类似于mysql的:
select a,b from my_table group by a,b;
golang如何借助第三方类oliver elastic实现es原生DSL(domain special language 特殊领域查询语言)的构造?
希望看完这篇文章,对你有所帮助。
先来看单字段聚合统计的es原生DSL语句的写法:
{
"query": {
"bool": {
"must": [
{
"query_string": {
"query": "baidu",
"fields": [
"sample_hash.raw",
"program_name.raw",
"package_name.raw"
]
}
}
],
"filter": [
{
"range": {
"record_time": {
"gte": "2016-09-01",
"lte": "2020-07-24"
}
}
},
{
"match": {
"sample_state": "B"
}
}
]
}
},
"aggs": {
"packageNameGroup": {
"terms": {
"field": "package_name.raw",
"size": 1000,
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false
}
}
}
}
为什么aggs.packageNameGroup.field的值需要在字段末尾加上.raw呢?
先看咱们的index的mapping设置是:
curl -XGET 'event Cluster/tip_sample_info_v2/info/_mapping'
看到这个字段的定义,你第一感觉是什么?如果你对mapping的字段定义有一定的认知,你应该会有所疑惑?
为啥外层的type是text,内容type是keyword?那么这个字段到底是text,还是keyword?
看官方解释:
大致意思是,针对index的mapping的字段类型的定义,可以定义为复合类型,一般就是keyword+text的组合,来达到一个字段加不加.raw后缀,有不同的含义;毕竟聚合、排序只能针对keyword类型;
那么不加.raw可以聚合查询吗?看下面这个查询,
报错的意思是,Fielddata这个设置默认是关闭的,你现在想针对text类型做聚合统计,那么你可以打开这个Fielddata选项;
但是,据说这个选项开启很吃资源,一般不会为了聚合,来开启Fielddata选项的,还不如给聚合的text字段设置.raw的属性为keyword;
接下来,多字段聚合怎么写呢?
curl -XPOST 'event Cluster/tip_sample_info_v2/info/_search' -d '{
"aggs": {
"vfamilyGroup": {
"terms": {
"field": "vfamily.raw",
"size": 1000,
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false
},
"aggs": {
"variantGroup": {
"terms": {
"field": "variant.raw",
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false
}
}
}
}
}
}'
多字段聚合,就是aggs.terms的嵌套,类似于mysql group by 多字段;
返回的聚合结果,当然也是嵌套组合的,同一个vfamily下面可能存在多个variant,所以解析的时候是双层循环;
最后,废话不多说,上代码(针对多字段的聚合统计,上一段我的代码,是基于oliver elasitic开源包实现DSL的构造和解析es返回的聚合结果,希望对你有所帮助):
/**
新版本:使用es的DSL原生语法,来实现聚合统计
目的:和首页的原生es的全文检索,更加匹配;不然容易造成两者结果矛盾
*/
func GetAggregateStatistics(c iris.Context) {
//1.接收请求参数
startTime:=c.FormValue("start_time")
endTime:=c.FormValue("end_time")
if startTime!="" {
startTime=tool.TimeParseToTZ(startTime)
}
if endTime!="" {
endTime=tool.TimeParseToTZ(endTime)
}
//样本状态过滤
sampleState:=c.FormValueDefault("sample_state","")
//搜索关键词
keyword:=c.FormValue("keyword")
//聚合分组的字段
groupType := c.FormValue("group_type")
//二次检索(聚合统计页面,点击特定文本的超链接,跳转到当前方法,在keyword检索条件下,带上二次检索的过滤)
secondKeyValue:=c.FormValueDefault("aggregate_key_value","")
var fieldName string = ""
var fieldValue string = ""
if secondKeyValue != ""{
kvArr := strings.Split(secondKeyValue, "=")
fieldName = kvArr[0]
fieldValue = kvArr[1]
if fieldName == "record_time"{ //时间筛选不加.raw,并且要转化为utc筛选
fieldValue = tool.TimeParseToTZ(fieldValue)
}
}
//迭代:在全文检索的逻辑上,融入精确检索(产品需求。。。)
termKeyValue:=c.FormValueDefault("term_key_value","")
var termKey string = ""
var termValue string = ""
if termKeyValue!=""{
termArr := strings.Split(termKeyValue, "=")
termKey = termArr[0]
termValue = termArr[1]
if termKey == "start_time" || termKey=="end_time" {
termValue = tool.TimeParseToTZ(termValue)
}else{
termKey=termKey+".raw"
}
}
if termKeyValue=="" && keyword == ""{
_,_=c.JSON(iris.Map{
"code": custom.ValidateErr,
"message": "keyword不能为空",
"data": []interface{}{},
})
return
}
if groupType == ""{
_,_=c.JSON(iris.Map{
"code": custom.ValidateErr,
"message": "groupType不能为空",
"data": []interface{}{},
})
return
}
//group字段的特属处理(record_time的es类型是date,聚合统计不能用.raw,查出来的是微妙的时间戳)
if groupType!="record_time"{
groupType=groupType+".raw"
}
//2.QueryDSL的json字符串参数拼接
boolQuery := elastic.NewBoolQuery()
var queryFields []string=[]string{}
if termKeyValue!=""{
queryFields=[]string{termKey}
}else{
queryFields=[]string{"variant.raw","version.raw","version_name.raw","vfamily.raw","vir_name.raw","vtag_list.raw","vtype.raw"}
}
if termKeyValue!=""{
escapeTermValue:=tool.QueryStringEscape(termValue)
termQuery:=elastic.NewTermQuery(queryFields[0],escapeTermValue)
boolQuery.Must(termQuery)
}else{
//针对keyword里面的特殊字符串:,需要转移,否则es直接报错
escapedKeyword:= tool.QueryStringEscape(keyword)
queryStringQuery:=elastic.NewQueryStringQuery("*"+escapedKeyword+"*")
for _,v:=range queryFields{
queryStringQuery=queryStringQuery.Field(v)
}
boolQuery.Must(queryStringQuery)
}
//filter (样本状态和入库时间的可选过滤)
var matchQuery *elastic.MatchQuery
if sampleState!="" {
matchQuery=elastic.NewMatchQuery("sample_state",sampleState)
boolQuery.Filter(matchQuery)
}
var rangeQuery *elastic.RangeQuery
if startTime!="" && endTime!="" {
rangeQuery=elastic.NewRangeQuery("record_time").From(startTime).To(endTime)
}else if startTime!="" && endTime==""{
rangeQuery=elastic.NewRangeQuery("record_time").From(startTime)
}else if startTime=="" && endTime!=""{
rangeQuery=elastic.NewRangeQuery("record_time").To(endTime)
}
if rangeQuery!=nil {
boolQuery.Filter(rangeQuery)
}
//二次检索(条件再次加强,特定key的特定value的匹配)
if termKeyValue=="" && fieldName!="" && fieldValue!="" {
secondMatchPhraseQuery :=elastic.NewMatchPhraseQuery(fieldName, fieldValue)
boolQuery.Filter(secondMatchPhraseQuery)
}
//实例化es的aggs
TermsAggregation:=elastic.NewTermsAggregation().Field(groupType).MinDocCount(1).Size(100).
ShowTermDocCountError(false).ShardMinDocCount(0)
//针对病毒家族名的聚合,是多字段聚合,特殊处理
var subAggName=""
if groupType=="vfamily.raw" {
//针对病毒家族字段排除掉Unknown的值
TermsAggregation=TermsAggregation.Exclude("Unknown")
subAggName="temp_sub_aggs_by_variant"
TermsAggregation.SubAggregation(subAggName, elastic.NewTermsAggregation().Field("variant.raw").OrderByCountDesc())
}
//elastic.Search并聚合的结果
aggName:="temp_aggs_by_field" //外层聚合的名称
searchResult, err := es.GetIns().Search("tip_sample_info_v2").Type("info").Query(boolQuery).Pretty(true).
Aggregation(aggName,TermsAggregation).
Do(context.Background())
//这段不要删除,用来打印基于链式的es语法,转换出来的原生query的json字符串是啥
src, err := boolQuery.Source()
data, err := json.Marshal(src)
if err != nil {
logrus.Error("marshaling to JSON failed",err)
}
got := string(data)
fmt.Println("query json:",got)
if err!=nil {
logrus.Error("tip_sample_info_v2 原生es查询失败:",err)
_,_=c.JSON(iris.Map{
"code": custom.ValidateErr,
"message": "es index:tip_sample_info_v2 查询异常,请稍后重试",
"data": "",
})
return
}
//打印聚合的DSL
src, err = TermsAggregation.Source()
data, err = json.Marshal(src)
if err != nil {
logrus.Error("marshaling to JSON failed",err)
}
got = string(data)
fmt.Println("aggs query json:",got)
if err!=nil {
logrus.Error("tip_sample_info_v2 原生聚合失败:",err)
_,_=c.JSON(iris.Map{
"code": custom.ValidateErr,
"message": "es index:tip_sample_info_v2 聚合异常,请稍后重试",
"data": "",
})
return
}
//处理聚合后的结果
// 使用Terms函数和前面定义的聚合条件名称,查询结果
agg, found := searchResult.Aggregations.Terms(aggName)
if !found {
logrus.Error("没有找到聚合数据")
}
// 遍历桶数据
var aggsForFrontEnd []map[string]interface{}=[]map[string]interface{}{}
var totalCount int64 =0
//单字段聚合结果解析
if groupType!="vfamily.raw"{
for key, bucket := range agg.Buckets {
// 每一个桶都有一个key值,其实就是分组的值,可以理解为SQL的group by值
bucketValue := bucket.Key
//当前分组的统计值
bucketCount:=bucket.DocCount
var tempNewRow map[string]interface{}=map[string]interface{}{}
tempNewRow["number"]=key+1
tempNewRow["count"]=bucketCount
tempNewRow["value"]=bucketValue
tempNewRow["percentage"]=0
aggsForFrontEnd=append(aggsForFrontEnd,tempNewRow)
totalCount=totalCount+bucketCount
// 打印结果, 默认桶聚合查询,都是统计文档总数
//fmt.Printf("bucket = %q 文档总数 = %d\n", bucketValue, bucket.DocCount)
// Now access the sub-aggregates directly via the bucket.
}
}else{
//针对subAgg单独写es的返回值的解析方法(多字段聚合结果解析)
flagNum:=1
for _, bucket := range agg.Buckets {
bucketKey:=bucket.Key
// Now access the sub-aggregates directly via the bucket.
subAgg, found := bucket.Terms(subAggName)
if !found {
logrus.Error("没有找到内层的聚合数据")
}
//解析内层数据
for _, subBucket := range subAgg.Buckets {
// sub
subBucketCount:=subBucket.DocCount
totalCount=totalCount+subBucketCount
subBucketValue:=subBucket.Key
var tempNewRow map[string]interface{}=map[string]interface{}{}
tempNewRow["number"]=flagNum
tempNewRow["count"]=subBucketCount
tempNewRow["value"]=bucketKey
tempNewRow["variant_value"]=subBucketValue
tempNewRow["percentage"]=0
aggsForFrontEnd=append(aggsForFrontEnd,tempNewRow)
flagNum++
}
}
//针对多字段聚合,需要按照百分比排序+array_slice 100(多字段组合出来会超过100条)
tool.SortByColumn("count",1,aggsForFrontEnd)
}
//美化输出:完善百分比、record_time的时间戳转换为日期
dataLen:=len(aggsForFrontEnd)
if dataLen>100{
dataLen=100
}
toolFunc := tool.ToolFunc{}
for i:=0;i
参考文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.10/index.html
https://github.com/olivere/elastic