lucene如何通过docId快速查找field字段以及最近距离等信息?

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(DealLuceneField.ATTR_ID, String.valueOf(id, Field.Store.YES));
doc.add(new StringField(DealLuceneField.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(DealLuceneField.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(DealLuceneField.ATTR_ID)));
   if (query.getSortField() == DealSortEnum.distance) {
       String locations = doc.get(DealLuceneField.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(DealLuceneField.ATTR_ID);
FunctionValues functionValues = distanceValueSource.getValues(null, context);
BinaryDocValues idValues = binaryDocValuesMap.get(DealLuceneField.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() == DealSortEnum.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.
        SieveFieldValueHitQueue<SieveFieldValueHitQueue.Entry> queue = (SieveFieldValueHitQueue<SieveFieldValueHitQueue.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等,以后将对这些方法进行探索。

时间: 2024-10-19 22:29:29

lucene如何通过docId快速查找field字段以及最近距离等信息?的相关文章

02 超级搜索术——资源搜索:全面、快速查找全网你想要的任何信息、情报

02 超级搜索术——资源搜索:全面.快速查找全网你想要的任何信息.情报 2018-07-30 目录 1. 超级搜索心法2. 资源搜索逻辑  场景1 庞杂资料搜索逻辑  场景2 专业文献资料的搜索逻辑  场景3 视频网站搜索3. 搜索技巧 1. 超级搜索心法 返回 超级搜索心法:遇到一切问题,首先想到的是搜索. 搜索资源时,要先百度,后淘宝,实在不行用知乎 这里的资源包括但不限于资料,视频,文献.软件,这里的百度不是单只百度,而是一切搜索引擎,包括不限于百度.搜狗微信.谷歌等等,淘宝代指一切购物网

PHP实现文本快速查找 - 二分查找

PHP实现文本快速查找 - 二分查找法 起因 先说说事情的起因,最近在分析数据时经常遇到一种场景,代码需要频繁的读某一张数据库的表,比如根据地区ID获取地区名称.根据网站分类ID获取分类名称.根据关键词ID获取关键词等.虽然以上需求都可以在原始建表时,通过冗余数据来解决.但仍有部分业务存的只是关联表的ID,数据分析时需要频繁的查表. 所读的表存在共同的特点 数据几乎不会变更 数据量适中,从一万到100多万,如果全加载到内存也不太合适. 纠结的地方 在做数据分析时,需要十分频繁的读这些表,每秒有可

最佳数据库无限分级快速查找所有子节点的方法

场景我们基本设计的表是这样的 temp表 id, name, parent_id 当我们查某个节点的所有子节点的时候,我们需要递归查询 id = 4 select * from temp where parent_id = 4 ids = [5,9,25] select * from temp where parent_id in [5,9,25] 那么这种方法在层数达到一定层的时候势必带来性能问题,因为需要多次查询数据库,就算写存储过程,性能也是十分低下的. 快速查询方法如下: 改造表,添加c

自定义快速查找字母控件

效果图如下: 首先看看布局文件,自定义的控件中包含一个 ListView,用于显示具体的数据内容: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="fill_parent"     a

《Java虚拟机原理图解》1.4 class文件中的字段表集合--field字段在class文件中是怎样组织的

0.前言 了解JVM虚拟机原理是每一个Java程序员修炼的必经之路.但是由于JVM虚拟机中有很多的东西讲述的比较宽泛,在当前接触到的关于JVM虚拟机原理的教程或者博客中,绝大部分都是充斥的文字性的描述,很难给人以形象化的认知,看完之后感觉还是稀里糊涂的. 感于以上的种种,我打算把我在学习JVM虚拟机的过程中学到的东西,结合自己的理解,总结成<Java虚拟机原理图解> 这个系列,以图解的形式,将抽象的JVM虚拟机的知识具体化,希望能够对想了解Java虚拟机原理的的Java程序员 提供点帮助. 读

JS获取中文拼音首字母,并通过拼音首字母快速查找页面内的中文内容

实现效果: 图一: 图二: 此例中输入的中文字符串"万万保重",有三个字是多音字,所以alert对话框中显示的是多种读音的组合: 如何实现? 如何实现通过拼音首字母快速查找页面内的中文内容呢? 过程原理是这样的:例如要对一些人名进行快速查找,当页面加载完成后,对所有人名建立一个索引,生成拼音首字母与姓名的对应关系:然后监听键盘事件,当用户按下键盘时,根据键值得到按下的是哪个字母,然后遍历索引中是否存在相同的拼音首字母: 这里还实现了根据字母组合来查找的功能,原理是这样的:当用户按键时,

关于素数的快速查找——素数筛选法

利用素数筛选法进行素数的快速查找.原理很简单,素数一定是奇数,素数的倍数一定不是素数.思路如下: 预定义N表示10000,即表示查找10000以内的素数,首先定义数组prime[]对N以内的数进行标记,奇数存为1,偶数存为0,最终实现结果为素数的prime值为1,因此将prime[2]赋值为1(2是素数).之后利用for循环,对N以内的奇数进行遍历(注意for循环的条件控制),for里用if判断是否为素数(奇数),若是,执行内部嵌套的for循环判断该奇数是否为素数,若是则标记为1,若不是则pri

NYOJ 快速查找素数

快速查找素数 时间限制:1000 ms  |  内存限制:65535 KB 难度:3 描述 现在给你一个正整数N,要你快速的找出在2.....N这些数里面所有的素数. 输入 给出一个正整数数N(N<=2000000) 但N为0时结束程序. 测试数据不超过100组 输出 将2~N范围内所有的素数输出.两个数之间用空格隔开 样例输入 5 10 11 0 样例输出 2 3 5 2 3 5 7 2 3 5 7 11 #include<cstdio> #include<cstdlib>

002 bitmap海量数据的快速查找和去重

题目描述 给你一个文件,里面包含40亿个整数,写一个算法找出该文件中不包含的一个整数, 假设你有1GB内存可用. 如果你只有10MB的内存呢? 对于40亿个整数,如果直接用int数组来表示的大约要用40*10^8*4B=16GB,超出了内存要求,这里 我们可以用bitmap来解决,bitmap基本思想是一位表示一个整数,比如我们有6个数据: 7   3  1  5  6  4 假设bitmap容量为8,当插入7时 bit[7]=1,一次类推 bit[3]=1 bit[1]=1 bit[5]=1