专栏首页可能是东半球最正规的API社区像SELECT*一样手撸Query DSL——ElasticSearch下篇

像SELECT*一样手撸Query DSL——ElasticSearch下篇

大家好 泥腿子安尼特又和大家见面了。不知道大家昨晚过的如何,容我再孤寡孤寡孤寡几声

我这个人比较懒,但是有些东西没完结,总是有时候脑子里挂念着,所以心心念念的想把ElasticSearch系列完结,当然自己也不想水完一篇文章,希望大家看完这篇,就能“精通”ES的查询了。

当年我还在读大学的时候,尽管我经常上课玩手机,睡觉,但是我数据库的老师的一句话深深的印在了我的脑海里,原话大概是这样的——这个世界上有一门编程语言,出来到现在几十年了,语法简单,基本没怎么变过,各种通用,从业人员的职业生涯也很持久,工资也高。大家猜猜是啥- - 那就是无所不能的sql。仔细想想,是不是很有道理,从普通的取数BI到数据分析师、策略师再到大数据之类的spark sql、hive sql、flink sql,就算你是个业务仔,不管数据库用的mysql、oracle、pgsql、tidb...等 sql基本上长的差不多。是不是发现sql这门语言有多无敌了,想想现在争论java好还是go好,真是too young too simple。

所以,我一开始摸到ElasticSearch的时候,我就想,这个是不是也能用sql语句来查询,一搜,果然是有ElasticSearch SQL,不过因为它并不支持完整的sql语法,所以如果你只是简单的查一查,又不想学习复杂的ES查询语句,那还是非常好用的!

数据源

PUT lib_sql
POST /lib_sql/_doc
{
  "name":"anit",
  "age":27,
  "interests":"lol, lol, lol"
}

POST /_sql?format=txt
{
  "query":"SELECT * from lib_sql"
}

结果

      age      |       interests        |     name      
---------------+------------------------+---------------
18             |chang, tiao, rap, lanqiu|cxk            
27             |lol, lol, lol           |anit

是不是感觉已经精通ES的查询了,我们再插入一条这样的数据。

POST /lib_sql/_doc
{
  "name":"main shen",
  "age":18,
  "interests":"qiao dai ma",
  "language":["php", "java", "go", "c"]
}

再次执行上述查询

{
  "error": {
    "root_cause": [
      {
        "type": "sql_illegal_argument_exception",
        "reason": "Arrays (returned by [language]) are not supported"
      }
    ],
    "type": "sql_illegal_argument_exception",
    "reason": "Arrays (returned by [language]) are not supported"
  },
  "status": 500
}

假如你是个刚接触ES的新手,如果做一些简单的业务,可以使用这种sql语法直接查,简单粗暴。

当然,ElasticSearch SQL的局限性不仅仅如此,比如你要查一些相关度 匹配程度的问题,有些dsl语句是没办法完全用sql展示出来的。直接进入我们今天的正题,手把手教你像写sql一样手撸query dsl.

dsl语句都是一个json串,然后通过一些关键词,不断构造对象、嵌套对象,最后拼成符合条件的查询json。我当时刚开始用的时候,就很疑惑,各个关键词有没有层级关系,我到底该怎么拼接我的dsl语句,这次查询该用什么关键词,感觉两个关键词都可以查出我要的结果,我该用哪个,所以这就把很多想直接用dsl语句来查询的老哥们给困惑住了,感觉这东西有点难用。

dsl语句的基本结构

{
  "query": {}, //具体的查询语句对象
  "from": 0,   //从第几条数据开始返回
  "size": 100, //返回的条数 默认ES最多返回10000条
  "highlight": { //高亮
    "pre_tags": {}, //高亮内容的前面标签 一般都是html比如<b> <p>这种
    "post_tags": {},//高亮内容的后面标签 一般都是html比如</b> </p>这种
    "fields": { //需要高亮的字段
    }
  },
  "sort": [{ //排序
    "FIELD": { //排序的字段(需要填上具体的字段名)
      "order": "desc"
    }
  }],
  "_source": "{field}" //指定返回的字段
}

1. 精确查询

select * from table where fileds=xx

select * from table where fileds in (x1,x2 ...)

POST /lib_sql/_search
{
  "query":{
    "term": {
      "name": {
        "value": "cxk"
      }
    }
  }
}

POST /lib_sql/_search
{
  "query":{
    "terms": {
      "name": [
        "main",
        "cxk"
      ]
    }
  }
}

term 跟terms 就基本上代表了我上面两条sql的意思。需要注意的事,默认情况下,一本文本类型的字段,mapping自动给analysis分词了 用term可能是直接查不出的。对于数值型的字段,是可以直接用term的。如果要对文本字段用term精准匹配,最好把字段设置成not analysis。还有ES里还有一种keyword字段类型,默认是1000长度,它也是支持term精确查询的,所以有些场景下我们可以手动设置mapping,把text字段设成keyword类型。

2.多条件

select * from table where a=xx and b=xxx

select * from table where a=xx or b=xxx

select * from table where a!=xx and (b=xxx or c=xxxx)

{
  "query": {
    "bool": {
      "should": [{}], //满足其中一个对象查询条件就行 想sql里的or
      "must": [{}],   //必须满足所有对象的查询条件 就像sql里的and
      "must_not": [{}] //必须不满足所有对象的查询条件 就像sql里的and !=
    }
  }
}

多条件的查询必须要用bool包在外层,然后再根据具体的业务来拼接。

举个例子比如我要查询name为cxk或者anit的

{
  "query": {
    "bool": {
      "should": [{
          "term": {
            "name": {
              "value": "cxk"
            }
          }
        },
        {
          "term": {
            "name": {
              "value": "anit"
            }
          }
        }
      ]
    }
  }
}

再比如我要查询name包含main并且年龄是18的

{
  "query": {
    "bool": {
      "must": [{
          "match": {
            "name": "main"
          }
        },
        {
          "term": {
            "age": {
              "value": 18
            }
          }
        }
      ]
    }
  }
}

3.distinct

select distinct(id) from table

{
  "query":{},
  "collapse": {
      "field": "age" //你需要distinct的字段
   }, 
}

4.order by

实现orderby很简单 就是前面有个提到过有个sort字段 直接就能实现了。

需要注意的是 ,日期格式、数值格式的字段才支持排序,文本类自动分词了的是不支持的直接排序的,如果你要排也可以,解决办法就是多增加一个相同的字段,把这个字段设置为not analysis

5.group by

select count(id) from table groyp by b

ES没有专门的group关键词,但是它也是支持聚合查询的,这里就要用到关键词aggs

{
  "query":{},//这里省略你的查询条件
  "aggs": {
    "age_group": {//这个是指你要返回字段名
      "terms": { //这里还可以用其它关键词 这里的terms才能实现group by效果
        "field": "age",//groupby的字段
        "size":1 //返回的条数 相当于group by limit
      }
    }
  }
}

问题来了,sql是支持group by多字段的 ES里的话 就得在aggs里再嵌套一个aggs这样也能达到聚合多字段的目的

当然,aggs关键词还能支持avg sum min max cardinality(求基数)之类的操作

如果想要执行像类似sql那种having count的 aggs里面也是支持的 需要子aggs里使用bucket_selector,但是这个东西我也基本没用过,所以就不举例了。

6.分页

从刚开始讲的form 跟size字段,我们就能实现简单的分页了。但是就像mysql的limit offset一样,数据量少的时候没啥问题 但是数据量很大的时候,傻逼了,分页速度超级慢。没关系,ES还支持一种类似游标的叫scroll,这样就可以一直加载更多了

POST /lib_sql/_search?scroll=10m
{
  "query": {
    "match": {
      "name": "user" //name是文档中的一个字段
    }
  },
  "size": 1 //scroll返回的数据条数
}

GET /_search/scroll
{
  "scroll":"1m",
  "scroll_id":"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAC51IWZmw4RzhoOEVUR2VWU2tsR1o4aWdaQQ=="
}

这里的scroll=10m指的是当然这次查询的快照保存10分钟 我们生成个游标后,后续只要用这个scroll_id不断查就行了 每次查询都会刷新快照的时间。这样虽然比from size这种方法快了很多,但是也有缺点,快照的数据实时性没那么高,所以具体用哪种还是得看具体业务场景。

再回过头讲讲match、filter、constant_score

上面讲了那么多,我感觉大家也对ES的查询语法有了基本了解,如果完全没了解过ES的,可能还有点懵逼。

大多数情况下,我们使用ES还是为了使用它的查询功能,大多数情况下,肯定不会是精准匹配,基本上都是用户输入一些内容,然后根据匹配程度 以及其它的权重来列出搜索结果。

{
  "query":{
    "match": { 
      "name": "user A"
    }
  }
}

{
  "query":{
    "match_phrase": {
      "name": "user A"
    }
  }
}

{
  "query":{
    "multi_match": {
      "query": "user A",
      "type": "cross_fields",
      "fields": ["interests", "name"]
    }
  }
}

如果只是单纯使用match的话 那就会把关键词分割掉 只要文档中有一个或者多个词匹配 都会返回结果

match_phrase比match严格,比如所有关键词全部匹配 并且顺序一样才会返回结果,但是实际场景中这种太严格了,搜出来的结果太少了。所以一般的解决方案就是外层用一个bool查询包一个should,然后should里面既有match跟match_phrase 然后使用boost来提升match_phrase的分数 让他排在前面。

multi_match是指匹配多个字段,所以它有个type,基本上可以满足各种查询需求

cross_fields

词是分配到不同字段中

best_fields

完全匹配词的文档占的评分高,会排在返回结果前面

most_fields

越多字段匹配的文档评分越高会排在返回结果前面

至于filter跟constant_score的应用场景,constant_score这个其实就跟它的字面意思一样,查询结果就不用计算分数了,ES有一大波计算量是统计文档的相关度,然后得出分数,这个分数其实挺耗性能的,所以有些查询如果你使用不到分数的话 外层包一个constant_score,会提升你的查询性能,并减少ES的负担。至于filter,比如我们直接可以在一个bool查询里面指定range,也能正常查出来结果,但是最好把这种数值类的range条件都放在filter里面。因为filter里过滤是不算评分的,同时filter的结果是可以被cache的。所以比你直接在查询里面过滤要高效的多。比如我日常搜索log基本结构就像下面这样,因为log很多情况下不需要匹配程度这种。

{
  "query": {
    "constant_score": {
      "filter": {
        "bool": {
          "must": [{
            "match": {}
          }],
          "filter": {
            "range": {
              "@timestamp": {
                "gte": "2020-08-25T07:48:24.000Z",
                "lt": "2020-08-25T15:48:24.000Z"
              }
            }
          }
        }
      }
    }
  }
}

随便聊聊

  1. 基本上我这一系列算是小完结了,后期如果还有研究ES,或者工作实际有更深入用到ES的话我可能还会再出。
  2. 我只是个为了用各种姿势查log的工具人,然后学会了这些查询,可能讲的不全,或者有部分是错的,欢迎公众号直接发消息指出,当然有疑问也可以提,如果在我力所能及保证基本正确答案的前提下,我会回复。
  3. 后面会写什么,我也不知道,不知道,如果我写点Java的从入门到还未精通的系列大家会看吗?

本文分享自微信公众号 - 高性能API社区(high-performance-api),作者:安尼特

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-08-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • PHP网络编程之抽象一个event-loop(十八节)

    一部分格局比较大的泥腿子估计内心早就已经预见到了这种「风口」,虽然可能没有做好迎接这种「风口」的物理准备,但也至少做好了心理准备,没关系迟早还会?回来的。

    老李秀
  • PHP网络编程之Libevent-EventBuffer(十七节)

    其实今天是接着上一篇文章尾巴的两个问题的延续,一个是多进程(线程)中使用Libevent,另一个是关于触发可写在时候那个需要自己维护的发送数据缓冲区。

    老李秀
  • 继续搞【附近】系列---MySQL搞LBS(二)

    考虑到在座的各位...都是泥腿子,唯一会做的就是用PHP CRUD,而且即便是只会搞CRUD,也还是离不开MySQL。

    老李秀
  • PostgreSQL概述

    PostgreSQL作为关系数据库中学院派的代表,在U.C. Berkeley完成了初始版本,其后U.C. Berkeley将其源码交于开源社区,Postgre...

    博文视点Broadview
  • Django之ORM F与Q查询

    Django 的 ORM 是创建 SQL 去查询和操作数据库的一个 Python 式的方式。

    小团子
  • 我的Mysql查询SQL优化总结

    当我们遇到一个慢查询语句时,首先要做的是检查所编写的 SQL 语句是否合理,优化 SQL 语句从而提升查询效率。所以对 SQL 有一个整体的认识是有必要的。

    程序员小明
  • Access生成表查询

    大家好前面已经介绍了选择查询、参数查询、交叉表查询,本节开始介绍操作查询部分内容。

    无言之月
  • Access参数查询(一)

    大家好前面分别介绍了选择查询中的汇总查询、重复项查询和不匹配项查询,本节将介绍参数查询。

    无言之月
  • 2018-06-04第11章 子查询

    用户1250179
  • Access查询设计界面

    大家好,上节介绍了Access查询的知识框架,其实Access数据库的查询的功能很类似于Excel表中的筛选功能,但是功能更为丰富和强大。

    无言之月

扫码关注云+社区

领取腾讯云代金券