• Lucene 如何实现高性能 GroupBy <一>


    注:以下讲解代码均以Lucene.net 2.9.2为例。GroupBy效果应用(http://www.tradetuber.com/search?key=led

    Lucene如果实现高性能的GroupBy、SortBy效果,我想这个应该是Lucener(Lucene使用者简称Lucener)遇到的最头大的问题。

    Lucene各个方面表现都很优异,唯独在GroupBy及SortBy方面显得蹩脚,那么接下来的文章中我将为Lucener讲解如何更好的解决这两大问题。

    1.GroupBy

     要实现GroupBy,我们应该从哪下手? 如何下手?

    从哪下手:从Collector类下手,Collector类相当于一个收集器,它收集的结果集是一个 ScoreDoc集合,ScoreDoc是DocID(文档ID)与Score(该文档得分)的一个对应关系,然后Lucene会使用移位运算等算法比较所有DocID的得分,并得到前多少条(pageSize*pageIndex)记录。

    如何下手:当然是在逐个比较DocID的得分时注入相应的代码。

    Collect
    public override void Collect(int doc)
    {
    float score = scorer.Score();

    // This collector cannot handle these scores:
    System.Diagnostics.Debug.Assert(score != float.NegativeInfinity);
    System.Diagnostics.Debug.Assert(
    !float.IsNaN(score));

    totalHits
    ++;
    if (score <= pqTop.score)
    {
    // Since docs are returned in-order (i.e., increasing doc Id), a document
    // with equal score to pqTop.score cannot compete since HitQueue favors
    // documents with lower doc Ids. Therefore reject those docs too.
    return;
    }
    pqTop.doc
    = doc + docBase;
    pqTop.score
    = score;
    pqTop
    = (ScoreDoc)pq.UpdateTop();
    }

    上面的方法是Lucene中用来逐个比较DocID得分的方法,我们在此段中加入我们的方法来实现GroupBy。

    Collect
    public override void Collect(int doc)
    {
    float score = scorer.Score();

    //根据DocID获取Document
    Document doc = _IndexReader.Document(doc);
    //取出分组字段值
    string strGroupByFieldValue = doc.Get("GroupByFieldName");
    //判断分组字段值是否已经存在,存在则返回,不存在继续,这样便保证了被分组字段对应的值有且仅出现一次。
    if(_Dictionary.ContainsKey(strGroupByFieldValue ))
    return;

    // This collector cannot handle these scores:
    System.Diagnostics.Debug.Assert(score != float.NegativeInfinity);
    System.Diagnostics.Debug.Assert(
    !float.IsNaN(score));

    totalHits
    ++;
    if (score <= pqTop.score)
    {
    // Since docs are returned in-order (i.e., increasing doc Id), a document
    // with equal score to pqTop.score cannot compete since HitQueue favors
    // documents with lower doc Ids. Therefore reject those docs too.
    return;
    }
    pqTop.doc
    = doc + docBase;
    pqTop.score
    = score;
    pqTop
    = (ScoreDoc)pq.UpdateTop();
    }

    通过上面的代码,我们可以保证被GroupBy的字段值一旦曾经出现过,便不再往下走了,同时我们可以此处过滤时保存被过滤的次数,这样GroupBy的初级效果便出现在了我们眼前了,但是,上面的方法存在的最大问题便是性能问题。

    e.g. string strGroupByFieldValue = doc.Get("strGroupByFieldName");

    这句代码是获取分组字段的值,这种取值方式是直接从磁盘中读取的,当搜索时被标中的记录越多,磁盘的IO操作也就越多,自然而然就成为了性能的瓶颈之所在。

    那么如何可以快速的进行读取呢? 当然是从内存中读取分组字段的值。

    那么如何来读取并将其存放在内存中呢? It's Term,Term即Lucene中的词。

    创建索引时,我们会对某些字段进行分词索引或不分词索引,然后这些词会生成一个字典,然后会对字典中的词按字母顺序排列,再然后会合并相同的词生成文档倒排链表(Posting List)。

    这个倒排链表实际上就是词与文档ID的对应关系,这个里面还包含了词频(词在文档中出现的频率)等重要的属性。正是因为有了这个由Term以及其它要素组成的Posting List,所以才可以进行快速搜索。

    这个过程类似于咱们很小的时候查新华字典一样,那么厚一本字典中想要找到自己想要的字,最快速的办法就是根据这个字的拼音或部首去找,然后定位到这个字所在的页数,这样我们就找到了我们想要的字。新华字典的拼音检字表或部首检字表就相当于是Posting List。

    由此可知,我们需要从这个倒排链表中将分组字段的值全部读出来,存放在内存中,供Collect方法中使用。

    注意:需要分组的字段不能被分词索引

    代码
    Term startTerm = new Term("GroupByFieldName");
    TermEnum te
    = _IndexReader.Terms(startTerm);
    if (te != null)
    {
    Term currTerm
    = te.Term();

    while ((currTerm != null) && (currTerm.Field() == startTerm.Field())) //term fieldnames are interned
    {
    TermDocs td
    = _IndexReader.TermDocs(currTerm);
    while (td.Next())
    {
    dict.Add(td.Doc(), currTerm.Text());
    }
    if (!te.Next())
    {
    break;
    }
    currTerm
    = te.Term();
    }
    }

    通过上面的代码,我们便得到了我们所需要的分组字段值,它是一个DocID(文档ID)与分组字段值对应关系的Dictionary<int, string>。

    现在我们把分组字段值缓存到了一个Dictionary中,这时出现了一个新的问题,索引库的更新一般都是实时更新的,一旦缓存起来了,当有新的记录加入到索引库时,缓存却无法更新。怎么办? 请看下面的代码:

    代码
    int oldMaxDoc = MaxDoc;//缓存上次更新时的MaxDoc
    int newMaxDoc = _IndexReader.MaxDoc();
    if (oldMaxDoc < newMaxDoc)
    {
    Term startTerm
    = new Term("GroupByFieldName");
    TermEnum te
    = _IndexReader.Terms(startTerm);
    if (te != null)
    {
    Term currTerm
    = te.Term();
    while ((currTerm != null) && (currTerm.Field() == startTerm.Field())) //term fieldnames are interned
    {
    TermDocs td
    = _IndexReader.TermDocs(currTerm);
    if (td.SkipTo(oldMaxDoc))
    {
    do
    {
    dict.Add(td.Doc(), currTerm.Text());
    }
    while (td.Next());
    }
    if (!te.Next())
    {
    break;
    }
    currTerm
    = te.Term();
    }
    }
    MaxDoc
    = newMaxDoc;
    }

    通过上面的代码,我们不难明白如何更新分组字段缓存。

    Lucene中文档的添加都会伴随有一个DocID的生成,DocID的生成如同关系性数据库的自动增长字段,MaxDoc = Max(DocID),所以我们根据MaxDoc值的变化来判断是否有新的文档产生,如果有则skipTo到OldMaxDoc位置,将新增长的文档ID对应的分组字段值加入到Dictionary中。

    注意:从Posting List中读取对应的Term值性能上也是个问题(60万条记录大概需要2S左右),建议使用其它线程来更新该Dictionary的值;或者使用另外的进程来缓存。

    这样,我们便可以在Collect方法中根据传入的DocID从Dictionary<int, string>取出对应的分组字段的值进行过滤,这种速度可想而知,代码如下:

    Collect
    public override void Collect(int doc)
    {
    float score = scorer.Score();

    //判断分组字段值是否已经存在,存在则返回,不存在继续,这样便保证了被分组字段对应的值有且仅出现一次。
    if(_Dictionary.ContainsKey(dict[doc]))
    return;

    // This collector cannot handle these scores:
    System.Diagnostics.Debug.Assert(score != float.NegativeInfinity);
    System.Diagnostics.Debug.Assert(
    !float.IsNaN(score));

    totalHits
    ++;
    if (score <= pqTop.score)
    {
    // Since docs are returned in-order (i.e., increasing doc Id), a document
    // with equal score to pqTop.score cannot compete since HitQueue favors
    // documents with lower doc Ids. Therefore reject those docs too.
    return;
    }
    pqTop.doc
    = doc + docBase;
    pqTop.score
    = score;
    pqTop
    = (ScoreDoc)pq.UpdateTop();
    }

    写到这里,新的问题又产生了!

    从上面的代码我们不难看出,虽然实现了GroupBy(假设我们按照公司ID来进行分组,每个公司只能出现一个产品),但是无法保证该产品是该公司得分最高的产品,这样也就影响了公司对应的产品排名。

    e.g.

    A公司(2个产品),得分依次为 0.12、0.33

    B公司(2个产品),得分依次为 0.23、0.31

    按照理论上,A公司一定排在B公司前面,因为A公司产品最高得分为0.33,B公司产品最高得分0.31分,按照上面的代码,有可能B公司就排在A公司前面了,当得分为0.12的产品先出现,那么A公司对应的最大产品分数就是0.12了,A公司就排在B公司之后了。

    针对上述问题,我们仅仅在Collect方法注入代码是无法实现的,我们还需要结合 “结果集得分排序” 来解决上述问题,这时我们需要看看这个类(Lucene--Util--PriorityQueue.cs)。

    PriorityQueue这个类是用来干什么的呢?它是用来对搜索结果按照得分高低进行顺序排列,并返回你需要的记录数的 ScoreDoc = {Doc, Score}。那接下来我们就一起研究研究它。

    此篇完,下一篇将接着此篇继续为大家讲解 Lucene 如何实现高性能 GroupBy<二>

  • 相关阅读:
    RocketMQ(4.8.0)——Broker读写分离机制
    RocketMQ(4.8.0)——Broker消息存储机制
    RocketMQ(4.8.0)——Broker 概述、启动和停止流程
    RocketMQ(4.8.0)——RocketMQ的路由原理
    RocketMQ(4.8.0)——Namesrv 服务
    RocketMQ(4.8.0)——RocketMQ部署拓扑和部署实践
    RocketMQ(4.8.0)——RocketMQ体系架构
    RocketMQ(4.8.0)——消费者最佳实践
    Java丨时间判断谁前谁后
    MySQL丨分页查询
  • 原文地址:https://www.cnblogs.com/zengen/p/1886244.html
Copyright © 2020-2023  润新知