1. 原理
全文檢索是ES的核心功能。ES中的數據按數據特性可分爲兩類:確切值及全文文本。ES中如keyword,date這些類型的值都可視爲確切值。而text類型的值則視爲全文文本數據。
爲了對全文文本進行檢索,ES使用分析器(analyzer,根據不同自然語言、不同要求選擇不同的分析器)將文本分析爲單獨的詞(英文爲terms或tokens,這裏符合中國人的習慣,稱爲詞),然後根據分詞結果創建倒排索引(inverted index)。倒排索引以文本中的詞爲鍵,該詞在文檔中出現位置爲值的一個數據結構,不同於常見的以文檔序號(或標題)爲鍵,文檔內容爲值的索引形式,所以稱爲倒排索引。ES中文檔對象是一個結構化的JSON數據對象,每一個JSON文檔中被索引的字段都有自己的倒排索引,全文檢索即在倒排索引中檢索。
ES中,分佈式檢索操作需要分散到所有相關的分片中,然後收集所有的結果。對於一個檢索請求,檢索結果在不同分片中的分佈密度可能是不同的,有可能出現一個分片包括了90%的記錄,而其它分片卻只包含極少記錄的情況。當需要對檢索結果排序時,也需要從各獨立的分片中收集到所有的檢索結果後再統一進行排序,這個過程類似於map-reduce的混洗(shuffle)過程。對於大數據集,這個過程可能會非常昂貴。因而在創建數據模型和設計檢索指令時我們應考慮檢索效率。
ES將檢索請求執行分爲兩階段:在第一階段查詢(query)階段,每個涉及檢索的分片執行檢索請求,獲取本地檢索記錄的排序後列表,並把這些可供協調節點(coordinating node)進行全局排序等操作的元數據信息返回給協調節點。在第二階段取回(fetch)階段,協調節點僅向包含檢索結果的分片請求具體的文檔對象數據返回給用戶。
2. 檢索API
ES提供了強大的數據檢索能力,在API級別提供基於URI和基於請求消息體兩種檢索操作方式。
2.1. 基於URI的檢索
2.1.1. 基本格式
基於URI的檢索使用一個HTTP的GET請求攜帶檢索請求參數。檢索參數爲(key,value)形式的名值對,以”&”連接。如果檢索請求中不帶檢索參數則表示檢索所有記錄。
仍以《編程隨筆-ElasticSearch知識導圖(3):映射》中第2節中的銀行賬號索引爲例,先來看一個使用URI的檢索請求:
curl -iXGET 'localhost:9200/bank/_search?pretty&q=(firstname:Amber)AND(lastname:Duke)'
上面的請求中URI中爲索引名(bank)後攜帶檢索路徑(_search),並在URI的“?”之後的查詢字符串中攜帶查詢參數q,查詢滿足firstname爲Amber且lastname爲Duke'的記錄(若要實現全文檢索,則不需要添加前面的域名),查詢參數q在子查詢表達式前(格式爲”域名:值”)使用“+”表示應滿足該條件, “-”表示不應滿足該條件。q中子查詢表達式使用”AND”或“OR”表示條件之間的邏輯與或關係。
2.1.2. 通配符與正則表達式
查詢字符串支持“?”和“*”這樣的簡單通配符,上面的查詢示例使用統配符之後可以變爲如下形式:
curl -iXGET 'localhost:9200/bank/_search?pretty&q=(f\*name:A?ber)AND(lastname:Duke)'
查詢條件字符串支持正則表達式(使用”/”包含起來),上面的查詢可表達爲如下形式:
curl -iXGET 'localhost:9200/bank/_search?pretty&q=(firstname:/A~r/)AND(lastname:Duke)'
2.1.3. 分頁與排序
ES默認在一次檢索請求中只返回檢索結果的前10條記錄。因而當檢索記錄較多時,檢索結果需要進行分頁。
ES提供檢索參數“from”和“size”用於指示返回結果開始的索引值與返回的數目。由於分頁的結果往往基於記錄的排序結果,因而使用“sort”參數實現檢索結果的排序。
考慮如下查詢:
curl -iXGET 'localhost:9200/bank/_search?pretty&q=(balance:>5000)&sort=balance:desc&from=0&size=3'
查詢餘額(balance)大於5000的人羣中最有錢的前三位。
如果不想檢索出所有字段,可以在檢索條件中使用” _source”參數指定在返回結果中指定的域。上面的檢索請求可轉換爲如下請求:
curl -iXGET 'localhost:9200/bank/_search?pretty&q=(balance:>5000)&sort=balance:desc&from=0&size=3&_source=balance,firstname,lastname'
在返回的檢索結果中可以看到“_source”對象中只有“balance,firstname,lastname”這三個字段。
現在我們發現基於URI的檢索格式已經非常類似於常用的SQL格式了。的確,ES支持SQL語言的查詢(在xpack插件中支持)。
2.1.4. 檢索參數
簡單列舉一下常用的基於URI檢索的參數:
參數 | 描述 |
---|---|
q | 查詢條件字符串 |
df | 查詢的缺省域 |
analyzer | 分析查詢字符串時使用的分析器 |
analyze_wildcard | 是否分析通配符 |
default_operator | 確定缺省邏輯關係是AND或OR,缺省是OR. |
explain | 在結果中包含對於命中記錄得分的解釋 |
_source | 選擇在結果中展示的字段 |
sort | 排序條件 |
timeout | 檢索超時時間 |
terminate_after | 每個分片上採集的最大檢索記錄數目 |
from | 檢索結果起始索引 |
size | 本次檢索結果的最大數目 |
search_type | 可以取值:dfs_query_then_fetch 或 query_then_fetch. 缺省爲query_then_fetch |
參數中的search_type用來定義不同的檢索類型來應對不同場景,其取值可爲:
- query_then_fetch:將檢索分爲query和fetch兩個階段,如前面的銀行賬戶查詢例子,與檢索條件相關的所有分片在第一階段返回給協調節點餘額大於5000的記錄元數據信息,協調節點排序後,發現餘額最大的前3名都在1個分片中,那麼第二階段它只需向包含前3名的分片請求完整的檢索結果數據。
- dfs_query_then_fetch:dfs是 Distributed Frequency Search的簡寫。與query_then_fetch方式基本相同,只是增加了一個從所有相關分片中獲取詞頻(本地IDF)以便計算全局詞頻(全局IDF)的預查詢階段,用於更精確的相關性評分(scoring)計算(文獻2中不建議在生產環境下使用此選項)。
2.2. 基於請求消息體的檢索
2.2.1. 何爲DSL
DSL(Domain Specific Language)旨在向目標用戶提供人性化的界面,其要旨在於溝通。DSL擁有貼近用戶思維的語法結構,這些語言抽象依賴於背後提供支撐的語義模型。
ES中定義用於檢索的DSL使用JSON格式,是一種外部DSL語言。
使用URI表示的檢索示例使用DLS改寫後的形式如下:curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d' { "query": { "bool": { "must": { "match_all": {} }, "filter": { "range": { "balance": { "gte": 5000 } } } } }, "sort": { "balance": { "order": "desc" } }, "from": 0, "size": 3, "_source": ["balance","firstname","lastname"] } '
上例中將請求消息體分爲兩個部分,“query”子句和其它檢索參數元素,“query”子句使用Query DSL描述。其它檢索參數同2.1.4節中描述的參數。
檢索DSL提供更加豐富的檢索參數與功能,更多內容參見文獻1。2.2.2. Query DSL
檢索請求中query子句可應用於查詢上下文(Query context)和/或過濾上下文(Filter context)中。查詢上下文中query子句用來回答文檔對象的匹配程度(計算score值衡量相關性),過濾上下文中query子句用來回答文檔對象是否符合過濾條件,只有是否兩個選擇。簡單來說,需要全文檢索或不精確匹配(考察相關性)時應用查詢上下文,需要精確過濾時應用過濾上下文。
上節的檢索示例既應用於查詢上下文又應用於過濾上下文中, “must”子句指示了查詢條件(示例中沒有設置查詢條件,其實可以省略);“filter”子句指示了過濾條件。
將示例中的“match_all”換上更具體的查詢條件,查找姓“Barry”的最有錢的前三位同學:curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d' { "query": { "bool": { "must": { "match": {"lastname":"Barry"} }, "filter": { "range": { "balance": { "gte": 5000 } } } } }, "sort": { "balance": { "order": "desc" } }, "from": 0, "size": 3, "_source": ["balance","firstname","lastname"] } '
上例在查詢子句中指定了域名爲“lastname”,該域表示一個確切值,不需要全文檢索。因而將match子句轉換爲term子句,放到"filter"子句中,檢索出的結果一樣:
curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d' { "query": { "bool": { "filter": [ { "term": { "lastname.keyword": "Barry" } }, { "range": { "balance": { "gte": 5000 } } } ] } }, "sort": { "balance": { "order": "desc" } }, "from": 0, "size": 3, "_source": [ "balance", "firstname", "lastname" ] } '
match和term子句常用來組建查詢子句: term用於搜索域的確切值,即term指示的值在檢索時不經過分析器的處理,對於text類型的域,term子句訪問的分析器產生的倒排索引,常常無法獲得精確匹配的檢索結果(如term檢索值中有大寫字母但不經過分析器轉換,而待檢索的文本域在經分析器分析後倒排索引中都變爲小寫字符,則無法精確匹配)。term常用於檢索keyword、date、數字這些具有確切值類型的域(上面示例中使用的是lastname.keyword這個域),而對需要全文檢索的域的常使用match子句(檢索值與文本域都經分析器轉換過,保持一致)。term子句檢索時不對檢索結果評分(score),因此效率要高。
實際應用 中,如果不涉及全文檢索需求,可在過濾上下文中使用term子句。match和term子句還衍生出諸多變化(如match_phrase,match_phrase_prefix,multi_match,terms,),這些衍生的語法可參考文獻1。
檢索語句中通常會使用多個match或term子句構建組合查詢,ES提供must,should, must_not等邏輯運算符來組合一個複雜查詢,構成一個布爾表達式。Query DSL可使用如下格式表示一個布爾表達式:{ "query": { "bool": { "must": [], "must_not":[], "should":[], "filter":[] } } }
bool子句爲真需要滿足bool子句中各子句的查詢條件。must、should、must_not、filter子句均可爲數組格式(只有一個子元素時可以寫爲對象格式),數組中每個子對象可爲match或term子句。其中:must約束子元素之間的邏輯與(AND)關係,should約束子元素之間的邏輯或(OR)關係,must_not約束各子元素的邏輯非(NOT)關係。bool子句之間是可以嵌套的,一個bool子句可做爲另一個bool子句的子條件,以構建更復雜層次的查詢。
將上面的檢索條件增加一個子條件,查找姓“Barry”或名爲“Burton”的最有錢的前三位同學,查詢命令爲如下形式:curl -iXPOST 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json' -d' { "query": { "bool": { "filter": [ { "bool": { "should": [ { "term": { "lastname.keyword": "Barry" } }, { "term": { "firstname.keyword": "Burton" } } ] } }, { "range": { "balance": { "gte": 5000 } } } ] } }, "sort": { "balance": { "order": "desc" } }, "from": 0, "size": 3, "_source": [ "balance", "firstname", "lastname" ] } '
Query DSL中對於通配符和正則表達式使用專用的子句“wildcard”和“regexp”,與基於URI的檢索方式有些區別,請大家注意。
Query DSL比較強大,語法也多。我的意見是:不用一次就全部弄清楚全部的子句語法,需要時再去文獻1查一下即可;使用本節的bool子句格式能夠解決80%以上的問題;查詢子句的層次越少越好,因爲能提高效率;對於確切值的查詢條件儘量放到filter子句中。
本節最後使用一張圖總結一下Query DSL的常用語法:2.2.3. 檢索模板
ES的檢索API支持使用mustache語言渲染的檢索模板,上節的檢索示例可以改寫成如下模板形式:
curl -iXPOST 'localhost:9200/bank/_search/template?pretty' -H 'Content-Type: application/json' -d' { "source": { "query": { "bool": { "must": { "match_all": {} }, "filter": { "range": { "{{field_1}}": { "gte": "{{_base}}" } } } } }, "sort": { "{{field_1}}": { "order": "desc" } }, "from": 0, "size": 3, "_source": [ "{{field_1}}", "{{field_2}}", "{{field_3}}" ] }, "params": { "_base": 5000, "field_1":"balance", "field_2":"firstname", "field_3":"lastname" } } '
在使用模板的檢索中,URI變爲“_search/template”,模板使用“source”子句描述,模板的變參使用“{{}}”來標識,實參在“params”被定義。
實際使用中,使用的模板的好處在於我們只需爲相同模式的查詢定義一次即可,運行時只需賦值對應的實參即可。設計應用系統時利用預置的檢索模板可以提升開發效率,對於需要二次開發的系統而言模板也對外則屏蔽了實現細節。2.2.4. 嵌套對象檢索
考慮對域“fullname”的映射:
"fullname": { "properties": { "age": { "type": "long" }, "firstname": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "lastname": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } }
要在該索引中檢索名爲“zhang”的記錄,可寫爲如下簡單的查詢語句:
{ "_source": [ "fullname" ], "query": { "match": { "fullname.firstname": "zhang" } } }
其中用於匹配的字段“firstname”,使用“.”表示法來表明該屬性屬於“fullname”這個對象,而並非索引中的頂層域。
對於未顯式在映射中定義嵌套子對象的屬性,只以“nested”類型定義的子對象,在查詢中需要使用“nested”子句查詢,將“fullname”的映射修改未如下形式:{ "fullname": { "type": "nested" } }
上面的查詢語句可改寫爲如下形式:
{ "_source": [ "fullname" ], "query": { "nested": { "path": "fullname", "query": { "match": { "fullname.firstname": "zhang" } } } } }
3. 關聯查詢
真實世界中數據之間的關係總是如此複雜。
關係數據庫提供多表聯合(join)查詢,使用鍵來關聯表之間的關係。如同其它Nosql數據庫一樣,ES建議在數據建模時扁平化,使得一個領域本體中的數據關係“內聚”在一個索引中。
在ES中對一個集羣的多個索引進行檢索非常方便,在請求URI中填寫需要參與檢索的索引即可,如“localhost:9200/_search?pretty”表示檢索集羣中所有索引。而對多個域進行同一條件檢索,Query DSL也提供了“multi_match”這樣的子句。
ES中對於相同的域名在檢索時不區分其屬於哪個索引,因此並不能直接支持關係數據庫中“table1.field1=table2.field1”這樣的索引間(相當於表間)關聯查詢,文獻2中討論了四種管理關聯數據的方式:
1. 在應用層模擬關係數據庫連接:這種方法實際是將關係數據庫的一次接連查詢操作分解爲兩次查詢操作,要求關聯的兩個索引的數據對象之間保存關聯的外鍵。假設有索引index1包含{id11,field11,field12}屬性,index2包含{id21,field21,field2,id11}屬性, id11爲index2的外鍵,對於涉及index1和index2的應用級查詢,將其分解爲兩次查詢:第一次根據查詢條件中index1的字段查詢出符合條件的id11,第二次根據第一次查詢出的id11值及查詢條件中index2的字段查詢出符合條件記錄。
2. 增加冗餘副本數據:在一個索引中增加需要關聯的另一個索引的數據,這樣增加了第一個索引的冗餘數據,使得關聯查詢只在第一個索引中進行。
3. 使用嵌套對象來保存關聯關係:這種方式的思路是從領域頂層建模,將需要關聯的原本獨立的領域本體合併爲一個總的本體,然後根據這個總的本體來建立映射。原來獨立的領域本體對應的數據對象在總的數據對象中成爲了嵌套的子對象。
4. 在對象之間建立父子關聯關係:還記得我們在《編程隨筆-ElasticSearch知識導圖(3):映射》一文中提到,ES中可以設置文檔對象之間爲父子關係的內容嗎?建立數據對象之間的父子關係本質上是建立一顆對象層次樹,將對象之間的關聯關係轉換爲樹上節點間的父子(祖先)關係。Query DSL中提供了has_child、has_parent、parent_id這樣的查詢子句。
綜上所述,在ES實現關聯查詢本質上使用的方式無外乎兩種:- 把所有數據都建模到一個索引中(6.0版本後一個索引中只有一個類型(type)),上面講到的後三種方式都基於這個思路;
- 數據對象分佈在多個索引中,但對象間有字段進行關聯,通過將聯合查詢分解爲多個分步查詢(只查詢一個索引)得到最終結果。
似乎在ES中無法再現關係數據庫強大的關聯查詢,但請注意,在關係數據庫中執行多表的join操作也是非常低效的。根據應用需求,可以在ES中設計合理的數據模型去儘量滿足關聯查詢。4. 一個設計實例
4.1. GA/T 1400.3的查詢指令
在《編程隨筆-ElasticSearch知識導圖(3):映射》一文中,介紹了遵循GA/T 1400.3的視頻圖像信息數據庫(以下簡稱視圖庫)的數據模型,並示例定義了視圖庫中索引的映射。
視圖庫中各索引對應GA/T 1400.3定義的各數據對象類型,視圖庫的數據模型是一個扁平的數據模型。不同類型的對象之間具有關聯關係,通過對象中的外鍵字段進行關聯。如人、車、物對象的來源標識字段與圖像對象的圖像標識關聯。
視圖庫提供的數據服務接口完成對視圖庫的查詢功能,視圖庫的查詢接口是標準的restful風格的GET接口。每個查詢接口對應一個數據對象類型(使用資源URI指示數據對象類型),使用查詢字符串指示查詢條件。GA/T 1400.3提供的一個查詢指令示例如下://查找身高在1.60m~1.70m 之間,攜帶紅色包的人員記錄,返回結果按年齡上限排序,且只返回PersonID、SourceID兩個屬性。 GET /VIID/Persons?((Person.HeightUpLimit <=170) AND (Person.HeightLowerLimit >=160))&(Person.BagColor=Red)&(Sort = Person.AgeUpLimit)& (Fields= (PersonID,SourceID))
GA/T 1400.3的附錄F中定義了視圖庫的查詢指令規範,主要參照了SQL語言規範:定義了算術運算符、邏輯運算符、比較運算符、聚合函數及分頁參數等。除了算術運算符在ES中不支持之外,其它的規範要求都可在ES的DSL中找到對應語法(注:根據視圖庫的對象類型定義,實際應用中在查詢時基本不會有需要進行加減乘除計算的字段,但如果一定要保證規範的完整性,需要考慮一些變通手段)。
如果在基於視圖庫中的應用系統中需要涉及到關聯查詢,可以使用上節提到的在應用層分步查詢的方法。4.2. 將查詢指令轉換爲檢索API
分析一下視圖庫查詢字符串的特點:查詢字符串由多個子查詢條件組成,每個子查詢條件使用“&”連接(爲邏輯與關係),子查詢條件可分爲三類:
1. 針對對象域的布爾表達式,該表達式可爲復合邏輯表達式。
2.返回結果字段表達式(使用“Fields”關鍵字),指示出現在查詢結果中的返回字段,這些字段中可能會有聚合函數(如最大、最小值)。
3.分頁排序表達式,指示分頁記錄和位置及排序方式等,使用Sort,PageRecordNum,RecordStartNo,MaxNumRecordReturn等關鍵字。
可在視圖庫中編寫一個查詢指令轉換器,解析查詢字符串中的各子查詢條件,並將這些子查詢條件組合爲ES支持的查詢URI或查詢消息體。
上節中提到的查詢指令示例,轉換爲URI後的查詢格式如下:curl -iXGET 'localhost:9200/person/_search?pretty&q=((PersonObject.HeightUpLimit:<=170)AND(PersonObject.HeightLowerLimit:>=160))AND(PersonObject.BagColor:4)&sort=PersonObject.AgeUpLimit:asc&_source=PersonObject.PersonID,PersonObject.SourceID'
轉換爲DSL描述的消息體如下:
{ "query": { "bool": { "filter": [ { "bool": { "must": [ { "range": { "PersonObject.HeightUpLimit": { "lte": 170 } } }, { "range": { "PersonObject.HeightLowerLimit": { "gte": 160 } } } ] } }, { "term": { "PersonObject.BagColor": "4" } } ] } }, "sort": { " PersonObject.AgeUpLimit": { "order": "asc" } }, "_source": [ "PersonObject.PersonID", "PersonObject.SourceID" ] }
5. 參考文獻
- https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
- Clinton Gormley &Zachary Tong, Elasticsearch: The Definitive Guide,2015
- Debasish Ghosh,DSLs in action, 2013
- GA/T 1400.3 公安視頻圖像信息應用系統 第3部分:數據庫技術要求,2017