1. 程式人生 > >solr 打分和排序機制(轉載)

solr 打分和排序機制(轉載)

以下來自solr in action。

包含:

  • 詞項頻次。查詢詞項出現在當前查詢文件中的次數。
  • 反向文件頻次。查詢詞項出現在所有文件總的次數。
  • 此項權重。
  • 標準化因子:
    • 欄位規範:
      • 文件權重。
      • 欄位權重。
      • 長度歸一化。消除長文件的優勢。因為長文件的詞項頻次一般會比較大。
    • 協調因子。避免一個文件中出現某一個詞項的次數太多導致總分值太大。目的是讓結果中包含更多的是出現所有詞項的文件。

具體說明見下文。

以下轉載自網路。原文地址: http://tec.5lulu.com/detail/110d8n2ehpg2j85ec.html

簡述

 

內容的相似性計算由搜尋引擎的幾種常見的檢索模型有:

向量空間模型

    簡述中介紹了好多種相似性計算方法,    Solr的索引檔案中有.tvx,.tvd,tvf儲存了term vector的資訊,首先我們學習如何利用term vector來反映相似性程度。

 

    v(d1)表示了term d1term向量,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.getBoostboost是人為給term提升權重的過程,我們可以在IndexQuery中分別加入term boost,但是由於Query過程比較靈活,所以這裡介紹給Query boostterm boost 不僅可以對Pharse進行,也可以對單個term進行,在查詢的時候用^後面加數字表示:
  • title:(solr in action)^2.5  solr in action 這個pharse設定boost
  • title:(solr in action)  預設的boost1.0
  • title:(solr^2in^.01action^1.5)^3OR"solrinaction"^2.5 
  • norm(t,d) field norm,它包含Document boostField boostlengthNorm。相比於t.getBoost()可以在查詢的時候進行動態的設定,norm裡面的f.getBoost()d.getBoost()只能建索引過程中設定,如果需要對這兩個boost進行修改,那麼只能重建索引。他們的值是儲存在.nrm檔案中。

     

    norm(t,d) = d.getBoost() • lengthNorm(f) • f.getBoost() 

  • d.getBoost() documentboost,對document設定boost是通過對每一個field設定boost實現的。
  • f .getBoost() fieldboost,這裡需要提以下,Solr是支援多值域方式建索引的,即同一個field多個value,如以下程式碼。當一個文件裡出現同名的多值域時候,倒排索引和項向量都會在邏輯上將這些域的詞彙單元附加進去。當對多值域進行儲存的時候,它們在文件中的儲存順序是分離的,因此當你在搜尋期間對文件進行檢索時,你會發現多個Field例項。如下圖例子所示,當查詢authorLucene時候出現兩個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的時候,那麼該fieldboost最後怎麼算呢?即為每一個值域的boost相乘。比如title這個field,第一次boost3.0,第二次1,第三次0.5,那麼結果就是3*1*0.5.
  • Boost: (3) · (1) · (0.5) = 1.5 
  • lengthNorm, Norm的長度是fieldterm的個數的平方根的倒數,fieldterm的個數被定義為field的長度。field長度越大,Norm Field越小,說明term越不重要,反之越重要,這很好理解,在10個詞的title中出現北京一次和在有200個詞的正文中出現北京2次,哪個field更加匹配,當然是title

     

  • 最後再說明下,document boostfield boost 以及lengthNorm在儲存為索引是以byte形式的,編解碼過程中會使得數值損失,該損失對相似值計算的影響微乎其微。
  • queryNorm,  計算每個查詢條目的方差和,此值並不影響排序,而僅僅使得不同的query之間的分數可以比較。也就說,對於同一詞查詢,他對所有的document的影響是一樣的,所以不影響查詢的結果,它主要是為了區分不同query了。

    queryNorm(q) = 1 / (sumOfSquaredWeights )

    sumOfSquaredWeights = q.getBoost()2 • ∑ ( idf(t) • t.getBoost() )

    coord(q,d),表示文件中符合查詢的term的個數,如果在文件中查詢的term個數越多,那麼這個文件的score就會更高。

    numTermsInDocumentFromQuery / numTermsInQuery 

            比如QueryAccountantAND("SanFrancisco"OR"NewYork"OR"Paris") 

            文件A包含了上面的3term,那麼coord就是3/4,如果包含了1個,則coord就是4/4

4原始碼

    上面介紹了相似值計算的公式,那麼現在就來檢視Solr實現的程式碼,這部分實現是在DefaultSimilarity類中。 

  1. @Override
  2. public float coord(int overlap, int maxOverlap) {
  3. return overlap / (float)maxOverlap;
  4. }
  5.  
  6. @Override
  7. public float queryNorm(float sumOfSquaredWeights) {
  8. return (float)(1.0 / Math.sqrt(sumOfSquaredWeights));
  9. }
  10.    
  11. @Override
  12. public float lengthNorm(FieldInvertState state) {
  13. final int numTerms;
  14. if (discountOverlaps)
  15. numTerms = state.getLength() - state.getNumOverlap();
  16. else
  17. numTerms = state.getLength();
  18. return state.getBoost() * ((float) (1.0 / Math.sqrt(numTerms)));
  19. }
  20.    
  21. @Override
  22. public float tf(float freq) {
  23. return (float)Math.sqrt(freq);
  24. }
  25.  
  26. @Override
  27. public float idf(long docFreq, long numDocs) {
  28. return (float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0);
  29. }

     

    Solr計算score(q,d)的過程如下:

    1:呼叫IndexSearcher.createNormalizedWeight()計算queryNorm() 

  30. public Weight createNormalizedWeight(Query query) throws IOException {
  31. query = rewrite(query);
  32. Weight weight = query.createWeight(this);
  33. float v = weight.getValueForNormalization();
  34. float norm = getSimilarity().queryNorm(v);
  35. if (Float.isInfinite(norm) || Float.isNaN(norm)) {
  36. norm = 1.0f;
  37. }
  38. weight.normalize(norm, 1.0f);
  39. return weight;
  40. }

     

    具體實現步驟如下:

  • 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() )如以下程式碼所示,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)*∑(this.weight)= (q.getBoost)*∑(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()*queryNormqueryWeight在前面已計算出) 
  • 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),並獲取每一個termdocFreq,並將docFreqtd從小到大排序。 

  1. if (optional.size() == 0 && prohibited.size() == 0) {
  2. float coord = disableCoord ? 1.0f : coord(required.size(), maxCoord);
  3. return new ConjunctionScorer(this, required.toArray(new Scorer[required.size()]), coord);
  4. }

     

    3score.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都為1idf都忽略查詢語句這篇小文件,得到如下公式

     

    所以餘弦公式變為:

     

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

     

    這裡需要討論的是,為什麼在打分過程中,需要除以文件的長度呢?

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

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

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

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

    而從Term的定義我們可以知道,Term是包含域資訊的,也即title:hellocontent:hello是不同的Term,也即一個Term只可能在文件中的一個域中出現。

    所以文件長度的公式為:

     

    代入餘弦公式:

     

    再加上各種boostcoord,則可得出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 d1term向量,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() 也叫此項權重。標準化因子,它包括三個引數:
  • :此值越大,說明此文件越重要。也叫文件權重。:此域越大,說明此域越重要。也叫欄位權重。:一個域中包含的總數越多(我理解的是所有這個文件的所有,而不侷限於查詢中的),也即文件越長,此值越小,文件越短,此值越大。也叫長度歸一化。目的是消除長欄位的優勢。