在進行Lucene的搜索過程解析之前,有必要單獨的一張把Lucene score公式的推導,各部分的意義闡述一下。因為Lucene的搜索過程,很重要的一個步驟就是逐步的計算各部分的分數。

Lucene的打分公式非常復雜,如下:

 

 

在推導之前,先逐個介紹每部分的意義:

  • t:Term,這里的Term是指包含域信息的Term,也即title:hello和content:hello是不同的Term
  • coord(q,d):一次搜索可能包含多個搜索詞,而一篇文檔中也可能包含多個搜索詞,此項表示,當一篇文檔中包含的搜索詞越多,則此文檔則打分越高。
  • queryNorm(q):計算每個查詢條目的方差和,此值并不影響排序,而僅僅使得不同的query之間的分數可以比較。其公式如下:
  • tf(t in d):Term t在文檔d中出現的詞頻
  • idf(t):Term t在幾篇文檔中出現過
  • norm(t, d):標準化因子,它包括三個參數:
    • Document boost:此值越大,說明此文檔越重要。
    • Field boost:此域越大,說明此域越重要。
    • lengthNorm(field) = (1.0 / Math.sqrt(numTerms)):一個域中包含的Term總數越多,也即文檔越長,此值越小,文檔越短,此值越大。

 

 

  • 各類Boost值
    • t.getBoost():查詢語句中每個詞的權重,可以在查詢中設定某個詞更加重要,common^4 hello
    • d.getBoost():文檔權重,在索引階段寫入nrm文件,表明某些文檔比其他文檔更重要。
    • f.getBoost():域的權重,在索引階段寫入nrm文件,表明某些域比其他的域更重要。

以上在Lucene的文檔中已經詳細提到,并在很多文章中也被闡述過,如何調整上面的各部分,以影響文檔的打分,請參考有關Lucene的問題(4):影響Lucene對文檔打分的四種方式一文。

然而上面各部分為什么要這樣計算在一起呢?這么復雜的公式是怎么得出來的呢?下面我們來推導。

首先,將以上各部分代入score(q, d)公式,將得到一個非常復雜的公式,讓我們忽略所有的boost,因為這些屬于人為的調整,也省略coord,這和公式所要表達的原理無關。得到下面的公式:

 

然后,有Lucene學習總結之一:全文檢索的基本原理中的描述我們知道,Lucene的打分機制是采用向量空間模型的:

我們把文檔看作一系列詞(Term),每一個詞(Term)都有一個權重(Term weight),不同的詞(Term)根據自己在文檔中的權重來影響文檔相關性的打分計算。

于是我們把所有此文檔中詞(term)的權重(term weight) 看作一個向量。

Document = {term1, term2, …… ,term N}

Document Vector = {weight1, weight2, …… ,weight N}

同樣我們把查詢語句看作一個簡單的文檔,也用向量來表示。

Query = {term1, term 2, …… , term N}

Query Vector = {weight1, weight2, …… , weight N}

我們把所有搜索出的文檔向量及查詢向量放到一個N維空間中,每個詞(term)是一維。

 

我們認為兩個向量之間的夾角越小,相關性越大。

所以我們計算夾角的余弦值作為相關性的打分,夾角越小,余弦值越大,打分越高,相關性越大。

余弦公式如下:

 

下面我們假設:

查詢向量為Vq = <w(t1, q), w(t2, q), ……, w(tn, q)>

文檔向量為Vd = <w(t1, d), w(t2, d), ……, w(tn, d)>

向量空間維數為n,是查詢語句和文檔的并集的長度,當某個Term不在查詢語句中出現的時候,w(t, q)為零,當某個Term不在文檔中出現的時候,w(t, d)為零。

w代表weight,計算公式一般為tf*idf。

我們首先計算余弦公式的分子部分,也即兩個向量的點積:

Vq*Vd = w(t1, q)*w(t1, d) + w(t2, q)*w(t2, d) + …… + w(tn ,q)*w(tn, d)

把w的公式代入,則為

Vq*Vd = tf(t1, q)*idf(t1, q)*tf(t1, d)*idf(t1, d) + tf(t2, q)*idf(t2, q)*tf(t2, d)*idf(t2, d) + …… + tf(tn ,q)*idf(tn, q)*tf(tn, d)*idf(tn, d)

在這里有三點需要指出:

  • 由于是點積,則此處的t1, t2, ……, tn只有查詢語句和文檔的并集有非零值,只在查詢語句出現的或只在文檔中出現的Term的項的值為零。
  • 在查詢的時候,很少有人會在查詢語句中輸入同樣的詞,因而可以假設tf(t, q)都為1
  • idf是指Term在多少篇文檔中出現過,其中也包括查詢語句這篇小文檔,因而idf(t, q)和idf(t, d)其實是一樣的,是索引中的文檔總數加一,當索引中的文檔總數足夠大的時候,查詢語句這篇小文檔可以忽略,因而可以假設idf(t, q) = idf(t, d) = idf(t)

基于上述三點,點積公式為:

Vq*Vd = tf(t1, d) * idf(t1) * idf(t1) + tf(t2, d) * idf(t2) * idf(t2) + …… + tf(tn, d) * idf(tn) * idf(tn)

所以余弦公式變為:

 

下面要推導的就是查詢語句的長度了。

由上面的討論,查詢語句中tf都為1,idf都忽略查詢語句這篇小文檔,得到如下公式

 

所以余弦公式變為:

 

下面推導的就是文檔的長度了,本來文檔長度的公式應該如下:

 

這里需要討論的是,為什么在打分過程中,需要除以文檔的長度呢?

因為在索引中,不同的文檔長度不一樣,很顯然,對于任意一個term,在長的文檔中的tf要大的多,因而分數也越高,這樣對小的文檔不公平,舉一個 極端的例子,在一篇1000萬個詞的鴻篇巨著中,"lucene"這個詞出現了11次,而在一篇12個詞的短小文檔中,"lucene"這個詞出現了10 次,如果不考慮長度在內,當然鴻篇巨著應該分數更高,然而顯然這篇小文檔才是真正關注"lucene"的。

然而如果按照標準的余弦計算公式,完全消除文檔長度的影響,則又對長文檔不公平(畢竟它是包含了更多的信息),偏向于首先返回短小的文檔的,這樣在實際應用中使得搜索結果很難看。

所以在Lucene中,Similarity的lengthNorm接口是開放出來,用戶可以根據自己應用的需要,改寫lengthNorm的計算 公式。比如我想做一個經濟學論文的搜索系統,經過一定時間的調研,發現大多數的經濟學論文的長度在8000到10000詞,因而lengthNorm的公 式應該是一個倒拋物線型的,8000到 10000詞的論文分數最高,更短或更長的分數都應該偏低,方能夠返回給用戶最好的數據。

在默認狀況下,Lucene采用DefaultSimilarity,認為在計算文檔的向量長度的時候,每個Term的權重就不再考慮在內了,而是全部為一。

而從Term的定義我們可以知道,Term是包含域信息的,也即title:hello和content:hello是不同的Term,也即一個Term只可能在文檔中的一個域中出現。

所以文檔長度的公式為:

 

代入余弦公式:

 

再加上各種boost和coord,則可得出Lucene的打分計算公式。