讀書筆記:從Lucene到Elasticsearch:全文檢索實戰

BlackHole1發表於2019-01-08

公司專案的的日誌埋點是發到 Elasticsearch 上的,有時開發會去在 kibana 上查詢相關的日誌資訊,用於診斷使用者的問題。但是 kibana 太過重量級,查詢起來比較慢。於是學習了下 Elasticsearch 的用法,此文章個人也只是點到為止。工作時夠用就行了,所有有些地方可能並沒有詳細說明。只能充當入門讀物吧。

當前的筆記只介紹 Elasticsearch 的搜尋部分。

文章中的搜尋都是在 kibanaDev tools 進行查詢的。

準備工作

需要安裝 Elasticsearchkibanaelasticsearch-analysis-ik

具體的安裝方式,這裡就不再闡述了。(安裝完,記得重啟 Elasticsearch

重啟完成後,開啟 kibanaDev tools,輸入下面的DSL程式碼,並執行:

PUT books{ 
"settings": {
"number_of_replicas": 1, "number_of_shards": 3
}, "mappings": {
"IT": {
"properties": {
"id": {
"type": "long"
}, "title": {
"type": "text", "analyzer": "ik_max_word"
}, "language": {
"type": "keyword"
}, "author": {
"type": "keyword"
}, "price": {
"type": "double"
}, "year": {
"type": "date", "format": "yyyy-MM-dd"
}, "description": {
"type": "text", "analyzer": "ik_max_word"
}
}
}
}
}複製程式碼

執行好後,下載 books.json 檔案,並進行匯入。如果你安裝的 Elasticsearch 版本小於6.0,使用下面的命令進行匯入 books.json

curl -XPOST "http://localhost:9200/_bulk?pretty" --data-binary @books.json複製程式碼

如果你的 Elasticsearch 版本大於6.0,則使用下面的命令進行匯入:

curl -H "Content-Type: application/json" -XPOST "http://localhost:9200/_bulk?pretty" --data-binary @books.json複製程式碼

基本搜尋

返回指定index的所有文件

GET books/_search{ 
"query": {
"match_all": {
}
}
}複製程式碼

可以簡寫為:

GET books/search複製程式碼

查詢指定欄位中包含給定單詞的文件

使用term來進行查詢,term查詢不會被解析,只有查詢的詞和文件中的詞精確匹配才會被搜尋到,應用場景為:查詢人名、地名等需要精準匹配的需求。

查詢title欄位中含有思想的書籍

GET books/_search{ 
"query": {
"term": {
"title": "思想"
}
}
}複製程式碼

返回如下:

Imgur

對查詢結果進行分頁

有時查詢時,會返回成千上萬的資料,這種情況下,分頁的作用就出來了。

分頁有兩個屬性,分別是fromsize

  • from: 從何處開始
  • size: 返回的文件最大數量

可以理解為:我從from位置把剩下的文件全部返回,然後size限制了返回的數量。

用js程式碼來詮釋就是:

const from = 100 - 1;
// 陣列從0開始,需要減一const size = 10;
const data = [1, 2, 3, ..., 999, 1000];
const fromDate = data.splice(from);
const result = fromData.splice(0, size);
console.log(result) //=>
[100, 101, 102, 103, 104, 105, 106, 107, 108, 109]
複製程式碼

限制返回欄位

一般我們查詢時,都是為了觀察某一個欄位,而不是想看全部的欄位。而如果是預設情況下,Elasticsearch 會返回的文件的全部欄位資訊。會對工作造成一定的影響。於是,Elasticsearch 提供了一個介面,用於限制返回的欄位。假設我只需要 titleauthor 欄位:

GET books/_search{ 
"_source": ["title", "author"], "query": {
"term": {
"title": "java"
}
}
}複製程式碼

結果如圖:

Imgur

基於最小評分過濾

因為 Elasticsearch 在做普通的搜尋時,是採用相關性進行搜尋的,而相關性是由評分 取決的。所以當我們進行模糊搜尋時,Elasticsearch 可能會返回一些相關性不那麼高的文件。所以我們可以通過 Elasticsearch 提供的介面,來設定一個評分最低標準,低於這個標準的文件,將不會出現在結果頁中。

比如,我想搜尋 title 裡包含 java 的文件,並且評分不低於0.7

GET books/_search{ 
"min_score": 0.7, "query": {
"term": {
"title": "java"
}
}
}複製程式碼

結果如圖:

Imgur

高亮關鍵字

有時,我們會把 Elasticsearch 結果直接匯入到網頁中,這個時候需要高亮關鍵字,讓使用者更加清楚自己想要的東西,Elasticsearch 已經提供了一個介面,比如我想讓搜尋出來的結果中的關鍵字高亮:

GET books/_search{ 
"_source": ["title"], "min_score": 0.7, "query": {
"term": {
"title": "java"
}
}, "highlight": {
"fields": {
"title": {
}
}
}
}複製程式碼

結果如圖:

Imgur

預設的標籤是<
em>
<
/em>
,如果你想自定義,可以使用: pre_tagspost_tags。最終查詢程式碼為:

GET books/_search{ 
"_source": ["title"], "min_score": 0.7, "query": {
"term": {
"title": "java"
}
}, "highlight" : {
"pre_tags" : ["<
h1>
"
], "post_tags" : ["<
/h1>
"
], "fields" : {
"title" : {
}
}
}
}複製程式碼

結果如圖:

Imgur

全文查詢

上節基本都是以 term 進行搜尋,但其實 Elasticsearch 提供了很多搜尋方法,本章就是介紹 Elasticsearch 有哪些搜尋方法、分別起的作用。

本章對 common_terms queryquery_string querysimple_query_string query 沒有解釋說明,因為使用起來較少,而且解釋起來較為麻煩。如果想了解,可以參考網上的文章。這裡就不在闡述了。

match query

我們先使用 term 進行一次查詢:

GET books/_search{ 
"_source": ["title", "author"], "query": {
"term": {
"title": "java程式設計"
}
}
}複製程式碼

你會發現,其結果為空(但是資料庫裡是有這個資料的),如圖:

Imgur

這是因為 term 是匹配分詞後的詞項來進行查詢的。比如剛剛我們查的 java程式設計 ,在 Elasticsearch 進行分詞時,會把 java程式設計 分為:java程式設計 。導致匹配不起來。

用程式碼詮釋的話就是:

const keyword = 'java程式設計';
const data = ['java', '程式設計'];
const result = data.includes(keyword);
console.log(result) //=>
false
複製程式碼

現在我們把 term 換成 match 來嘗試下:

GET books/_search{ 
"_source": ["title", "author"], "query": {
"match": {
"title": "java程式設計"
}
}
}複製程式碼

結果如圖:

Imgur

可以發現,已經有結果了,但是為什麼會有兩個呢?

原因是因為 match 會對你的關鍵字進行分詞,然後去匹配文件分詞後的結果,只要文件裡的詞項能匹配關鍵字分詞後的任何一個,都會返回到結果裡。

程式碼詮釋:

const data = ['java', '程式設計', '思想'];
// 分詞後的文件裡的資料const keywords = ['java', '程式設計', '思想'];
// 分詞後的關鍵字const result = (() =>
{
for (let x = 0;
x <
data.length;
x++) {
const dataItem = data[x];
for (let y = 0;
y <
keywords.length;
y++) {
const keywordItem = keywords[y];
if (dataItem === keywordItem) {
return true;

}
}
} return false;

})()複製程式碼

如果我只想讓它返回一個呢,並且只能用 match 來做,可以麼?

是可以的,match 提供了一個屬性:operator。可以用這個來幫助完成這個需求:

GET books/_search{ 
"_source": ["title", "author"], "query": {
"match": {
"title": {
"query": "java程式設計", "operator": "and"
}
}
}
}複製程式碼

最終的結果如圖:

Imgur

原理是因為 operator 屬性的值為 and,這樣的話,就告訴 Elasticsearch 我要讓我的關鍵字都能和文件裡的詞項匹配上。有一個沒匹配上,我都不要。

如果 operator 屬性的值為 or,那結果就和之前是一樣的了。

match_phrase query

你可以把這個方法理解為自帶了 operator 屬性的值為 andmatch

這個方法有兩個限制條件,只有都滿足,才會在結果中顯示出:

  • 分詞後的所有詞項都在該欄位中,相當於 operator: "and"
  • 順序要一致

順序一致指的是什麼呢?

假設你使用 match 來匹配: 程式設計java,那麼結果還是和上面一樣。所以如果你需要要求順序一致性,那麼你就可以使用 match_phrase 來做。

如果使用 程式設計java 來搜尋:

Imgur

如果使用 java程式設計

Imgur

match_phrase_prefix query

這個方法和 match_phrase 方法類似,不過這個方法可以可以把最後一個詞項作為字首進行匹配,想象一下:使用者在搜尋欄中搜尋 辣雞UZ,然後下面列表中出現了 辣雞UZI

首先 match_phrase_prefix 會先分詞為: 辣雞,然後找了一個文件,再然後匹配 辣雞 後面的字串是否以 UZ 開頭的。這個時候文件滿足條件,就返回出結果。可以假想後面一直有一個(.*)的萬用字元,如:辣雞UZ(.*)

知道原理了,我們現在寫一個查詢語句:

GET books/_search{ 
"_source": ["title", "author"], "query": {
"match_phrase_prefix": {
"title": "java編"
}
}
}複製程式碼

結果如圖:

Imgur

multi_match query

multi_matchmatch 的升級方法,可以用來搜尋多個欄位。

比如我不想只在 title 裡搜尋 java程式設計,我還想在 description 裡進行搜尋。那應該怎麼做呢?

Elasticsearch 已經提供了 multi_match 專門用來處理這件事情:

GET books/_search{ 
"_source": ["title", "description"], "query": {
"multi_match": {
"query": "java程式設計", "fields": ["title", "description"]
}
}
}複製程式碼

最終結果如圖:

Imgur

並且 multi_match 還支援萬用字元。上面的查詢語句,可以寫成:

GET books/_search{ 
"_source": ["title", "description"], "query": {
"multi_match": {
"query": "java程式設計", "fields": ["title", "*tion"]
}
}
}複製程式碼

詞項查詢

上一章是全文查詢,這一章是詞項查詢。他們倆的區別在於:

  • 全文查詢:會對查詢語句(query)進行分詞,然後匹配文件裡分詞後的資料
  • 詞項查詢:不會對查詢語句進行分詞

term query

第一章節已經介紹過了,這裡就不再闡述了。

terms query

termsterm 查詢的升級版本,可以用來查詢文件中某一欄位,是否包含了其關鍵字。比如,我想查詢 title 欄位中包含了 優化 或者 基礎 的文件:

GET books/_search{ 
"_source": ["title"], "query": {
"terms": {
"title": ["優化", "基礎"]
}
}
}複製程式碼

其結果如圖:

Imgur

range query

從名字就能猜測出 range 是範圍匹配。可以匹配 numberdatestring (字串範圍查詢比較特殊,比較少用,就不再闡述了)

range 支援以下查詢引數:

  • gt: 大於
  • gte: 大於等於
  • lt: 小於
  • lte: 小於等於

number 範圍查詢

現在我想查詢價格低於70,並大於等於50的書籍。虛擬碼既:(price >
= 50 &
&
price <
70)

GET books/_search{ 
"_source": ["title", "price"], "query": {
"range": {
"price": {
"gte": 50, "lt": 70
}
}
}
}複製程式碼

其結果如圖:

Imgur

date 範圍查詢

如果我想查詢,出版日期在 2016-1-12016-12-31 之間的書籍,那麼DSL查詢語句就如同以下這樣:

GET books/_search{ 
"_source": ["title", "publish_time"], "query": {
"range": {
"publish_time": {
"gte": "2016-1-1", "lte": "2016-12-31", "format": "yyyy-MM-dd"
}
}
}
}複製程式碼

其結果如圖:

Imgur

exists query

匹配有這個屬性的文件。比如我想找到存在 title 欄位的文件:

GET books/_search{ 
"_source": "title", "query": {
"exists": {
"field": "title"
}
}
}複製程式碼

結果會返回所有的文件。那麼如何定義 有這個屬性 呢?

定義的規則如下:

  • {"title": "js"
    }
    : 存在
  • {"title": ""
    }
    : 存在
  • {"title": ["js"]
    }
    : 存在
  • {"title": ["js", null]
    }
    : 存在(有一個值不為空就行)
  • {"title": null
    }
    : 不存在
  • {"title": []
    }
    不存在
  • {"title": [null]
    }
    不存在
  • {"foo": "bar"
    }
    : 不存在

perfix query

用來匹配文件分詞後的詞項中的字首。我們先寫個DSL進行匹配下:

GET books/_search{ 
"_source": "description", "query": {
"prefix": {
"description": "wi"
}
}
}複製程式碼

其結果如圖:

Imgur

為何 wi 可以匹配到這個呢?因為 Elasticsearch 會對 description 進行分詞,其中會把 winPython 分為 win Python。那麼這兩個就是文件分詞後的詞項,而 prefix 匹配每個詞項的開頭是否匹配,相當於js的 startsWith 方法。用程式碼詮釋的話就是:

const dataItem = ['win', 'python'];
const prefixKeyword = 'wi';
const result = dataItem.some(item =>
item.startsWith(prefixKeyword));
console.log(result);
//=>
true
複製程式碼

wildcard query

wildcard 為萬用字元查詢。不過目前只支援 *?。所代表的含義為:

  • *: 零個或多個
  • ?: 一個或多個

注意:wildcard 不是匹配全文,還是會對文件的欄位進行分詞,然後應用於每個詞項

比如,我現在想查詢 wi* 的文件:

GET books/_search{ 
"_source": "description", "query": {
"wildcard": {
"description": "wi*"
}
}
}複製程式碼

其結果如圖:

Imgur

首先 Elasticsearch 會先對 description 進行分詞為:winpython。然後 wi* 會應用到每個詞項裡,其中 win 符合規則,則顯示在結果中。

如果我用 win?,則不會有任何的結果,因為 ? 代表的是一個或多個。那麼匹配到 win 的時候,後面沒有字串了,則結果為空。

regexp query

其為正規表示式查詢,原理同 wildcard,這裡就不在闡述了。

fuzzy query

可以把 fuzzy 理解為模糊查詢。比如使用者輸入關鍵字時,一不小心輸入錯了,變成了 javascrpit,那麼 fuzzy 的作用就出來了。它仍可以搜尋到 javascript:

GET books/_search{ 
"_source": "description", "query": {
"fuzzy": {
"description": "javascrpit"
}
}
}複製程式碼

其結果如圖:

Imgur

複合查詢

複合查詢就是把簡單的查詢組合在一起,從而實現更加複雜的查詢。並且複合查詢還可以控制另一個查詢的行為。

constant_score query

不太常用,用於對返回結果的文件進行打分。

這裡就不在闡述了,如果感興趣,可見:[Elasticsearch] 控制相關度 (四) – 忽略TF/IDF

bool query

這個查詢方法,還是非常重要的。這個方法提供了以下操作方法:

  • must: 文件必須滿足 must 下面的查詢條件,相當於AND 或者 &
    &
  • should: 文件可以匹配 should 下的查詢條件,匹配不出來也沒事。相當於 OR 或者 ||
  • must_not: 和 must 相反,必須不滿足 must_not 下面的查詢條件,相當於 !==
  • filter: 其功能和 must 一樣,但是不會打分,也就說不會影響文件的 _score 欄位

現在,我們想要查詢:書籍作者(author)是 葛一鳴,書籍名稱(title)裡包含 java 的書籍,價格(price)不能高於 70 低於 40,並且書籍描述(description)可以包含或者不包含 虛擬機器 的書籍。

GET books/_search{ 
"query": {
"bool": {
"filter": {
"term": {
"author": "葛一鳴"
}
}, "must": [ {
"match": {
"title": "java"
}
} ], "should": [ {
"match": {
"description": "虛擬機器"
}
} ], "must_not": [ {
"range": {
"price": {
"gt": 70, "lt": 40
}
}
} ]
}
}
}複製程式碼

其結果如圖:

Imgur

dis_max query、function_score query、boosting query

這三個就不在闡述了,其主要作用是關係到 _score,也就是關係到查詢的結果的評分。感興趣的,可以在網上搜下。

結尾

此文章是《從Lucene到Elasticsearch:全文檢索實戰》一書的讀書筆記,如果造成侵權,可與我聯絡,我將刪除此文。

這本書是十分棒的,如果大家有興趣想深入瞭解的話,可以去進行購買此書。

作者資訊

Black-Hole: 158blackhole@gmail.com

Blog: www.bugs.cc

Github: github.com/BlackHole1

來源:https://juejin.im/post/5c346a2cf265da61193c02b4

相關文章