十九種Elasticsearch字符串搜索方式終極介紹

前言

剛開始接觸Elasticsearch的時候被Elasticsearch的搜索功能搞得暈頭轉向,每次想在Kibana裏面查詢某個字段的時候,查出來的結果常常不是本身想要的,然而又不知道問題出在了哪裏。出現這個問題歸根結底是由於對於Elasticsearch的底層索引原理以及各個查詢搜索方式的不瞭解,在Elasticsearch中僅僅字符串相關的查詢就有19個之多,若是不弄清楚查詢語句的工做方式,應用可能就不會按照咱們預想的方式運做。這篇文章就詳細介紹了Elasticsearch的19種搜索方式及其原理,老闆不再用擔憂我用錯搜索語句啦!php

簡介

Elasticsearch爲全部類型的數據提供實時搜索和分析,無論數據是結構化文本仍是非結構化文本、數字數據或地理空間數據,都能保證在支持快速搜索的前提下對數據進行高效的存儲和索引。用戶不只能夠進行簡單的數據檢索,還能夠聚合信息來發現數據中的趨勢和模式。html

搜索是Elasticsearch系統中最重要的一個功能,它支持結構化查詢、全文查詢以及結合兩者的複雜查詢。結構化查詢有點像SQL查詢,能夠對特定的字段進行篩選,而後按照特定的字段進行排序獲得結果。全文查詢會根據查詢字符串尋找相關的文檔,而且按照相關性排序。java

Elasticsearch內包含不少種查詢類型,下面介紹是其中最重要的19種。若是你的app想要添加一個搜索框,爲用戶提供搜索操做,而且數據量很大用MySQL會形成慢查詢想改用Elasticsearch,那麼我相信這篇文章會給你帶來很大的幫助。c++

query和filter區別

在正式進入到搜索部分以前,咱們須要區分query(查詢)和filter(過濾)的區別。正則表達式

在進行query的時候,除了完成匹配的過程,咱們實際上在問「這個結果到底有多匹配咱們的搜索關鍵詞」。在全部的返回結果的後面都會有一個_score字段表示這個結果的匹配程度,也就是相關性。相關性越高的結果就越排在前面,相關性越低就越靠後。當兩個文檔的相關性相同的時候,會根據lucene內部的doc_id字段來排序,這個字段對於用戶是不可見的也不能控制。segmentfault

而在進行filter的時候,僅僅是在問「這個文檔符不符合要求」,這僅僅是一個過濾的操做判斷文檔是否知足咱們的篩選要求,不會計算任何的相關性。好比timestamp的範圍是否在2019和2020之間,status狀態是不是1等等。數組

在一個查詢語句裏面能夠同時存在queryfilter,只不過只有query的查詢字段會進行相關性_score的計算,而filter僅僅用來篩選。好比在下面的查詢語句裏面,只有title字段會進行相關性的計算,而下面的status只是爲了篩選並不會計算相關性。緩存

GET /_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "Search"}}
      ],
      "filter": [
        {"term": {"state": 1}}
      ]
    }
  }
}

對於在實際應用中應該用query仍是用filter須要根據實際的業務場景來看。若是你的產品的搜索只是須要篩選獲得最後的搜索結果並不須要Elasticsearch的相關性排序(你可能自定義了其餘的排序規則),那麼使用filter就徹底可以知足要求而且可以有更好的性能(filter不須要計算相關性並且會緩存結果);若是須要考慮文檔和搜索詞的相關性,那麼使用query就是最好的選擇。app

相關性

上面講到了在使用query查詢的時候會計算相關性而且進行排序,不少人都會好奇相關性是怎麼計算的?elasticsearch

相關性的計算是比較複雜的,詳細的文檔能夠看這兩篇博客——什麼是相關性ElasticSearch 使用教程之_score(評分)介紹,我這裏只是作一個簡單的介紹。

Elasticsearch的類似度計算主要是利用了全文檢索領域的計算標準——TF/IDF(Term Frequency/Inverted Document Frequency)也就是檢索詞頻率反向文檔頻率

  1. TF(檢索詞頻率):檢索詞在這個字段裏面出現的頻率越高,相關性越高。好比搜索詞出現5次確定比出現1次的文檔相關性更高。
  2. IDF(反向文檔頻率):包含檢索詞的文檔的頻率越高,這個檢索詞的相關性比重越低。若是一個檢索詞在全部的文檔裏面都出現了,好比中文的,那麼這個檢索詞確定就不重要,相對應的根據這個檢索詞匹配的文檔的相關性權重應該降低。
  3. 字段長度:注意這個字段是文檔的裏面被搜索的字段,不是檢索詞。若是這個字段的長度越長,相關性就越低。這個主要是由於這個檢索詞在字段內的重要性下降了,文檔就相對來講不那麼匹配了。

在複合查詢裏面,好比bool查詢,每一個子查詢計算出來的評分會根據特定的公式合併到綜合評分裏面,最後根據這個綜合評分來排序。當咱們想要修改不一樣的查詢語句的在綜合評分裏面的比重的時候,能夠在查詢字段裏面添加boost參數,這個值是相對於1來講的。若是大於1則這個查詢參數的權重會提升;若是小於1,權重就降低。

這個評分系統通常是系統默認的,咱們能夠根據須要定製化咱們本身的相關性計算方法,好比經過腳本自定義評分。

分析器

分析器是針對text字段進行文本分析的工具。文本分析是把非結構化的數據(好比產品描述或者郵件內容)轉化成結構化的格式從而提升搜索效率的過程,一般在搜索引擎裏面應用的比較多。

text格式的數據和keyword格式的數據在存儲和索引的時候差異比較大。keyword會直接被當成整個字符串保存在文檔裏面,而text格式數據,須要通過分析器解析以後,轉化成結構化的文檔再保存起來。好比對於the quick fox字符串,若是使用keyword類型,保存直接就是the quick fox,使用the quick fox做爲關鍵詞能夠直接匹配,可是使用the或者quick就不能匹配;可是若是使用text保存,那麼分析器會把這句話解析成thequickfox三個token進行保存,使用the quick fox就沒法匹配,可是單獨用thequickfox三個字符串就能夠匹配。因此對於text類型的數據的搜索須要格外注意,若是你的搜索詞得不到想要的結果,頗有多是你的搜索語句有問題。

分析器的工做過程大概分紅兩步:

  1. 分詞(Tokenization):根據中止詞把文本分割成不少的小的token,好比the quick fox會被分紅thequickfox,其中的中止詞就是空格,還有不少其餘的中止詞好比&或者#,大多數的標點符號都是中止詞
  2. 歸一化(Normalization):把分隔的token變成統一的形式方便匹配,好比下面幾種

    • 把單詞變成小寫,Quick會變成quick
    • 提取詞幹,foxes變成fox
    • 合併同義詞,jumpleap是同義詞,會被統一索引成jump

Elasticsearch自帶了一個分析器,是系統默認的標準分析器,使用標準分詞器,大多數狀況下都可以有不錯的分析效果。用戶也能夠定義本身的分析器,用於知足不一樣的業務需求。

想要知道某個解析器的分析結果,能夠直接在ES裏面進行分析,執行下面的語句就好了:

POST /_analyze
{
  "analyzer": "standard",
  "text": "1 Fire's foxes"
}

返回的結果是:

{
  "tokens" : [
    {
      "token" : "1",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<NUM>",
      "position" : 0
    },
    {
      "token" : "fire's",
      "start_offset" : 2,
      "end_offset" : 8,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "fox",
      "start_offset" : 9,
      "end_offset" : 12,
      "type" : "<ALPHANUM>",
      "position" : 2
    }
  ]
}

返回的tokens內部就是全部的解析結果,token表示解析的詞語部分,start_offsetend_offset分別表示token在原text內的起始和終止位置,type表示類型,position表示這個token在整個tokens列表裏面的位置。

OK!有了上面的基礎知識,就能夠進行下面的搜索的介紹了。

term搜索

term搜索不只僅能夠對keyword類型的字段使用,也能夠對text類型的數據使用,前提是使用的搜索詞必需要預先處理一下——不包含中止詞而且都是小寫(標準解析器),由於文檔裏面保存的text字段分詞後的結果,用term是能夠匹配的。

exists

返回全部指定字段不爲空的文檔,好比這個字段對應的值是null或者[]或者沒有爲這個字段創建索引。

GET /_search
{
  "query": {
    "exists": {
      "field": "user"
    }
  }
}

若是字段是空字符串""或者包含null的數組[null,"foo"],都會被看成字段存在。

這個方法能夠用來搜索沒有被索引的值或者不存在的值。

fuzzy

fuzzy查詢是一種模糊查詢,會根據檢索詞和檢索字段的編輯距離(Levenshtein Distance)來判斷是否匹配。一個編輯距離就是對單詞進行一個字符的修改,這種修改多是

  • 修改一個字符,好比boxfox
  • 刪除一個字符,好比blacklack
  • 插入一個字符,好比sicsick
  • 交換兩個相鄰的字符的位置,好比actcat

在進行fuzzy搜索的時候,ES會生成一系列的在特定編輯距離內的變形,而後返回這些變形的準確匹配。默認狀況下,當檢索詞的長度在0..2中間時,必須準確匹配;長度在3..5之間的時候,編輯距離最大爲1;長度大於5的時候,最多容許編輯距離爲2

能夠經過配置fuzziness修改最大編輯距離,max_expansions修改最多的變形的token的數量

好比搜索是如下條件的時候:

GET /_search
{
  "query": {
    "fuzzy": {
      "name": "Accha"
    }
  }
}

返回結果有IcchaAccHaaccha還有ccha

ids

根據文檔的_id數組返回對應的文檔信息

GET /_search
{
  "query": {
    "ids": {
      "values": ["1","4","100"]
    }
  }
}

prefix

返回全部包含以檢索詞爲前綴的字段的文檔。

GET /_search
{
  "query": {
    "prefix": {
      "name": "ac"
    }
  }
}

返回全部以ac開頭的字段,好比acchuachuachar等等

在某些場景下面好比搜索框裏面,須要用戶在輸入內容的同時也要實時展現與輸入內容前綴匹配的搜索結果,就可使用prefix查詢。爲了加速prefix查詢,還能夠在設置字段映射的時候,使用index_prefixes映射。ES會額外創建一個長度在2和5之間索引,在進行前綴匹配的時候效率會有很大的提升。

range

對字段進行範圍的匹配。

GET /_search
{
  "query": {
    "range": {
      "age": {
        "gte": 10,
        "lte": 20
      }
    }
  }
}

搜索年齡在10(包含)和20(包含)之間的結果

regexp

正則表達式匹配。經過正則表達式來尋找匹配的字段,lucene會在搜索的時候生成有限狀態機,其中包含不少的狀態,默認的最多狀態數量是10000

GET /_search
{
  "query": {
    "regexp": {
      "name": "ac.*ha"
    }
  }
}

這個搜索會匹配achhaachintha還有achutha

term

根據檢索詞來準確匹配字段。官方文檔建議不要用term去搜索text類型的字段,由於分析器的緣由頗有可能不會出現你想要的結果。可是直接使用term去搜索text字段仍是能夠工做的,前提是明白爲何會返回這些數據。好比經過下面的搜索:

GET /_search
{
  "query": {
    "term": {
      "name": {
        "value": "accha"
      }
    }
  }
}

若是name字段是keyword類型的,沒有進行解析,那麼只會匹配全部nameaccha的文檔。

若是name字段是text類型的,原字段通過分詞、小寫化處理以後,只能匹配到解析以後的單獨token,好比使用標準解析器,這個搜索會匹配Accha Bacchaso cute accha baccha或者Accha Baccha Shivam等字段。

terms

根據檢索詞列表來批量搜索文檔,每一個檢索詞在搜索的時候至關於or的關係,只要一個匹配就好了。Elasticsearch最多容許65,536個term同時查詢。

GET /_search
{
  "query": {
    "terms": {
      "name": [
        "accha",
        "ghazali"
      ]
    }
  }
}

上面的查詢會匹配name字段爲acchaghazali的文檔。

除了直接指定查詢的term列表,還可使用Terms lookUp功能,也就是指定某一個存在的文檔的某一個字段(多是數字、字符串或者列表)來做爲搜索條件,進行terms搜索。

好比有一個文件indexmy_docid10name字段是term而且值爲accha,搜索能夠這樣寫:

{
  "query": {
    "terms": {
      "name": {
        "index": "my_doc",
        "id": "10",
        "path": "name"
      }
    }
  }
}

這樣就能夠返回全部name字段值是accha的文檔裏,這個一般能夠用來查詢全部和某個文檔某個字段重複的文檔而且不須要提早知道這個字段的值是什麼。

terms_set

terms_set和terms十分相似,只不過是多了一個最少須要匹配數量minimum_should_match_field參數。當進行匹配的時候,只有至少包含了這麼多的terms中的term的時候,纔會返回對應的結果。

GET /_search
{
  "query": {
    "terms_set": {
      "programming_languages": {
        "terms": ["c++","java","php"],
        "minimum_should_match_field": "required_match"
      }
    }
  }
}
{
    "name":"Jane Smith",
    "programming_languages":[
        "c++",
        "java"
    ],
    "required_matches":2
}

那麼只有programming_languages列表裏面至少包含["c++", "java", "php"]其中的2項才能知足條件

還可使用minimum_should_match_script腳原本配置動態查詢

{
  "query": {
    "terms_set": {
      "programming_languages": {
        "terms": ["c++","java","php"],
        "minimum_should_match_script": {
          "source": "Math.min(params.num_terms, doc['required_matches'].value)"
        }
      }
    }
  }
}

其中params.num_terms是在terms字段中的元素的個數

wildcard

通配符匹配,返回匹配包含通配符的檢索詞的結果。

目前只支持兩種通配符:

  • ?:匹配任何單一的字符
  • *:匹配0個或者多個字符

在進行wildcard搜索的時候最好避免在檢索詞的開頭使用*或者?,這會下降搜索性能。

GET /_search
{
  "query": {
    "wildcard": {
      "name": {
        "value": "acc*"
      }
    }
  }
}

這個搜索會匹配acchuacche或者accio父

text搜索

text搜索其實是針對被定義爲text類型的字段的搜索,一般搜索的時候不能根據輸入的字符串的總體來理解,而是要預先處理一下,把搜索詞變成小的token,再來查看每一個token的匹配。

interval

返回按照檢索詞的特定排列順序排列的文檔。這個查詢比較複雜,這裏只是簡單的介紹,詳細的介紹能夠看官方文檔

好比咱們想查詢同時包含rajnayaka的字段而且ray正好在nayaka前面,查詢語句以下:

POST /_search
{
  "query": {
    "intervals": {
      "name": {
        "match": {
          "query": "raj nayaka",
          "max_gaps": 0,
          "ordered": true
        }
      }
    }
  }
}

上面的查詢會匹配Raj Nayaka Acchu ValmikiYateesh Raj Nayaka

若是把ordered:true去掉,就會匹配nayaka raj

若是把max_gaps:0去掉,系統會用默認值-1也就是沒有距離要求,就會匹配Raj Raja nayaka或者Raj Kumar Nayaka

其中有兩個關鍵詞orderedmax_gaps分別用來控制這個篩選條件是否須要排序以及兩個token之間的最大間隔

match

查找和檢索詞短語匹配的文檔,這些檢索詞在進行搜索以前會先被分析器解析,檢索詞能夠是文本、數字、日期或者布爾值。match檢索也能夠進行模糊匹配。

GET /_search
{
  "query": {
    "match": {
      "name": "nagesh acchu"
    }
  }
}

以上的查詢會匹配NaGesh AcchuAcchu Acchuacchu。系統默認是在分詞後匹配任何一個token均可以完成匹配,若是修改operatorAND,則會匹配同時包含nageshacchu的字段。

GET /_search
{
  "query": {
    "match": {
      "name": {
        "query": "nagesh acchu",
        "operator": "and"
      }
    }
  }
}

上面這個查詢就只會返回NaGesh Acchu

查詢的時候也可使用模糊查詢,修改fuzziness參數

GET /_search
{
  "query": {
    "match": {
      "name": {
        "query": "nagesh acchu",
        "operator": "and",
        "fuzziness": 1
      }
    }
  }
}

上面的語句會匹配NaGesh Acchu還有Nagesh Bacchu

match_bool_prefix

match_bool_prefix會解析檢索詞,而後生成一個bool複合檢索語句。若是檢索詞由不少個token構成,除了最後一個會進行prefix匹配,其餘的會進行term匹配。

好比使用nagesh ac進行match_bool_prefix搜索

GET /_search
{
  "query": {
    "match_bool_prefix": {
      "name": "nagesh ac"
    }
  }
}

上面的查詢會匹配Nagesh NageshRakshith Achar或者ACoco

實際查詢等價於

GET /_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "name": {
              "value": "nagesh"
            }
          }
        },
        {
          "prefix": {
            "name": {
              "value": "ac"
            }
          }
        }
      ]
    }
  }
}

match_phrase

詞組匹配會先解析檢索詞,而且標註出每一個的token相對位置,搜索匹配的字段的必須包含全部的檢索詞的token,而且他們的相對位置也要和檢索詞裏面相同。

GET /_search
{
  "query": {
    "match_phrase": {
      "name": "Bade Acche"
    }
  }
}

這個搜索會匹配Bade Acche Lagte,可是不會匹配Acche Bade Lagte或者Bade Lagte Acche

若是咱們不要求這兩個單詞相鄰,但願放鬆一點條件,能夠添加slop參數,好比設置成1,表明兩個token之間相隔的最多的距離(最多須要移動多少次才能相鄰)。下面的查詢語句會匹配Bade Lagte Acche

GET /_search
{
  "query": {
    "match_phrase": {
      "name": {
        "query": "Bade Acche",
        "slop": 1
      }
    }
  }
}

match_phrase_prefix

match_phrase_prefix至關因而結合了match_bool_prefix和match_phrase。ES會先解析檢索詞,分紅不少個token,而後除去最後一個token,對其餘的token進行match_phrase的匹配,即所有都要匹配而且相對位置相同;對於最後一個token,須要進行前綴匹配而且匹配的這個單詞在前面的match_phrase匹配的結果的後面。

GET /_search
{
  "query": {
    "match_phrase_prefix": {
      "name": "acchu ac"
    }
  }
}

上面的查詢可以匹配Acchu Acchu1Acchu Acchu Papu,可是不能匹配acc acchu或者acchu pa

multi_match

multi_match能夠同時對多個字段進行查詢匹配,ES支持不少種不一樣的查詢類型好比best_fields(任何字段match檢索詞都表示匹配成功)、phrase(用match_phrase代替match)還有cross_field(交叉匹配,一般用在全部的token必須在至少一個字段中出現)等等

下面是普通的best_fields的匹配

GET /_search
{
  "query": {
    "multi_match": {
      "query": "acchu",
      "fields": [
        "name",
        "intro"
      ]
    }
  }
}

只要name或者intro字段任何一個包含acchu都會完成匹配。

若是使用cross_fields匹配以下

GET /_search
{
  "query": {
    "multi_match": {
      "query": "call acchu",
      "type": "cross_fields",
      "fields": [
        "name",
        "intro"
      ],
      "operator": "and"
    }
  }
}

上面的匹配須要同時知足下面兩個條件:

  • name中出現callintro中出現call
  • name中出現acchuintro中出現acchu

因此這個查詢可以匹配name包含acchuintro包含call的文檔,或者匹配name同時包含callacchu的文檔。

common

common查詢會把查詢語句分紅兩個部分,較爲重要的分爲一個部分(這個部分的token一般在文章中出現頻率比較低),不那麼重要的爲一個部分(出現頻率比較高,之前可能被看成中止詞),而後分別用low_freq_operatorhigh_freq_operator以及minimum_should_match來控制這些語句的表現。

在進行查詢以前須要指定一個區分高頻和低頻詞的分界點,也就是cutoff_frequency,它既能夠是小數好比0.001表明該字段全部的token的集合裏面出現的頻率也能夠是大於1的整數表明這個詞出現的次數。當token的頻率高於這一個閾值的時候,他就會被看成高頻詞。

GET /_search
{
  "query": {
    "common": {
      "body": {
        "query": "nelly the elephant as a cartoon",
        "cutoff_frequency": 0.001,
        "low_freq_operator": "and"
      }
    }
  }
}

其中高頻詞是theaas,低頻詞是nellyelephantcartoon,上面的搜索大體等價於下面的查詢

GET /_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {"body": "nelly"}},
        {"term": {"body": "elephant"}},
        {"term": {"body": "cartoon"}}
      ],
      "should": [
        {"term": {"body": "the"}},
        {"term": {"body": "as"}},
        {"term": {"body": "a"}}
      ]
    }
  }
}

可是第一個查詢的效率要優於第二個,由於common語句有性能上的優化,只有重要的token匹配以後的文檔,纔會在不重要的文檔的查詢時候計算_score;不重要的token在查詢的時候不會計算_score

query_string

輸入一個查詢語句,返回和這個查詢語句匹配的全部的文檔。

這個查詢語句不是簡單的檢索詞,而是包含特定語法的的搜索語句,裏面包含操做符好比ANDOR,在進行查詢以前會被一個語法解析器解析,轉化成能夠執行的搜索語句進行搜索。用戶能夠生成一個特別複雜的查詢語句,裏面可能包含通配符、多字段匹配等等。在搜索以前ES會檢查查詢語句的語法,若是有語法錯誤會直接報錯。

GET /_search
{
  "query": {
    "query_string": {
      "default_field": "name",
      "query": "acchu AND nagesh"
    }
  }
}

上面的查詢會匹配全部的同時包含acchunagesh的結果。簡化一下能夠這樣寫:

GET /_search
{
  "query": {
    "query_string": {
      "query": "name: acchu AND nagesh"
    }
  }
}

query_string裏面還支持更加複雜的寫法:

  • name: acchu nagesh:查詢name包含acchunagesh其中的任意一個
  • book.\*:(quick OR brown)book的任何子字段好比book.titlebook.content,包含quick或者brown
  • _exists_: titletitle字段包含非null
  • name: acch*:通配符,匹配任何acch開頭的字段
  • name:/joh?n(ath[oa]n)/:正則表達式,須要把內容放到兩個斜槓/中間
  • name: acch~:模糊匹配,默認編輯距離爲2,不過80%的狀況編輯距離爲1就能解決問題name: acch~1
  • count:[1 TO 5]:範圍查詢,或者count: >10

下面的查詢容許匹配多個字段,字段之間時OR的關係

GET /_search
{
  "query": {
    "query_string": {
      "fields": [
        "name",
        "intro"
      ],
      "query": "nagesh"
    }
  }
}

simple_query_string

和上面的query_string相似,可是使用了更加簡單的語法。使用了下面的操做符:

  • +表示AND操做
  • |表示OR操做
  • -表示否認
  • "用於圈定一個短語
  • *放在token的後面表示前綴匹配
  • ()表示優先級
  • ~N放在token後面表示模糊查詢的最大編輯距離fuzziness
  • ~N放在phrase後面表示模糊匹配短語的slop
GET /_search
{
  "query": {
    "simple_query_string": {
      "query": "acch* + foll~2 + -Karen",
      "fields": [
        "intro"
      ]
    }
  }
}

上面的搜索至關於搜索包含前綴爲acch的、和foll編輯距離最大是2的而且不包含Karen的字段,這樣的語句會匹配call me acchu或者acchu follow me

總結

Elasticsearch提供了強大的搜索功能,使用query匹配能夠進行相關性的計算排序可是filter可能更加適用於大多數的過濾查詢的狀況,若是用戶對於標準解析器不太滿意能夠自定義解析器或者第三方解析器好比支持中文的IK解析器

在進行搜索的時候必定要注意搜索keywordtext字段時候的區別,使用term相關的查詢只能匹配單個的token可是使用text相關的搜索能夠利用前面的term搜索進行組合查詢,text搜索更加靈活強大,可是性能相對差一點。

參考

什麼是相關性?
ElasticSearch 使用教程之_score(評分)介紹
Full text queries
Term-level queries
Elasticsearch query performance using filter query
Unicode Text Segmentation
短詞匹配
Top hits query with same score?

更多精彩內容請看個人我的博客

相關文章
相關標籤/搜索