Elasticsearch Search API之(Request Body Search 查詢主體)-上篇

本節將詳細介紹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\n

  • boundary_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"
        }
    }
}

?

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容