Lucene通過計算文檔的得分來確定查詢結果文檔的相似度。如果你希望通過干預Lucene查詢來改變查詢結果的排序,你就需要對Lucene的得分計算有所理解。Lucene得分計算公式如下所示:
score(q,d) = coord(q,d)·queryNorm(q)·∑( tf(t in d)·idf(t)^2·t.getBoost()·norm(t,d) )
其中,t in q。
下面詳細解釋公式乘積的每個因子的含義,以及是如何計算的,這樣能夠加深對Lucene得分計算的理解,才能在實際應用中根據需要調整各個參數,從而制定滿足應用的排序策略。
coord(q,d)因子
coord(q,d)能夠影響到檢索結果文檔的得分,它的計算公式如下:
coord(q,d) = overlap / maxOverlap
例如,查詢關鍵詞中經過解析(QueryParser)處理,得到兩個Term分別爲title:search和content:lucene,那麼對應於公式中有,maxOverlap=2。
例如,對於下面這個索引的Document,代碼如下:
Document doc = new Document();
doc.add(new Field("title", "search engine", Field.Store.YES, Field.Index.ANALYZED));
doc.add(new Field("content", "good lucene luke lucene search server", Field.Store.YES, Field.Index.ANALYZED));
indexWriter.addDocument(doc);
我們看到,檢索title:search和content:lucene這兩個Term,在title這個Field中("search engine")匹配上title:search,
在content這個Field中("good lucene luke lucene search server")匹配上了content:lucene,所以對應於公式中有,overlap=2,所以coord(q,d)=2/2=1。
queryNorm(q)因子
queryNorm(q)是查詢權重對得分的影響,它的計算公式如下:
queryNorm(q) = queryNorm(sumOfSquaredWeights)=1/(sumOfSquaredWeights^(1/2))
sumOfSquaredWeights
繼續看一下,在BooleanQuery中sumOfSquaredWeights的計算:
sumOfSquaredWeights = q.getBoost()^2·∑( idf(t)·t.getBoost() )^2
因爲這是在計算查詢的權重,所以上式求和部分中出現的t都是在q裏面出現的Term(t in q)。
q.getBoost()
上式中q.getBoost()是一個查詢子句被賦予的boost值,因爲Lucene中任何一個Query對象是可以通過setBoost(boost)方法設置一個boost值的,下面我們通過一個相對比較複雜的例子來說明一下:
BooleanQuery bq1 = new BooleanQuery(); // 第一個BooleanQuery查詢子句
TermQuery tq1 = new TermQuery(new Term("title", "search"));
tq1.setBoost(2.0f);
bq1.add(tq1, Occur.MUST);
TermQuery tq2 = new TermQuery(new Term("content", "lucene"));
tq2.setBoost(5.0f);
bq1.add(tq2, Occur.MUST);
bq1.setBoost(0.1f); // 給第一個查詢子句乘上0.1,實際是減弱了其貢獻得分的重要性
BooleanQuery bq2 = new BooleanQuery(); // 第二個BooleanQuery查詢子句
TermQuery tq3 = new TermQuery(new Term("title", "book"));
tq3.setBoost(8.0f);
bq2.add(tq3, Occur.MUST);
TermQuery tq4 = new TermQuery(new Term("content", "lucene"));
tq4.setBoost(5.0f);
bq2.add(tq4, Occur.MUST);
bq2.setBoost(10.0f); // 給第二個查詢子句乘上10.0,該子句更重要
BooleanQuery bq = new BooleanQuery(); // 對上述兩個BooleanQuery查詢子句再進行OR運算
bq.add(bq1, Occur.SHOULD);
bq.add(bq2, Occur.SHOULD);
上述代碼可以這樣理解:“我想要查詢包含Lucene的文章,但標題最好是含有book的”,也就是說“我想查找介紹Lucene的書籍,如果沒有沒有關於Lucene的書籍,包含介紹Lucene查詢search的文章也可以”。
所以上述兩個布爾查詢子句設置的boost值(0.1<<10.0),就對應於我們上述公式中的q.getBoost()。
idf(t)
idf(t)就是反轉文檔頻率,含義是如果文檔中出現Term的頻率越高顯得文檔越不重要,Lucene中計算該值的公式如下:
idf(t) = 1.0 + log(numDocs/(docFreq+1))
其中,numDocs表示索引中文檔的總數,docFreq表示查詢中Term在多個文檔中出現。
t.getBoost()
t.getBoost()表示查詢中的Term給予的boost值,例如上面代碼中:
TermQuery tq3 = new TermQuery(new Term("title", "book"));
tq3.setBoost(8.0f);
title中包含book的Term,對匹配上的文檔,通過上面公式計算,乘上t.getBoost()的值。
∑( tf(t in d)·idf(t)^2·t.getBoost()·norm(t,d) )因子
上面t還是在q中出現的Term即t in q。
norm(t,d)
這裏,解釋一下norm(t,d)的含義,計算公式如下所示:
norm(t,d) = doc.getBoost()· lengthNorm· ∏ f.getBoost()
norm(t,d)是在索引時(index-time)進行計算並存儲的,在查詢時(search-time)是無法再改變的,除非再重建索引。另外,Lucene在索引時存儲norm值,而且是被壓縮存儲的,在查詢時取出該值進行文檔相關度計算,即文檔得分計算。
需要注意的是,norm在進行codec的過程中,是有精度損失的,即不能保證decode(encode(x)) = x永遠成立,例如 decode(encode(0.89)) = 0.75。
如果你在相關度調優過程中,發現norm的值破壞了文檔相關性,嚴重的話,可以通過Field.setOmitNorms(true)方法來禁用norm,同時減少了該norm的存儲開銷,在一定程度上加快了查詢過程中文檔得分的計算。是否使用norm,需要根據你的應用來決定,例如,如果一個Field只存儲一個Term,或者Field很短(包含的Term很少),一般是不需要存儲norm的。
doc.getBoost()
這個就是Document的boost值,在索引的時候可以通過setBoost(boost)方法設置,例如我們一般認爲title會比content更重要,所以在索引時可以對title進行boost(大於1.0)。
lengthNorm
lengthNorm是一個與Field長度(包含Term數量)有關的因子,Lucene中計算公式如下:
lengthNorm = 1.0 / Math.sqrt(numTerms)
其中,numTerms表示一個Field中Term的數量。
一般來說,一個Term在越短的Field中出現,表示該Term更重要,有點類似idf的含義。
∏ f.getBoost()
Lucene索引時,一個Document實例中,可以多次添加具有同一個Field名稱的Field對象,但是值不相同,如下代碼:
Document doc = new Document();
doc.add(new Field("title", "search engine", Field.Store.YES, Field.Index.ANALYZED));
Field fcontent1 = new Field("content", "nutch solr lucene lucene search server", Field.Store.YES, Field.Index.ANALYZED);
fcontent1.setBoost(2.0f);
doc.add(fcontent1);
Field fcontent2 = new Field("content", "good lucene luke lucene index server", Field.Store.YES, Field.Index.ANALYZED);
fcontent2.setBoost(5.0f);
doc.add(fcontent2);
indexWriter.addDocument(doc);
我們在doc裏面添加了同名content的兩個字符串,對與這種情況,在計算得分的時候,是通過 ∏ f.getBoost()連乘積來計算得到的。
例如,我們查詢content:lucene,上面Document doc中兩個content的Field都匹配上了,在計算的時候有: ∏ f.getBoost() = 2.0 * 5.0 = 10.0。如果查詢content:solr,則只有一個Field匹配上了,則 ∏ f.getBoost()=2.0。
參考內容
http://lucene.apache.org/java/3_1_0/api/core/org/apache/lucene/search/Similarity.html