【工作筆記】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> 原有文字中高亮的片段