lucene如何通过docId快速查找field字段以及最近距离等信息?
http://www.cnblogs.com/LBSer/p/4419052.html
1 问题描述
我们的检索排序服务往往需要结合个性化算法来进行重排序,一般来说分两步:1)进行粗排序,这一过程由检索引擎快速完成;2)重排序,粗排序后将排名靠前的结果发送给个性化服务引擎,由个性化服务引擎进行深度排序。在我们的业务场景下检索引擎除了传递doc列表,还要传业务字段如商家id以及用户位置与该doc的最近距离。
我们的检索引擎基于lucene,而lucene查询的结果只包含docId以及对应的score,并未直接提供我们要传给个性化服务的业务字段列表以及对应的距离,因此本文要解决的问题是:如何根据docId快速查找field字段以及该doc对应的距离?
2 传统方法—从正排文件中获取数据
通过倒排检索得到的是docId,而直观上看可以根据docId从正排中得到具体的doc内容字段例如dealId等。
首先需要将数据写入正排,如果没写入当然就查询不了。如何写入呢?我们将dealId(DealLuceneField.ATTR_ID)、该deal对应的经纬度字符串(DealLuceneField.ATTR_LOCATIONS,多个时以“,”分隔)写入索引中,Field.Store.YES表示将信息存储在正排里,lucene会将正排信息存放在fdx、fdt两个文件中,fdt存放具体的数据,fdx是对fdt的一个索引(第n个doc数据在fdt中的位置)。
Document doc = new Document();
doc.add(new StringField(LuceneField.ATTR_ID, String.valueOf(id, Field.Store.YES));
doc.add(new StringField(LuceneField.ATTR_LOCATIONS, buildMlls(mllsSet, id), Field.Store.YES));
如何查询呢?
1)直接查询
通过docId直接查询得到document,并将document的内容取出,比如取出经纬度字符串后需要计算最近的距离。
for (int i = 0; i < sd.length; i++) {
Document doc = searcher.doc(sd[i].doc); //sd[i].doc就是docId,earcher.doc(sd[i].doc)就是根据docId查找相应的document
didList.add(Integer.parseInt(doc.get(LuceneField.ATTR_ID)));
if (query.getSortField() == DealSortEnum.distance) {
。。。
String[] mlls = locations.split(" ");
double dis = findMinDistance(mlls, query.getMyPos()) / 1000;
distBuilder.append(dis).append(",");
}
}
在实际运行中,根据docId获取经纬度信息并计算最短距离这一过程将耗费8ms左右,而且有的时候抖动至20多ms。
2)优化查询
直接查询时将返回所有Field.Store.YES的field数据,而事实上我们仅需要获取id、localtion这两个field的数据,因此优化方法是调用doc函数时传入需要获取的field集合,这样避免获取了整个数据带来的开销。
for (int i = 0; i < sd.length; i++) {
Document doc = searcher.doc(sd[i].doc, fieldsToLoad);
didList.add(Integer.parseInt(doc.get(LuceneField.ATTR_ID)));
if (query.getSortField() == DealSortEnum.distance) {
String locations = doc.get(LuceneField.ATTR_LOCATIONS);
String[] mlls = locations.split(" ");
double dis = findMinDistance(mlls, query.getMyPos()) / 1000;
distBuilder.append(dis).append(",");
}
}
然而在实际应用中相对于直接查询性能上并未有所提升。
原因有两点:1)使用Field.Store.YES的字段较少,除了id和location之外,只有两个field存进正排索引中,这种优化对于大量field存储进正排索引才有效果;2)从正排获取数据底层是通过读取文件来获得的,虽然我们已经通过内存映射打开索引文件,但是由于每次查询还需要定位解析数据,浪费大量开销。
3 优化方法1—从倒排的fieldcache中获取数据
从正排获取dealId以及location这两个字段的数据比较缓慢,如果能将这两个字段进行缓存那么将大大提高计算效率,比如类似一个map,key是docId,value是dealId或者mlls。可惜lucene并未向正排提供这种缓存,因为lucene主要优化的是倒排。
在lucene中,一些用于排序的字段,比如我们使用的“weight”字段,为了加快速度,lucene 在首次使用的时候将该“weight”这个field下所有term转换成float(如下图所示),并存放入FieldCache中,这样在第二次使用的时候就能直接从该缓存中获取。

FieldCache.Floats weights = FieldCache.DEFAULT.getFloats(reader, "weight", true); //获取“weights”这一field的缓存,该缓存key是docId,value是相应的值
float weightvalue = weights.get(docId); // 通过docId获取值
for (int i = 0; i < sd.length; i++) {
。。。
if (query.getSortField() == DealSortEnum.distance) {
BytesRef bytesRefMlls = new BytesRef();
mllsValues.get(sd[i].doc, bytesRefMlls);
String locations = bytesRefMlls.utf8ToString();
if (StringUtils.isBlank(locations))
continue;
String[] mlls = locations.split(" ");
double dis = findMinDistance(mlls, query.getMyPos())/1000;
distBuilder.append(dis).append(",");
}
}
通过这种方式优化之后根据docId获取经纬度信息并计算最短距离这一过程平均响应时间从8ms降低为2ms左右,即使抖动响应时间也不超过10ms。
4 优化方法2—使用ShapeFieldCache
使用fieldcache增加了内存消耗,尤其是location这一字段,这里面存放的是该文档对应的经纬度字符串,对内存的消耗尤其巨大,尤其是某些文档的location字段存放着几千个经纬度(这在我们业务场景里不算少见)。
事实上我们不需要location这一字段,因为我们在建立索引的时候已经通过如下方式将经纬度写入到索引中,而且lucene在使用时会一次性将所有doc对应的经纬度都放至ShapeFieldCache这一缓存中。
for (String mll : mllsSet) {
String[] mlls = mll.split(",");
Point point = ctx.makePoint(Double.parseDouble(mlls[1]),Double.parseDouble(mlls[0]));
for (IndexableField f : strategy.createIndexableFields(point)) {
doc.add(f);
}
}
查询代码如下。
StringBuilder distBuilder = new StringBuilder();
BinaryDocValues idValues = binaryDocValuesMap.get(LuceneField.ATTR_ID);
FunctionValues functionValues = distanceValueSource.getValues(null, context);
BinaryDocValues idValues = binaryDocValuesMap.get(LuceneField.ATTR_ID);
for (int i = 0; i < sd.length; i++) {
BytesRef bytesRef = new BytesRef();
idValues.get(sd[i].doc, bytesRef);
String id = bytesRef.utf8ToString();
didList.add(Integer.parseInt(id));
if (query.getSortField() == SortEnum.distance) {
double dis = functionValues.doubleVal(doc)/1000;
distBuilder.append(dis).append(",");
}
}
a)进一步优化
上面方法节省了内存开销,但未避免计算开销。我们知道lucene是提供按距离排序功能的,但是lucene只是完成了排序,并告诉我们相应的docId以及score,但并未告诉我们每个deal与用户的最近距离值。有没有什么方法能将距离保存下来呢?
我的方法是通过改写lucene的collector以及lucene使用的队列PriorityQueue,通过重新实现这两个数据结构从而将距离值保存为score,这样就避免了冗余计算。核心代码如下:
@Override
protected void populateResults(ScoreDoc[] results, int howMany) {
// avoid casting if unnecessary.
FieldValueHitQueue<SieveFieldValueHitQueue.Entry> queue = (FieldValueHitQueue<FieldValueHitQueue.Entry>) pq;
for (int i = howMany - 1; i >= 0; i--) {
FieldDoc fieldDoc = queue.fillFields(queue.pop());
results[i] = fieldDoc;
results[i].score = Float.valueOf(String.valueOf(fieldDoc.fields[0])); //记录距离
}
}
这样优化后,获取数据的平均响应时间从2ms将至0ms,且从未出现抖动。
此外由于避免了在内存中加载location这个字段,gc的响应时间下降一半,服务整体平均响应时间也下降许多。
5 展望
针对如何通过docId快速查找field字段以及最近距离等信息这一问题,本文提供了多种方法并一一尝试,包括从正排文件获取,从倒排fieldcache里获取,以及经纬度从ShapeFieldCache获取。此外通过改造lucene的收集器和队列,避免了距离的二次计算。上述这些优化大幅度提升了检索服务的性能。
通过docId获取field数据的方式还有很多,例如docvalue等,以后将对这些方法进行探索。
检索实践文章系列:
lucene如何通过docId快速查找field字段以及最近距离等信息?的更多相关文章
- [SQL Server]如何快速查找使用了某个字段的所有存储过程
[SQL Server]如何快速查找使用了某个字段的所有存储过程 当一个系统中使用了很多的表,并且存在大量的存储过程,当数据库中的某个表删除了某个字段,那么相应的存储过程也需要改动,但是我 ...
- (私人收藏)[开发必备]最全Java离线快速查找手册(可查询可学习,带实例)
(私人收藏)[开发必备]最全Java离线快速查找手册(可查询可学习,带实例) https://pan.baidu.com/s/1L54VuFwCdKVnQGVc8vD1TQnwmj java手册 Ja ...
- PHP实现文本快速查找 - 二分查找
PHP实现文本快速查找 - 二分查找法 起因 先说说事情的起因,最近在分析数据时经常遇到一种场景,代码需要频繁的读某一张数据库的表,比如根据地区ID获取地区名称.根据网站分类ID获取分类名称.根据关键 ...
- 【NYOJ-187】快速查找素数—— 枚举法、筛选法、打表法
快速查找素数 时间限制:1000 ms | 内存限制:65535 KB 难度:3 描述 现在给你一个正整数N,要你快速的找出在2.....N这些数里面所有的素数. 输入 给出一个正整数数N(N ...
- Eclipse添加快速查找Dao中方法所对应的Mybatis XML映射SQL的插件
Dao关联Mybatis快速查找的插件安装地址:http://dl.bintray.com/harawata/eclipse 安装步骤: ①Eclipse ==> Help ==> Ins ...
- 《Java虚拟机原理图解》1.4 class文件里的字段表集合--field字段在class文件里是如何组织的
0.前言 了解JVM虚拟机原理是每个Java程序猿修炼的必经之路.可是因为JVM虚拟机中有非常多的东西讲述的比較宽泛.在当前接触到的关于JVM虚拟机原理的教程或者博客中,绝大部分都是充斥的文字性的描写 ...
- jQuery实现的快速查找
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- 快速排序/快速查找(第k个, 前k个问题)
//快速排序:Partition分割函数,三数中值分割 bool g_bInvalidInput = false; int median3(int* data, int start, int end) ...
- hash数组快速查找一个字符串中出现最多的字符,并统计出现的次数
如何快速查找一个字符串中出现最多的字符,并统计出现的次数? 可以使用hash数组,也就是关联数组实现快速查找功能. function seek(str) { var hash = []; var ma ...
随机推荐
- (四) 一起学 Unix 环境高级编程(APUE) 之 系统数据文件和信息
. . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...
- [原创][LaTex]LaTex学习笔记入门
0. 简介 LaTEX(/ˈlɑːtɛx/,常被读作/ˈlɑːtɛk/或/ˈleɪtɛk/),文字形式写作LaTeX,是一种基于TEX的排版系统,由美国电脑学家莱斯利·兰伯特在20世纪80年代初期开发 ...
- python 文件拷贝
用python实现了一个小型的自动发版本的工具.这个“自动发版本”有点虚, 只是简单地把debug 目录下的配置文件复制到指定目录,把Release下的生成文件复制到同一指定,过滤掉不需要的文件夹(. ...
- ios NSThred多线程简单使用
关于NSThred开启多线程的方法 - (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg [self perfor ...
- Java学习笔记16--异常
异常 异常是导致程序中断运行的一种指令流,如果不对异常进行正确的处理,则可能导致程序的中断执行,造成不必要的损失, 所以在程序的设计中必须要考虑各种异常的发生,并正确的做好相应的处理,这样才能保证程序 ...
- ios 用户相册
## 获得自定义的所有相簿 // 获得所有的自定义相簿 PHFetchResult<PHAssetCollection *> *assetCollections = [PHAssetCol ...
- 网易视频云技术分享:linux软raid的bitmap分析
网易视频云是网易倾力打造的一款基于云计算的分布式多媒体处理集群和专业音视频技术,提供稳定流畅.低时延.高并发的视频直播.录制.存储.转码及点播等音视频的PAAS服务,在线教育.远程医疗.娱乐秀场.在线 ...
- Android学习--摘录
今天准备开始毕业设计--做一个android应用.无奈android基础木有一点,以前做过一个蓝牙应用,不过对android各方面的知识还是不懂啊,今天在android主页上看英文的android开发 ...
- C#读写xml文件的常用方法
已知有一个XML文件(bookshop.xml)如下: <?xml version="1.0" encoding="gb2312" ?> <b ...
- 并查集+关系的传递(poj 1182)
题目:食物链 题意:给定一些关系.判断关系的正确性,后给出的关系服从之前的关系: 思路:难点不在并查集,在于关系的判断,尤其是子节点与根节点的关系的判断: 这个关系看似没给出,但是给出子节点与父节点的 ...