solr 打分和排序機制(轉載)
以下來自solr in action。
包含:
- 詞項頻次。查詢詞項出現在當前查詢文件中的次數。
- 反向文件頻次。查詢詞項出現在所有文件總的次數。
- 此項權重。
-
標準化因子:
-
欄位規範:
- 文件權重。
- 欄位權重。
- 長度歸一化。消除長文件的優勢。因為長文件的詞項頻次一般會比較大。
- 協調因子。避免一個文件中出現某一個詞項的次數太多導致總分值太大。目的是讓結果中包含更多的是出現所有詞項的文件。
-
欄位規範:
具體說明見下文。
以下轉載自網路。原文地址: http://tec.5lulu.com/detail/110d8n2ehpg2j85ec.html
簡述
內容的相似性計算由搜尋引擎的幾種常見的檢索模型有:
向量空間模型
簡述中介紹了好多種相似性計算方法, Solr的索引檔案中有.tvx,.tvd,tvf儲存了term vector的資訊,首先我們學習如何利用term vector來反映相似性程度。
用v(d1)表示了term d1的term向量,termterm 給定一個查詢以及一個文件,如何計算他們的相似值呢,請看以下公式,它使用了以下概念:term frequency (tf), inverse document frequency (idf), term boosts (t.getBoost), field normalization (norm), coordination factor (coord), and query normalization (queryNorm).
- tf(t in d ) 表示該idf(t) 表示 t.getBoost() 也叫此項權重。標準化因子,它包括三個引數:
- :此值越大,說明此文件越重要。也叫文件權重。:此域越大,說明此域越重要。也叫欄位權重。:一個域中包含的總數越多(我理解的是所有這個文件的所有,而不侷限於查詢中的),也即文件越長,此值越小,文件越短,此值越大。也叫長度歸一化。目的是消除長欄位的優勢。
coord(q,d):一次搜尋可能包含多個搜尋詞,而一篇文件中也可能包含多個搜尋詞,此項表示,當一篇文件中包含的搜尋詞越多,則此文件則打分越高 ,numTermsInDocumentFromQuery / numTermsInQuery
queryNorm(q):計算每個查詢條目的方差和,此值並不影響排序,而僅僅使得不同的query之間的分數可以比較。
ps:理解到這裡就可以了,下面的細節可以以後再研究。
3評分機制
表示匹配文章的程度,如果在一篇文章中該出現了次數越多,說明該對該文章的重要性越大,因而更加匹配。相反的出現越少說明該越不匹配文章。但是這裡需要注意,出現次數與重要性並不是成正比的,比如出現次,出現次,對於該文章的重要性並不是的倍,所以這裡的值進行平方根計算。
tf(t in d) = numTermOccurrencesInDocument 1/2
-
idf, 表示包含該文章的個數,與tf不同,idf 越大表明該term越不重要。比如this很多文章都包含,但是它對於匹配文章幫助不大。這也如我們程式設計師所學的技術,對於程式設計師本身來說,這項技術掌握越深越好(掌握越深說明花時間看的越多,tf越大),找工作時越有競爭力。然而對於所有程式設計師來說,這項技術懂得的人越少越好(懂得的人少df小),找工作越有競爭力。人的價值在於不可替代性就是這個道理。
idf(t) = 1 + log (numDocs / (docFreq +1))
- t.getBoost,boost是人為給term提升權重的過程,我們可以在Index和Query中分別加入term boost,但是由於Query過程比較靈活,所以這裡介紹給Query boost。term boost 不僅可以對Pharse進行,也可以對單個term進行,在查詢的時候用^後面加數字表示:
- title:(solr in action)^2.5 對solr in action 這個pharse設定boost
- title:(solr in action) 預設的boost時1.0
- title:(solr^2in^.01action^1.5)^3OR"solrinaction"^2.5
-
norm(t,d) 即field norm,它包含Document boost,Field boost,lengthNorm。相比於t.getBoost()可以在查詢的時候進行動態的設定,norm裡面的f.getBoost()和d.getBoost()只能建索引過程中設定,如果需要對這兩個boost進行修改,那麼只能重建索引。他們的值是儲存在.nrm檔案中。
norm(t,d) = d.getBoost() • lengthNorm(f) • f.getBoost()
- d.getBoost() document的boost,對document設定boost是通過對每一個field設定boost實現的。
- f .getBoost() field的boost,這裡需要提以下,Solr是支援多值域方式建索引的,即同一個field多個value,如以下程式碼。當一個文件裡出現同名的多值域時候,倒排索引和項向量都會在邏輯上將這些域的詞彙單元附加進去。當對多值域進行儲存的時候,它們在文件中的儲存順序是分離的,因此當你在搜尋期間對文件進行檢索時,你會發現多個Field例項。如下圖例子所示,當查詢author:Lucene時候出現兩個author域,這就是所謂的多值域現象。
- Document doc = new Document();
- for (String author : authors){
- doc.add(new Field("author",author,Field.Store.YES,Field.Index.ANALYZED));
- }
- //首先對多值域建立索引
- Directory dir = FSDirectory.open(new File("/Users/rcf/workspace/java/solr/Lucene"));
- IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LUCENE_48,new WhitespaceAnalyzer(Version.LUCENE_48));
- @SuppressWarnings("resource")
- IndexWriter writer = new IndexWriter(dir,indexWriterConfig);
- Document doc = new Document();
- doc.add(new Field("author","lucene",Field.Store.YES,Field.Index.ANALYZED));
- doc.add(new Field("author","solr",Field.Store.YES,Field.Index.ANALYZED));
- doc.add(new Field("text","helloworld",Field.Store.YES,Field.Index.ANALYZED));
- writer.addDocument(doc);
- writer.commit();
- //對多值域進行查詢
- IndexReader reader = IndexReader.open(dir);
- IndexSearcher search = new IndexSearcher(reader);
- Query query = new TermQuery(new Term("author","lucene"));
- TopDocs docs = search.search(query, 1);
- Document doc = search.doc(docs.scoreDocs[0].doc);
- for(IndexableField field : doc.getFields()){
- System.out.println(field.name()+":"+field.stringValue());
- }
- System.out.print(docs.totalHits);
- //執行結果
- author:lucene
- author:solr
- text:helloworld
- 2
- 當對多值域設定boost的時候,那麼該field的boost最後怎麼算呢?即為每一個值域的boost相乘。比如title這個field,第一次boost是3.0,第二次1,第三次0.5,那麼結果就是3*1*0.5.
- Boost: (3) · (1) · (0.5) = 1.5
-
lengthNorm, Norm的長度是field中term的個數的平方根的倒數,field的term的個數被定義為field的長度。field長度越大,Norm Field越小,說明term越不重要,反之越重要,這很好理解,在10個詞的title中出現北京一次和在有200個詞的正文中出現北京2次,哪個field更加匹配,當然是title。
- 最後再說明下,document boost,field boost 以及lengthNorm在儲存為索引是以byte形式的,編解碼過程中會使得數值損失,該損失對相似值計算的影響微乎其微。
-
queryNorm, 計算每個查詢條目的方差和,此值並不影響排序,而僅僅使得不同的query之間的分數可以比較。也就說,對於同一詞查詢,他對所有的document的影響是一樣的,所以不影響查詢的結果,它主要是為了區分不同query了。
queryNorm(q) = 1 / (sumOfSquaredWeights )
sumOfSquaredWeights = q.getBoost()2 • ∑ ( idf(t) • t.getBoost() )2
coord(q,d),表示文件中符合查詢的term的個數,如果在文件中查詢的term個數越多,那麼這個文件的score就會更高。
numTermsInDocumentFromQuery / numTermsInQuery
比如Query:AccountantAND("SanFrancisco"OR"NewYork"OR"Paris")
文件A包含了上面的3個term,那麼coord就是3/4,如果包含了1個,則coord就是4/4
4原始碼
上面介紹了相似值計算的公式,那麼現在就來檢視Solr實現的程式碼,這部分實現是在DefaultSimilarity類中。
- @Override
- public float coord(int overlap, int maxOverlap) {
- return overlap / (float)maxOverlap;
- }
- @Override
- public float queryNorm(float sumOfSquaredWeights) {
- return (float)(1.0 / Math.sqrt(sumOfSquaredWeights));
- }
- @Override
- public float lengthNorm(FieldInvertState state) {
- final int numTerms;
- if (discountOverlaps)
- numTerms = state.getLength() - state.getNumOverlap();
- else
- numTerms = state.getLength();
- return state.getBoost() * ((float) (1.0 / Math.sqrt(numTerms)));
- }
- @Override
- public float tf(float freq) {
- return (float)Math.sqrt(freq);
- }
- @Override
- public float idf(long docFreq, long numDocs) {
- return (float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0);
-
}
Solr計算score(q,d)的過程如下:
1:呼叫IndexSearcher.createNormalizedWeight()計算queryNorm()
- public Weight createNormalizedWeight(Query query) throws IOException {
- query = rewrite(query);
- Weight weight = query.createWeight(this);
- float v = weight.getValueForNormalization();
- float norm = getSimilarity().queryNorm(v);
- if (Float.isInfinite(norm) || Float.isNaN(norm)) {
- norm = 1.0f;
- }
- weight.normalize(norm, 1.0f);
- return weight;
-
}
具體實現步驟如下:
- Weight weight = query.createWeight(this);
- 建立BooleanWeight->new TermWeight()->this.stats = similarity.computeWeight)->this.weight = idf * t.getBoost()
- public IDFStats(String field, Explanation idf, float queryBoost) {
- // TODO: Validate?
- this.field = field;
- this.idf = idf;
- this.queryBoost = queryBoost;
- this.queryWeight = idf.getValue() * queryBoost; // compute query weight
- }
- 計算sumOfSquaredWeights
- s = weights.get(i).getValueForNormalization()計算( idf(t) • t.getBoost() )2 如以下程式碼所示,queryWeight在上一部中計算出
- public float getValueForNormalization() {
- // TODO: (sorta LUCENE-1907) make non-static class and expose this squaring via a nice method to subclasses?
- return queryWeight * queryWeight; // sum of squared weights
-
}
- BooleanWeight->getValueForNormalization->sum = (q.getBoost)2 *∑(this.weight)2 = (q.getBoost)2 *∑(idf * t.getBoost())2
- public float getValueForNormalization() throws IOException {
- float sum = 0.0f;
- for (int i = 0 ; i < weights.size(); i++) {
- // call sumOfSquaredWeights for all clauses in case of side effects
- float s = weights.get(i).getValueForNormalization(); // sum sub weights
- if (!clauses.get(i).isProhibited()) {
- // only add to sum for non-prohibited clauses
- sum += s;
- }
- }
- sum *= getBoost() * getBoost(); // boost each sub-weight
- return sum ;
-
}
- 計算完整的querynorm() = 1 / Math.sqrt(sumOfSquaredWeights));
- public float queryNorm(float sumOfSquaredWeights) {
- return (float)(1.0 / Math.sqrt(sumOfSquaredWeights));
-
}
- weight.normalize(norm, 1.0f) 計算norm()
- topLevelBoost *= getBoost();
- 計算value = idf()*queryWeight*queryNorm=idf()2*t.getBoost()*queryNorm(queryWeight在前面已計算出)
- public void normalize(float queryNorm, float topLevelBoost) {
- this.queryNorm = queryNorm * topLevelBoost;
- queryWeight *= this.queryNorm; // normalize query weight
- value = queryWeight * idf.getValue(); // idf for document
- }
-
2:呼叫IndexSearch.weight.bulkScorer()計算coord(q,d),並獲取每一個term的docFreq,並將docFreq按td從小到大排序。
- if (optional.size() == 0 && prohibited.size() == 0) {
- float coord = disableCoord ? 1.0f : coord(required.size(), maxCoord);
- return new ConjunctionScorer(this, required.toArray(new Scorer[required.size()]), coord);
-
}
3:score.score()進行評分計算,獲取相似值,並放入優先順序佇列中獲取評分最高的doc id。
- weightValue= value =idf()2*t.getBoost()*queryNorm
- sore = ∑(tf()*weightValue)*cood 計算出最終的相似值
- 這裡貌似沒有用到lengthNorm,
- public float score(int doc, float freq) {
- final float raw = tf(freq) * weightValue; // compute tf(f)*weight
- return norms == null ? raw : raw * decodeNormValue(norms.get(doc)); // normalize for field
- }
- public float score() throws IOException {
- // TODO: sum into a double and cast to float if we ever send required clauses to BS1
- float sum = 0.0f;
- for (DocsAndFreqs docs : docsAndFreqs) {
- sum += docs.scorer.score();
- }
- return sum * coord;
- }
- public void collect(int doc) throws IOException {
- float score = scorer.score();
- // This collector cannot handle these scores:
- assert score != Float.NEGATIVE_INFINITY;
- assert !Float.isNaN(score);
- totalHits++;
- if (score <= pqTop.score) {
- // Since docs are returned in-order (i.e., increasing doc Id), a document
- // with equal score to pqTop.score cannot compete since HitQueue favors
- // documents with lower doc Ids. Therefore reject those docs too.
- return;
- }
- pqTop.doc = doc + docBase;
- pqTop.score = score;
- pqTop = pq.updateTop();
- }
5公式推導
關於公式的推導覺先的《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的打分計算公式。
6總結
前面學習了Solr的評分機制,雖然對理論的推導以及公式有了一些瞭解,但是在Solr具體實現上我卻產生了不少疑惑:
1. BooleanQuery查詢,為什麼沒有用到LengthNorm。
2. BooleanQuery 多條件查詢時候,Not And Or 對文件進行打分時候是否具有影響。
3. PharseQuery查詢時候,打分又是怎麼進行的。
4. 怎麼樣對這個進行打分進行定製。
這些都是接下來需要去理解的。
我們習慣用自己的行為準則審視他人,並時刻準備加以指摘。以下來自solr in action。
包含:
- 詞項頻次。查詢詞項出現在當前查詢文件中的次數。
- 反向文件頻次。查詢詞項出現在所有文件總的次數。
- 此項權重。
-
標準化因子:
-
欄位規範:
- 文件權重。
- 欄位權重。
- 長度歸一化。消除長文件的優勢。因為長文件的詞項頻次一般會比較大。
- 協調因子。避免一個文件中出現某一個詞項的次數太多導致總分值太大。目的是讓結果中包含更多的是出現所有詞項的文件。
-
欄位規範:
具體說明見下文。
以下轉載自網路。原文地址: http://tec.5lulu.com/detail/110d8n2ehpg2j85ec.html
簡述
內容的相似性計算由搜尋引擎的幾種常見的檢索模型有:
向量空間模型
簡述中介紹了好多種相似性計算方法, Solr的索引檔案中有.tvx,.tvd,tvf儲存了term vector的資訊,首先我們學習如何利用term vector來反映相似性程度。
用v(d1)表示了term d1的term向量,termterm 給定一個查詢以及一個文件,如何計算他們的相似值呢,請看以下公式,它使用了以下概念:term frequency (tf), inverse document frequency (idf), term boosts (t.getBoost), field normalization (norm), coordination factor (coord), and query normalization (queryNorm).
- tf(t in d ) 表示該idf(t) 表示 t.getBoost() 也叫此項權重。標準化因子,它包括三個引數:
- :此值越大,說明此文件越重要。也叫文件權重。:此域越大,說明此域越重要。也叫欄位權重。:一個域中包含的總數越多(我理解的是所有這個文件的所有,而不侷限於查詢中的),也即文件越長,此值越小,文件越短,此值越大。也叫長度歸一化。目的是消除長欄位的優勢。