原文載于Elastic中文社區(qū): https://elasticsearch.cn/article/142
現(xiàn)代的搜索引擎稀蟋,一般會具備"Suggest As You Type"功能棉姐,即在用戶輸入搜索的過程中,進(jìn)行自動補全或者糾錯糜俗。 通過協(xié)助用戶輸入更精準(zhǔn)的關(guān)鍵詞丑掺,提高后續(xù)全文搜索階段文檔匹配的程度陕见。例如在Google上輸入部分關(guān)鍵詞,甚至輸入拼寫錯誤的關(guān)鍵詞時局雄,它依然能夠提示出用戶想要輸入的內(nèi)容:
如果自己親手去試一試,可以看到Google在用戶剛開始輸入的時候是自動補全的存炮,而當(dāng)輸入到一定長度炬搭,如果因為單詞拼寫錯誤無法補全蜈漓,就開始嘗試提示相似的詞。
那么類似的功能在Elasticsearch里如何實現(xiàn)呢宫盔? 答案就在Suggesters API融虽。 Suggesters基本的運作原理是將輸入的文本分解為token,然后在索引的字典里查找相似的term并返回灼芭。 根據(jù)使用場景的不同有额,Elasticsearch里設(shè)計了4種類別的Suggester,分別是:
- Term Suggester
- Phrase Suggester
- Completion Suggester
- Context Suggester
在官方的參考文檔里彼绷,對這4種Suggester API都有比較詳細(xì)的介紹巍佑,但苦于只有英文版,部分國內(nèi)開發(fā)者看完文檔后仍然難以理解其運作機制寄悯。 本文將在Elasticsearch 5.x上通過示例講解Suggester的基礎(chǔ)用法萤衰,希望能幫助部分國內(nèi)開發(fā)者快速用于實際項目開發(fā)。限于篇幅猜旬,更為高級的Context Suggester會被略過脆栋。
首先來看一個Term Suggester的示例:
準(zhǔn)備一個叫做blogs的索引,配置一個text字段洒擦。
PUT /blogs/
{
"mappings": {
"tech": {
"properties": {
"body": {
"type": "text"
}
}
}
}
}
通過bulk api寫入幾條文檔
POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Lucene is cool"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "elk rocks"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "elasticsearch is rock solid"}
此時blogs索引里已經(jīng)有一些文檔了筹吐,可以進(jìn)行下一步的探索。為幫助理解秘遏,我們先看看哪些term會存在于詞典里丘薛。
將輸入的文本分析一下:
POST _analyze
{
"text": [
"Lucene is cool",
"Elasticsearch builds on top of lucene",
"Elasticsearch rocks",
"Elastic is the company behind ELK stack",
"elk rocks",
"elasticsearch is rock solid"
]
}
(由于結(jié)果太長,此處略去)
這些分出來的token都會成為詞典里一個term邦危,注意有些token會出現(xiàn)多次洋侨,因此在倒排索引里記錄的詞頻會比較高,同時記錄的還有這些token在原文檔里的偏移量和相對位置信息倦蚪。
執(zhí)行一次suggester搜索看看效果:
POST /blogs/_search
{
"suggest": {
"my-suggestion": {
"text": "lucne rock",
"term": {
"suggest_mode": "missing",
"field": "body"
}
}
}
}
suggest就是一種特殊類型的搜索希坚,DSL內(nèi)部的text
指的是api調(diào)用方提供的文本,也就是通常用戶界面上用戶輸入的內(nèi)容陵且。這里的lucne是錯誤的拼寫裁僧,模擬用戶輸入錯誤。 term
表示這是一個term suggester慕购。 field
指定suggester針對的字段聊疲,另外有一個可選的suggest_mode
。 范例里的"missing"實際上就是缺省值沪悲,它是什么意思获洲?有點撓頭... 還是先看看返回結(jié)果吧:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits": {
"total": 0,
"max_score": 0,
"hits":
},
"suggest": {
"my-suggestion": [
{
"text": "lucne",
"offset": 0,
"length": 5,
"options": [
{
"text": "lucene",
"score": 0.8,
"freq": 2
}
]
},
{
"text": "rock",
"offset": 6,
"length": 4,
"options":
}
]
}
}
在返回結(jié)果里"suggest" -> "my-suggestion"
部分包含了一個數(shù)組,每個數(shù)組項對應(yīng)從輸入文本分解出來的token(存放在"text"這個key里)以及為該token提供的建議詞項(存放在options數(shù)組里)殿如。 示例里返回了"lucne"贡珊,"rock"這2個詞的建議項(options)最爬,其中"rock"的options是空的,表示沒有可以建議的選項门岔,為什么爱致? 上面提到了,我們?yōu)椴樵兲峁┑?code>suggest mode是"missing",由于"rock"在索引的詞典里已經(jīng)存在了寒随,夠精準(zhǔn)蒜鸡,就不建議啦。 只有詞典里找不到詞牢裳,才會為其提供相似的選項逢防。
如果將suggest_mode
換成"popular"會是什么效果?
嘗試一下蒲讯,重新執(zhí)行查詢忘朝,返回結(jié)果里"rock"這個詞的option不再是空的,而是建議為rocks判帮。
"suggest": {
"my-suggestion": [
{
"text": "lucne",
"offset": 0,
"length": 5,
"options": [
{
"text": "lucene",
"score": 0.8,
"freq": 2
}
]
},
{
"text": "rock",
"offset": 6,
"length": 4,
"options": [
{
"text": "rocks",
"score": 0.75,
"freq": 2
}
]
}
]
}
回想一下局嘁,rock和rocks在索引詞典里都是有的。 不難看出即使用戶輸入的token在索引的詞典里已經(jīng)有了晦墙,但是因為存在一個詞頻更高的相似項悦昵,這個相似項可能是更合適的,就被挑選到options里了晌畅。 最后還有一個"always" mode但指,其含義是不管token是否存在于索引詞典里都要給出相似項。
有人可能會問抗楔,兩個term的相似性是如何判斷的棋凳? ES使用了一種叫做Levenstein edit distance
的算法,其核心思想是根據(jù)一個詞改動多少個字符就可以和另外一個詞一致作為相似距離的評判连躏。 Term suggester還有其他很多可選參數(shù)來控制這個相似性的模糊程度养篓,這里就不一一贅述了弛随。
Term suggester正如其名,只基于analyze過的單個term去提供建議祷舀,并不會考慮多個term之間的關(guān)系轩端。API調(diào)用方只需為每個token挑選options里的詞坏快,組合在一起返回給用戶前端即可往史。 那么有無更直接辦法花枫,API直接給出和用戶輸入文本相似的內(nèi)容? 答案是有郑气,這就要求助Phrase Suggester了幅垮。
Phrase suggester在Term suggester的基礎(chǔ)上腰池,會考量多個term之間的關(guān)系尾组,比如是否同時出現(xiàn)在索引的原文里忙芒,相鄰程度,以及詞頻等等讳侨『侨看個范例就比較容易明白了:
POST /blogs/_search
{
"suggest": {
"my-suggestion": {
"text": "lucne and elasticsear rock",
"phrase": {
"field": "body",
"highlight": {
"pre_tag": "<em>",
"post_tag": "</em>"
}
}
}
}
}
返回結(jié)果:
"suggest": {
"my-suggestion": [
{
"text": "lucne and elasticsear rock",
"offset": 0,
"length": 26,
"options": [
{
"text": "lucene and elasticsearch rock",
"highlighted": "<em>lucene</em> and <em>elasticsearch</em> rock",
"score": 0.004993905
},
{
"text": "lucne and elasticsearch rock",
"highlighted": "lucne and <em>elasticsearch</em> rock",
"score": 0.0033391973
},
{
"text": "lucene and elasticsear rock",
"highlighted": "<em>lucene</em> and elasticsear rock",
"score": 0.0029183894
}
]
}
]
}
options直接返回一個phrase列表,由于加了highlight選項跨跨,被替換的term會被高亮潮峦。因為lucene和elasticsearch曾經(jīng)在同一條原文里出現(xiàn)過,同時替換2個term的可信度更高勇婴,所以打分較高忱嘹,排在第一位返回。Phrase suggester有相當(dāng)多的參數(shù)用于控制匹配的模糊程度耕渴,需要根據(jù)實際應(yīng)用情況去挑選和調(diào)試拘悦。
最后來談一下Completion Suggester,它主要針對的應(yīng)用場景就是"Auto Completion"橱脸。 此場景下用戶每輸入一個字符的時候础米,就需要即時發(fā)送一次查詢請求到后端查找匹配項,在用戶輸入速度較高的情況下對后端響應(yīng)速度要求比較苛刻添诉。因此實現(xiàn)上它和前面兩個Suggester采用了不同的數(shù)據(jù)結(jié)構(gòu)屁桑,索引并非通過倒排來完成,而是將analyze過的數(shù)據(jù)編碼成FST和索引一起存放栏赴。對于一個open狀態(tài)的索引蘑斧,F(xiàn)ST會被ES整個裝載到內(nèi)存里的,進(jìn)行前綴查找速度極快须眷。但是FST只能用于前綴查找乌叶,這也是Completion Suggester的局限所在。
為了使用Completion Suggester柒爸,字段的類型需要專門定義如下:
PUT /blogs_completion/
{
"mappings": {
"tech": {
"properties": {
"body": {
"type": "completion"
}
}
}
}
}
用bulk API索引點數(shù)據(jù):
POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Lucene is cool"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "the elk stack rocks"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "elasticsearch is rock solid"}
查找:
POST blogs_completion/_search?pretty
{ "size": 0,
"suggest": {
"blog-suggest": {
"prefix": "elastic i",
"completion": {
"field": "body"
}
}
}
}
結(jié)果:
"suggest": {
"blog-suggest": [
{
"text": "elastic i",
"offset": 0,
"length": 9,
"options": [
{
"text": "Elastic is the company behind ELK stack",
"_index": "blogs_completion",
"_type": "tech",
"_id": "AVrXFyn-cpYmMpGqDdcd",
"_score": 1,
"_source": {
"body": "Elastic is the company behind ELK stack"
}
}
]
}
]
}
值得注意的一點是Completion Suggester在索引原始數(shù)據(jù)的時候也要經(jīng)過analyze階段准浴,取決于選用的analyzer不同,某些詞可能會被轉(zhuǎn)換捎稚,某些詞可能被去除乐横,這些會影響FST編碼結(jié)果,也會影響查找匹配的效果今野。
比如我們刪除上面的索引葡公,重新設(shè)置索引的mapping,將analyzer更改為"english":
PUT /blogs_completion/
{
"mappings": {
"tech": {
"properties": {
"body": {
"type": "completion",
"analyzer": "english"
}
}
}
}
}
bulk api索引同樣的數(shù)據(jù)后条霜,執(zhí)行下面的查詢:
POST blogs_completion/_search?pretty
{ "size": 0,
"suggest": {
"blog-suggest": {
"prefix": "elastic i",
"completion": {
"field": "body"
}
}
}
}
居然沒有匹配結(jié)果了催什,多么費解! 原來我們用的english analyzer會剝離掉stop word宰睡,而is就是其中一個蒲凶,被剝離掉了气筋!
用analyze api測試一下:
POST _analyze?analyzer=english
{
"text": "elasticsearch is rock solid"
}
會發(fā)現(xiàn)只有3個token:
{
"tokens": [
{
"token": "elasticsearch",
"start_offset": 0,
"end_offset": 13,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "rock",
"start_offset": 17,
"end_offset": 21,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "solid",
"start_offset": 22,
"end_offset": 27,
"type": "<ALPHANUM>",
"position": 3
}
]
}
FST只編碼了這3個token,并且默認(rèn)的還會記錄他們在文檔中的位置和分隔符旋圆。 用戶輸入"elastic i"進(jìn)行查找的時候宠默,輸入被分解成"elastic"和"i",F(xiàn)ST沒有編碼這個“i” , 匹配失敗灵巧。
好吧搀矫,如果你現(xiàn)在還足夠清醒的話,試一下搜索"elastic is"刻肄,會發(fā)現(xiàn)又有結(jié)果瓤球,什么幺蛾子? 因為這次輸入的text經(jīng)過english analyzer的時候is也被剝離了,只需在FST里查詢"elastic"這個前綴敏弃,自然就可以匹配到了冰垄。
其他能影響completion suggester結(jié)果的,還有諸如preserve_separators
权她,preserve_position_increments
等等mapping參數(shù)來控制匹配的模糊程度虹茶。以及搜索時可以選用Fuzzy Queries,使得上面例子里的"elastic i"在使用english analyzer的情況下依然可以匹配到結(jié)果隅要。
因此用好Completion Sugester并不是一件容易的事蝴罪,實際應(yīng)用開發(fā)過程中,需要根據(jù)數(shù)據(jù)特性和業(yè)務(wù)需要步清,靈活搭配analyzer和mapping參數(shù)要门,反復(fù)調(diào)試才可能獲得理想的補全效果。
回到篇首Google搜索框的補全/糾錯功能廓啊,如果用ES怎么實現(xiàn)呢欢搜?我能想到的一個的實現(xiàn)方式:
- 在用戶剛開始輸入的過程中,優(yōu)先使用Completion Suggester進(jìn)行關(guān)鍵詞前綴匹配谴轮,剛開始匹配項會比較多炒瘟,隨著用戶輸入字符增多,匹配項越來越少第步。如果用戶輸入比較精準(zhǔn)疮装,可能Completion Suggester的結(jié)果已經(jīng)夠好,用戶已經(jīng)可以看到理想的備選項了粘都。
- 如果Completion Suggester已經(jīng)到了零匹配廓推,那么可以猜測是否用戶有輸入錯誤,這時候可以嘗試一下Phrase Suggester翩隧。
- 如果Phrase Suggester沒有找到任何option樊展,開始嘗試term Suggester。
精準(zhǔn)程度上(Precision)看: Completion > Phrase > term, 而召回率上(Recall)則反之专缠。從性能上看雷酪,Completion Suggester是最快的,如果能滿足業(yè)務(wù)需求藤肢,只用Completion Suggester做前綴匹配是最理想的太闺。 Phrase和Term由于是做倒排索引的搜索糯景,相比較而言性能應(yīng)該要低不少嘁圈,應(yīng)盡量控制suggester用到的索引的數(shù)據(jù)量,最理想的狀況是經(jīng)過一定時間預(yù)熱后蟀淮,讓磁盤上的索引文件可以全量mmap到OS Page Cache最住。