Lucene底层原理和优化经验分享(2)-Lucene优化经验总结

转自:https://blog.csdn.net/njpjsoftdev/article/details/54133548

系统优化遵从木桶原理:一只木桶能盛多少水,并不取决于最高的木板,而取决于最短的那块木板。Lucene优化也一样,找到性能瓶颈,找对解决方法,才能事半功倍,本文将从三方面阐述我们的Lucene优化经验:
  1. 找准方向 -> Lucene性能瓶颈分析。
  2. 找对方法 -> Lucene代码架构分析。
  3. 方法落地 -> 优化经验总结。

  1. Lucene性能瓶颈分析

      上篇Lucene底层原理分析了Lucene索引结构:内存+磁盘,打开索引库时只有tip和fdx文件会被加载到内存中,tip为FST的前缀索引,fdx为正向文件索引,其他文件tim、doc、fdt都放在硬盘,一次完整的检索过程与索引文件的交互过程如图:
    这里写图片描述 
      整个流程至少发生三次随机IO:
      1. 读后缀词块
      2. 读倒排表
      3. 取文档(如果文档号跳跃性很大或者因为打分完全乱序,那么会发生更多次随机IO,极端情况就是取多少文档就发生多少次随机IO)
      当前机械硬盘随机IO响应时间平均在10ms左右,远大于CPU+内存计算时间,而且这只是针对一个查询条件,若多个查询条件、跨多列、甚至模糊查询,随机IO请求更多,因此Lucene查询性能瓶颈主要集中磁盘IO性能上,尤其随机IO性能。所以我们的优化方向就是:
      1. 减少IO请求。
      2. 顺序IO代替随机IO。

  2. Lucene代码架构

      上一节分析了Lucene性能瓶颈,这一节分析Lucene代码架构,找到从哪里下手去实现优化。
      Lucene从4.0版本后,代码全面模块化,并开放了很多接口,包括索引格式接口Codec、打分接口Similarity、文档收集接口Collector,开发者想基于Lucene再开发,不再需要侵入式修改源代码,而是基于接口,插件式修改。我们结合业务场景和开放接口自定义了Lucene检索模式。
      Lucene检索大致时序图:
      这里写图片描述   
      1. APP解析用户查询生成查询条件Query。
      2. IndexSearcher重写Query并生成Weight。
      3. Weight会生成Scorer,Scorer创建相应查询条件的倒排表迭代器。
      4. 调用scoreALl(),遍历所有文档ID,依次传给传给Collector。
      5. Collector得到文档ID后,调用打分模块Similarity得到文档分值,并根据分值和文档收集器具体实现决定是否返回。Lucene默认的收集器TopScoreDocCollector,会根据用户定义的文档数如100,返回分值前100的文档ID。
      
      我们对Lucene的修改主要在图中标红的文档收集过程,一是屏蔽打分,二是修改文档收集模式,下一节会详细阐述。

  3. 优化经验总结

      基于底层原理和代码架构,我们知道了需要做什么和怎么做:IO、IO、还是IO,以下我们全文检索系统的主要优化方案:

3.1单盘优化

解决问题:
  硬盘随机IO性能低。
解决方案:
  1. 将原先的Raid5拆分,改用单盘,因为Raid5随机读写性能 < n*单盘。
  2. 将索引文件tim、doc使用固态硬盘SSD存放,正向文件fdt使用机械硬盘,这样综合了SSD随机读写性能高,机械硬盘成本低、存储空间大的优点。
  3. 对同一磁盘上索引库进行统一管理,单线程处理对同一硬盘上索引库的检索请求,防止同一硬盘多库之间同时访问降低磁盘性能。这里可以根据实际测试情况调整具体线程数,但线程数不宜过多。
  

3.2布隆过滤器

解决问题:
  有些单词不在索引库里,但还需要进索引库查询,发起不必要的IO请求。

解决方案:
  使用布隆过滤器,预先判断单词是不是在该索引库里。布隆过滤器原理很简单,对一单词哈希,并映射到相应bit,设置为1,判断时同样做哈希,并去相应bit位取值,若为1,则可能存在,进库查询,若为0,则肯定不存在,不需进库查询。
这里写图片描述

  对Lucene实现布隆过滤器有两种方式:
  1. 在应用层,Lucene之外实现。
  2. 改写Lucene的Codec接口,添加布隆过滤器功能,使用布隆过滤器预先过滤查询条件。
  后来我们经过测试,选用了第一种方案,因为布隆过滤器十分消耗内存、加载时间很长,而且我们同一索引库为提高性能,复制到多个硬盘上,所以如果布隆过滤器放在Lucene里,相同过滤器会被加载多次,会浪费相当多的内存,所以我们在Lucene之外做了布隆过滤器,同一索引库共享一个布隆过滤器,节约了内存。
  

3.3屏蔽打分/排序机制

解决问题:
  一次测试发现,同样的条件,精确查询速度还没有模糊查询速度快
这里写图片描述
  研究源代码发现,Lucene会对分词列的精确查询条件进行打分。打分是搜索引擎重要一部分,倒排索引只能回答是不是的问题,打分能够评判查询条件和文档的匹配度,提高检索质量。Lucene打分过程集成了多种经典模型,如TF-IDF、VSM,如图:
  这里写图片描述
  1. coord 一个document满足几个查询,满足多的分值高。
  2. queryNorm,查询归一化,它的意义是让同一文档但不同查询的打分结果有可比较。
  3. tf-idf,tf是term在文档中出现次数,idf逆文档频率是term在多少个文档中出现过除以总文档数。
  4. getBoost,查询时赋的权重。
  5. 归一化,主要三个因素文档权重、field权重、文档长度,这个很重要,因为这个需要单独加载nvm文件,而且在打开库时不会加载,而是在第一次查询时会加载,因此才会造成查询时间的巨大差异。
  这里不详细阐述,只说下它的几个基本原则:
  1. 一个文档符合的查询条件越多分越高。
  2. 一个文档关键词出现次数越多分越高,文档内容越多分越低。
  3. 一个查询词在越多文档中出现权重越低。
  有兴趣的可查看LuceneAPI文档TFIDFSimilarity类说明:
  http://lucene.apache.org/core/4_10_3/core/index.html
  打分会消耗额外IO、需要更多CPU计算、加载整个倒排表,拖累了查询速度,特别实在文档数非常多的情况下。而对模糊查询,Lucene不会进行打分,所以反而更快。在我们的业务场景下,我们不需要TF-IDF这种打分方式,所以我们完全屏蔽了打分这个过程,大大提高了检索速度。
解决方案:
  1. 实现EmptySimilarity,去掉所有计算过程,打分过程完全为空。

public class EmptySimilarity extends Similarity {

private static long ZERO=0;
@Override
public long computeNorm(FieldInvertState state) {
    return ZERO;
}

@Override
public SimWeight computeWeight(float queryBoost,
        CollectionStatistics collectionStats, TermStatistics... termStats) {
    return new EmptySimWeight();
}

@Override
public SimScorer simScorer(SimWeight weight, AtomicReaderContext context)
        throws IOException {
    return new EmptyScorer();
}

public class EmptySimWeight extends SimWeight {

    @Override
    public float getValueForNormalization() {
        return ZERO;
    }

    @Override
    public void normalize(float queryNorm, float topLevelBoost) {

    }

}

public static class EmptyScorer extends SimScorer {

    @Override
    public float score(int doc, float freq) {
        return ZERO;
    }

    @Override
    public float computeSlopFactor(int distance) {
        return ZERO;
    }

    @Override
    public float computePayloadFactor(int doc, int start, int end,
            BytesRef payload) {
        return ZERO;
    }

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
  2. 自定义Collector,结果数满足了抛异常退出,防止读入多余倒排表。

public class SimpleCollector extends Collector implements Iterable {

private final List<Integer> hitList;
private final int numHits;
private int docBase;

public SimpleCollector(int numHits) {
    if(numHits<0)
        throw new IllegalArgumentException("numHits should > 0");
    this.numHits = numHits;
    this.hitList = new ArrayList<Integer>(numHits);
}


@Override
public void collect(int doc) throws IOException {
    if(hitList.size()<numHits)
    {
        hitList.add(docBase+doc);
    }
    else{
        //若结果满了抛异常退出
        throw new HitListFullException();
    }
}
public int size(){
    return hitList.size();
}
@Override
public void setScorer(Scorer scorer) throws IOException {
    //ignore scorer
}

@Override
public void setNextReader(AtomicReaderContext context) throws IOException {
    //因为是分段的,所以需要记载每个段起始文档号
    this.docBase=context.docBase;
}

@Override
public boolean acceptsDocsOutOfOrder() {
    //接受乱序,提高性能,因为最后要自己排序
    return true;
}

@Override
public Iterator<Integer> iterator() {
    Collections.sort(hitList);
    return hitList.iterator();
}

public static class HitListFullException extends RuntimeException{

    public HitListFullException()
    {
        super("HitList already full");
    }
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
  使用如下:

IndexSearcher indexSearcher = new IndexSearcher(
            DirectoryReader.open(FSDirectory
                    .open(new File("/index/lucene_test"))));
    //使用空打分器
    indexSearcher.setSimilarity(new EmptySimilarity());
    SimpleCollector simpleCollector=new SimpleCollector(2);
    try {
        indexSearcher.search(query, simpleCollector);
    } catch (HitListFullException e) {
        //e.printStackTrace();
        // ignore
    }
    System.out.println(simpleCollector.size());
    //遍历文档号
    for(int hit:simpleCollector)
    {
        indexSearcher.doc(hit);
    }
    indexSearcher.getIndexReader().close();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
3.4 取结果优化

解决问题:
  上面的测试条件还有一个问题,就是他们取同样数量的文档数,时间却差了很多。
这里写图片描述

  原因就是因为模糊查询不打分,所以文档ID是顺序的,为顺序IO读方式,而打分后文档ID完全乱序,为随机IO读方式。
解决方案:
  1. 自定义Collector,按文档ID升序排序且结果数满足立即退出。
  2. 多任务合并取结果操作,这样相同ID的文档只会取一次。
  

3.5解决Query被转成Filter

解决问题:
  我们有一个组合条件:

select * from indexdb where Time > 20170104 AND Time < 20170105 AND Protocol = ‘TCP’ AND Content =’not exist’
1
  这里需要合并多个查询条件的倒排表,Lucene在合并倒排表时,并不会一次性读出所有倒排表,而是将倒排表抽象成迭代器,延迟获取,而且如果有一个AND条件查询结果为空,它就直接返回,不会读任一倒排表。这里Content查询结果为空,但这个查询还是很久才返回,debug跟踪Lucene源代码发现,Lucene会对Query查询重写来优化性能,这里的Time条件因为匹配到词数太多,而被Lucene改写成Filter,Filter一个特点就是会读出符合查询条件的所有倒排表,并做成BitSet,所以查询时间都消耗在了读倒排表上。
解决方案:
  1. 去掉了CapTime条件,改由应用层去做,按时间预先分库。
  2. 调整子查询顺序,将匹配结果更少的放前面。
  3. 留心Lucene的重写机制,有时候重写过的查询条件不一定符合我们预期。
  

3.6索引库大小性能比较

解决问题:
  Lucene一个索引库多大合适?
解决方案:
  这里涉及到Lucene索引结构设计:Lucene是分段的。分段是指Lucene接收到索引请求后,会先放缓存,缓存满后才会写到磁盘中去,变成一个Segment,Segment创建好了之后就不会再修改,每个Segment相当于一个功能完整的小索引库,它包含之前说的所有索引文件。当然这样会导致索引库中有很多段,所以Lucene后台会有合并线程定期去合并小的段。
  段数越少,检索时随机IO次数请求就越少,段结果合并操作越少。如果只有一个段,那么一个查询条件就需要加载一个后缀词块,但有10个段,就需要分别加载10个段的后缀词块和倒排表,再合并10个段的查询结果。分库本质上跟分段是一样的,调整库大小,减少库数量,就是减少段数来提高性能。
  库大小测试结果:
  总共575G的索引库,我们分为6个100g的库和71个10g的库来分别测试
  打开库测试

库类型 打开时间(s) 库内存占用(g)
大库 11 1.3
小库 18 2.2
  查询测试

库类型 查询条件 查询时间(ms)
大库 content=’trump’ 1100
小库 content=’trump’ 5700
  可以看出大库相比小库不管再打开时间、内存占用、查询效率上都有着很大优势,所以在条件允许下,尽量把库调大。但也需注意两个问题:
  1. 合并大库是有成本的。
  2. 库越大,分发成本越高,容错率越低。

总结

 以上就是我们对Lucene的一些优化经验,回顾起来就是三点:
  1. 认清业务需求。
  2. 分析底层原理,找出性能瓶颈。
  3. 研究代码架构,找到优化切入点。
 这也是我们对其他开源项目的使用方法,知其然更只知其所以然。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
lucene 是一种开源的全文搜索引擎库,虽然它已经具备了很好的性能,但在处理海量数据时,还需要进行一些性能优化的工作。以下是一些优化技巧: 1. 建立索引:建立合适的索引结构对于搜索性能至关重要。可以通过调整分词器、字段类型等参数来优化索引的构建过程。 2. 使用缓存:Lucene 提供了一些缓存机制,可以缓存频繁使用的结果,例如过滤器缓存、排序缓存等。合理利用缓存可以减少磁盘 I/O 操作,提升搜索效率。 3. 硬件优化:在硬件方面,可以使用更高性能的硬盘、增加内存等方式来提高 Lucene 的性能。特别是加大文件系统缓存可以加快索引和搜索的速度。 4. 查询性能优化:可以通过使用合适的搜索技术(如布尔查询、短语查询等)、调整查询权重、优化查询语句等方式来提高搜索效率。 5. 批量操作:对于大规模数据的操作,可以尽量使用批量操作方式,减少频繁的单条数据操作,以提高效率。比如通过批量提交文档,批量删除文档等方式。 6. 关键词匹配优化:对于一些特定的场景,可以通过使用同义词字典、拼写纠错、中文分词等方式来优化关键词的匹配,提高搜索的准确性。 7. 数据分片:对于大规模数据,可以将索引数据分片存储在多台机器上,通过分布式的方式来提高搜索的并发能力和吞吐量。 总的来说,lucene 性能优化涉及多个方面,包括索引构建、查询优化、硬件优化、批量操作等,需要根据具体应用场景和需求来选择合适的优化策略。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值