1. 程式人生 > >Lucene 4.0 原理與程式碼分析

Lucene 4.0 原理與程式碼分析

  搜尋演算法的核心實際是對搜尋項之間相似度的打分策略,一個好的打分策略應該能夠綜合各種與搜尋項內容相關並對搜尋目的有幫助的所有因素,一般將這種策略叫做建模(modeling),由量化後的相關因素即特徵(feature)構成檢索(評分)模型,最後通過模型得到搜尋項之間的相似度分數(similarity score)。

  一般來講相似度分數應該是與內容相關的程度成正比的,因此當計算過所有的搜尋項的相似度分數後降序排列,將值最大的k個項作為搜尋結果展示。

  資訊檢索技術的發展也有些年頭了,過程中誕生了不少優秀的檢索模型,其中比較早的是VSM模型,雖然誕生的較早,但由於其原理簡單直觀並在檢索效能(performance)和效率(efficiency)方面有較好的平衡,成為經典並直到今天依然被廣泛使用,同時也是Lucene最先實現並被設為預設使用的檢索模型,接下來我就對VSM檢索模型的原理和在Lucene中的實現進行分析。

  首先集中解釋一下在下文中會使用到的一些概念和標識:

  • d,文件(document);即檢索結果的項,是最基本的內容物件。
  • q,查詢(query);即檢索的需求,一般情況為使用者提供的短文字描述,當然某些情況下也可以是某篇特定的文件d,或是其它。
  • f,域(field);及文件中表示不同內容的片段或者屬性,比如標題、作者或正文,是比文件粒度更小的內容物件,Lucene支援對特定域或多域的檢索。
  • t,詞項(term);也被稱為詞元,原始狀態下就是構成文件的詞或字(word),但通常檢索前都會對文字內容進行一定的處理,比如去停用詞、詞幹化、分詞等,使得詞項不在是原始的文字形式,總之,是粒度最小的內容物件。需要特殊說明的是,由於Lucene是可多域檢索的,所以詞項是與域繫結的,每個詞項都包含文字和所屬域兩個基本屬性,也就是說兩個詞項(“百度”,“標題”)和(“百度”,“正文”)在Lucene內視為兩個不同的詞項。

  好了,下面開始分析VSM,VSM是(Vector Space Model)的縮寫,中譯就是向量空間模型,其實就是把每個文件和查詢對映為高維空間上的向量(如圖1-1),每一維都對應了在整個文件集中出現過的詞項,藉助夾角越小余弦值越大的性質,最終通過計算向量間夾角的餘弦值作為兩個項之間的相似度(如公式1)。


圖1-1


公式1-1

  其中分子部分為兩個向量的點乘積,分母部分則分別為兩個向量的歐幾里德正規化。原始的VSM模型思想很直觀,在這裡就不過多做介紹了,還不太明白的朋友請參考《Introduction To Information Retrieval》的第6章,我將主要分析VSM在Lucene中的實現。

  在公式1-1的分母中兩個歐式長度項的意義在於將向量規範化為單位向量以消除冗餘長度對相似度的影響,例如兩篇文件“百度”和“百度百度百度”相比兩者包含完全相同的資訊,所以對同一個查詢的相似度分數理應相同。但並不是所有的情況下的規範化都是合理的,例如兩篇文件“百度”和“百度是個搜尋引擎”相比前者包含的資訊是後者的子集,因此如果查詢為“百度”兩篇文件的相似度分數應當相似,但如果依照公式1-1計算,則完全消除了文件長度的資訊,就會有些問題。為此Lucene將分母中文件長度項進行一個替換,以求一個更平滑和平衡的模型(如公式1-2)。


公式1-2

  其中doc_norm(d)為文件長度的規範化項,但並非一定是歸一化,一般對文件長度進行平滑幷包含越多資訊量的文件其值越大,該值將在索引過程中計算;doc_boost(d)則是該篇文件的先驗權重,一般情況下,視為所有文件都有相同的先驗權重,但Lucene允許使用者在索引過程中對文件設定先驗權重。

  同理對公式1-1中查詢項歐式長度進行相似替換(如公式1-3)。


公式1-3

  通常在一次檢索中查詢只有一個,因此對於該查詢則無需設定先驗的權重。同時,還需要說明的是,在一次檢索過程中該項是一個常數,對檢索結果沒有影響可以忽略,Lucene保留該項的原因是在有些情況下需要使不同查詢的檢索結果之間有可比性,比如相關反饋或者計算文件間的相似度。

  至此,相似度分數計算公式演變為公式1-4:


公式1-4

  接下來就可以開始計算兩個向量的點乘了,向量點乘是各維的乘積和,那麼就需要知道向量各維的值是如何構成的,在經典的VSM模型實現中,使用TF-IDF公式來計算如下:


公式1-5

  即tf和idf兩項的乘積,分別解釋一下:

  • tf,詞項頻數(term frequency);即詞項在文件中出現的頻數(次數)。一般情況下不會直接使用頻次,而是做一定形式的平滑或變換,但總的來說應與詞項在文件中出現的頻次成正比,後文會繼續說明。
  • df,文件頻數(document frequency);即整個資料集中出現過該指定詞項的文件的頻數(數量)。同樣一般也不會直接使用,後文會繼續說明。
  • idf,逆文件頻數(invert document frequency);即與文件頻數成反比的值。

  針對查詢q來說,每個詞項一般最多出現一次,因此一般tf(t,q)為一個常數,但為了使得檢索更加靈活,Lucene允許對查詢中的詞項設定不同的權重,所以各維為詞項權重與逆文件頻數的乘積(如公式1-6)。


公式1-6

  如我前文所述的,Lucene支援多域檢索且詞項也與域繫結,因此文件的歸一化長度其實也是與域繫結的,而非每篇文件一個值,而是每個域一個值:


公式1-7

  另外,Lucene還支援邏輯查詢和批量查詢,因此查詢有可能還有子查詢,在有多個並列析取子查詢的情況下,如果文件命中的子查詢越多,即對這個查詢的覆蓋率越高,則理應獲得越高的相似度得分,因此Lucene在相似度分值計算時加入了文件與查詢的協調分數(coordination):


公式1-8

  至此,就得到了Lucene相似度計算模型如(公式1-9):


公式1-9

  有了以上的原理分析,下面我們進入Lucene的程式碼部分,所有實現相似度評分相關的程式碼都位於org.apache.lucene.search.similarities包下,由於對VSM改動較大,因此在命名上Lucene並沒有延續VSM這個名字,而是將模型實現程式碼放在了TFIDFSimilarity類中,該類主要提供了以下的功能:

  • public abstract float coord(int overlap, int maxOverlap)
    抽象方法,用於為公式1-8部分的計算提供介面。
  • public abstract float queryNorm(float sumOfSquaredWeights)
    抽象方法,用於為公式1-3部分的計算提供介面。
  • public abstract float tf(float freq)
    抽象方法,用於為計算詞項頻數和相關平滑提供介面。
  • public abstract float idf(long docFreq, long numDocs)
    抽象方法,用於為計算你文件頻率和相關平滑提供介面。
  • public abstract void computeNorm(FieldInvertState state, Norm norm)
    抽象方法,用於為公式1-7部分的計算提供介面。

  如上所述,TFIDFSimilarity也只是個抽象類,實現了VSM模型計算的框架,但對於各構成元素並沒有給出具體的計算,事實上具體的計算是有其派生類DefaultSimilarity實現的,通過類名也能看出該類為Lucene預設的相似度評分類,所以綜合DefaultSimilarity、TFIDFSimilarity以及它們的基類Similarity的所有程式碼,才是實現了VSM模型的所有計算過程,下面就詳細分析一下程式碼。

(1)我們先來看一下公式1-8部分的計算。

  在DefaultSimilarity中重寫的coord給出了具體的計算過程:

程式碼1-1

public float coord(int overlap, int maxOverlap) {
    return overlap / (float)maxOverlap;
}

  就是文件覆蓋查詢詞項的數量除以查詢中所有詞項的數量。該方法會被查詢物件xxxQuery呼叫,並實際由查詢評分物件xxxScorer使用,例如在布林邏輯查詢過程中:

程式碼1-2  BooleanQuery程式碼片段

public float coord(int overlap, int maxOverlap) {
    return similarity.coord(overlap, maxOverlap);
}

程式碼1-3  BooleanScorer程式碼片段

final class BooleanScorer extends Scorer {
    …
    BooleanScorer(BooleanWeight weight, boolean disableCoord, int minNrShouldMatch,
                  List<Scorer> optionalScorers, List<Scorer> prohibitedScorers, int maxCoord) throws IOException {
        …
        coordFactors = new float[optionalScorers.size() + 1];
        for (int i = 0; i < coordFactors.length; i++) {
            coordFactors[i] = disableCoord ? 1.0f : weight.coord(i, maxCoord);
        }
    }
    …
    public boolean score(Collector collector, int max, int firstDocID) throws IOException {
        …
        if (current.coord >= minNrShouldMatch) {
            bs.score = current.score * coordFactors[current.coord];
            bs.doc = current.doc;
            bs.freq = current.coord;
            collector.collect(current.doc);
        }
        …
    }
}

  從程式碼1-2和1-3中可以看到,在BooleanQuery物件的coord方法呼叫了Similarity類中的coord方法,也就是DefaultSimility類重寫過的,BooleanScorer類在構造方法中建立了coord值的快取,即將所有可能的coord值全部計算出來放入成員陣列中,之後對於每篇文件的計算(score方法)直接通過棧定址即可,比3層的方法呼叫加除法要高效許多,Lucene中類似這種快取的運用隨處可見。最後將取到的coord值與其它項計算得到的分數相乘,得到最後的文件相似度分數。

(2)公式1-3計算。

  同樣是在DefaultSimilarity中重寫的queryNorm給出了具體的計算過程:

程式碼1-4

public float queryNorm(float sumOfSquaredWeights) {
    return (float)(1.0 / Math.sqrt(sumOfSquaredWeights));
}

  可以看出,該方法只是將一箇中間值進行開方後取倒數,而這個中間值並不是在Similarity類中得到的,而是通過查詢樹的Weight物件通過getValueForNormalization方法遞迴計算得到的,具體可以參考BooleanWeight和TermWeight類中的程式碼:

程式碼1-5  BooleanWeight程式碼片段

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();
        if (!clauses.get(i).isProhibited())
            // only add to sum for non-prohibited clauses
            sum += s;
    }
    sum *= getBoost() * getBoost();             // boost each sub-weight

    return sum ;
}

程式碼1-6  TermWeight程式碼片段

public TermWeight(IndexSearcher searcher, TermContext termStates)
throws IOException {
assert termStates != null : "TermContext must not be null";
    this.termStates = termStates;
    this.similarity = searcher.getSimilarity();
    this.stats = similarity.computeWeight(
        getBoost(),
        searcher.collectionStatistics(term.field()),
        searcher.termStatistics(term, termStates));
}
…
public float getValueForNormalization() {
    return stats.getValueForNormalization();
}

  程式碼1-5中是將查詢樹中所有的葉節點也就是詞項節點的getValueForNormalization值累加後再乘以查詢樹整體的權重平方,而程式碼1-6顯示詞項節點的getValueForNormalization計算直接呼叫了Similarity類中通過computeWeight方法得到的SimWeight類的同名方法如下:

程式碼1-7  TFIDFSimilarity程式碼片段

public final SimWeight computeWeight(float queryBoost, CollectionStatistics collectionStats, TermStatistics... termStats) {
    final Explanation idf = termStats.length == 1
                            ? idfExplain(collectionStats, termStats[0])
                            : idfExplain(collectionStats, termStats);
    return new IDFStats(collectionStats.field(), idf, queryBoost);
}
…
public Explanation idfExplain(CollectionStatistics collectionStats, TermStatistics termStats) {
    final long df = termStats.docFreq();
    final long max = collectionStats.maxDoc();
    final float idf = idf(df, max);
    return new Explanation(idf, "idf(docFreq=" + df + ", maxDocs=" + max + ")");
}
…
private static class IDFStats extends SimWeight {
    private final String field;
    private final Explanation idf;
    private float queryNorm;
    private float queryWeight;
    private final float queryBoost;
    private float value;

    public IDFStats(String field, Explanation idf, float queryBoost) {
        this.field = field;
        this.idf = idf;
        this.queryBoost = queryBoost;
        this.queryWeight = idf.getValue() * queryBoost; // compute query weight
    }

    @Override
    public float getValueForNormalization() {
        return queryWeight * queryWeight;  // sum of squared weights
    }

    @Override
    public void normalize(float queryNorm, float topLevelBoost) {
        this.queryNorm = queryNorm * topLevelBoost;
        queryWeight *= this.queryNorm;              // normalize query weight
        value = queryWeight * idf.getValue();         // idf for document
    }
}

  可以看出,TermWeight的getValueForNormalization方法呼叫IDFStats中的同名方法最終得到的是idf.getValue()* queryBoost的平方,而對於TermWeight來說,其queryBoost值即為查詢中該詞項的權重值,因此該值即為公式1-6的平方,至此我們可以得到query_norm(q)的具體計算公式如下:


公式1-10

(3)和式計算。

  最後在公式1-9中只剩下和式部分沒有計算,也就是向量點乘積和文件向量規範項的計算。這部分的計算主要由Similarity類的子類ExactSimScorer實現,在TFIDFSimilarity中即ExactTFIDFDocScorer,通過exactSimScorer方法生成(如程式碼1-8)。

程式碼1-8  TFIDFSimilarity程式碼片段

public final ExactSimScorer exactSimScorer(SimWeight stats, AtomicReaderContext context) throws IOException {
    IDFStats idfstats = (IDFStats) stats;
    return new ExactTFIDFDocScorer(idfstats, context.reader().normValues(idfstats.field));
}
…
private final class ExactTFIDFDocScorer extends ExactSimScorer {
    private final IDFStats stats;
    private final float weightValue;
    private final byte[] norms;
    private static final int SCORE_CACHE_SIZE = 32;
    private float[] scoreCache = new float[SCORE_CACHE_SIZE];

    ExactTFIDFDocScorer(IDFStats stats, DocValues norms) throws IOException {
        this.stats = stats;
        this.weightValue = stats.value;
        this.norms = norms == null ? null : (byte[])norms.getSource().getArray();
        for (int i = 0; i < SCORE_CACHE_SIZE; i++)
            scoreCache[i] = tf(i) * weightValue;
    }

    @Override
    public float score(int doc, int freq) {
        final float raw =                                // compute tf(f)*weight
            freq < SCORE_CACHE_SIZE                        // check cache
            ? scoreCache[freq]                             // cache hit
            : tf(freq)*weightValue;        // cache miss

        return norms == null ? raw : raw * decodeNormValue(norms[doc]); // normalize for field
    }

    @Override
    public Explanation explain(int doc, Explanation freq) {
        return explainScore(doc, freq, stats, norms);
    }
}

  如程式碼所示,在ExactTFIDFDocScorer的構造方法中主要是建立了分值的快取,在呼叫score方法進行分值計算將tf值與IDFStats物件中value欄位的值相乘,而value值的計算如程式碼1-7的normalize方法,是將公式1-6計算得到的值乘以一個規範化後的查詢權重(該值沒有出現在官方文件的計算公式1-9中,筆者也無法理解,需要繼續求證),之後再乘以idf,即的到了如下值:


公式1-11

  然後再乘以從索引檔案中解碼讀取到的文件(域)規範項decodeNormValue(norms[doc]),即得到了累加和式中的一次迭代,最後通過查詢樹進行累加完成和式的計算。而具體的tf’、idf’和norm計算實現都是在DefaultSimilarity類中(如程式碼1-9)。

程式碼1-9  DefaultSimilrity程式碼片段

public void computeNorm(FieldInvertState state, Norm norm) {
    final int numTerms;
    if (discountOverlaps)
        numTerms = state.getLength() - state.getNumOverlap();
    else
        numTerms = state.getLength();
    norm.setByte(encodeNormValue(state.getBoost() * ((float) (1.0 / Math.sqrt(numTerms)))));
}
…
public float tf(float freq) {
    return (float)Math.sqrt(freq);
}
…
public float idf(long docFreq, long numDocs) {
    return (float)(Math.log(numDocs/(double)(docFreq+1)) + 1.0);
}

  程式碼中計算過程如下:


公式1-12


公式1-13


公式1-14

  需要特別注意的是norm(t,d)的計算過程,首先該計算可通過設定選擇在計算指定域的詞項數量(長度)時是否包含位置相同的詞項(同義詞等);另外該計算是在索引階段進行而不是檢索階段,是因為所需的所有資訊在一篇文件被新增到索引時都為已知,將其計算一次並儲存在索引中可以提高檢索過程的效能,但同時也使得索引依賴相似度評分類的選擇,這是4.0與之前版本的較大變化之一,建立索引的時候需要注意。

  關於Lucene 4.0中使用的預設檢索模型VSM的介紹就到此為止,由於Lucene是一個較為複雜的系統,本節介紹的部分概念設計其它模組,會在分析響應模組時再詳細解釋。

相關推薦

Lucene 4.0 原理程式碼分析

  搜尋演算法的核心實際是對搜尋項之間相似度的打分策略,一個好的打分策略應該能夠綜合各種與搜尋項內容相關並對搜尋目的有幫助的所有因素,一般將這種策略叫做建模(modeling),由量化後的相關因素即特徵(feature)構成檢索(評分)模型,最後通過模型得到搜尋項之間的相似

Lucene原理程式碼分析解讀筆記

Lucene是一個基於Java的高效的全文檢索庫。 那麼什麼是全文檢索,為什麼需要全文檢索? 目前人們生活中出現的資料總的來說分為兩類:結構化資料和非結構化資料。很容易理解,結構化資料是有固定格式和

程式碼雨實現原理程式碼分析

閒來無事,好奇程式碼雨是怎麼實現的,早就聽說是利用連結串列,但自己卻想不出實現的思路,花了兩個晚上把程式碼看完了,分析都在程式碼裡,先看下效果吧。 在貼程式碼之前先簡單說下程式碼,方便讀者加深理解。 程式碼雨所用到的知識=簡單的windows api + C/C++的迴圈

《大型網站技術架構:核心原理案例分析》-- 讀書筆記 (5) :網購秒殺系統

案例 並發 刷新 隨機 url 對策 -- 技術 動態生成 1. 秒殺活動的技術挑戰及應對策略 1.1 對現有網站業務造成沖擊 秒殺活動具有時間短,並發訪問量大的特點,必然會對現有業務造成沖擊。對策:秒殺系統獨立部署 1.2 高並發下的應用、

【轉】Android 4.0 Launcher2源碼分析——啟動過程分析

handler flag 這一 第一次啟動 asynctask pla size ontouch wait Android的應用程序的入口定義在AndroidManifest.xml文件中可以找出:[html] <manifest xmlns:android="htt

Android 4.0 Launcher2源碼分析——主布局文件(轉)

not sch png 默認 顯示效果 target ots eat col 本文來自http://blog.csdn.net/chenshaoyang0011 Android系統的一大特色是它擁有的桌面通知系統,不同於IOS的桌面管理,Android有一個桌面系統用於管

《大型網站技術架構:核心原理案例分析》【PDF】下載

優化 均衡 1.7 3.3 架設 框架 應用服務器 博客 分布式服務框架 《大型網站技術架構:核心原理與案例分析》【PDF】下載鏈接: https://u253469.pipipan.com/fs/253469-230062557 內容簡介 本書通過梳理大型網站技

Redis實現分布式鎖原理實現分析

數據表 防止 中一 csdn 訂單 not 產生 www 整體 一、關於分布式鎖 關於分布式鎖,可能絕大部分人都會或多或少涉及到。 我舉二個例子: 場景一:從前端界面發起一筆支付請求,如果前端沒有做防重處理,那麽可能在某一個時刻會有二筆一樣的單子同時到達系統後臺。 場

閱讀《大型網站技術架構:核心原理案例分析》第五、六、七章,結合《河北省重大技術需求征集系統》,列舉實例分析采用的可用性和可修改性戰術

定時 並不會 表現 做出 span class 硬件 進行 情況   網站的可用性描述網站可有效訪問的特性,網站的頁面能完整呈現在用戶面前,需要經過很多個環節,任何一個環節出了問題,都可能導致網站頁面不可訪問。可用性指標是網站架構設計的重要指標,對外是服務承諾,對內是考核指

《大型網站技術架構:核心原理案例分析》結合需求征集系統分析

運行 模塊 正常 一致性hash 產品 進行 OS 很多 層次 閱讀《大型網站技術架構:核心原理與案例分析》第五、六、七章,結合《河北省重大技術需求征集系統》,列舉實例分析采用的可用性和可修改性戰術,將上述內容撰寫成一篇1500字左右的博客闡述你的觀點。 閱

《大型網站技術架構:核心原理案例分析》讀後感

TP bubuko 一個 nbsp 分享 架構 優化 技術分享 src 李智慧的著作《大型網站技術架構:核心原理與案例分析》,寫得非常好, 本著學習的態度,對於書中的關於性能優化的講解做了一個思維導圖,供大家梳理思路和學習之用。拋磚引玉。 《大型網站技術架構

OpenCV學習筆記(31)KAZE 演算法原理原始碼分析(五)KAZE的原始碼優化及SIFT的比較

  KAZE系列筆記: 1.  OpenCV學習筆記(27)KAZE 演算法原理與原始碼分析(一)非線性擴散濾波 2.  OpenCV學習筆記(28)KAZE 演算法原理與原始碼分析(二)非線性尺度空間構建 3.  Op

OpenCV學習筆記(30)KAZE 演算法原理原始碼分析(四)KAZE特徵的效能分析比較

      KAZE系列筆記: 1.  OpenCV學習筆記(27)KAZE 演算法原理與原始碼分析(一)非線性擴散濾波 2.  OpenCV學習筆記(28)KAZE 演算法原理與原始碼分析(二)非線性尺度空間構

Ceilometer Compute Agent 原理程式碼分析

本部落格所有文章採用的授權方式為 自由轉載-非商用-非衍生-保持署名 ,轉載請務必註明出處,謝謝。 宣告: 本部落格歡迎轉發,但請註明出處,保留原作者資訊 部落格地址:孟阿龍的部落格 所有內容為本人學習、研究、總結。如有雷同,實屬榮幸 注: 本文以Opens

SURF演算法原理原始碼分析

如果說SIFT演算法中使用DOG對LOG進行了簡化,提高了搜尋特徵點的速度,那麼SURF演算法則是對DoH的簡化與近似。雖然SIFT演算法已經被認為是最有效的,也是最常用的特徵點提取的演算法,但如果不借助於硬體的加速和專用影象處理器的配合,SIFT演算法以現有的計算機仍然很難達到實時的程度。對於需要

SIFT原理原始碼分析:DoG尺度空間構造

《SIFT原理與原始碼分析》系列文章索引:http://blog.csdn.net/xiaowei_cqu/article/details/8069548 尺度空間理論   自然界中的物體隨著觀測尺度不同有不同的表現形態。例如我們形容建築物用“米”,觀測分子、原子等用“納米”。

《大型網站技術架構之核心原理案例分析》讀書筆記

      架構!對於工作經驗尚淺的我是理應遠遠不用考慮的倆字。不過就像這本書所說到的一個好的網站架構體系,不僅僅是架構師個人的架構,而是和參與共同建設的人共同貢獻,讓參與的人覺得自己是架構體系的建設者之一,就越是自動承擔開發過程的責任和共同維護架構和改善軟體。  

《大型網站技術架構:核心原理案例分析》筆記

· 大型網站軟體系統的特點 · 大型網站架構演化發展歷程     · 初始階段的網站架構         · 需求/解決問題         · 架構     · 應用服務和資料

Shuffle操作的原理原始碼分析

普通的shuffle操作 第一個特點,     在Spark早期版本中,那個bucket快取是非常非常重要的,因為需要將一個ShuffleMapTask所有的資料都寫入記憶體快取之後,才會重新整理到磁碟。但是這就有一個問題,如果map side資料過多

TaskScheduler原理原始碼分析

接DAGScheduler原始碼分析stage劃分演算法,task最佳位置計算演算法taskScheduler的submitTask()方法 在TaskScheduler類中 ①在SparkContext原始碼分析中,會建立TaskScheduler的時候,會建立一個SparkDe