隨着業務快速發展,基於lucene的索引文件zip壓縮後也接近了GB量級,而保持索引文件大小爲一個能夠接受的範圍很是有必要,不只能夠提升索引傳輸、讀取速度,還能提升索引cache效率(lucene打開索引文件的時候每每會進行緩存,好比MMapDirectory經過內存映射方式進行緩存)。html
如何下降咱們的索引文件大小呢?本文進行了一些嘗試,下文將一一介紹。java
lucene本質上是一個全文檢索引擎而非傳統的數據庫系統,它基於倒排索引,很是適合處理文本,而處理數值類型卻不是強項。數據庫
舉個應用場景,假設咱們倒排存儲的是商家,每一個商家都有人均消費,用戶想查詢範圍在500~1000這一價格區間內的商家。緩存
一種簡單直接的想法就是,將商家人均消費當作字符串寫入倒排(如圖所示),在進行區間查詢時:1)遍歷價格分詞表,將落在此區間範圍內的倒排id記錄表找出來;2)合併倒排id記錄表。這裏兩個步驟都存在性能問題:1)遍歷價格分詞表,比較暴力,並且經過term查找倒排id記錄表次數過多,性能很是差,在lucene裏查詢次數過多,可能會拋出Too Many Boolean Clause的Exception。2)合併倒排id記錄表很是耗時,說白了這些倒排id記錄表都在磁盤裏。數據結構
固然還有種思路就是將其數字長度補齊,假設全部商家的人均消費在[0,10000]這一區間內,咱們存儲1時寫到倒排裏就是00001(補齊爲5位),因爲分詞表會按照字符串排序好,所以咱們沒必要遍歷價格分詞表,經過二分查找能快速找到在某一區間範圍內的倒排id記錄表,但這裏一樣未能解決查詢次數過多、合併倒排id記錄表次數過多的問題。此外怎樣補齊也是問題,補齊太多浪費空間,補齊太少存儲不了太大範圍值。函數
爲解決這一問題, Schindler和 Diepenbroek提出了基於trie的解決方法,此方法08年發表在 Computers & Geosciences (地理信息科學sci期刊,影響因子1.9),也被lucene 2.9以後版本採用。( Schindler, U, Diepenbroek, M, 2008. Generic XML-based Framework for Metadata Portals. Computers & Geosciences 34 (12),論文:http://epic.awi.de/17813/1/Sch2007br.pdf)post
簡單來講,整數423不是直接寫入倒排,而是分割成幾段寫入倒排,以十進制分割爲例,423將被分割爲42三、4二、4這三個term寫入, 本質上這些term造成了trie樹(如圖所示)。性能
如何查詢呢?假設咱們要查詢[422, 642]這一區間範圍的doc,首先在樹的最底層找到第一個比422大的值,即423,以後查找423的右兄弟節點,發現沒有便找其父節點的右兄弟(找到44),對於642也是,找其左兄弟節點(641),以後找父節點的左兄弟(63),一直找到二者的公共節點,最終找出42三、4四、五、6三、64一、642這6個term便可。經過這種方法,原先須要查詢42三、44五、44六、44八、52一、52二、63二、63三、63四、64一、642這11次term對應的倒排id列表,併合並這11個term對應的倒排id列表,如今僅須要查詢42三、4四、五、6三、64一、642這6個term對應的倒排id列表併合並,大大下降了查詢次數以及合併次數,尤爲是查詢區間範圍較大時效果更爲明顯。學習
這種優化方法本質上是一種以空間換時間的方法,能夠看到term數目將增大許多。優化
在實際操做中,lucene將數字轉換成2進制來處理,並且實際上這顆trie樹也無需保存數據結構,傳統trie一個節點會有指向孩子節點的指針, 同時會有指向父節點的指針,而在這裏只要知道一個節點,其父節點、右兄弟節點均可以經過計算獲得。此外lucene也提供了precisionstep這一字段用於設置分割長度,默認狀況下int、double、float等數字類型precisionstep爲4,就是按4位二進制進行分割。precisionstep長度設置得越短,分割的term越多,大範圍查詢速度也越快,precisionstep設置得越長,極端狀況下設置爲無窮大,那麼不會進行trie分割,範圍查詢也沒有優化效果,precisionstep長度須要結合自身業務進行優化。
咱們的應用中不少field都是數值類型,好比id、avescore(評價分)、price(價格)等等,可是用於區間範圍查詢的數值類型很是少,大部分都是直接查詢或者爲進行排序使用。
所以優化方法很是簡單,將不須要使用範圍查詢的數字字段設置precisionstep爲Intger.max,這樣數字寫入倒排僅存一個term,能極大下降term數量。
1 public final class CustomFieldType { 2 public static final FieldType INT_TYPE_NOT_STORED_NO_TIRE = new FieldType(); 3 static { 4 INT_TYPE_NOT_STORED_NO_TIRE.setIndexed(true); 5 INT_TYPE_NOT_STORED_NO_TIRE.setTokenized(true); 6 INT_TYPE_NOT_STORED_NO_TIRE.setOmitNorms(true); 7 INT_TYPE_NOT_STORED_NO_TIRE.setIndexOptions(FieldInfo.IndexOptions.DOCS_ONLY); 8 INT_TYPE_NOT_STORED_NO_TIRE.setNumericType(FieldType.NumericType.INT); 9 INT_TYPE_NOT_STORED_NO_TIRE.setNumericPrecisionStep(Integer.MAX_VALUE); 10 INT_TYPE_NOT_STORED_NO_TIRE.freeze(); 11 } 12 }
doc.add(new IntField("price", price, CustomFieldType.INT_TYPE_NOT_STORED_NO_TIRE));//人均消費
優化以後效果明顯,索引壓縮包大小直接減小了一倍。
仍是同樣的話,lucene基於倒排索引,很是適合文本,而對於空間類型數據卻不是強項。
舉個應用場景,每個商家都有惟一的經緯度座標(x, y),用戶想篩選附近5公里的商家。
一種直觀的想法是將經度x、維度y分別當作兩個數值類型字段寫到倒排裏,而後查詢的時候遍歷全部的商家,計算與用戶的距離,並保留小於5公里的商家。這種方法缺點很明顯:1)須要遍歷全部的商家,很是暴力;2)此外球面距離計算非涉及到大量的三角函數計算,效率較低(博主研發了一種快速距離計算方法,能提升至少10倍計算速度:地理空間距離計算優化)。
簡單的優化方法使用矩形框對這些商家進行過濾,以後對過濾後的商家進行距離計算,保留小於5公里的商家,這種方法儘管極大下降了計算量,但仍是須要遍歷全部的商家。
lucene採用geohash的方法對經緯度進行編碼(geohash介紹參見:GeoHash)。簡單描述下,geohash對空間不斷進行劃分並對每個劃分子空間進行編碼,好比咱們整個北京地區被編碼爲「w」,那麼再對北京一分爲4,某一子空間編碼爲「WX」,對「WX」子空間再進行劃分,對各個子空間再進行標識,例如「WX4」(簡單能夠這麼理解)。
那麼一個經緯度(x,y)怎樣寫入到倒排索引呢?假設某一經緯度落在「WX4」子空間內,那麼經緯度將以「W」、「WX」、「WX4」這三個term寫入到倒排。
如何進行附近查詢呢?首先將咱們附近5km劃分一個個格子,每一個格子有geohash的編碼,將這些編碼當作查詢term,去倒排查詢便可,好比附近5km的geohash格子對應的編碼是「WX4」,那麼直接就能將落在此空間範圍的商家找出。
上述方法本質上也是一種以空間換時間的方法,好比一個經緯度(x,y),只有兩個字段,可是以geohash進行編碼將產生許多term並寫入倒排。
lucene默認最長的geohash長度爲24,也就是一個經緯度將以24個字符串的形式來寫入到倒排中。最初採用的geohash長度爲11,但實際上針對咱們的需求,geohash長度爲9的時候已經足夠知足咱們的需求(geohash長度爲9大約表明了5*4米的格子)。
下表表示geohash長度對應的精度,摘自維基百科:http://en.wikipedia.org/wiki/Geohash
geohash length
|
lat bits
|
lng bits
|
lat error
|
lng error
|
km error
|
---|---|---|---|---|---|
1 | 2 | 3 | ±23 | ±23 | ±2500 |
2 | 5 | 5 | ± 2.8 | ± 5.6 | ±630 |
3 | 7 | 8 | ± 0.70 | ± 0.7 | ±78 |
4 | 10 | 10 | ± 0.087 | ± 0.18 | ±20 |
5 | 12 | 13 | ± 0.022 | ± 0.022 | ±2.4 |
6 | 15 | 15 | ± 0.0027 | ± 0.0055 | ±0.61 |
7 | 17 | 18 | ±0.00068 | ±0.00068 | ±0.076 |
8 | 20 | 20 | ±0.000085 | ±0.00017 | ±0.019 |
1 private void spatialInit() { 2 this.ctx = SpatialContext.GEO; // 選擇geo表示經緯度座標,會按照球面計算距離,不然是平面歐式距離 3 int maxLevels = 9; // geohash長度爲9表示5*5米的格子,長度過長會形成查詢匹配開銷 4 SpatialPrefixTree grid = new GeohashPrefixTree(ctx, maxLevels); // geohash字符串匹配樹 5 this.strategy = new RecursivePrefixTreeStrategy(grid, "poi"); // 遞歸匹配 6 }
此優化效果結果未作記錄,不過經緯度geohash編碼佔據了term數量的25%,而咱們又將geohash長度從11減小到9(下降18%),至關於整個term數量下降了25%*18%=4.5%。
上面兩種方法本質上經過減小term數量來減小索引文件大小,下面的方法走的是另外一種方式。
從lucene查出一堆docid以後,須要經過docid找出相應的document,並找出裏面一些須要的字段,例如id,人均消費等等,而後返回給客戶端。但實際上咱們只須要獲取id,經過這些id再去請求DB/Cache獲取額外的字段。
所以優化方法是隻存儲id等必須的字段,對於大部分字段咱們只索引而不存儲,經過這種方法,索引壓縮文件下降了10%左右。
1 doc.add(new StringField("price", each, Field.Store.NO));
本文基於lucene的一些基礎原理以及自身業務,對索引文件大小進行了優化,使得索引文件大小降低了一半多。
檢索實踐文章系列: