1. 程式人生 > >Elasticsearch(十一)elasticsearch搜尋--最基本的工具

Elasticsearch(十一)elasticsearch搜尋--最基本的工具

我們已經學會了如何使用 Elasticsearch 作為一個簡單的 NoSQL 風格的分散式文件儲存系統。我們可以將一個 JSON 文件扔到 Elasticsearch 裡,然後根據 ID 檢索。但 Elasticsearch 真正強大之處在於可以從無規律的資料中找出有意義的資訊——從“大資料”到“大資訊”。

Elasticsearch 不只會儲存(stores) 文件,為了能被搜尋到也會為文件新增索引(indexes) ,這也是為什麼我們使用結構化的 JSON 文件,而不是無結構的二進位制資料。

文件中的每個欄位都將被索引並且可以被查詢 。不僅如此,在簡單查詢時,Elasticsearch 可以使用 所有(all) 這些索引欄位,以驚人的速度返回結果。這是你永遠不會考慮用傳統資料庫去做的一些事情。

搜尋(search) 可以做到:

• 在類似於 gender 或者 age 這樣的欄位 上使用結構化查詢,join_date 這樣的欄位上使用排序,就像SQL的結構化查詢一樣。
• 全文檢索,找出所有匹配關鍵字的文件並按照相關性(relevance) 排序後返回結果。
• 以上二者兼而有之。

很多搜尋都是開箱即用的,為了充分挖掘 Elasticsearch 的潛力,你需要理解以下三個概念:

對映(Mapping)
描述資料在每個欄位內如何儲存

分析(Analysis)
全文是如何處理使之可以被搜尋的

領域特定查詢語言(Query DSL)
Elasticsearch 中強大靈活的查詢語言

以上提到的每個點都是一個大話題,我們將在 深入搜尋 一章詳細闡述它們。本章節我們將介紹這三點的一些基本概念——僅僅幫助你大致瞭解搜尋是如何工作的。

我們將使用最簡單的形式開始介紹 search API。
這一節我們只做簡單的介紹,使之前的例子能夠更清晰。

空搜尋

搜尋API的最基礎的形式是沒有指定任何查詢的空搜尋 ,它簡單地返回叢集中所有索引下的所有文件:

GET /_search

返回的結果(為了介面簡潔編輯過的)像這樣:

{
“hits” : {
“total” : 14,
“hits” : [
{
“_index”: “us”,
“_type”: “tweet”,
“_id”: “7”,
“_score”: 1,
“_source”: {
“date”: “2014-09-17”,
“name”: “John Smith”,
“tweet”: “The Query DSL is really powerful and flexible”,
“user_id”: 2
}
},
… 9 RESULTS REMOVED …
],
“max_score” : 1
},
“took” : 4,
“_shards” : {
“failed” : 0,
“successful” : 10,
“total” : 10
},
“timed_out” : false
}

hits

返回結果中最重要的部分是 hits ,它 包含 total 欄位來表示匹配到的文件總數,並且一個 hits 陣列包含所查詢結果的前十個文件。

在 hits 陣列中每個結果包含文件的 _index 、 _type 、 _id ,加上 _source 欄位。這意味著我們可以直接從返回的搜尋結果中使用整個文件。這不像其他的搜尋引擎,僅僅返回文件的ID,需要你單獨去獲取文件。

_score

每個結果還有一個 _score ,它衡量了文件與查詢的匹配程度。預設情況下,首先返回最相關的文件結果,就是說,返回的文件是按照 _score 降序排列的。在這個例子中,我們沒有指定任何查詢,故所有的文件具有相同的相關性,因此對所有的結果而言 1 是中性的 _score 。

max_score

值是與查詢所匹配文件的 _score 的最大值。

took

took 值告訴我們執行整個搜尋請求耗費了多少毫秒。

shards

_shards 部分 告訴我們在查詢中參與分片的總數,以及這些分片成功了多少個失敗了多少個。正常情況下我們不希望分片失敗,但是分片失敗是可能發生的。如果我們遭遇到一種災難級別的故障,在這個故障中丟失了相同分片的原始資料和副本,那麼對這個分片將沒有可用副本來對搜尋請求作出響應。假若這樣,Elasticsearch 將報告這個分片是失敗的,但是會繼續返回剩餘分片的結果。

timeout

timed_out 值告訴我們查詢是否超時。預設情況下,搜尋請求不會超時。 如果低響應時間比完成結果更重要,你可以指定 timeout 為 10 或者 10ms(10毫秒),或者 1s(1秒):

GET /_search?timeout=10ms
在請求超時之前,Elasticsearch 將會返回已經成功從每個分片獲取的結果。

Warning
應當注意的是 timeout 不是停止執行查詢,它僅僅是告知正在協調的節點返回到目前為止收集的結果並且關閉連線。在後臺,其他的分片可能仍在執行查詢即使是結果已經被髮送了。
使用超時是因為 SLA(服務等級協議)對你是很重要的,而不是因為想去中止長時間執行的查詢。

Client程式演示

PS:在程式碼中引入靜態
import static org.elasticsearch.index.query.QueryBuilders.*;
就能直接呼叫它的方法不用每次寫出。例如
import static org.elasticsearch.index.query.QueryBuilders.*;
QueryBuilder qb = matchAllQuery();

不引入則:
import org.elasticsearch.index.query.QueryBuilders;
QueryBuilder qb = QueryBuilders.matchAllQuery();

在之前演示僱員例子中,我們已經演示過,這裡可以感覺很容易的理解他們了。

/*
     * 空索引
     * 認識搜尋響應
     */
    private static void showQueryAll(Client client) {
        SearchResponse response = client.prepareSearch().get();
        //分析查詢結果
        // took--執行整個搜尋請求耗費了多少毫秒
        long took = response.getTookInMillis();
        System.out.println(took);
        /*
         * timed_out--告訴我們查詢是否超時。預設情況下,搜尋請求不會超時。 如果低響應時間比完成結果更重要,
         * 你可以指定 timeout 為 10 或者 10ms(10毫秒),或者 1s(1秒):
         * GET /_search?timeout=10ms
         * 在請求超時之前,Elasticsearch 將會返回已經成功從每個分片獲取的結果。
         * Warning
應當注意的是 timeout  不是停止執行查詢,它僅僅是告知正在協調的節點返回到目前為止收集的結果並且關閉連線。在後臺,其他的分片可能仍在執行查詢即使是結果已經被髮送了。
使用超時是因為 SLA(服務等級協議)對你是很重要的,而不是因為想去中止長時間執行的查詢。
         */
        boolean isTimedOut = response.isTimedOut();
        System.out.println("timed_out:"+isTimedOut);
        /*
         *  _shards--告訴我們在查詢中參與分片的總數,以及這些分片成功了多少個失敗了多少個。
         *  正常情況下我們不希望分片失敗,但是分片失敗是可能發生的。如果我們遭遇到一種災難級別的故障,
         *  在這個故障中丟失了相同分片的原始資料和副本,那麼對這個分片將沒有可用副本來對搜尋請求作出響應。
         *  假若這樣,Elasticsearch 將報告這個分片是失敗的,但是會繼續返回剩餘分片的結果。
         */
        int totalShards = response.getTotalShards();
        int successfulShards = response.getSuccessfulShards();
        int failedShards = response.getFailedShards();
        System.out.println("_shards:{ total="+totalShards+" successful="+successfulShards+" failed="+failedShards+"}");


        // hits總結果
        SearchHits searchHits = response.getHits();
        // max_score--與查詢所匹配文件的 _score 的最大值
        float maxScore = searchHits.getMaxScore();
        // 一共文件數
        long totalHits = searchHits.getTotalHits();
        System.out.println("total="+totalHits+" max_score="+maxScore);

        // 文件在hit陣列中,預設返回前10條
        Iterator<SearchHit> iterator = searchHits.iterator();
        while(iterator.hasNext()) {
            SearchHit hit = iterator.next();
            //索引
            String index = hit.getIndex();
            //型別
            String type = hit.getType();
            //id
            String id = hit.getId();
            //每個結果還有一個 _score   ,它衡量了文件與查詢的匹配程度。預設情況下,首先返回最相關的文件結果,就是說,返回的文件是按照 _score 降序排列的。
            float score = hit.getScore();
            System.out.println("index="+index+" type="+type+" id="+id+" score="+score+" source-->"+hit.getSourceAsString());
        }
        System.out.println("查詢結束...");
    }

呼叫:

//1.空索引
showQueryAll(client);

結果顯示:
連線成功…
連線成功…
3
timed_out:false
_shards:{ total=30 successful=30 failed=0}
total=53 max_score=1.0
index=logstash-car-msg type=carmsg id=AV9_t-4zDK8yG4k-MolH score=1.0 source–>{“host”:”YFCSPT-SUSE-86”,”vehicleId”:”BCAAAD0005”,”vehicleName”:”巴斯達BBL5054XJE1公共安全監測車”,”searchCode”:”BSD-BBL5054XJE1”,”vehicleClass”:”特種車類”,”vehicleClassPicc”:”C49”,”vehicleType”:”1”,”brandNameNew”:”巴斯達”}
index=logstash-car-msg type=carmsg id=BCAAAD0000 score=1.0 source–>{“host”:”YFCSPT-SUSE-86”,”vehicleId”:”BCAAAD0000”,”vehicleName”:”巴斯達BBL5054XJE1公共安全監測車0”,”searchCode”:”BSD-BBL5054XJE0”,”vehicleClass”:”特種車類”,”vehicleClassPicc”:”C49”,”vehicleType”:”1”,”brandNameNew”:”巴斯達0”,”useYears”:0,”makeDate”:”2017-01-01 11:23:01”}
index=logstash-car-msg type=carmsg id=BCAAAD0005 score=1.0 source–>{“host”:”YFCSPT-SUSE-86”,”vehicleId”:”BCAAAD0005”,”vehicleName”:”巴斯達BBL5054XJE1公共安全監測車5”,”searchCode”:”BSD-BBL5054XJE5”,”vehicleClass”:”特種車類11”,”vehicleClassPicc”:”C49”,”vehicleType”:”1”,”brandNameNew”:”巴斯達5”,”useYears”:5,”makeDate”:”2017-01-01 11:23:01”}
index=logstash-car-msg type=carmsg id=BCAAAD0008 score=1.0 source–>{“vehicleName”:”車88”,”searchCode”:”BSD-BBL5054XJE8”}
index=logstash-car-msg1 type=carmsg1 id=BCAAAD0000 score=1.0 source–>{“host”:”YFCSPT-SUSE-86”,”vehicleId”:”BCAAAD0000”,”vehicleName”:”巴斯達BBL5054XJE1公共安全監測車0”,”searchCode”:”BSD-BBL5054XJE0”,”vehicleClass”:”特種車類”,”vehicleClassPicc”:”C49”,”vehicleType”:”1”,”brandNameNew”:”巴斯達0”,”useYears”:0,”makeDate”:”2017-01-01 11:23:01”}
。。。。。。
查詢結束…

Head外掛示例

這裡寫圖片描述

多索引,多型別

你有沒有注意到之前的 empty search 的結果,不同型別的文件 — user 和 tweet 來自不同的索引— us 和 gb ?

如果不對某一特殊的索引或者型別做限制,就會搜尋叢集中的所有文件。Elasticsearch 轉發搜尋請求到每一個主分片或者副本分片,彙集查詢出的前10個結果,並且返回給我們。

然而,經常的情況下,你 想在一個或多個特殊的索引並且在一個或者多個特殊的型別中進行搜尋。我們可以通過在URL中指定特殊的索引和型別達到這種效果,如下所示:

/_search 在所有的索引中搜索所有的型別
/gb/_search 在 gb 索引中搜索所有的型別
/gb,us/_search 在 gb 和 us 索引中搜索所有的文件
/g*,u*/_search 在任何以 g 或者 u 開頭的索引中搜索所有的型別
/gb/user/_search 在 gb 索引中搜索 user 型別
/gb,us/user,tweet/_search 在 gb 和 us 索引中搜索 user 和 tweet 型別 /_all/user,tweet/_search 在所有的索引中搜索 user 和 tweet 型別
當在單一的索引下進行搜尋的時候,Elasticsearch 轉發請求到索引的每個分片中,可以是主分片也可以是副本分片,然後從每個分片中收集結果。多索引搜尋恰好也是用相同的方式工作的–只是會涉及到更多的分片。

Tip
搜尋一個索引有五個主分片和搜尋五個索引各有一個分片準確來所說是等價的。

Client演示示例

在之前演示僱員例子中,我們已經演示過,這裡可以感覺很容易的理解他們了。

SearchResponse response = client.prepareSearch("logstash-car-msg","logstash-car-msg1")
        .setTypes("carmsg", "carmsg1")
        .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
        .get();

還有MultiGet

MultiGetRequestBuilder builder = client.prepareMultiGet()
                .add("website","blog","1")    //單一ID       
                .add("website","blog","1","2") //多ID
                .add("logstash-car-msg", "carmsg","BCAAAD0005");  

等等,多獲取的都能做到如此。

分頁

在之前的 空搜尋 中說明了叢集中有 14 個文件匹配了(empty)query 。 但是在 hits 陣列中只有 10 個文件。如何才能看到其他的文件?

和 SQL 使用 LIMIT 關鍵字返回單個 page 結果的方法相同,Elasticsearch 接受 from 和 size 引數:

size 顯示應該返回的結果數量,預設是 10 from 顯示應該跳過的初始結果數量,預設是 0
如果每頁展示 5 條結果,可以用下面方式請求得到 1 到 3 頁的結果:

GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10
考慮到分頁過深以及一次請求太多結果的情況,結果集在返回之前先進行排序。 但請記住一個請求經常跨越多個分片,每個分片都產生自己的排序結果,這些結果需要進行集中排序以保證整體順序是正確的。

在分散式系統中深度分頁

理解為什麼深度分頁是有問題的,我們可以假設在一個有 5 個主分片的索引中搜索。 當我們請求結果的第一頁(結果從 1 到 10 ),每一個分片產生前 10 的結果,並且返回給 協調節點 ,協調節點對 50 個結果排序得到全部結果的前 10 個。

現在假設我們請求第 1000 頁–結果從 10001 到 10010 。所有都以相同的方式工作除了每個分片不得不產生前10010個結果以外。 然後協調節點對全部 50050 個結果排序最後丟棄掉這些結果中的 50040 個結果。

可以看到,在分散式系統中,對結果排序的成本隨分頁的深度成指數上升。這就是 web 搜尋引擎對任何查詢都不要返回超過 1000 個結果的原因。

Tip
在 重新索引你的資料 中解釋瞭如何 能夠 有效獲取大量的文件。

Client演示示例

/*
     * 分頁
     */
    private static void showScrollQuery(Client client) {
        SearchResponse response = client.prepareSearch("megacorp1")
                .setQuery(termQuery("first_name", "John"))
                .setSize(5).setFrom(0).get(); //size每一頁最多5條記錄,from跳過多少個數據顯示

        SearchHits searchHits = response.getHits();
        // 文件在hit陣列中,預設返回前10條
        Iterator<SearchHit> iterator = searchHits.iterator();
        while(iterator.hasNext()) {
            SearchHit hit = iterator.next();
            //為了效果只打印id
            String id = hit.getId();
            System.out.println("id="+id);
        }

    }

呼叫:

//3.分頁
showScrollQuery(client);

結果顯示:
連線成功…
id=6
id=11
id=1
id=13
id=5

改為setFrom(3):
id=13
id=5
id=9
id=10
id=12

Head外掛示例

這裡寫圖片描述

輕量搜尋

有兩種形式的 搜尋 API:一種是 “輕量的” 查詢字串 版本,要求在查詢字串中傳遞所有的 引數,另一種是更完整的 請求體 版本,要求使用 JSON 格式和更豐富的查詢表示式作為搜尋語言。

查詢字串搜尋非常適用於通過命令列做即席查詢。例如,查詢在 tweet 型別中 tweet 欄位包含 elasticsearch 單詞的所有文件:

GET /_all/tweet/_search?q=tweet:elasticsearch

下一個查詢在 name 欄位中包含 john 並且在 tweet 欄位中包含 mary 的文件。實際的查詢就是這樣
+name:john +tweet:mary

但是查詢字串引數所需要的 百分比編碼 (譯者注:URL編碼)實際上更加難懂:

GET /_search?q=%2Bname%3Ajohn+%2Btweet%3Amary

  • 字首表示必須與查詢條件匹配。類似地, - 字首表示一定不與查詢條件匹配。沒有 + 或者 - 的所有其他條件都是可選的——匹配的越多,文件就越相關。

_all 欄位

這個簡單搜尋返回包含 mary 的所有文件:

GET /_search?q=mary

之前的例子中,我們在 tweet 和 name 欄位中搜索內容。然而,這個查詢的結果在三個地方提到了 mary :

• 有一個使用者叫做 Mary
• 6條微博發自 Mary
• 一條微博直接 @mary

Elasticsearch 是如何在三個不同的欄位中查詢到結果的呢?

當索引一個文件的時候,Elasticsearch 取出所有欄位的值拼接成一個大的字串,作為 _all 欄位進行索引。例如,當索引這個文件時:

{
“tweet”: “However did I manage before Elasticsearch?”,
“date”: “2014-09-14”,
“name”: “Mary Jones”,
“user_id”: 1
}

這就好似增加了一個名叫 _all 的額外欄位:

“However did I manage before Elasticsearch? 2014-09-14 Mary Jones 1”

除非設定特定欄位,否則查詢字串就使用 _all 欄位進行搜尋。

Tip
在剛開始開發一個應用時,_all 欄位是一個很實用的特性。之後,你會發現如果搜尋時用指定欄位來代替 _all 欄位,將會更好控制搜尋結果。當 _all 欄位不再有用的時候,可以將它置為失效,正如在 元資料: _all 欄位 中所解釋的。

更復雜的查詢

下面的查詢針對tweents型別,並使用以下的條件:

• name 欄位中包含 mary 或者 john
• date 值大於 2014-09-10
all 欄位包含 aggregations 或者 geo

+name:(mary john) +date:>2014-09-10 +(aggregations geo)

View in Sense

查詢字串在做了適當的編碼後,可讀性很差:

?q=%2Bname%3A(mary+john)+%2Bdate%3A%3E2014-09-10+%2B(aggregations+geo)

從之前的例子中可以看出,這種 輕量 的查詢字串搜尋效果還是挺讓人驚喜的。 它的查詢語法在相關參考文件中有詳細解釋,以便簡潔的表達很複雜的查詢。對於通過命令做一次性查詢,或者是在開發階段,都非常方便。

但同時也可以看到,這種精簡讓除錯更加晦澀和困難。而且很脆弱,一些查詢字串中很小的語法錯誤,像 - , : , / 或者 ” 不匹配等,將會返回錯誤而不是搜尋結果。

最後,查詢字串搜尋允許任何使用者在索引的任意欄位上執行可能較慢且重量級的查詢,這可能會暴露隱私資訊,甚至將叢集拖垮。

Tip

因為這些原因,不推薦直接向用戶暴露查詢字串搜尋功能,除非對於叢集和資料來說非常信任他們。

相反,我們經常在生產環境中更多地使用功能全面的 request body 查詢API,除了能完成以上所有功能,還有一些附加功能。但在到達那個階段之前,首先需要了解資料在 Elasticsearch 中是如何被索引的。

這個很複雜,所以我們例子中一般是使用request body 查詢API,這個不做研究