Luene的核心應用場景是全文檢索。簡單來講,就是經過用戶輸入的關鍵詞來匹配相關文檔,而後根據匹配程度返回TopN的查詢結果給用戶。 這裏須要解決的一個核心問題就是如何快速返回TopN的結果,這本質上是一個排序的問題。提及排序,咱們有不少選擇,冒泡,快排,歸併...。 這些排序算法在數據量小的時候,不是問題。一旦數據量過大,就成爲問題了。算法
例如對1000萬的數組排序:數組
Integer[] a = new Integer[10000000]; for(int i=0;i<10000000;i++){ a[i] = (int) (Math.random()*10000000); } long start = System.currentTimeMillis(); Arrays.sort(a); System.out.println((System.currentTimeMillis() - start) +" 毫秒");
在個人電腦耗時須要5秒左右, 這個等待時間對於用戶體驗來講,就不那麼feeling good了。 數據結構
這時候,該考慮優化了。優化基本上是一個作減法的過程。再回到咱們的核心需求: 基於搜索關鍵詞返回TopN的結果。 也就是說,咱們只須要TopN的結果有序就能夠了。 基於上述需求,咱們引入一個新的數據結構: 堆(Heap)。less
堆是一種特殊的二叉樹。所謂二叉樹就是每一個節點最多有兩個子節點: 最多生二胎,超生不被容許的。dom
對於二叉樹這種樹形結構,最核心的關係就是父子節點關係。 定義不一樣的節點關係,咱們就能獲得豐富多彩的數據結構,以應對不一樣場景的業務問題。好比:ide
規定「子節點不能大於父節點」, 咱們能夠得出根節點是最大的節點, 獲得大頂堆。性能
規定「子節點不能小於父節點」, 咱們能夠得出根節點是最小的節點, 獲得小頂堆。學習
規定「根節點大於左子樹,小於右子樹;子樹亦是如此」, 咱們獲得二叉搜索樹;爲了使二叉搜索樹的左右儘可能平衡,咱們又獲得了「紅黑樹」,「AVL樹」,Treap等不一樣策略的平衡樹。優化
這些概念性的東西,能理解就OK.this
理解了堆的前因後果, 咱們可能會有點困惑,它並無直接維護一個有序的結構。 是的,它沒有直接維護有序的結構,它是經過刪除數據實現排序功能的。理解這一點特別重要。 以大頂堆爲例: 因爲堆頂是最大的元素,因此咱們能確信,對於一個堆: 咱們只要不斷地刪除堆頂的數據,直至空堆,就能獲得一個有序的結果。這就是堆排序的思想。
那麼如何利用堆實現TopN的有序輸出呢? 以搜索的打分做爲排序項,咱們但願輸出得分最高的N個結果。 咱們先遍歷N個結果,獲得有N個元素的小頂堆。因爲堆頂的元素最小, 遍歷剩下的打分結果,只須要跟堆的根節點對比便可。若是打分結果小於堆的根節點,棄之;若是打分結果大於堆的根節點,刪除根節點;而後使用該打分結果更新到堆中。 這樣最後這個堆就維護了咱們想要的TopN。
例如對1000萬的數據,咱們給出最大的前100個數,代碼以下:
Integer[] a = new Integer[10000000]; for(int i=0;i<10000000;i++){ a[i] = (int) (Math.random()*10000000); } long start = System.currentTimeMillis(); PriorityQueue<Integer> pq = new PriorityQueue<Integer>(100) { @Override protected boolean lessThan(Integer t1, Integer t2) { return t1 < t2; } }; for(int i=0;i<10000000;i++){ pq.insertWithOverflow(a[i]); } Integer[] b = new Integer[100]; for(int i=99;i>=0;i--){ b[i] = pq.pop(); } System.out.println((System.currentTimeMillis() - start) +" 毫秒"); System.out.println(Arrays.asList(b));
這個耗時只須要50多毫秒。 這個性能差距幾乎是100倍。可見堆這種數據結構在TopN這個場景下是多麼適合。
其實JDK有本身基於堆實現的優先隊列PriorityQueue, 爲啥Lucene要再造一遍輪子呢?
JDK默認的PriorityQueue是能夠自動擴展的,Lucene須要定長的。
JDK默認的PriorityQueue將數據結構封裝得比較緊密,而Lucene須要必定的靈活性,好比調整堆頂。
小頂堆是一種二叉樹,因此其邏輯結構大體以下:
1 3 2 5 8 7 6
若是觀察,能夠發現這個一個規律,就是第一層只有1個元素;第二層最多有2個元素; 第三層最多有4個元素, 即第N層有2^(n-1)個元素。 這個規律後面有用。
那麼怎麼編碼實現一個堆呢? 最簡單的實現方式是基於數組,以Lucene的實現爲例,學習一下:
public abstract class PriorityQueue<T> { private int size; private final int maxSize; private final T[] heap;
定義了一個數組。 只須要作以下的規定,那麼就能知足對的邏輯結構:
1. heap[0]位空置不用。 2. heap[1]爲根節點。 3. heap[2~3]爲第二層,heap[4~7] 爲第三層 ... heap[2^n ~ 2^(n+1)-1]爲第n+1層
這樣,元素在數組的哪一個位置,咱們就能知道它屬於哪一層了。
接下來要解決的問題是:
假設前面有N個元素了, 那麼代碼很簡單
public final T add(T element) { ++this.size; this.heap[this.size] = element; this.upHeap(this.size); return this.heap[1]; }
兩步走: s1 將元素添加到尾巴上。 s2: 因爲這個元素有可能比其父節點小,因此遞歸地跟其父節點比較,換位置便可,這裏有點冒泡的感受。即想象把乒乓球按入水中,鬆手後就會上浮。
public final T pop() { if (this.size > 0) { T result = this.heap[1]; this.heap[1] = this.heap[this.size]; this.heap[this.size] = null; --this.size; this.downHeap(1); return result; } else { return null; } }
兩步走: s1: 用數組尾巴上的元素覆蓋跟節點元素。 s2: 因爲這個元素是否能勝任根節點這個位置還不肯定,所以須要跟兩個子節點比較,調整位置。這裏有絲下沉的感受。即想象把鐵球丟入水中,本身就沉了下去。
這裏,堆的插入和刪除操做仍是思路仍是比較輕奇的,值得好好揣摩一番。
在Lucene中,PriorityQueue有那些應用場景呢?
總之,該數據結構在Lucene中有30~40個子類,應用十分普遍。瞭解其實現機制,對於瞭解其餘的功能大有裨益。