1. 程式人生 > >【工作筆記】ElasticSearch從零開始學(二)—— 入門(搜尋)

【工作筆記】ElasticSearch從零開始學(二)—— 入門(搜尋)

建立一個員工目錄

假設我們剛好在Megacorp工作,這時人力資源部門出於某種目的需要讓我們建立一個員工目錄,這個目錄用於促進人文關懷和用於實時協同工作,所以它有以下不同的需求

  • 資料能夠包含多個值的標籤、數字和純文字。
  • 檢索任何員工的所有資訊。
  • 支援結構化搜尋,例如查詢30歲以上的員工。
  • 支援簡單的全文搜尋和更復雜的短語(phrase)搜尋
  • 高亮搜尋結果中的關鍵字
  • 能夠利用圖表管理分析這些資料

索引員工文件

首先要做的是儲存員工資料,每個文件代表一個員工。

在Elasticsearch中儲存資料的行為就叫做索引(indexing),不過在索引之前,我們需要明確資料應該儲存在哪裡。

在Elasticsearch中,文件歸屬於一種型別(type),而這些型別存在於索引(index)中,我們可以畫一些簡單的對比圖來類比傳統關係型資料庫

Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices   -> Types  -> Documents -> Fields

Elasticsearch叢集可以包含多個索引(indices)(資料庫),每一個索引可以包含多個型別(types)(表),每一個型別包含多個文件(documents)(行),然後每個文件包含多個欄位(Fields)(列)

「索引」含義的區分
你可能已經注意到索引(index)這個詞在Elasticsearch中有著不同的含義,所以有必要在此做一下區分:
索引(名詞) 如上文所述,一個索引(index)就像是傳統關係資料庫中的資料庫,它是相關文件儲存的地方,index的複數是indices 或indexes。
索引(動詞) 「索引一個文件」表示把一個文件儲存到索引(名詞)裡,以便它可以被檢索或者查詢。這很像SQL中的INSERT關鍵字,差別是,如果文件已經存在,新的文件將覆蓋舊的文件
倒排索引 傳統資料庫為特定列增加一個索引,例如B-Tree索引來加速檢索。Elasticsearch和Lucene使用一種叫做倒排索引(inverted index)的資料結構來達到相同目的。

注:預設情況下,文件中的所有欄位都會被索引(擁有一個倒排索引),只有這樣他們才是可被搜尋的

為了建立員工目錄,我們將進行如下操作

  • 為每個員工的文件(document)建立索引,每個文件包含了相應員工的所有資訊。
  • 每個文件的型別為employee。
  • employee型別歸屬於索引megacorp。
  • megacorp索引儲存在Elasticsearch叢集中
//新增  /索引名/型別名/員工ID
PUT /megacorp/employee/1
{
    "first_name" : "John",
    "last_name" :  "Smith",
    "age" :        25,
    "about" :      "I love to go rock climbing",
    "interests": [ "sports", "music" ]
}

PUT /megacorp/employee/3
{
    "first_name" :  "Douglas",
    "last_name" :   "Fir",
    "age" :         35,
    "about":        "I like to build cabinets",
    "interests":  [ "forestry" ]
}

檢索文件

第一個需求是能夠檢索單個員工的資訊

GET /megacorp/employee/1

響應的內容中包含一些文件的元資訊,John Smith的原始JSON文件包含在_source欄位中

{
  "_index" :   "megacorp",
  "_type" :    "employee",
  "_id" :      "1",
  "_version" : 1,
  "found" :    true,
  "_source" :  {
      "first_name" :  "John",
      "last_name" :   "Smith",
      "age" :         25,
      "about" :       "I love to go rock climbing",
      "interests":  [ "sports", "music" ]
  }
}

我們通過HTTP方法GET來檢索文件,同樣的,我們可以使用DELETE方法刪除文件,使用HEAD方法檢查某文件是否存在。如果想更新已存在的文件,我們只需再PUT一次

簡單搜尋

嘗試一個最簡單的搜尋全部員工的請求

GET /megacorp/employee/_search

響應內容的hits陣列中包含了我們所有的三個文件。預設情況下搜尋會返回前10個結果

{
   "took":      6,
   "timed_out": false,
   "_shards": { ... },
   "hits": {
      "total":      3,
      "max_score":  1,
      "hits": [
         {
            "_index":         "megacorp",
            "_type":          "employee",
            "_id":            "3",
            "_score":         1,
            "_source": {
               "first_name":  "Douglas",
               "last_name":   "Fir",
               "age":         35,
               "about":       "I like to build cabinets",
               "interests": [ "forestry" ]
            }
         },
         {
            "_index":         "megacorp",
            "_type":          "employee",
            "_id":            "1",
            "_score":         1,
            "_source": {
               "first_name":  "John",
               "last_name":   "Smith",
               "age":         25,
               "about":       "I love to go rock climbing",
               "interests": [ "sports", "music" ]
            }
         },
         {
            "_index":         "megacorp",
            "_type":          "employee",
            "_id":            "2",
            "_score":         1,
            "_source": {
               "first_name":  "Jane",
               "last_name":   "Smith",
               "age":         32,
               "about":       "I like to collect rock albums",
               "interests": [ "music" ]
            }
         }
      ]
   }
}

搜尋姓氏中包含“Smith”的員工。要做到這一點,我們將在命令列中使用輕量級的搜尋方法。這種方法常被稱作查詢字串(query string)搜尋

//在請求中依舊使用_search關鍵字,然後將查詢語句傳遞給引數q=
GET /megacorp/employee/_search?q=last_name:Smith
{
   ...
   "hits": {
      "total":      2,
      "max_score":  0.30685282,
      "hits": [
         {
            ...
            "_source": {
               "first_name":  "John",
               "last_name":   "Smith",
               "age":         25,
               "about":       "I love to go rock climbing",
               "interests": [ "sports", "music" ]
            }
         },
         {
            ...
            "_source": {
               "first_name":  "Jane",
               "last_name":   "Smith",
               "age":         32,
               "about":       "I like to collect rock albums",
               "interests": [ "music" ]
            }
         }
      ]
   }
}

使用DSL語句查詢

查詢字串搜尋便於通過命令列完成特定(ad hoc)的搜尋,但是它也有侷限性(參閱簡單搜尋章節)。Elasticsearch提供豐富且靈活的查詢語言叫做DSL查詢(Query DSL),它允許你構建更加複雜、強大的查詢

DSL(Domain Specific Language特定領域語言)以JSON請求體的形式出現。我們可以這樣表示之前關於“Smith”的查詢

//相當於/megacorp/employee/_search?q=last_name:"Smith"
GET /megacorp/employee/_search
{
    "query" : { //查詢
        "match" : { //匹配
            "last_name" : "Smith" //條件...
        }
    }
}

我們不再使用查詢字串(query string)做為引數,而是使用請求體代替。這個請求體使用JSON表示,其中使用了match語句

更復雜的搜尋

讓搜尋稍微再變的複雜一些。我們依舊想要找到姓氏為“Smith”的員工,但是我們只想得到年齡大於30歲的員工。我們的語句將新增過濾器(filter),它使得我們高效率的執行一個結構化搜尋

GET /megacorp/employee/_search
{
    "query" : {
        "filtered" : {
            "filter" : {
                "range" : {
                    "age" : { "gt" : 30 } <1>
                }
            },
            "query" : {
                "match" : {
                    "last_name" : "smith" <2>
                }
            }
        }
    }
}

<1> 這部分查詢屬於區間過濾器(range filter),它用於查詢所有年齡大於30歲的資料——gt為”greater than”的縮寫。
<2> 這部分查詢與之前的match語句(query)一致

{
   ...
   "hits": {
      "total":      1,
      "max_score":  0.30685282,
      "hits": [
         {
            ...
            "_source": {
               "first_name":  "Jane",
               "last_name":   "Smith",
               "age":         32,
               "about":       "I like to collect rock albums",
               "interests": [ "music" ]
            }
         }
      ]
   }
}

全文搜尋

到目前為止搜尋都很簡單:搜尋特定的名字,通過年齡篩選。讓我們嘗試一種更高階的搜尋,全文搜尋——一種傳統資料庫很難實現的功能。
將會搜尋所有喜歡“rock climbing”的員工

GET /megacorp/employee/_search
{
    "query" : {
        "match" : {
            "about" : "rock climbing"
        }
    }
}

使用了之前的match查詢,從about欄位中搜索”rock climbing”,我們得到了兩個匹配文件

{
   ...
   "hits": {
      "total":      2,
      "max_score":  0.16273327,
      "hits": [
         {
            ...
            "_score": 0.16273327, <1>
            "_source": {
               "first_name":  "John",
               "last_name":   "Smith",
               "age":         25,
               "about":       "I love to go rock climbing",
               "interests": [ "sports", "music" ]
            }
         },
         {
            ...
            "_score": 0.016878016, <2>
            "_source": {
               "first_name":  "Jane",
               "last_name":   "Smith",
               "age":         32,
               "about":       "I like to collect rock albums",
               "interests": [ "music" ]
            }
         }
      ]
   }
}

<1><2> 結果相關性評分

預設情況下,Elasticsearch根據結果相關性評分來對結果集進行排序,所謂的「結果相關性評分」就是文件與查詢條件的匹配程度。很顯然,排名第一的John Smith的about欄位明確的寫到“rock climbing”。
但是為什麼Jane Smith也會出現在結果裡呢?原因是“rock”在她的abuot欄位中被提及了。因為只有“rock”被提及而“climbing”沒有,所以她的_score要低於John

這個例子很好的解釋了Elasticsearch如何在各種文字欄位中進行全文搜尋,並且返回相關性最大的結果集。相關性(relevance)的概念在Elasticsearch中非常重要,而這個概念在傳統關係型資料庫中是不可想象的,因為傳統資料庫對記錄的查詢只有匹配或者不匹配

短語搜尋

目前我們可以在欄位中搜索單獨的一個詞,這挺好的,但是有時候你想要確切的匹配若干個單詞或者短語(phrases)。例如我們想要查詢同時包含”rock”和”climbing”(並且是相鄰的)的員工記錄

只要將match查詢變更為match_phrase查詢即可

GET /megacorp/employee/_search
{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    }
}
{
   ...
   "hits": {
      "total":      1,
      "max_score":  0.23013961,
      "hits": [
         {
            ...
            "_score":         0.23013961,
            "_source": {
               "first_name":  "John",
               "last_name":   "Smith",
               "age":         25,
               "about":       "I love to go rock climbing",
               "interests": [ "sports", "music" ]
            }
         }
      ]
   }
}

高亮搜尋

很多應用喜歡從每個搜尋結果中高亮(highlight)匹配到的關鍵字,這樣使用者可以知道為什麼這些文件和查詢相匹配。在Elasticsearch中高亮片段是非常容易的

在之前的語句上增加highlight引數

GET /megacorp/employee/_search
{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    },
    "highlight": {
        "fields" : {
            "about" : {}
        }
    }
}

當我們執行這個語句時,會命中與之前相同的結果,但是在返回結果中會有一個新的部分叫做highlight,這裡包含了來自about欄位中的文字,並且用來標識匹配到的單詞

{
   ...
   "hits": {
      "total":      1,
      "max_score":  0.23013961,
      "hits": [
         {
            ...
            "_score":         0.23013961,
            "_source": {
               "first_name":  "John",
               "last_name":   "Smith",
               "age":         25,
               "about":       "I love to go rock climbing",
               "interests": [ "sports", "music" ]
            },
            "highlight": {
               "about": [
                  "I love to go <em>rock</em> <em>climbing</em>" <1>
               ]
            }
         }
      ]
   }
}

<1> 原有文字中高亮的片段