搜索是什么
對(duì)于初學(xué)者來(lái)說(shuō),搜索引擎就是一個(gè)神秘的黑盒敛瓷。只包含少數(shù)幾種交互模式:將內(nèi)容加入搜索引擎苛败,然后允許用戶對(duì)內(nèi)容進(jìn)行查詢满葛。而在底層,搜索引擎的數(shù)據(jù)結(jié)構(gòu)其實(shí)執(zhí)行著相當(dāng)機(jī)械的詞匹配操作罢屈,然后對(duì)結(jié)果用簡(jiǎn)單的方式加以排名嘀韧。
搜索引擎的核心功能是存儲(chǔ)、查詢并返回結(jié)果缠捌。存儲(chǔ)锄贷、搜索和返回的都是文檔。
一般我們有很多數(shù)據(jù)來(lái)源如:社區(qū)UGC數(shù)據(jù)曼月、互聯(lián)網(wǎng)爬取數(shù)據(jù)谊却、廣告數(shù)據(jù)、電商數(shù)據(jù)等等哑芹。其中有結(jié)構(gòu)化數(shù)據(jù)炎辨、半結(jié)構(gòu)化數(shù)據(jù)、非結(jié)構(gòu)化數(shù)據(jù)等等聪姿。我們要事先對(duì)這些數(shù)據(jù)做預(yù)處理碴萧,將其轉(zhuǎn)換成搜索引擎所需要的文檔結(jié)構(gòu),然后在經(jīng)過(guò)一系列的動(dòng)作(分詞末购、倒排索引等)最終存儲(chǔ)到搜索引擎中破喻。
文檔
文檔是一組字段的集合。字段是有類型的盟榴,可以分為標(biāo)準(zhǔn)類型和特殊類型曹质。標(biāo)準(zhǔn)類型如string、int、float羽德、boolean几莽、date等,特殊類型:數(shù)組宅静、IP银觅、經(jīng)緯度、對(duì)象等坏为。其中字符串類型尤其受搜索青睞究驴。人們經(jīng)常在字符串中進(jìn)行搜索。那么如何更好的進(jìn)行字符串搜索呢匀伏,這里就需要提到分詞這個(gè)動(dòng)作了洒忧。
分詞
分詞將較長(zhǎng)的句子分割成一個(gè)一個(gè)的單詞或詞組,并對(duì)分割后的詞做一些加工處理:去除停用詞够颠、大寫轉(zhuǎn)小寫熙侍、復(fù)數(shù)轉(zhuǎn)單數(shù)烤宙、繁轉(zhuǎn)簡(jiǎn)引颈、中文轉(zhuǎn)拼音等。
分詞過(guò)程如圖二所示雄右,目前常用的中文分詞有很多剃诅,如:IK分詞巷送、結(jié)巴分詞、Jcseg4j等等矛辕。
倒排索引
搜索引擎的核心部分是一種被稱為倒排索引的數(shù)據(jù)結(jié)構(gòu)笑跛,它類似于圖書后面的索引。由兩個(gè)主要部分組成:詞典和倒排表聊品。這樣的數(shù)據(jù)結(jié)構(gòu)讓文檔能快速的匹配查詢條件飞蹂。
除了這兩部分,倒排索引中還包含有文檔頻率翻屈、詞頻陈哑、詞位置、詞偏移量伸眶、儲(chǔ)存字段惊窖、文檔值等。這些信息能更好的提高搜索的相關(guān)性和排序性能赚抡。
lucene 的相關(guān)度評(píng)分
TF:詞頻爬坑,詞語(yǔ)在文檔中出現(xiàn)的頻率纠屋。如果一個(gè)詞頻繁的出現(xiàn)在一篇文檔的某個(gè)字段涂臣,那么我們可以認(rèn)為這個(gè)字段很可能涉及到該詞。
IDF:逆向文件頻率,是一個(gè)詞語(yǔ)普遍重要性的度量赁遗。IDF為文檔頻率的倒數(shù)署辉。如果該詞很常見(jiàn),文檔頻率就會(huì)很高岩四。稀有的詞被認(rèn)為更有價(jià)值哭尝,而常見(jiàn)的詞則相反。
TF*IDF貌似是一個(gè)很直觀的權(quán)重計(jì)算公式剖煌。單詞被提到的次數(shù)越多確實(shí)會(huì)與相關(guān)性有關(guān)系材鹦,但這種關(guān)系并不是線性的。一個(gè)搜索詞在文本中可能出現(xiàn)10次以上耕姊,但并不能說(shuō)明它具有10倍的相關(guān)性桶唐。在Lucene中通過(guò)相似度類來(lái)對(duì)TF和IDF的影響進(jìn)行抑制。
抑制TF茉兰、IDF:
TF Weight = sqrt(tf)
IDF Weight = log(numDoc /(df +1)) + 1
文本長(zhǎng)度歸一化因子
字段中包含的項(xiàng)數(shù)量.該值在索引期間計(jì)算尤泽,并保存在索引norm中.對(duì)于該因子,更短的字段(或更少的語(yǔ)匯單元)能獲得更大的加權(quán)
fieldNorm = 1 / sqrt(fieldlength)
Lucene中的TF*IDF公式
TF Weight * IDF Weight * fieldNorm
<lst name="params">
<str name="q">taboo_title:蘋果</str>
<str name="fl">taboo_title</str>
<str name="rows">2</str>
<str name="wt">xml</str>
<str name="debugQuery">true</str>
</lst>
<result name="response" numFound="3" start="0">
<doc>
<str name="taboo_title">蘋果</str></doc>
<doc>
<str name="taboo_title">蘋果醋</str></doc>
</result>
<lst name="explain">
<str name="124">
6.7861304 = weight(taboo_title:蘋果 in 110) [DefaultSimilarity], result of:
6.7861304 = fieldWeight in 110, product of:
1.0 = tf(freq=1.0), with freq of:
1.0 = termFreq=1.0
6.7861304 = idf(docFreq=3, maxDocs=1303)
1.0 = fieldNorm(doc=110)
</str>
<str name="312">
4.2413316 = weight(taboo_title:蘋果 in 286) [DefaultSimilarity], result of:
4.2413316 = fieldWeight in 286, product of:
1.0 = tf(freq=1.0), with freq of:
1.0 = termFreq=1.0
6.7861304 = idf(docFreq=3, maxDocs=1303)
0.625 = fieldNorm(doc=286)
</str>
</lst>
如上述Solr代碼展示的规脸。對(duì)于單字段搜索Solr返回的相關(guān)度得分滿足Lucene的TF*IDF公式坯约。當(dāng)然對(duì)于較為復(fù)雜的查詢其相關(guān)度得分的計(jì)算公式會(huì)更復(fù)雜些。大家有興趣的話可以看看這一篇博文:lucene 的評(píng)分機(jī)制
Lucene在4.0版本之后也實(shí)現(xiàn)了另一種相似度評(píng)分算法:BM25莫鸭。這是一種不同于TFxIDF的概率模型算法闹丐。大家有興趣的話可以了解下這兩篇博文:BM25算法在Lucene中的應(yīng)用、BM25和Lucene Default Similarity比較
搜索排序
上述篇幅簡(jiǎn)單介紹了搜索引擎的底層儲(chǔ)存結(jié)構(gòu)和相關(guān)度評(píng)分機(jī)制被因。而搜索的最終目的是根據(jù)用戶輸入的搜索詞妇智,將與搜索詞相匹配的搜索結(jié)果(文檔)返回給用戶。用戶最終看到的是搜索引擎返回的搜索結(jié)果氏身,一個(gè)好的搜索結(jié)果能給用戶帶來(lái)較好的搜索體驗(yàn)巍棱。那么如何衡量一個(gè)搜索結(jié)果的好壞呢?說(shuō)起來(lái)很簡(jiǎn)單蛋欣,在搜索結(jié)果中優(yōu)先展示與用戶搜索最相關(guān)的數(shù)據(jù)航徙。那么如何做呢?這些就是搜索排序需要做的工作陷虎。搜索排序是搜索服務(wù)中最為復(fù)雜的一個(gè)部分到踏。搜索排序邏輯的制定一般需要結(jié)合具體的業(yè)務(wù)場(chǎng)景甚至需要用戶的特征和行為偏好數(shù)據(jù)。以下結(jié)合一個(gè)搜索場(chǎng)景分別介紹下在Solr和ElasticSearch上的搜索排序?qū)嵺`尚猿。
場(chǎng)景描述
在資訊搜索場(chǎng)景中窝稿,假如我們對(duì)優(yōu)質(zhì)搜索結(jié)果定義滿足如下幾個(gè)標(biāo)準(zhǔn):
- 與用戶輸入搜索詞相關(guān)性越高排序越靠前
- 越新資訊排序越靠前
- 優(yōu)質(zhì)資訊排序靠前
- 用戶感興趣的資訊排序靠前
假設(shè)一份資訊文檔有三個(gè)主要字段,分別是標(biāo)題(title)凿掂、正文(content)伴榔、關(guān)鍵字(keywords)纹蝴。另外有發(fā)布時(shí)間(published_date)、是否加精(is_elite)踪少、是否推薦(is_recommend)塘安、資訊標(biāo)簽(topic_id)等其他字段。還有我們通過(guò)資訊的自身屬性(標(biāo)題正文長(zhǎng)度援奢、圖片個(gè)數(shù)兼犯、視頻時(shí)長(zhǎng)等)和用戶瀏覽行為(CTR、評(píng)論集漾、點(diǎn)贊切黔、收藏、分享等)數(shù)據(jù)計(jì)算資訊的質(zhì)量得分(item_score)具篇。
我們可以通過(guò)搜索詞與標(biāo)題绕娘、正文、關(guān)鍵字的匹配程度得到相關(guān)度栽连。通過(guò)發(fā)布時(shí)間來(lái)確定資訊的新舊险领。通過(guò)是否加精、是否推薦秒紧、質(zhì)量得分等數(shù)據(jù)來(lái)確定一篇資訊的優(yōu)劣绢陌。通過(guò)用戶歷史瀏覽行為獲得其感興趣的資訊標(biāo)簽,從而確認(rèn)用戶感興趣的資訊分類熔恢。然而這四個(gè)維度的信息如何整合才能得到我們想要的排序結(jié)果呢脐湾?
我們知道搜索引擎的對(duì)搜索結(jié)果的默認(rèn)排序邏輯是按相關(guān)度得分降序,這顯然不滿足我們的要求叙淌。另外也可以對(duì)某個(gè)字段做升降序操作(例如發(fā)布時(shí)間字段)秤掌,不過(guò)這種排序也不能滿足當(dāng)前場(chǎng)景,而且可能造成頂部數(shù)據(jù)與搜索詞相關(guān)度較低的現(xiàn)象鹰霍。那么如何實(shí)現(xiàn)這樣的排序邏輯呢闻鉴?下面通過(guò)兩個(gè)例子來(lái)講解下。
一個(gè)使用Solr edismax查詢的例子
edismax中將查詢字段和查詢?cè)~項(xiàng)分開(kāi)了茂洒,如我們?cè)跇?biāo)準(zhǔn)用法中使用name:周杰倫孟岛,那么在edismax中,需要在qf中設(shè)置查詢字段為name督勺,然后在q中輸入周杰倫渠羞。查詢?cè)~項(xiàng)會(huì)去所有的qf列(query field,可以設(shè)置多列智哀,也可以配置為每列配置權(quán)重)上進(jìn)行查詢次询,如果要針對(duì)每個(gè)字段有不同的查詢,如name:周杰倫,age:30瓷叫,那么需要將其中一個(gè)移入到fq中屯吊,但是注意送巡,fq中的查詢是不影響評(píng)分的。
edismax的查詢參數(shù)詳見(jiàn)Solr官方文檔介紹:https://lucene.apache.org/solr/guide/6_6/the-extended-dismax-query-parser.html
其中boost參數(shù)可以設(shè)置一個(gè)函數(shù)表達(dá)式雌芽,其表達(dá)式計(jì)算出來(lái)的得分會(huì)乘以主查詢的相似度得分作為edismax查詢的最終得分”嫠裕可用函數(shù)詳見(jiàn)Solr官方文檔介紹:https://wiki.apache.org/solr/FunctionQuery
查詢代碼如下:
<lst name="params">
<str name="q">寶寶長(zhǎng)牙</str>
<str name="defType">edismax</str>
<str name="qf">title^5 content^0.5 keywords^0.5</str>
<str name="fl">title,keywords,published_date,item_score</str>
<str name="pf">title content keywords</str>
<str name="boost">product(sum(item_score,map(is_elite,1,3,0.2),
map(is_recommend,1,1,0.5)),pow(recip(ms(NOW/HOUR,published_date),3.16e-11,1,1),10))</str>
<str name="wt">xml</str>
<str name="debugQuery">true</str>
</lst>
我們?cè)诓樵冎袑?duì)標(biāo)題加了5倍的權(quán)重世落,而又分別下降了正文和關(guān)鍵字一半的權(quán)重。因?yàn)樵谶@個(gè)場(chǎng)景中我們覺(jué)得標(biāo)題匹配的相關(guān)度更高糟需。
查詢結(jié)果如下:
<result name="response" numFound="741" start="0" maxScore="2.8703315">
<doc>
<str name="title">寶寶幾個(gè)月長(zhǎng)牙屉佳?寶寶長(zhǎng)牙的癥狀護(hù)理</str>
<!-- content字段太長(zhǎng) 未列出 -->
<arr name="keywords">
<str>長(zhǎng)牙</str>
<str>寶寶</str>
<str>乳牙</str>
</arr>
<date name="published_date">2018-05-11T13:18:11Z</date>
<float name="item_score">0.75903535</float></doc>
<doc>
<str name="title">寶寶幾個(gè)月才的會(huì)長(zhǎng)牙 寶寶長(zhǎng)牙的癥狀!</str>
<arr name="keywords">
<str>長(zhǎng)牙</str>
<str>寶寶</str>
<str>乳牙</str>
</arr>
<date name="published_date">2018-04-25T02:27:21Z</date>
<float name="item_score">0.7273869</float></doc>
<doc>
<str name="title">寶寶長(zhǎng)牙期間的一些小動(dòng)態(tài),寶媽一定要知道洲押,不然容易遭罪武花!</str>
<arr name="keywords">
<str>寶媽</str>
<str>長(zhǎng)牙</str>
<str>寶寶</str>
</arr>
<date name="published_date">2018-04-19T07:53:10Z</date>
<float name="item_score">0.8170003</float></doc>
<doc>
....
</result>
從結(jié)果可以看出發(fā)布時(shí)間越近的文章排序越靠前,質(zhì)量分越高的文章排序越靠前杈帐。我們分析下boost函數(shù)
product(sum(item_score,map(is_elite,1,3,0.2),map(is_recommend,1,1,0.5)),pow(recip(ms(NOW/HOUR,published_date),3.16e-11,1,1),10))
函數(shù)表達(dá)式就是質(zhì)量分加上推薦体箕、加精屬性再乘以發(fā)布時(shí)間的一個(gè)衰減函數(shù)。
recip(ms(NOW/HOUR,published_date),3.16e-11,1,1)
該函數(shù)時(shí)間的衰減比較平緩挑童,比如昨天的權(quán)重是0.999累铅,前天是0.998,一年前的今天是0.5...我們?cè)诒磉_(dá)式中對(duì)該函數(shù)做了10次方運(yùn)算來(lái)放大其衰減速度站叼。
在查詢debug模式中我們可以看出最終的文檔得分為表達(dá)式計(jì)算出來(lái)的得分會(huì)乘以主查詢的相似度得分娃兽。
1.7736654 = boost(+(title:"寶寶 長(zhǎng)牙"^5.0 | keywords:寶寶長(zhǎng)牙^0.5 | content:"寶寶 長(zhǎng)牙"^0.5) (title:"寶寶 長(zhǎng)牙" | content:"寶寶 長(zhǎng)牙"),product(sum(float(item_score),map(int(is_elite),1.0,3.0,const(0.2)),map(int(is_recommend),1.0,1.0,const(0.5))),pow(1.0/(3.16E-11*float(ms(const(1526634000000),date(published_date)))+1.0),const(10))))
edismax算出的最終得分在某些應(yīng)用場(chǎng)景下還是比較尷尬的。例如在一些比較看重文本相關(guān)度的搜索場(chǎng)景中尽楔。由于用乘積的方式計(jì)算出來(lái)的最終得分受boost得分影響比較大投储,可能會(huì)出現(xiàn)文本相關(guān)度較高的結(jié)果排名比較靠后的情況。相對(duì)于Solr的edismax查詢阔馋,ElasticSearch提供了更加靈活的相關(guān)性打分機(jī)制玛荞。
一個(gè)使用ElasticSearch function_score查詢的例子
function_score是用于處理文檔分值的DSL,它會(huì)在查詢結(jié)束后對(duì)每一個(gè)匹配的文檔進(jìn)行一系列的重打分操作呕寝,最后以生成的最終分?jǐn)?shù)進(jìn)行排序冲泥。它提供了多種計(jì)算分值的函數(shù):
- script_score:自定義腳本計(jì)算
- weight:權(quán)重計(jì)算
- field_value_factor:根據(jù)某個(gè)字段計(jì)算
- random_score:隨機(jī)計(jì)算
- decay functions:衰減函數(shù),支持gauss, linear, exp等類型
其最終得分計(jì)算也相對(duì)豐富壁涎,通過(guò)boost_mode可以指定計(jì)算后的分?jǐn)?shù)與原始的_score如何合并凡恍,有以下選項(xiàng):
- multiply:將結(jié)果乘以_score
- sum:將結(jié)果加上_score
- min:取結(jié)果與_score的較小值
- max:取結(jié)果與_score的較大值
- replace:使結(jié)果替換掉_score
- avg:取結(jié)果與_score的平均值
下面還是寶寶長(zhǎng)牙這個(gè)搜索為例來(lái)想想描述下ElasticSearch的function score查詢,其索引文檔結(jié)構(gòu)與上例一致怔球。查詢代碼如下嚼酝,使用ElasticSearch的DSL查詢語(yǔ)法:
{
"explain":false,
"_source":["item_id","item_type","type","title","item_score","published_date","topic_id","keywords","is_recommend","is_elite"],
"query": {
"function_score": {
"query": {
"bool":{
"must":{
"multi_match":{
"query":"寶寶長(zhǎng)牙",
"type":"cross_fields",
"fields":["title","title_standard","title_pinyin","content","keywords"],
"minimum_should_match": "100%" // should查詢最小匹配值
}
},
"filter":[
{
"term":{
"status":"1" // 過(guò)濾條件,限定有效數(shù)據(jù)
}
}
]
}
},
"functions":[
{
"weight":"10",
"filter":{
"match":{
"topic_id":{
"query":"42" // 權(quán)重計(jì)算竟坛,這里對(duì)標(biāo)簽是嬰兒期的查詢結(jié)果加10分闽巩。假設(shè)搜索用戶是一個(gè)辣媽
}
}
}
},
{
"gauss": {
"published_date": {
"origin": "now",
"scale": "7d",
"decay": "0.5" // 衰減函數(shù) 對(duì)日期做高斯衰減钧舌,提高新數(shù)據(jù)得分
}
}
},
{
"field_value_factor": {
"field": "item_score",
"modifier": "sqrt",
"factor": 100 // 根據(jù)質(zhì)量得分計(jì)算 函數(shù)為 sqrt(100 * item_score)
}
},
{
"script_score": {
"script": "return 1 + doc['is_recommend'].value * 1 + doc['is_elite'].value * 0.5" // 自定義腳本函數(shù)
}
}
],
"score_mode": "multiply",
"boost_mode":"avg" // 最終結(jié)果得分計(jì)算方式 這里采用avg
}
},
"from":"0",
"size":"10"
}
查詢中使用的minimum_should_match屬性詳見(jiàn)ES官方文檔:minimum-should-match
查詢結(jié)果如下:
"hits": [
{
"_index": "news",
"_type": "item",
"_id": "20009070_1",
"_score": 39.12396,
"_source": {
"keywords": [
"長(zhǎng)牙",
"寶寶",
"乳牙"
],
"item_id": 20009070,
"item_type": 2,
"is_recommend": 0,
"is_elite": 0,
"item_score": 0.2600713,
"topic_id": [
42
],
"title": "寶寶幾個(gè)月才的會(huì)長(zhǎng)牙 寶寶長(zhǎng)牙的癥狀!",
"type": 1,
"published_date": 1524594441000
}
},
{
"_index": "news",
"_type": "item",
"_id": "19589591_1",
"_score": 37.407104,
"_source": {
"keywords": [
"出牙",
"長(zhǎng)牙",
"寶寶"
],
"item_id": 19589591,
"item_type": 2,
"is_recommend": 0,
"is_elite": 0,
"item_score": 0.7617033,
"topic_id": [
42
],
"title": "寶寶幾個(gè)月長(zhǎng)牙?長(zhǎng)牙有什么癥狀以及長(zhǎng)牙晚怎么辦涎跨?",
"type": 1,
"published_date": 1523958754000
}
}
...
]
ElasticSearch 還提供另一種重打分機(jī)制rescore洼冻。該打分機(jī)制對(duì)query和post_filter階段返回的頂部文檔(TOP N)使用更復(fù)雜的算法進(jìn)行重排打分,而不需要計(jì)算所有的文檔隅很∽怖危可以較大的提高查詢性能和精準(zhǔn)度。詳見(jiàn)ES官方文檔Rescore
本文簡(jiǎn)單的介紹了下在資訊搜索場(chǎng)景下叔营,Solr屋彪、ElasticSearch兩個(gè)主流搜索引擎服務(wù)的相關(guān)度排序與重排調(diào)整的一些機(jī)制,比較粗淺绒尊。大家若想深入了解畜挥,推薦閱讀Solr和ElasticSearch上的官方文檔。另外對(duì)于一個(gè)好的搜索服務(wù)來(lái)說(shuō)婴谱,如何能更好收集用戶行為蟹但、特征和偏好,達(dá)到了解您的用戶谭羔,從而針對(duì)不同的用戶做個(gè)性化的搜索也是至關(guān)重要的矮湘。本文也只是簡(jiǎn)單提了下。對(duì)于這方面大家有興趣的話可以去了解一下下面這兩篇博文口糕,講的是目前業(yè)界比較流行的Learning To Rank缅阳。
LTR(Learning To Rank)在個(gè)性化電商搜索領(lǐng)域的應(yīng)用
在Elasticsearch中應(yīng)用機(jī)器學(xué)習(xí)排序LTR