本節將詳細介紹es Search API的查詢主體,定制化查詢條件的實現主體。
query
搜索請求體中查詢條件使用es DSL查詢語法來定義。通過使用query來定義查詢體。
GET /_search2{3 "query" : {4 "term" : { "user" : "kimchy" }5 }6}
From / Size
es的一種分頁語法。通過使用from和size參數來對結果集進行分頁。
from設置第一條數據的偏移量。size設置返回的條數(針對每個分片生效),由于es天生就是分布式的,通過設置主分片個數來進行數據水平切分,一個查詢請求通常需要從多個后臺節點(分片)進行數據匯聚。
From/Size方式會遇到分布式存儲的一個共性問題:深度分頁,也就是頁數越大需要訪問的數據則越大。es提供了另外一種分頁方式,滾動API(Scroll),后續會詳細分析。
注意:from + size 不能超過index.max-_result_window配置項的值,其默認值為10000。
sort (排序)
與傳統關系型數據庫類似,es支持根據一個或多個字段進行排序,同時支持asc升序或desc降序。另外es可以按照_sco-re(基于得分)的排序,默認值。
如果使用了排序,響應結果中每一條命中數據將包含一個響應字段sort,其類型為Object[],表示該文檔當前的排序值,該值在ES支持的第三種分頁方式S-earch After中會使用到。
排序順序
es提供了兩種排序順序,SortOrder.AS-C(asc)升序、SortOr-der.DESC(desc)降序,如果排序類型為_score,其默認排序順序為降序(desc),如果排序類型為字段,則默認排序順序為升序(asc)。
排序模型選型
es支持按數組或多值字段進行排序。模式選項控制選擇的數組值,以便對它所屬的文檔進行排序。模式選項可以有以下值:
min 使用數組中最小的值參與排序
max 使用數組中最大的值參與排序
sum 使用數組中的總和參與排序
avg 使用數組中的平均值參與排序
median 使用數組中的中位數參與排序
如果是一個數組類型的值參與排序,通常會對該數組元素進行一些計算得出一個最終參與排序的值,例如取平均數、最大值、最小值、求和等運算。es通過排序模型mode來指定。
嵌套字段排序
es還支持在一個或多個嵌套對象內部的字段進行排序。一個嵌套查詢提包含如下選項(參數):
path
定義要排序的嵌套對象。排序字段必須是這個嵌套對象中的一個直接字段(非嵌套字段),并且排序字段必須存在。filter
定義過濾上下文,定義排序環境中的過濾上下文。max_children
排序是要考慮根文檔下子屬性文檔的最大個數,默認為無限制。nested
排序體支持嵌套。
"sort" : [
{
"parent.child.age" : { // @1
"mode" : "min",
"order" : "asc",
"nested": { // @2
"path": "parent",
"filter": {
"range": {"parent.age": {"gte": 21}}
},
"nested": { // @3
"path": "parent.child",
"filter": {
"match": {"parent.child.name": "matt"}
}
}
}
}
}
]
代碼@1:排序字段名,支持級聯表示字段名。
代碼@2:通過nested屬性定義排序嵌套語法,其中path定義當前的嵌套層級,f-ilter定義過濾上下文。
@3內部可以再通過nested屬性再次嵌套定義。
missing values
由于es的索引,類型下的字段可以在索引文檔時動態增加,那如果有些文檔沒有包含排序字段,這部分文檔的順序如何確定呢?es通過missing屬性來確定,其可選值為:
_last
默認值,排在最后。_first
排在最前。
ignoring unmapped fields
默認情況下,如果排序字段為未映射的字段將拋出異常。可通過unmapped_ty-pe來忽略該異常,該參數指定一個類型,也就是告訴ES如果未找該字段名的映射,就認為該字段是一個unmapped_-type指定的類型,所有文檔都未存該字段的值。
Geo sorting
地圖類型排序,該部分將在后續專題介紹geo類型時講解。
字段過濾
默認情況下,對命中的結果會返回_so-urce字段下的所有內容。字段過濾機制允許用戶按需要返回_source字段里面部分字段。其過濾設置機制已在Elasticse-arch Document Get API詳解、原理與示例中已詳細介紹,在這里就不重復介紹了。
Doc Value Fields
使用方式如下:
GET /_search
{
"query" : {
"match_all": {}
},
"docvalue_fields" : [
{
"field": "my_date_field",
"format": "epoch_millis"
}
]
}
通過使用docvalue_fields指定需要轉換的字段與格式,它對于在映射文件中定義stored=false的字段同樣生效。字段支持用通配符,例如"field":"myfield*"。
docvalue_fields中指定的字段并不會改變_souce字段中的值,而是使用fields返回值進行額外返回。
java實例代碼片段如下(完整的Demo示例將在文末給出):
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.termQuery("user", "dingw"))
.sort(new FieldSortBuilder("post_date").order(SortOrder.DESC))
.docValueField("post_date", "epoch_millis")
其返回結果如下:
{
"took":88,
"timed_out":false,
"_shards":{
"total":5,
"successful":5,
"skipped":0,
"failed":0
},
"hits":{
"total":2,
"max_score":null,
"hits":[
{
"_index":"twitter",
"_type":"_doc",
"_id":"11",
"_score":null,
"_source":{
"post_date":"2009-11-19T14:12:12",
"message":"test bulk update",
"user":"dingw"
},
"fields":{
"post_date":[
"1258639932000"
]
},
"sort":[
1258639932000
]
},
{
"_index":"twitter",
"_type":"_doc",
"_id":"12",
"_score":null,
"_source":{
"post_date":"2009-11-18T14:12:12",
"message":"test bulk",
"user":"dingw"
},
"fields":{
"post_date":[
"1258553532000"
]
},
"sort":[
1258553532000
]
}
]
}
}
Post Filter
post filter對查詢條件命中后的文檔再做一次篩選。
{
"query": {
"bool": {
"filter": {
"term": { "brand": "gucci" } // @1
}
}
},
"post_filter": { // @2
"term": { "color": "red" }
}
}
首先根據@1條件對索引進行檢索,然后得到匹配的文檔后,再利用@2過濾條件對結果再一次篩選。
Highlighting
查詢結果高亮顯示。
Es支持的高亮分析器
用于對查詢結果中對查詢關鍵字進行高亮顯示,高亮顯示查詢條件在查詢結果中匹配的部分。
注意:高亮顯示器在提取要高亮顯示的術語時不能反映查詢的布爾邏輯。因此對于一些復雜的布爾查詢(例如嵌套的布爾查詢,或使用minimum_should_mat-ch等查詢)可能高亮顯示會出現一些誤差。
高亮顯示需要字段的實際內容。如果字段沒有存儲(映射沒有將store設置為tru-e),則從_source中提取相關字段。
es支持三種高亮顯示工具,通過為每個字段指定type來使用。
unified highlighter
使用Lucene unified高亮顯示器。首先將文本分解成句子并使用BM25算法對單個句子進行評分。支持精確的短語和多術語(模糊、前綴、正則表達式)高亮顯示。這是es默認的高亮顯示器。-
plain highlighter
使用Lucene標準高亮顯示器。plain highlighter最適合單個字段的匹配高亮顯示需求。為了準確地反映查詢邏輯,它在內存中創建一個很小的索引,并通過Lucene的查詢執行計劃重新運行原來的查詢條件,以便獲取當前文檔的更低級別的匹配信息。如果需要對多個字段進行高亮顯示,建議還是使用unified高亮顯示器或term_vector fields。plain高亮顯示器是一個實時分析處理高亮器。即用戶在查詢的時候,搜索引擎查詢到了目標數據docid后,將需要高亮的字段數據提取到內存,再調用該字段的分析器進行處理,分析完后采用相似度算法計算得分最高的前n組并高亮段返回數據。
plain高亮器是實時分析高亮器,這種實時分析機制會讓ES占用較少的IO資源同時也占用較少的存儲空間(詞庫較全的話相比fvh方式能節省一半的存儲空間),其策略是采用cpu資源來換取磁盤IO壓力,在需要高亮字段較短(比如高亮文章的標題)時候速度較快,同時因IO訪問的次數少,IO壓力較小,有利于提高系統吞吐量。
-
fast vector highlighter(fvh)
使用lucene fast vector highlingter,基于詞向量,該高亮處理器必須開啟term_vector=with_positions_off-sets,存儲詞向量、即位置與偏移量。為解決大文本字段上高亮速度性能的問題,lucene高亮模塊提供了基于向量的高亮方式 fvh。fvh高亮顯示器利用建索引時候保存好的詞向量來直接計算高亮段落,在高亮過程中比plain高亮方式少了實時分析過程,取而代之的是直接從磁盤中將分詞結果直接讀取到內存中進行計算。故要使用fvh的前置條件就是在建索引時候,需要配置存儲詞向量,詞向量需要包含詞位置信息、詞偏移量信息。注意:fvh高亮器不支持span查詢。如果您需要對span查詢的支持,請嘗試其他高亮顯示,例如unified hi-ghlighter。
Offsets Strategy
獲取偏移量策略。高亮顯示要解決的一個核心就是高亮顯示的詞根以及該詞根的位置(位置與偏移量)。
ES中提供了3中獲取偏移量信息(Offset-s)的策略:
The postings list
如果將index_options設置為offset-s,unified高亮器將使用該信息突出顯示文檔,而無需重新分析文本。它直接對索引重新運行原始查詢,并從索引中提取匹配偏移量。如果字段很大,這一點很重要,因為它不需要重新分析需要高亮顯示的文本。比term_vector方式占用更少的磁盤空間。Term vectors
如果在字段映射中將term_vector設置為with_positions_offset,unified highlighter將自動使用term_vector來高亮顯示字段。它特別適用于大字段和高亮顯示多詞根查詢(如前綴或通配符),因為它可以訪問每個文檔的術語字典。fvh高亮器必須將字段映射term_vector設置為with_pos-itions_offset時才能生效。-
Plain highlighting
當沒有其他選擇時,統一使用這種模式。它在內存中創建一個很小的索引,并通過Lucene的查詢執行計劃重新運行原來的查詢條件,以訪問當前文檔上的低級匹配信息。對于每個需要突出顯示的字段和文檔,都要重復此操作。Plain高亮顯示器就是這種模式。注意:對于大型文本,Plain顯示器可能需要大量的時間消耗和內存。為了防止這種情況,在下一個版本中,對要分析的文本字符的最大數量將限制在100萬。6.x版本默認無限制,但是可以使用索引設置參數index.highlight.max_analyzed_offset為特定索引設置。
高亮顯示配置項
高亮顯示的全局配置會被字段級別的覆蓋。
boundary_chars
設置邊界字符串集合,默認包含:.,!? \t\nboundary_max_scan
掃描邊界字符。默認為20。boundary_scanner
指定如何分解高亮顯示的片段,可選值為chars、sentence、word。chars
字符。使用由bordery_chars指定的字符作為高亮顯示邊界。通過boun-dary_max_scan控制掃描邊界字符的距離。該掃描方式只適用于fvh。sentence
句子,使用Java的BreakIterator確定的下一個句子邊界處的突出顯示片段。您可以使用boundary_scann-er_locale指定要使用的區域設置。unified 高亮器默認行為。word
單詞,由Java的BreakIterator確定的下一個單詞邊界處高亮顯示的片段。boundary_scanner_locale
區域設置。該參數采用語言標記的形式,例如。“en - us”、“- fr”、“ja-JP”。更多信息可以在Locale語言標記文檔中找到。默認值是local.roo-t。encoder
指示代碼段是否應該編碼為HTML:默認(無編碼)或HTML (HTML-轉義代碼段文本,然后插入高亮標記)。fields
指定要檢索高亮顯示的字段,支持通配符。例如,您可以指定comme-nt_*來獲得以comment_開頭的所有文本和關鍵字字段的高亮顯示。
注意:當您使用通配符時,只會匹配text、keyword類型字段。force_source
是否強制從_source高亮顯示,默認為false。其實默認情況就是根據源字段內容(_source)內容高亮顯示,即使字段是單獨存儲的。fragmenter
指定如何在高亮顯示代碼片段中拆分文本:可選值為simple、span。僅適用于Plain高亮顯示器。默認為sp-an。simple
將文本分成大小相同的片段。span
將文本分割成大小相同的片段,但盡量避免在突出顯示的術語之間分割文本。這在查詢短語時很有用。fragment_offset
控制開始高亮顯示的margin(空白),僅適用于fvh。fragment_size
高亮顯示的片段,默認100。highlight_query
高亮顯示匹配搜索查詢以外的查詢。如果您使用rescore查詢,這尤其有用,因為默認情況下高亮顯示并不會考慮這些查詢。通常,應該將搜索查詢包含在highlight_query中。matched_fields
組合多個字段上的匹配項以突出顯示單個字段。對于以不同方式分析相同字符串的多個字段,這是最直觀的。所有matched_fields必須將term_vector設置為with_positions-_offset,但是只加載匹配項組合到的字段,所以建議該字段store設置為true。只適用于fvh。no_match_size
如果沒有要高亮顯示的匹配片段,則希望從字段開頭返回的文本數量。默認值為0(不返回任何內容)。number_of_fragments
返回的高亮顯示片段的最大數量。如果片段的數量設置為0,則不返回片段。默認為5。order
該值默認為none,按照字段的順序返回高亮文檔,可以設置為score(-按相關性排序)。phrase_limit
控制要考慮的文檔中匹配短語的數量。防止fvh分析太多的短語和消耗太多的內存。在使用matched_fields時,將考慮每個匹配字段的phrase-_limit短語。提高限制會增加查詢時間并消耗更多內存。只支持fvh。默認為256。pre_tags
用于高亮顯示HTML標簽,與post_-tags一起使用,默認用高亮顯示文本。post_tags
用于高亮顯示HTML標簽,與pre_t-ags一起使用,默認用高亮顯示文本。require_field_match
默認情況下,只有包含查詢匹配的字段才會高亮顯示。將require_fiel-d_match設置為false以突出顯示所有字段。默認值為true。tags_schema
定義高亮顯示樣式,例如。type
指定高亮顯示器,可選值:unified、plain、fvh。默認值為unified。
高亮顯示demo
public static void testSearch_highlighting() {
RestHighLevelClient client = EsClient.getClient();
try {
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("map_highlighting_01");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(
// QueryBuilders.matchAllQuery()
QueryBuilders.termQuery("context", "身份證")
);
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("context");
sourceBuilder.highlighter(highlightBuilder);
searchRequest.source(sourceBuilder);
System.out.println(client.search(searchRequest, RequestOptions.DEFAULT));
} catch (Exception e) {
// TODO: handle exception
}
}
其返回值如下:
{
"took":2,
"timed_out":false,
"_shards":{
"total":5,
"successful":5,
"skipped":0,
"failed":0
},
"hits":{
"total":1,
"max_score":0.2876821,
"hits":[
{
"_index":"map_highlighting_01",
"_type":"_doc",
"_id":"erYsbmcBeEynCj5VqVTI",
"_score":0.2876821,
"_source":{
"context":"城中西路可以受理外地二代身份證的辦理。"
},
"highlight":{ // @1
"context":[
"城中西路可以受理外地二代<em>身份證</em>的辦理。"
]
}
}
]
}
}
這里主要對highlight再做一次說明,其中每一個字段返回的內容是對應原始數據的子集,最多fragmentSize個待關鍵字的匹配條目,通常,在頁面上顯示文本時,應該用該字段取代原始值,這樣才能有高亮顯示的效果。
Rescoring
重打分機制。一個查詢首先使用高效的算法查找文檔,然后對返回結果的top n 文檔運用另外的查詢算法,通常這些算法效率低效但能提供匹配精度。
resoring查詢與原始查詢分數的合計方式如下:
total
兩個評分相加multiply
將原始分數乘以rescore查詢分數。用于函數查詢重定向。avg
取平均數max
取最大值min
取最小值。
Search Type
查詢類型,默認值為query_then_f-etch。
QUERY_THEN_FETCH
首先根據路由算法向相關分片(多個)發送請求,此時只返回docid與一些必要信息(例如用于排序等),然后對各個分片的結果進行匯聚,排序,然后選取客戶端指定需要獲取的數據條數前N條數據,然后根據docid再向各個分片請求具體的文檔信息。QUERY_AND_FETCH
在5.4.x版本開始廢棄,是直接向各個分片節點請求數據,每個分片返回客戶端請求數量的文檔信息,然后匯聚全部返回給客戶端,返回的數據為客戶端請求數量size * (路由后的分片數量)。DFS_QUERY_THEN_FETCH
在開始向各個節點發送請求之前,會進行一次詞頻、相關性的計算,后續流程與QUERY_THEN_FETCH相同,可以看出,該查詢類型的文檔相關性會更高,但性能比QUER-Y_THEN_FETCH要差。
scroll
滾動查詢。es另外一種分頁方式。雖然搜索請求返回結果的單個頁面,但scroll API可以用于從單個搜索請求檢索大量結果(甚至所有結果),這與在傳統數據庫上使用游標的方式非常相似。
scroll api不用于實時用戶請求,而是用于處理大量數據,例如為了將一個索引的內容重新索引到具有不同配置的新索引中。
如何使用scroll API
scroll API使用分為兩步:
1、第一步,首先通過scroll參數,指定該滾動查詢(類似于數據庫的游標的存活時間)
{
"size": 100,
"query": {
"match" : {
"title" : "elasticsearch"
}
}
}
該方法會返回一個重要的參數scrollId。
2、第二步,使用該scrollId去es服務器拉取下一批(下一頁數據)
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
循環第三步,可以循環批量處理數據。
3、第三步,清除scrollId,類似于清除數據庫游標,快速釋放資源。
DELETE /_search/scroll
{
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
下面給出scoll api的java版本示例程序
public static void testScoll() {
RestHighLevelClient client = EsClient.getClient();
String scrollId = null;
try {
System.out.println("step 1 start ");
// step 1 start
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("map_highlighting_01");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(
QueryBuilders.termQuery("context", "身份證")
);
searchRequest.source(sourceBuilder);
searchRequest.scroll(TimeValue.timeValueMinutes(1));
SearchResponse result = client.search(searchRequest, RequestOptions.DEFAULT);
scrollId = result.getScrollId();
// step 1 end
// step 2 start
if(!StringUtils.isEmpty(scrollId)) {
System.out.println("step 2 start ");
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(TimeValue.timeValueMinutes(1));
while(true) { //循環遍歷
SearchResponse scollResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
if(scollResponse.getHits().getHits() == null ||
scollResponse.getHits().getHits().length < 1) {
break;
}
scrollId = scollResponse.getScrollId();
// 處理文檔
scrollRequest.scrollId(scrollId);
}
// step 2 end
}
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(!StringUtils.isEmpty(scrollId)) {
System.out.println("step 3 start ");
// step 3 start
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
try {
client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// step 3 end
}
}
}
這里重點闡述一下第一次查詢時,不僅返回scrollId,也會返回第一批數據。
Keeping the search context alive
scroll參數(傳遞給搜索請求和每個滾動請求)告訴es它應該保持搜索上下文活動多長時間。只需要足夠長的時間來處理前一批結果。
每個scroll請求(帶有scroll參數)設置一個新的過期時間。如果scroll請求沒有傳入scroll,那么搜索上下文將作為scroll請求的一部分被釋放。
scroll其內部實現類似于快照,當第一次收到一個scroll請求時,就會為該搜索上下文所匹配的結果創建一個快照,隨后文檔的變化并不會反映到該API的結果。
sliced scroll
對于返回大量文檔的scroll查詢,可以將滾動分割為多個可以獨立使用的片,通過slice指定。例如:
GET /twitter/_search?scroll=1m // @1
{
"slice": { // @11
"id": 0, // @12
"max": 2 // @13
},
"query": {
"match" : {
"title" : "elasticsearch"
}
}
}
GET /twitter/_search?scroll=1m // @2
{
"slice": {
"id": 1,
"max": 2
},
"query": {
"match" : {
"title" : "elasticsearch"
}
}
}
@1,@2兩個并列的查詢,按分片去查詢。
@11:通過slice定義分片查詢。
@12:該分片查詢的ID。
@13:本次查詢總片數。
這個機制非常適合多線程處理數據。
具體分片機制是,首先將請求轉發到各分片節點,然后在每個節點使用匹配到的文檔(hashcode(_uid)%slice片數),然后各分片節點返回數據到協調節點。也就是默認情況下,分片是根據文檔的_uid,為了提高分片過程,可以通過如下方式進行優化,并指定分片字段。
分片字段類型為數值型。
字段的doc_values設置為true。
每個文檔中都索引了該字段。
該字段值只在創建時賦值,并不會更新。
字段的基數應該很高(相當于數據庫索引選擇度),這樣能確保每個片返回的數據相當,數據分布較均勻。
注意,默認slice片數最大為1024,可以通過索引設置項index.max_slices_per-_scroll來改變默認值。例如:
GET /twitter/_search?scroll=1m
{
"slice": {
"field": "date",
"id": 0,
"max": 10
},
"query": {
"match" : {
"title" : "elasticsearch"
}
}
}
?