随着业务快速发展,基于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的一些基础原理以及自身业务,对索引文件大小进行了优化,使得索引文件大小降低了一半多。
检索实践文章系列: