垂直搜索結果的優化包括對搜索結果的控制和排序優化兩方面,其中排序又是重中之重。本文將全面深刻探討垂直搜索的排序模型的演化過程,最後推導出BM25模型的排序。而後將演示如何修改lucene的排序源代碼,下一篇將深刻解讀目前比較火熱的機器學習排序在垂直搜索中的應用。本文的結構以下:java
1、VSM模型簡單介紹;程序員
2、lucene默認的評分公式介紹;算法
3、機率語言模型中的二元獨立模型BIM介紹;apache
4、BM25介紹;編程
5、lucene中的edismax解析器介紹以及評分公式源代碼介紹;數組
6、修改排序源代碼;微信
7、機器學習排序:①爲何須要機器學習排序②機器學習排序相關算法介紹③關於ListNet算法的英語原版學術論文的解讀④機器學習排序實施思路網絡
寫這篇文章,花費了很大的精力,一部分是對原有的經驗和技術的總結,另外一方面又必然會涉及到改進和新技術的探索。任何開源框架都不是最完美的,以前探索了對lucene內部boolean查詢AND邏輯實現的算法的改進,做爲一個優秀的開源框架,lucene有不少閃光的地方值得借鑑,好比優先級隊列的設計。lucene在搜索排序方面的設計思想是最傑出的一個閃光點,所以有必要從理論層面進行全面探討,而後看看lucene是如何進行簡化的。由於在工程實際應用中,尤爲是設計一款優秀的開源框架或者是企業級的應用級軟件,必定會在準確度和時間複雜度上折中處理。好比,BM25排序lucene就進行了簡化。目前,在業內,機器學習排序的關於listwise方法很火熱,早在2007年,微軟研究院就研究出了listnet方法,用神經網絡構造luce機率模型,運用交叉熵構造損失函數,採用SGD做爲優化方法。可是,截止到2014以前,貌似Google也沒有采用機器學習排序方法。也就是說,每一個公司,只有探索出適合本身的算法,纔是最優化的。脫離了應用場景,算法就變得毫無心義了,即便在理論上是最優的,實際狀況有多是很拙劣的。就技術創新而言,70%以上來源於對原有技術的整合,可是整合不等同於抄襲或者簡單的拼湊。好比listnet機器學習排序方法,在原有算法(pairwise)上提出改進。任何算法都不是憑空產生的,數學模型絕大部分來源於觀察,總結概括,演繹推理,遷移,改進,這種思惟方法的培養,遠遠勝於知識量的積累,就像程序員想提高編程水平,僅僅靠代碼量的堆積,是拙劣的,不可行的。本文在提出修改lucene底層排序源代碼以前,也是認真研讀了不少經典著做,而且詳細分析了lucene源代碼的實現,結合具體的業務需求,通過屢次測試和改進纔有所進展。冰凍三尺,非一日之寒,在程序員的道路上,只有多讀經典,多實踐,多思考,勇於提出本身的想法,本着大膽假設,當心求證的原則,不斷試錯和探索,纔會取得一點兒成就。多線程
信息論在信息檢索中發揮了重要的做用,以前有人問我,若是在搜索框中輸入蘋果,你如何判斷用戶想要的是蘋果手機仍是蘋果電腦,仍是蘋果水果自己?我相信,在搜索領域,應該不止一我的會提出這樣的問題。很遺憾的是,若是你把它看成研究方向,我只能說你應該重修《信息論》這門課程。牛頓在年輕時曾經對永動機很是瘋狂,後來認識到了是個僞命題,及時收手了。犯錯誤並不可怕,可怕的是認識不到這是個錯誤方向。按照信息論,信息檢索的本質是不斷減小信息不肯定的過程,也就是減小信息熵的過程。蘋果的信息熵很大,也就是不肯定特別大,信息檢索的目標是減小這個不肯定性,方法是增長特徵信息。在進入搜索系統以前,能夠增長一些分類信息,而後在排序過程當中,能夠考慮增長一些有用的因素,好比pagerank,point等等。這些手段都是爲了一個共同的目標:減小信息的不肯定性。若是方向搞錯了,即便搞出一個算法來,效果也不會太好,不會具備普適性。在第三代搜索系統的研發中,目前百度已經走在了前列,度祕機器人v3.0版本跟以前相比,有了很大的提高。以以前提出的問題爲例,若是單獨對度祕說出蘋果,她很難知道用戶的需求,可是若是你對她說:"我想要蘋果"和"我想吃蘋果",這下度祕就知道了用戶的準確需求了。很顯然,吃蘋果中的蘋果是水果,若是什麼特徵信息都沒有,再智能的機器人也沒法判斷。也許有人會說,我能夠在進入搜索系統以前,分詞以後,挖掘用戶(id)的歷史記錄,若是以前買水果的概率比較大,就判斷爲水果。這種方法毫無心義,無異於猜謎。第三代智能化的搜索,主要體如今個性化,可以理解部分人的意圖和情感,個性化的推薦,人機智能問答等等。RNN(遞歸神經網絡)將發揮重要做用,包括機器翻譯。。。其中,消除歧義分詞,語義分析是重中之重。好比吃蘋果,分詞結果是吃/蘋果,蘋果的語義標註有不少,例如水果,手機,電腦,logo等等。基於CRF和viterbi算法,能夠預測出這句話中的蘋果語義是水果,這樣在搜索時,就能夠構造出搜索詞:蘋果水果的分類,下降了不肯定性。中文分詞是nlp的基礎,而信息檢索又離不開NLP。在目前國內的聊天機器人中,度祕是最優秀的,小黃雞等還差的很遠。在學習RNN等深度學習技術以前,必定要把基礎性的知識學好,不可好高騖遠。下面進入到第一部分:app
第一部分:VSM
VSM簡稱向量空間模型,主要用於計算文檔的類似度。計算文檔類似度時,須要提取重要特徵。特徵提取通常用最通用常規的方法:TF-IDF算法。這個方法很是簡單可是卻很是實用。給你一篇文章,用中文分詞工具(目前最好的是opennlp社區中的開源源碼包HanLP)對文檔進行切分,處理成詞向量(去除停詞後的結果),而後計算TF-IDF,按降序排列,排在前幾位的就是重要特徵。這裏不論述TF-IDF,由於太簡單了。那麼對於一個查詢q來講,通過分詞處理後造成查詢向量T[t1,t2……],給每一個t賦予權重值,假設總共查詢到n個文檔,把每一個文檔處理成向量(按t處理),計算每一個t在各自文檔中的TF-IDF。而後分別計算與T向量的餘弦類似度,得出的分數按降序排列。
VSM的本質是:計算查詢和文檔內容的類似度。沒有考慮到相關性。由於用戶輸入一個查詢,最想獲得的是相關度大的文檔,而不僅是這個文檔中出現了查詢詞。由於某篇文檔出現了查詢詞,也不必定是相關性的,因此須要引入機率模型。後面要講的BIM還有BM25本質是:計算查詢和用戶需求的類似度。因此BM25會有很好的表現。而lucen底層默認的評分擴展了VSM。下面進入第二部分,lucent的默認評分公式:
2、lucene默認的評分公式介紹
Lucene 評分體系/機制(lucene scoring)是 Lucene 出名的一核心部分。它對用戶來講隱藏了不少複雜的細節,導致用戶能夠簡單地使用 lucene。但我的以爲:若是要根據本身的應用調節評分(或結構排序),十分有必須深刻了解 lucene 的評分機制。
Lucene scoring 組合使用了 信息檢索的向量空間模型 和 布爾模型 。
首先來看下 lucene 的評分公式(在 Similarity 類裏的說明)
|
其中:
tf(t in d) = | frequency½ |
idf(t) = | 1 + log ( |
|
) |
queryNorm(q) = queryNorm(sumOfSquaredWeights) = |
|
每一個查詢項權重的平分方和(sumOfSquaredWeights)由 Weight 類完成。例如 BooleanQuery 地計算:
sumOfSquaredWeights = q.getBoost() 2 · | ∑ | ( idf(t) · t.getBoost() ) 2 |
|
t in q |
norm(t,d) = doc.getBoost() · lengthNorm(field) · | ∏ | f.getBoost() |
|
field f in d named as t |
|
索引的時候,把 norm 值壓縮(encode)成一個 byte 保存在索引中。搜索的時候再把索引中 norm 值解壓(decode)成一個 float 值,這個 encode/decode 由 Similarity 提供。官方說:這個過程因爲精度問題,以致不是可逆的,如:decode(encode(0.89)) = 0.75。
整體來講,這個評分公式仍然是基於查詢與文檔內容的類似度計算分數。並且,lengthNorm(field) = 1/sqrt(numTerms),即文檔的索引列越長,分值越低。這個顯然是不合理的,須要改進。並且這個評分公式僅僅考慮了查詢詞在文檔向量中的TF,並無考慮在T(查詢向量)中的TF,並且,若是一篇文檔越長,它的TF通常會越高,會成必定的正相關性。這對於短文檔來講計算TF是不公平的。在用這個公式打分的時候,須要對文檔向量歸一化處理,其中的lengthNorm如何處理是個問題。舉個例子,在用球拍打羽毛球的時候,球拍會有一個最佳擊球和回球的區域,被成爲"甜區"。在處理文檔向量的長度時候,咱們一樣能夠規定一個"甜區",好比min/max,超過這個範圍的,lengthNorm設置爲1。基於以上缺點,須要改進排序模型,讓查詢和用戶的需求更加相關,因此提出了機率模型,下面進入第三部分:
3、機率語言模型中的二元獨立模型BIM介紹
機率檢索模型是從機率排序原理推導出來的,因此理解這一原理對於理解機率檢索模型很是重要。機率排序模型的思想是:給定一個查詢,返回的文檔可以按照查詢和用戶需求的相關性得分高低排序。這是一種對用戶需求相關性建模的方法。按照以下思路進行思考:首先,咱們能夠對查詢後獲得的文檔進行分類:相關文檔和非相關文檔。這個能夠按照樸素貝葉斯的生成學習模型進行考慮。若是這個文檔屬於相關性的機率大於非相關性的,那麼它就是相關性文檔,反之屬於非相關性文檔。因此,引入機率模型:P(R|D)是一個文檔相關性的機率,P(NR|D)是一個文檔非相關性的機率。若是P(R|D) > P(NR|D),說明它與查詢相關,是用戶想要的。按照這個思路繼續,怎樣才能計算這個機率呢?若是你熟悉樸素貝葉斯的話,就容易了。P(R|D) = P(D|R)P(R)/P(D),P(NR|D) = P(D|NR)P(NR)/P(D)。用機率模型計算相關性的目的就是判斷一個文檔是否P(R|D) > P(NR|D),即P(D|R)P(R)/P(D) > P(D|NR)P(NR)/P(D) <=> P(D|R)P(R) > P(D|NR)P(NR) <=> P(D|R)/P(D|NR) > P(NR)/P(R)。對於搜索來講,並不須要真的進行分類,只需計算P(D|R)/P(D|NR)而後按降序排列便可。因而引入二元獨立模型(Binary Independent Model) 假設=>
①二元假設:在對文檔向量進行數據建模時,假設特徵的值屬於Bernoulli分佈,其值爲0或者1(樸素貝葉斯就適用於特整值和分類值都屬於Bernoulli分佈的狀況,而loggistic Regression適用於分類值爲Bernoulli分佈)。在文本處理領域,就是這個特徵在文檔中出現或者不出現,不考慮詞頻。
②詞彙獨立性假設:假設構成每一個特徵的詞是相互獨立的,不存在關聯性。在機器學習領域裏,進行聯合似然估計或者條件似然估計時,都是假設數據遵循iid分佈。事實上,詞彙獨立假設是很是不合理的。好比"喬布斯"和"ipad"和"蘋果"是存在關聯的。
有了上面的假設,就能夠計算機率了。好比,有一篇文檔D,查詢向量由5個Term組成,在D中的分佈狀況以下:[1,0,1,01]。那麼,P(D|R) = P1*(1-P2)*P3*(1-P4)*P5。Pi爲特徵在D中出現的機率,第二個和第四個詞彙沒有出現,因此用(1-P2)和(1-P4)。這是文檔屬於相關性的機率,生成模型還須要計算非相關性的機率狀況。用Si表示特徵在非相關性文檔中出現的機率,那麼P(D|NR)=S1*(1-S2)*S3*(1-S4)*S5。=>
,這個公式中第一項表明在D中出現的各個特徵機率乘積,第二項表示沒有在D中出現的機率乘積。進一步變換獲得:
這個公式裏,第一部分是文檔裏出現的特徵機率乘積,第二項是全部特徵的機率乘積,是從全局計算得出。對於特定的文檔,第二項對排序沒有影響,計算結果都是同樣的,因此去掉。因而,得出最終結果:。爲了計算方便,對這個公式取對數:
。進一步求解這個公式:
,
。其中,N表示文檔集合總數,R表示相關文檔總數,那麼N-R就是非相關文檔數目,ni表示包含特徵di的文檔數目,在這其中屬於相關文檔的數目是ri。因而,
。當出現一個查詢q和返回文檔時,只需計算出現的特徵的機率乘積,和樸素貝葉斯的predict原理是同樣的。這個公式,在特定狀況下能夠轉化爲IDF模型。上述公式就是BM25模型的基礎。下面來說述第四部分。
4、BM25模型
BIM模型基於二元獨立假設推導出,只考慮特徵是否出現,不考慮TF因素。那麼,若是在這個基礎之上再考慮Tf因素的話,會更加完美,因而,有人提出了BM25模型。加入了詞彙再查詢向量中的權值以及在文檔中的權值還有一系列經驗因子。公式以下:
第一項就是BIM模型推導出的公式,由於在搜索的時候,咱們不知道哪些是相關的哪些不是相關的,因此把ri和R設置爲0,因而,第一項退化成了
就是IDF!很是神奇!,fi是特徵在文檔D中的TF權值,qfi是特徵在查詢向量中的TF權值。通常狀況下,k1=1.2,b=0.75,k2=200.當查詢向量比較短的時候,qfi一般取值爲1。分析來看,當K1=0時,第二項不起做用,也就是不考慮特徵在文檔中的TF權值,當k2=0時,第三項也失效。從中能夠看出,k1和k2值是對特徵在文檔或者查詢向量中TF權值的懲罰因子。綜合來看,BM25考慮了4個因素:IDF因子,文檔長度因子,文檔詞頻因子和查詢詞頻因子。lucene內部的BM25要比上面公式的簡單一些,我的認爲並非很好。其實lucene內部有不少的算法並非最優的,有待提高!有了以上4個部分,相信大部分人會對lucene的評分公式有了很深刻的瞭解,下面進入源代碼解讀和修改階段,主要是爲了可以知足根據時間業務場景自定義排序。進入第五部分:
5、edismax解析器介紹:
之因此介紹這個查詢解析器,是由於特殊的業務場景須要。lucene的源碼包中,兩大核心包,org.apache.lucene.index和org.apache.lucene.search。其中第一個包會調用store、util和document子包,第二個會和queryParser和analysis、message子包交互。在查詢中,最重要的就是queryParser。當用戶輸入查詢字符串後,調用lucene的查詢服務,要調用QueryParser類,第一步是調用analyzer(分詞)造成查詢向量T[t1,t2……tn],這一步是詞法分析,接下來是句法分析,造成查詢語法,即先造成QueryNode--->QueryTree .t1和t2之間是邏輯與的關係,用Boolean查詢。這樣lucene就能理解查詢語法了。爲了加深理解,先看一段代碼:
package com.txq.lucene.queryParser;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.util.Version;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.index.Term;
/**
* 自定義一個查詢解析器,BooleanQuery
* @author XueQiang Tong
*
*/
public class BlankAndQueryParser extends QueryParser {
// analyzer = new IKAnalyzer(false);
public BlankAndQueryParser(Version matchVersion, String field, Analyzer analyzer) {
super(matchVersion, field, analyzer);
}
protected Query getFieldQuery(String field,String queryText,int slop) throws ParseException{
try {
TokenStream ts = this.getAnalyzer().tokenStream(field, new StringReader(queryText));
OffsetAttribute offset = (OffsetAttribute) ts.addAttribute(OffsetAttribute.class);
CharTermAttribute term = (CharTermAttribute) ts.addAttribute(CharTermAttribute.class);
TypeAttribute type = (TypeAttribute) ts.addAttribute(TypeAttribute.class);
ts.reset();
ArrayList<CharTermAttribute> v = new ArrayList<CharTermAttribute>();
while (ts.incrementToken()) {
// System.out.println(offset.startOffset() + " - "
// + offset.endOffset() + " : " + term.toString() + " | "
// + type.type());
if(term.toString() == null){
break;
}
v.add(term);
}
ts.end();
ts.close();
if(v.size() == 0){
return null;
} else if (v.size() == 1){
return new TermQuery(new Term(field,v.get(0).toString()));
} else {
PhraseQuery q = new PhraseQuery();
BooleanQuery b = new BooleanQuery();
q.setBoost(2048.0f);
b.setBoost(0.001f);
for(int i = 0;i < v.size();i++){
CharTermAttribute t = v.get(i);
//q.add(new Term(field,t.toString()));
TermQuery tmp = new TermQuery(new Term(field,t.toString()));
tmp.setBoost(0.01f);
b.add(tmp, Occur.MUST);
}
return b;
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
protected Query getFieldQuery(String field,String queryText) throws ParseException{
return getFieldQuery(field,queryText,0);
}
}
上面這段代碼,展現了QueryParser的基本步驟,須要分詞器,我用的是去年本身寫的基於逆向最大匹配算法的分詞器(由IK分詞改造而來)。從上面可以基本瞭解BooleanQuery的工做原理了。去年寫的兩個數組取交集的算法,就是布爾查詢爲AND的邏輯問題抽象。短語查詢精確度最高,在倒排索引項中存儲有詞元的位置信息,就是提供短語查詢功能支持的。
如今迴歸到第五部分的內容,如今有一個排序業務場景:有一個電商平臺,交易量很是火,點擊量比較大,須要自定義一個更加合理的符合本身公司的排序需求。先提出以下需求:要求按照入住商家的時間,商家是否爲VIP以及商品的點擊率(point)綜合考慮三者因素得出最後的評分。爲了完成這樣一項排序任務,先梳理如下思路:這個排序要求,按照lucene現有的score公式確定知足不了,這是屬於用戶在外部自定義的排序規則,與底層的排序規則不相干。可以知足這樣需求的只有solr的edismax解析器。因此按照以下思路:先了解一下edismax怎麼使用(好比能夠在外部定義linear函數實現規則),而後還須要解析器的內部原理(看源代碼),看看它與底層的score有何種關係(不可能沒有關係,因此須要深刻研讀源代碼,看看有沒有必要修改lucene底層的score源代碼)。按照上面的思路開展工做,查看源代碼後發現,最終得分是外部傳遞的評分函數與底層score的乘積,dismax解析器是相加。若是用dismax解析器的話,相加不能突出上述規則的做用,因此最好用edismax解析器。從理論上分析,若是底層使用VSM模型或者是BM25模型的話,score打分會對業務排序規則產生影響,好比有的商家是VIP,點擊率很高,可是底層的score可能很低,這樣一相乘的話,最後得分就不許確了,因此須要把底層的score寫死,改成1,消除影響。因此,按照這個規則,評分函數,點擊率高的排在前面(在point前設置比較高的權重)是比較合理的。
爲了驗證以上想法的正確性,能夠先定義評分函數,不修改底層的score,看看排序效果,排序是混亂的 。因此,根據上面的分析,須要從基本的lucene底層的score打分源代碼開始研究,而後edismax源代碼。在修改lucene的score源代碼的時候,最好不要用jd-gui反編譯工具,最開始用的時候,獲得的代碼只有部分是正確的。用maven構建項目時,直接下載以來的包,包括lucene-core-4.9.0-sources.jar,修改源碼包,而後從新編譯打包,替換掉原來的包。這是一項繁瑣的工程,包括後面的博客中介紹的機器學習排序,構建文檔數據特徵時,須要獲取BM25信息,一樣須要lucene源代碼,構建訓練系統和預測系統。
lucene評分流程:以BooleanQuery爲例,能夠參看上面寫的QueryParser,BooleanQuery須要用到TermQuery,那麼這個打分就由它完成。TermQuery繼承了Query,因此須要實現createWeight方法,獲得的是Weight的子類TermWeight。TermWeight須要實現scorer方法獲得Scorer,而後調用Scorer的score方法。先看一下TermQuery的createWeight:
public Weight createWeight(IndexSearcher searcher)
throws IOException
{
IndexReaderContext context = searcher.getTopReaderContext();
TermContext termState;
if(perReaderTermState == null || perReaderTermState.topReaderContext != context)
termState = TermContext.build(context, term);
else
termState = perReaderTermState;
if(docFreq != -1)
termState.setDocFreq(docFreq);
return new TermWeight(searcher, termState);
}
查詢文檔由IndexSearcher完成,而後獲得TermWeight類。再看看TermWeight的scorer:
public Scorer scorer(AtomicReaderContext context, Bits acceptDocs)
throws IOException
{
if(!$assertionsDisabled && termStates.topReaderContext != ReaderUtil.getTopLevelContext(context))
throw new AssertionError((new StringBuilder()).append("The top-reader used to create Weight (").append(termStates.topReaderContext).append(") is not the same as the current reader's top-reader (").append(ReaderUtil.getTopLevelContext(context)).toString());
TermsEnum termsEnum = getTermsEnum(context);
if(termsEnum == null)
return null;
DocsEnum docs = termsEnum.docs(acceptDocs, null);
if(!$assertionsDisabled && docs == null)
throw new AssertionError();
else
return new TermScorer(this, docs, similarity.simScorer(stats, context));
}
獲得了TermScorer類。調用這個對象的score方法(調用了Similarity)
public float score()
throws IOException
{
if(!$assertionsDisabled && docID() == 2147483647)
throw new AssertionError();
else
return docScorer.score(docsEnum.docID(), docsEnum.freq());
}
private final org.apache.lucene.search.similarities.Similarity.SimScorer docScorer;//這是Similarity的內部抽象類
docScorer.score方法由不少實現者,這裏用BM25Similarity extends Similarity,主要實現SimScorer的explain方法,這是最終打分的函數,經過Explain對象獲取到得分。
public abstract class Similarity
{
public static abstract class SimWeight
{
public abstract float getValueForNormalization();
public abstract void normalize(float f, float f1);
public SimWeight()
{
}
}
public static abstract class SimScorer
{
public abstract float score(int i, float f);
public abstract float computeSlopFactor(int i);
public abstract float computePayloadFactor(int i, int j, int k, BytesRef bytesref);
public Explanation explain(int doc, Explanation freq)
{
Explanation result = new Explanation(score(doc, freq.getValue()), (new StringBuilder()).append("score(doc=").append(doc).append(",freq=").append(freq.getValue()).append("), with freq of:").toString());
result.addDetail(freq);
return result;
}
public SimScorer()
{
}
}
看一看BM25Similarity:
package org.apache.lucene.search.similarities;
import java.io.IOException;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.SmallFloat;
public class BM25Similarity extends Similarity
{
private static class BM25Stats extends Similarity.SimWeight
{
public float getValueForNormalization()
{
float queryWeight = idf.getValue() * queryBoost;
return queryWeight * queryWeight;
}
public void normalize(float queryNorm, float topLevelBoost)
{
this.topLevelBoost = topLevelBoost;
weight = idf.getValue() * queryBoost * topLevelBoost;
}
private final Explanation idf;
private final float avgdl;
private final float queryBoost;
private float topLevelBoost;
private float weight;
private final String field;
private final float cache[];
BM25Stats(String field, Explanation idf, float queryBoost, float avgdl, float cache[])
{
this.field = field;
this.idf = idf;
this.queryBoost = queryBoost;
this.avgdl = avgdl;
this.cache = cache;
}
}
private class BM25DocScorer extends Similarity.SimScorer
{
public float score(int doc, float freq)
{
float norm = norms != null ? cache[(byte)(int)norms.get(doc) & 255] : k1;
return (weightValue * freq) / (freq + norm);
}
public Explanation explain(int doc, Explanation freq)
{
return explainScore(doc, freq, stats, norms);//這是最終打分函數
}
public float computeSlopFactor(int distance)
{
return sloppyFreq(distance);
}
public float computePayloadFactor(int doc, int start, int end, BytesRef payload)
{
return scorePayload(doc, start, end, payload);
}
private final BM25Stats stats;
private final float weightValue;
private final NumericDocValues norms;
private final float cache[];
final BM25Similarity this$0;
BM25DocScorer(BM25Stats stats, NumericDocValues norms)
throws IOException
{
this$0 = BM25Similarity.this;
super();
this.stats = stats;
weightValue = stats.weight * (k1 + 1.0F);
cache = stats.cache;
this.norms = norms;
}
}
public BM25Similarity(float k1, float b)
{
discountOverlaps = true;
this.k1 = k1;
this.b = b;
}
public BM25Similarity()
{
discountOverlaps = true;
k1 = 1.2F;
b = 0.75F;
}
protected float idf(long docFreq, long numDocs)
{
return (float)Math.log(1.0D + ((double)(numDocs - docFreq) + 0.5D) / ((double)docFreq + 0.5D));
}
protected float sloppyFreq(int distance)
{
return 1.0F / (float)(distance + 1);
}
protected float scorePayload(int doc, int start, int end, BytesRef bytesref)
{
return 1.0F;
}
protected float avgFieldLength(CollectionStatistics collectionStats)
{
long sumTotalTermFreq = collectionStats.sumTotalTermFreq();
if(sumTotalTermFreq <= 0L)
return 1.0F;
else
return (float)((double)sumTotalTermFreq / (double)collectionStats.maxDoc());
}
protected byte encodeNormValue(float boost, int fieldLength)
{
return SmallFloat.floatToByte315(boost / (float)Math.sqrt(fieldLength));
}
protected float decodeNormValue(byte b)
{
return NORM_TABLE[b & 255];
}
public void setDiscountOverlaps(boolean v)
{
discountOverlaps = v;
}
public boolean getDiscountOverlaps()
{
return discountOverlaps;
}
public final long computeNorm(FieldInvertState state)
{
int numTerms = discountOverlaps ? state.getLength() - state.getNumOverlap() : state.getLength();
return (long)encodeNormValue(state.getBoost(), numTerms);
}
public Explanation idfExplain(CollectionStatistics collectionStats, TermStatistics termStats)
{
long df = termStats.docFreq();
long max = collectionStats.maxDoc();
float idf = idf(df, max);
return new Explanation(idf, (new StringBuilder()).append("idf(docFreq=").append(df).append(", maxDocs=").append(max).append(")").toString());
}
public Explanation idfExplain(CollectionStatistics collectionStats, TermStatistics termStats[])
{
long max = collectionStats.maxDoc();
float idf = 0.0F;
Explanation exp = new Explanation();
exp.setDescription("idf(), sum of:");
TermStatistics arr$[] = termStats;
int len$ = arr$.length;
for(int i$ = 0; i$ < len$; i$++)
{
TermStatistics stat = arr$[i$];
long df = stat.docFreq();
float termIdf = idf(df, max);
exp.addDetail(new Explanation(termIdf, (new StringBuilder()).append("idf(docFreq=").append(df).append(", maxDocs=").append(max).append(")").toString()));
idf += termIdf;
}
exp.setValue(idf);
return exp;
}
public final transient Similarity.SimWeight computeWeight(float queryBoost, CollectionStatistics collectionStats, TermStatistics termStats[])
{
Explanation idf = termStats.length != 1 ? idfExplain(collectionStats, termStats) : idfExplain(collectionStats, termStats[0]);
float avgdl = avgFieldLength(collectionStats);
float cache[] = new float[256];
for(int i = 0; i < cache.length; i++)
cache[i] = k1 * ((1.0F - b) + (b * decodeNormValue((byte)i)) / avgdl);
return new BM25Stats(collectionStats.field(), idf, queryBoost, avgdl, cache);
}
public final Similarity.SimScorer simScorer(Similarity.SimWeight stats, AtomicReaderContext context)
throws IOException
{
BM25Stats bm25stats = (BM25Stats)stats;
return new BM25DocScorer(bm25stats, context.reader().getNormValues(bm25stats.field));
}
private Explanation explainScore(int doc, Explanation freq, BM25Stats stats, NumericDocValues norms)
{
Explanation result = new Explanation();
result.setDescription((new StringBuilder()).append("score(doc=").append(doc).append(",freq=").append(freq).append("), product of:").toString());
Explanation boostExpl = new Explanation(stats.queryBoost * stats.topLevelBoost, "boost");
if(boostExpl.getValue() != 1.0F)
result.addDetail(boostExpl);
result.addDetail(stats.idf);
Explanation tfNormExpl = new Explanation();
tfNormExpl.setDescription("tfNorm, computed from:");
tfNormExpl.addDetail(freq);
tfNormExpl.addDetail(new Explanation(k1, "parameter k1"));
if(norms == null)
{
tfNormExpl.addDetail(new Explanation(0.0F, "parameter b (norms omitted for field)"));
tfNormExpl.setValue((freq.getValue() * (k1 + 1.0F)) / (freq.getValue() + k1));
} else
{
float doclen = decodeNormValue((byte)(int)norms.get(doc));
tfNormExpl.addDetail(new Explanation(b, "parameter b"));
tfNormExpl.addDetail(new Explanation(stats.avgdl, "avgFieldLength"));
tfNormExpl.addDetail(new Explanation(doclen, "fieldLength"));
tfNormExpl.setValue((freq.getValue() * (k1 + 1.0F)) / (freq.getValue() + k1 * ((1.0F - b) + (b * doclen) / stats.avgdl)));
}
result.addDetail(tfNormExpl);
result.setValue(boostExpl.getValue() * stats.idf.getValue() * tfNormExpl.getValue());
return result;
}
public String toString()
{
return (new StringBuilder()).append("BM25(k1=").append(k1).append(",b=").append(b).append(")").toString();
}
public float getK1()
{
return k1;
}
public float getB()
{
return b;
}
private final float k1;
private final float b;
protected boolean discountOverlaps;
private static final float NORM_TABLE[];
static
{
NORM_TABLE = new float[256];
for(int i = 0; i < 256; i++)
{
float f = SmallFloat.byte315ToFloat((byte)i);
NORM_TABLE[i] = 1.0F / (f * f);
}
}
}
從上面的代碼能夠看出,要修改的話,就修改explainScore方法,把影響因素所有改成1就好了,由於獲取評分是經過Explain對象。
以上就是lucene底層的評分流程:BooleanQuery----->TermQuery----->createWeight----->TermWeight.scorer()------>TermScorer----TermScorer.score()------->(Similarity內部抽象類)SimScorer.explain()------->BM25Similarity的explainScore方法。最後看一下edismax解析器的源代碼:
6、爲機器學習排序作準備:
下面進一步深刻思考,加入由以下示例的查詢:
String[] fields = {"name","content"};
QueryParser queryParser = new MultiFieldQueryParser(matchVersion, fields,analyzer);
Query query = queryParser.parse(queryString);
BooleanQuery bq = new BooleanQuery();
bq.add(query, Occur.MUST);
IndexSearcher indexSearcher = new IndexSearcher((IndexReader)DirectoryReader.open(FSDirectory.open(new File("/Users/ChinaMWorld/Desktop/index/"))));
Filter filter = null;
//查詢前10000條記錄
TopDocs topDocs = indexSearcher.search(bq,filter,10000);
如今我要求不用lucene以及solr的全部的評分,用機器學習排序,先構建訓練系統,而後預測,最後排序。問題的關鍵是在lucene返回排序的文檔以前截取結果(ScoreDocs),截取到的這些文檔具備BM25信息,可是尚未排序,咱們把它先截取下來,而後構建文檔向量,開始數據建模(訓練時須要樣本的評分,能夠在點擊圖中轉化,把點擊率轉化爲評分),而後進入機器學習系統訓練評分函數。訓練時能夠這樣,獲取一段時間內用戶的搜索圖和點擊圖,獲得文檔及對應的評分。而後再模擬用戶的搜索詞,獲取到相同的文檔,這個時候用咱們改造過的代碼,截取到未排序前的ScoreDoc。而後開始數據建模,訓練。當用戶再搜索時,仍然截取到上述文檔,把這些文檔轉到predict系統中,最後加入到自定義的PriorityQueue(區別於JDK的)排序,獲得最終結果。有了思路後,就開始實施,在實施的過程當中實際上是有必定難度的。先從最外部的代碼一步步抽絲剝繭,找到答案。上面的示例代碼是有問題的,在正式的生產環境中,IndexSearch()構造器中必定要傳遞CompletionService,知足多線程的要求。從indexSearcher.search(bq,filter,10000)開始進入源代碼內部,咱們的任務是找到未排序前的代碼,截取下來進行改造。------>
public TopDocs search(Query query, Filter filter, int n)throws IOException
{
return search(createNormalizedWeight(wrapFilter(query, filter)), ((ScoreDoc) (null)), n);
}//這個query是咱們制定的BooleanQuery,createNormalizedWeight方法產生Weight的子類BooleanWeight,裏面的評分方法須要的Similarity由咱們在配置文件中指定:<similarity class="org.apache.lucene.search.similarities.BM25Similarity"/>,運行原理跟前面的同樣了,都是Bm25Similarity評分。接着繼續看serach方法:
protected TopDocs search(Weight weight, ScoreDoc after, int nDocs)
throws IOException
{
int limit = reader.maxDoc();
if(limit == 0)
limit = 1;
if(after != null && after.doc >= limit)
throw new IllegalArgumentException((new StringBuilder()).append("after.doc exceeds the number of documents in the reader: after.doc=").append(after.doc).append(" limit=").append(limit).toString());
nDocs = Math.min(nDocs, limit);
if(executor == null)
return search(leafContexts, weight, after, nDocs);-------------①
HitQueue hq = new HitQueue(nDocs, false);
Lock lock = new ReentrantLock();
ExecutionHelper runner = new ExecutionHelper(executor);------------②
for(int i = 0; i < leafSlices.length; i++)
runner.submit(new SearcherCallableNoSort(lock, this, leafSlices[i], weight, after, nDocs, hq));---------------------③
int totalHits = 0;
float maxScore = (-1.0F / 0.0F);
Iterator i$ = runner.iterator();
do
{
if(!i$.hasNext())
break;
TopDocs topDocs = (TopDocs)i$.next();
if(topDocs.totalHits != 0)
{
totalHits += topDocs.totalHits;
maxScore = Math.max(maxScore, topDocs.getMaxScore());
}
} while(true);
ScoreDoc scoreDocs[] = new ScoreDoc[hq.size()];
for(int i = hq.size() - 1; i >= 0; i--)
scoreDocs[i] = (ScoreDoc)hq.pop();//排序--------------④
return new TopDocs(totalHits, scoreDocs, maxScore);
}
標出了4個序號:第①出直接越過去了,第②處的ExecutionHelper對象封裝了CompletionService,若是對jdk1.7及之後版本的多線程,還有lunece內部的PriorityQueue的設計思想以及CAS,ReentrantLock這些都不瞭解的話,本身補一補。把代碼貼出來:
private static final class ExecutionHelper
implements Iterator, Iterable
{
public boolean hasNext()
{
return numTasks > 0;
}
public void submit(Callable task)
{
service.submit(task);
numTasks++;
}
public Object next()
{
if(!hasNext())
throw new NoSuchElementException("next() is called but hasNext() returned false");
Object obj;
try
{
obj = service.take().get();
}
catch(InterruptedException e)
{
throw new ThreadInterruptedException(e);
}
catch(ExecutionException e)
{
throw new RuntimeException(e);
}
numTasks--;
return obj;
Exception exception;
exception;
numTasks--;
throw exception;
}
public void remove()
{
throw new UnsupportedOperationException();
}
public Iterator iterator()
{
return this;
}
private final CompletionService service;
private int numTasks;
ExecutionHelper(Executor executor)
{
service = new ExecutorCompletionService(executor);
}
}
第③處是咱們要改造的地方,是submit()方法裏面的東東:new SearcherCallableNoSort(lock, this, leafSlices[i], weight, after, nDocs, hq),從名字上就能看出來,是非排序的查詢結果集。submit方法調用的是Callabel,因此看一下SearcherCallableNoSort的call()方法---------------->
private static final class SearcherCallableNoSort
implements Callable
{
public TopDocs call()
throws IOException
{
TopDocs docs;
ScoreDoc scoreDocs[];
docs = searcher.search(Arrays.asList(slice.leaves), weight, after, nDocs);//這裏就不往下追蹤了,跟前面講的BM25Similarity排序是同樣的
scoreDocs = docs.scoreDocs;
lock.lock();
int j = 0;
do
{
if(j >= scoreDocs.length)
break;
ScoreDoc scoreDoc = scoreDocs[j];
//問題的關鍵在這裏,獲得scoreDoc後,因爲已經獲取了BM25參數,接下來把ScoreDoc處理成向量,開始
//數據建模,而後進入機器學習訓練系統,學習評分函數,後面的代碼能夠用在predict後得到每一個文檔的
//分數,而後加入到優先級隊列中排序
if(scoreDoc == hq.insertWithOverflow(scoreDoc))
break;
j++;
} while(true);
lock.unlock();
break MISSING_BLOCK_LABEL_106;
Exception exception;
exception;
lock.unlock();
throw exception;
return docs;
}
public volatile Object call()
throws Exception
{
return call();
}
private final Lock lock;
private final IndexSearcher searcher;
private final Weight weight;
private final ScoreDoc after;
private final int nDocs;
private final HitQueue hq;
private final LeafSlice slice;
public SearcherCallableNoSort(Lock lock, IndexSearcher searcher, LeafSlice slice, Weight weight, ScoreDoc after, int nDocs, HitQueue hq)
{
this.lock = lock;
this.searcher = searcher;
this.weight = weight;
this.after = after;
this.nDocs = nDocs;
this.hq = hq;
this.slice = slice;
}
}
須要把代碼改形成訓練系統和predict系統兩個版本。到此爲止,這篇博客算是寫完了,還差edismax解析器的源代碼解讀,單獨寫一篇文章吧,下一篇博客將開始研究ListNet算法(機器學習排序的一種)……
另外,關於RNN的理解和應用,能夠參考一些一線專家從實踐中總結出來的心得,在結合一下理論效果會比較好。在微信公衆號中搜索深度學習大講堂 公衆號,中科視拓的公衆號,很不錯的,包括tensorflow的源碼解析,很系統。學習這些技術,沒有捷徑,必須多實踐,多動手,多編程,多思考。上帝是公平的,付出多了,會有回報的。
佟氏出品,必屬精品!堅持獨立思考,大膽假設,當心求證,技術進步永無止境………………………………