Elasticsearch系列---實戰搜索語法

概要

本篇介紹Query DSL的語法案例,查詢語句的調試,以及排序的相關內容。

基本語法

空查詢

最簡單的搜索命令,不指定索引和類型的空搜索,它將返回集羣下所有索引的所有文檔(默認顯示10條):

GET /_search
{}
搜索多個索引
GET /index1,index2/_doc/_search
{}
指定分頁搜索
GET /_search
{
  "from": 0,
  "size": 10
}
get帶request body

HTTP協議,GET請求帶body是不規範的做法,但由於ES搜索的複雜性,加上HTTP協議GET/POST方法表述的語義,GET更適合用來表述查詢的動作,雖然不規範,但還是這麼用了。現在大多數瀏覽器也支持GET+request body,如果遇到不支持的,換成POST即可。瞭解一下就行,不用太慌張。

查詢表達式Query DSL

Query DSL是一種非常靈活、可讀性高的查詢語言,body爲JSON格式,絕大部分功能都可以用它來展現,並且這種查詢語句更純粹,讓學習者更專注於本身的功能,避免Client API的干擾。

上一節的空查詢,等價於這個:

GET /_search
{
    "query": {
        "match_all": {}
    }
}
基本語法
# 查詢語句結構
{
    QUERY_NAME: {
        ARGUMENT: VALUE,
        ARGUMENT: VALUE,...
    }
}

# 針對某個字段的查詢
{
    QUERY_NAME: {
        FIELD_NAME: {
            ARGUMENT: VALUE,
            ARGUMENT: VALUE,...
        }
    }
}
合併查詢語句

再複雜的查詢語句,也是由一個一個的查詢條件疊加而成的,查詢語句有兩種形式:

  • 葉子語句:單個條件組成的語句,如match語句,類似mysql的"id = 1"這種。
  • 複合語句:有多個條件,需要合併在一起才能組成一個完整的語句,需要使用bool進行組合,裏面的條件可以用must必須匹配、must not必須不匹配、should可以匹配修飾,也可以包含過濾器filter。類似mysql的"(status = 1 && language != 'french' && (author = 'John' || author = 'Tom'))"這種。

舉個例子:

{
    "bool": {
        "must":     { "match": { "status": 1 }},
        "must_not": { "match": { "language":  "french" }},
        "should":   { "match": { "author": "John Tom" }},
        "filter":   { "range": { "length" : { "gt" : 30 }} }
    }
}

複合語句可以嵌套,來實現更復雜的查詢需求,在上面的例子上簡單延伸一下:

"bool": {
        "must":     { "match": { "status": 1 }},
        "must_not": { "match": { "language":  "french" }},
        "should":   [
            {"match": { "author": "John Tom" }},
            {"bool": {
                "must":     { "match": { "name": "friend" }},
                "must_not": { "match": { "content":  "star" }}
            }}
        ],
        "filter":   { "range": { "length" : { "gt" : 30 }} }
    }
複合語句相關性分數計算

每一個子查詢都獨自地計算文檔的相關性得分。一旦他們的得分被計算出來,bool 查詢就將這些得分進行合併並且返回一個代表整個布爾操作的得分,得分高的顯示在前面,filter內的條件不參與分數計算。

過濾器filter

我們還是以英文兒歌的索引爲案例,看一個搜索需求:歌詞內容包含friend,同時歌長大於30秒的記錄

GET /music/children/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "content": "friend"
          }
        }
      ],
      "filter": {
        "range": {
          "length": {
            "gte": 30
          }
        }
      }
    }
  }
}

filter與query

  • 過濾情況filtering context

僅按照搜索條件把需要的數據篩選出來,不計算相關度分數。

  • 查詢情況query context

匹配條件的數據,會根據搜索條件的相關度,計算每個document的分數,然後按照分數進行排序,這個纔是全文搜索的情況。

性能差異

filter只做過濾,不作排序,並且會緩存結果到內存中,性能非常高。 query匹配條件,要做評分,沒有緩存,性能要低一些。

應用場景

filter一個非常重要的作用就是減少不相關數據對query的影響,提升query的性能,二者常常搭配在一起使用。 組合使用的時候,把期望符合條件的document的搜索條件放在query裏,把要濾掉的條件放在filter裏。

constant_score查詢

如果一個查詢只有filter過濾條件,可以用constant_score來替代bool查詢,這樣的查詢語句更簡潔、更清晰,只是沒有評分,示例如下:

GET /music/children/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { "content": "gymbo"}
      }
    }
  }
}

filter內不支持terms語法,注意一下。

最常用的查詢

再複雜的查詢語句,也是由最基礎的查詢變化而來的,而最常用的查詢其實也就那麼幾個。

  1. match_all查詢

查詢簡單的匹配所有文檔

GET /_search
{
    "query": {
        "match_all": {}
    }
}
  1. match查詢

無論是全文搜索還是精確查詢,match查詢是最基本的標準

# 全文搜索例子
{ "match": { "content": "loves smile" }}

# 精確搜索
{ "match": { "likes":    15           }}
{ "match": { "date":   "2019-12-05" }}
{ "match": { "isOwner": true         }}
{ "match": { "keyword":    "love you"  }}

對於精確值的查詢,我們可以使用filter來替代,filter有緩存的效果。

  1. multi_match查詢

可以在多個字段上執行相同的match查詢

{
    "multi_match": {
        "query":    "my sunshine",
        "fields":   [ "name", "content" ]
    }
}
  1. range查詢

查詢指定區間內的數字或時間,query和filter都支持,一般是filter用得多,允許的操作符如下:

  • gt 大於
  • gte 大於或等於
  • lt 小於
  • lte 小於或等於
{
    "range": {
        "length": {
            "gte":  45,
            "lt":   60
        }
    }
}
  1. term查詢

用於精確值匹配,精確值可以是數字,日期,boolean或keyword類型的字符串

{ "term": { "likes":    15           }}
{ "term": { "date":   "2019-12-05" }}
{ "term": { "isOwner": true         }}
{ "term": { "keyword":    "love you"  }}

建立索引時mapping設置爲not_analyzed時,match等同於term,用得多的是match和range。

  1. terms查詢

跟term類似,只是允許一次指定多個值進行匹配,只要有任何一個匹配上,都滿足條件

{ "terms": { "content": [ "love", "gymbo", "sunshine" ] }}

查詢語句調試

複雜的查詢語句,可能會有幾百行,可以先使用調試工具檢測一下查詢語句,定位不合法的搜索及原因,完整語法如下:

GET /index/type/_validate/query?explain
{
  "query": {
    ...
  }
}

explain參數可以提供更詳細的查詢不合法的信息,便於問題定位。寫一個錯誤的例子,比如使用中文標點符號:

GET /music/children/_validate/query?explain
{
  "query": {
    "terms": { "content“: [ "love", "gymbo", "sunshine" ] }
  }
}

錯誤提示如下:

{
  "valid": false,
  "error": """
ParsingException[Failed to parse]; nested: JsonParseException[Unexpected character ('l' (code 108)): was expecting a colon to separate field name and value
 at [Source: org.elasticsearch.transport.netty4.ByteBufStreamInput@5e57280e; line: 3, column: 33]];; com.fasterxml.jackson.core.JsonParseException: Unexpected character ('l' (code 108)): was expecting a colon to separate field name and value
 at [Source: org.elasticsearch.transport.netty4.ByteBufStreamInput@5e57280e; line: 3, column: 33]
"""
}

valid關鍵字,true爲驗證通過,false爲不通過,如上提示信息,會指明3行33列錯誤,原因是使用了中文的引號。將語法修正後,得到的正確響應如下:

{
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "valid": true,
  "explanations": [
    {
      "index": "music",
      "valid": true,
      "explanation": "+content:(gymbo love sunshine) #*:*"
    }
  ]
}

排序

查詢請求得到的結果,默認排序是相關性得分降序。如果我們只使用filter過濾,符合filter條件的文檔,評分都是一樣的(bool的filter得分是null,constant_score得分是1),結果文檔還是隨機返回,顯然這樣的排序不符合我們的預期。

sort排序規則

爲此,我們可以使用sort屬性,對文檔進行排序,sort的用法與mysql如出一轍,示例如下:

GET /music/children/_search
{
  "query": {
    "bool": {
        "filter":   { "range": { "length" : { "gt" : 30 }} }
    }
  },
  "sort": [
    {
      "length": {
        "order": "desc"
      }
    }
  ]
}

sort內可以同時指定多個排序字段,一旦使用sort排序後,_score得分將變成null,因爲我們指定了排序規則,_score沒有實際意義了,就不用耗費精力再去計算它。

字符串排序問題

我們知道text類型的字段,會有關鍵詞分詞處理,對這樣的字段進行排序,結果往往都不準確,6.x版本以後的text類型,會再自動建立一個keyword類型的字段,這個字段是不分詞的,所以這樣就有了分工,text類型的負責搜索,keyword類型則負責排序。 我們回顧一下music索引的mapping信息(節選):

{
  "music": {
    "mappings": {
      "children": {
        "properties": {
          "content": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "name": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}

例如name字段,有一個text類型的,裏面fields還有一個類型爲keyword,名稱也爲keyword的字段,所以在排序的場景中,我們應該使用name.keyword,示例如下:

GET /music/children/_search
{
  "sort": [
    {
      "name.keyword": {
        "order": "asc"
      }
    }
  ]
}

小結

本篇介紹Query DSL的語法及基礎實戰內容,順帶點了一下filter與query的區別,面對複雜查詢語句時,建議先用驗證工具進行排查,最後介紹了一下排序方面的知識,基礎語法、上機案例多實踐即可。

專注Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公衆號:Java架構社區 Java架構社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章