• 有关Lucene的问题(8):用Lucene构建实时索引的文档更新问题


    在有关Lucene的问题(7),讨论了使用Lucene内存索引和硬盘索引构建实时索引的问题。

    然而有的读者提到,如果涉及到文档的删除及更新,那么如何构建实时的索引呢?本节来讨论这个问题。

    1、Lucene删除文档的几种方式

    • IndexReader.deleteDocument(int docID)是用 IndexReader 按文档号删除。  
    • IndexReader.deleteDocuments(Term  term)是用 IndexReader 删除包含此词(Term)的文档。  
    • IndexWriter.deleteDocuments(Term  term)是用 IndexWriter 删除包含此词(Term)的文档。  
    • IndexWriter.deleteDocuments(Term[]  terms)是用 IndexWriter 删除包含这些词(Term)的文档。  
    • IndexWriter.deleteDocuments(Query  query)是用 IndexWriter 删除能满足此查询(Query)的文档。  
    • IndexWriter.deleteDocuments(Query[] queries)是用 IndexWriter 删除能满足这些查询(Query)的文档。

    删除文档既可以用reader进行删除,也可以用writer进行删除,不同的是,reader进行删除后,此reader马上能够生效,而用writer删除后,会被缓存,只有写入到索引文件中,当reader再次打开的时候,才能够看到。

    2、Lucene文档更新的几个问题

    2.1、使用IndexReader还是IndexWriter进行删除

    既然IndexReader和IndexWriter都能够进行文档删除,那么到底是应该用哪个来进行删除呢?

    本文的建议是,用IndexWriter来进行删除。

    因为用IndexReader可能存在以下的问题:

    (1) 当有一个IndexWriter打开的时候,IndexReader的删除操作是不能够进行的,否则会报LockObtainFailedException

    (2) 当IndexReader被多个线程使用的时候,一个线程用其进行删除,会使得另一个线程看到的索引有所改变,使得另一个线程的结果带有不确定性。

    (3) 对于更新操作,在Lucene中是先删除,再添加的,然而删除的被立刻看到的,而添加却不能够立刻看到,造成了数据的不一致性。

    (4) 即便以上问题可以通过锁来解决,然而背后的操作影响到了搜索的速度,是我们不想看到的。

    2.2、如何在内存中缓存文档的删除

    在上一节中,为了能够做到实时性,我们使用内存中的索引,而硬盘上的索引则不经常打开,即便打开也在背后线程中打开。

    而要删除的文档如果在硬盘索引中,如果不重新打开则看不到新的删除,则需要将删除的文档缓存到内存中。

    那如何将缓存在内存中的文档删除在不重新打开IndexReader的情况下应用于硬盘上的索引呢?

    在Lucene中,有一种IndexReader为FilterIndexReader,可以对一个IndexReader进行封装,我们可以实现一个自己的FilterIndexReader来过滤掉删除的文档。

    一个例子如下:

    public class MyFilterIndexReader extends FilterIndexReader {

      OpenBitSet dels;

      public MyFilterIndexReader(IndexReader in) {

        super(in);

        dels = new OpenBitSet(in.maxDoc());

      }

      public MyFilterIndexReader(IndexReader in, List<String> idToDelete) throws IOException {

        super(in);

        dels = new OpenBitSet(in.maxDoc());

        for(String id : idToDelete){

          TermDocs td = in.termDocs(new Term("id", id)); //如果能在内存中Cache从Lucene的ID到应用的ID的映射,Reader的生成将快得多。

          if(td.next()){

            dels.set(td.doc());

          }

        }

      }

      @Override

      public int numDocs() {

        return in.numDocs() - (int) dels.cardinality();

      }

      @Override

      public TermDocs termDocs(Term term) throws IOException {

        return new FilterTermDocs(in.termDocs(term)) {

          @Override

          public boolean next() throws IOException {

            boolean res;

            while ((res = super.next())) {

              if (!dels.get(doc())) {

                break;

              }

            }

            return res;

          }

        };

      }

      @Override

      public TermDocs termDocs() throws IOException {

        return new FilterTermDocs(in.termDocs()) {

          @Override

          public boolean next() throws IOException {

            boolean res;

            while ((res = super.next())) {

              if (!dels.get(doc())) {

                break;

              }

            }

            return res;

          }

        };

      }

    }

    2.3、文档更新的顺序性问题

    Lucene的文档更新其实是删除旧的文档,然后添加新的文档。如上所述,删除的文档是缓存在内存中的,并通过FilterIndexReader应用于硬盘上的索引,然而新的文档也是以相同的id加入到索引中去的,这就需要保证缓存的删除不会将新的文档也过滤掉,将缓存的删除合并到索引中的时候不会将新的文档也删除掉。

    Lucene的两次更新一定要后一次覆盖前一次,而不能让前一次覆盖后一次。

    所以内存中已经硬盘中的多个索引是要被保持一个顺序的,哪个是老的索引,哪个是新的索引,缓存的删除自然是应该应用于所有比他老的索引的,而不应该应用于他自己以及比他新的索引。

    3、具有更新功能的Lucene实时索引方案

    3.1、初始化

    首先假设我们硬盘上已经有一个索引FileSystemIndex,被事先打开的,其中包含文档1,2,3,4,5,6。

    我们在内存中有一个索引MemoryIndex,新来的文档全部索引到内存索引中,并且是索引完IndexWriter就commit,IndexReader就重新打开,其中包含文档7,8。

    绘图8

    3.2、更新文档5

    这时候来一个新的更新文档5, 需要首先将文档5删除,然后加入新的文档5。

    需要做的事情是:

    • 首先在内存索引中删除文档5,当然没有文档5,删除无效。
    • 其次将对文档5的删除放入内存文档删除列表,并与硬盘的IndexReader组成FilterIndexReader
    • 最后,将新的文档5加入内存索引,这时候,用户可以看到的就是新的文档5了。
    • 将文档5放入删除列表以及将文档5提交到内存索引两者应该是一个原子操作,好在这两者都是比较块的。

    注:此处对硬盘上的索引,也可以进行对文档5的删除,由于IndexReader没有重新打开,此删除是删不掉的,我们之所以没有这样做,是想保持此次更新要么全部在内存中,要么全部在硬盘中,而非删除部分已经应用到硬盘中,而新文档却在内存中,此时,如果系统crash,则新的文档5丢失了,而旧的文档5也已经在硬盘上被删除。我们将硬盘上对文档5的删除放到从内存索引向硬盘索引的合并过程。

    更新文档5

    如果再有一次对文档5的更新,则首先将内存索引中的文档5删除,添加新的文档5,然后将文档5加入删除列表,发现已经存在,则不必删除。

    3.3、合并索引

    然而经过一段时间,内存中的索引需要合并到硬盘上。

    在合并的过程中,需要重新建立一个空的内存索引,用于合并阶段索引新的文档,而合并中的索引的IndexReader以及硬盘索引和删除列表所组成的FilterIndexReader仍然保持打开,对外提供服务,而合并阶段从后台进行。

    后台的合并包括以下几步:

    • 将删除列表应用到硬盘索引中。
    • 将内存索引合并到硬盘索引中。
    • IndexWriter提交。

    合并

    3.4、合并的过程中更新文档5

    在合并的过程中,如果还有更新那怎么办呢?

    • 首先将合并中索引的文档5删除,此删除不会影响合并,因为合并之前,合并中索引的IndexReader已经打开,索引合并中索引的文档5还是会合并到硬盘中去的。此删除影响的是此后的查询在合并中索引是看不到文档5的。
    • 然后将文档5的删除放入删除列表,并同合并中索引的删除列表,已经硬盘索引一起构成FilterIndexReader。
    • 将新的文档5添加到内存中索引。
    • 提交在合并中索引对文档5的删除,将文档5添加到删除列表,提交在内存索引中对文档5的添加三者应该是一个原子操作,好在三者也是很快的。

    合并时更新

    3.5、重新打开硬盘索引的IndexReader

    当合并中索引合并到硬盘中的时候,是时候重新打开硬盘上的索引了,新打开的IndexReader是可以看到文档5的删除的。

    如果这个时候有新的更新,也是添加到内存索引和删除列表的,比如我们更新文档6.

    重新打开

    3.6、替代IndexReader 

    当IndexReader被重新打开后,则需要删除合并中的索引及其删除列表,将硬盘索引原来的IndexReader关闭,使用新的IndexReader。

    替换IndexReader

  • 相关阅读:
    多项式的一些操作
    AtCoder Grand Contest 036E
    THUWC2017 随机二分图
    THUWC2017 在美妙的数学王国中畅游
    SDOI2017 切树游戏
    ZJOI2017 树状数组
    HNOI2015 接水果
    LOJ6503 Magic
    Charles 抓去app接口的使用
    mysql 字符串类型和数字对比的坑
  • 原文地址:https://www.cnblogs.com/forfuture1978/p/1766162.html
Copyright © 2020-2023  润新知