天天看點

像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的從入門到還未精通的系列大家會看嗎?