`
mozhenghua
  • 浏览: 319132 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Solr/Lucene使用docValue查询的一个坑

阅读更多

发现问题

最近在使用docValue发现了一个坑,初学者稍不注意很有可能入坑,进而会得出Lucene性能有问题的结论,所以我需要将这个坑填平以正视听。

接到业务方的一个需求,需要在查询结果上按照某一个字段去除重复,假设有以下两条记录:

学号

班级id

班级排名

001

1

1

002

1

2

003

2

1

004

2

2

 

查询结果需要按照班级ID去重,取班级中排名最靠前的一名同学,这样结果应该是这样的:

学号

班级id

班级排名

001

1

1

003

2

1

当然这里要说明的是,如果引擎中存储的数据量小,或者索引不需要实时更新的话,不会发生问题。但是当引擎中有海量的数据,比如上千万的索引记录加上同时需要实时更新索引的话,之前在索引上每一个不合理的设置就会被放大,导致查询效率急剧下降。

 

接到这样的需求,很直接地就想到使用Solr现成的QueryParser collapse

fq={!collapse field=class_id min=class_ordinar}

上线之后发现查询响应速度有问题,查询会周期性的变慢,在一个softcommit周期之后查询就会出现一个峰值,RT瞬间飙高到4~5秒才能完成查询。

 

定位问题

每隔一个solrcommit周期就会发生一次查询超时,直觉告诉我与docValue有关系,因字段class_idschema设置上必须要开启docValue=true属性,如下:

<field name="class_id"        

    type="string"   stored="true"  indexed="true"  docValues="true"/>

 

docValue功能说明:

docValue是一个正排索引,的数据结构就是一个超级大mapLucene实现方式利用了Linux系统虚拟内存机制,keydocidvalue为某列的值。功能是当需要对某列进行排序操作,或者在命中结果集上进行基数统计,分组操作。这些操作都需要利用正排索引通过docidfieldvalue,像上面说到的按照classid去重需要利用docValue

 

以下是一段执行在SearchComponent中的示例代码,里面只把操作docValues相关的片段抽取了出来,生产环境的代码会更复杂。

 

import org.apache.solr.handler.component.SearchComponent;
public class TestDocValueComponent extends SearchComponent {
public void process(ResponseBuilder rb) throws IOException {
final long start = System.currentTimeMillis();
try {
SortedDocValues single
            = DocValues.getSorted(rb.req.getSearcher().getLeafReader(), "class_id");
} finally {
log.info((System.currentTimeMillis() - start) + "ms");
}
}
}

 

问题就出在调用getSortedDocValues这个方法上面,来看一下系统调用DocValue的时序图:



 

从调用的时序图可以看出,最后真正返回docValueMultiSortedDocValues。它是聚合了各个子reader的聚合代理类。MultiSortedDocValues 的构造函数之一的OrdinalMap这个参数是很重要的,简单说明一下该类的作用,需要先说明一下LuceneIndexReader类型,实际运行时提供给上层搜索器使用的是一个两层树状结构的数据结构,上层是一个根Reader,将下层的N个子LeafReader通过引用的方式聚合在一起,随着系统不断的有数据更新,下面的LeafReader也会变得越来越多(所以系统查询效率会越来越低,这也是搜索引擎不能当作数据库用的原因, 因为它需要定期作全量数据重建才能保证查询性能不至于变得太低),且每个leafReader都有一个docValue与之对应,根reader也可以取到与之对应的docValue

 

嘿嘿,说了半天还是没有说OrdinalMap是干啥用的,稍等一下,还要再说明一个事儿,对于string类型docValue 的实现类SortedDocValues上有一个getOrd()方法,这个方法很有用,查询时需要按照某个string类型的字段排序输出结果时,getOrd方法就要用上,Lucene在通过docid调用getOrd方法就能取得一个排序值(ordinal),两个不同doc对应的string类型的字段大小不需要通过原值进行比大小,只需要通过int型的值比大小就好了,这样就大大提降低了排序过程中IO开销(无论多大的文本字段,与文本内容无关,只与排序的顺序有关),因为docValue在物理存储时已经排好序了。

 

试想一下,在每个子Reader上都有一个字段值“美丽”,在第一个Readerdoc1的排序是n1,在第二个Readerdoc2的排序为n2,那么在根Reader对应的docValue上(base1 +doc1)和(base2+doc2)所对应顺序应该是多少呢?毫无疑问这个顺序值是同一个,这就需要有一个映射函数帮助了。这时候OrdinalMap就该上场啦,它的作用就是帮子docValueRoot docValue上的ordinal作映射。以下是OrdinalMap构造函数截取:

 

OrdinalMap(Object owner, TermsEnum subs[], SegmentMap segmentMap, float acceptableOverheadRatio) throws IOException {
      PackedLongValues.Builder globalOrdDeltas = PackedLongValues.monotonicBuilder(PackedInts.COMPACT);
      PackedLongValues.Builder firstSegments = PackedLongValues.packedBuilder(PackedInts.COMPACT);
      final PackedLongValues.Builder[] ordDeltas = new PackedLongValues.Builder[subs.length];
      for (int i = 0; i < ordDeltas.length; i++) {
        ordDeltas[i] = PackedLongValues.monotonicBuilder(acceptableOverheadRatio);
      }
      long[] ordDeltaBits = new long[subs.length];
      long segmentOrds[] = new long[subs.length];
      ReaderSlice slices[] = new ReaderSlice[subs.length];
      TermsEnumIndex indexes[] = new TermsEnumIndex[slices.length];
      for (int i = 0; i < slices.length; i++) {
        slices[i] = new ReaderSlice(0, 0, i);
        indexes[i] = new TermsEnumIndex(subs[segmentMap.newToOld(i)], i);
      }
      MultiTermsEnum mte = new MultiTermsEnum(slices);
      mte.reset(indexes);
      long globalOrd = 0;
      while (mte.next() != null) {        
        TermsEnumWithSlice matches[] = mte.getMatchArray();
        int firstSegmentIndex = Integer.MAX_VALUE;
        long globalOrdDelta = Long.MAX_VALUE;
        for (int i = 0; i < mte.getMatchCount(); i++) {
          int segmentIndex = matches[i].index;
          long segmentOrd = matches[i].terms.ord();
          long delta = globalOrd - segmentOrd;
          // We compute the least segment where the term occurs. In case the
          // first segment contains most (or better all) values, this will
          // help save significant memory
          if (segmentIndex < firstSegmentIndex) {
            firstSegmentIndex = segmentIndex;
            globalOrdDelta = delta;
          }
          while (segmentOrds[segmentIndex] <= segmentOrd) {
            ordDeltaBits[segmentIndex] |= delta;
            ordDeltas[segmentIndex].add(delta);
            segmentOrds[segmentIndex]++;
          }
        }
        // for each unique term, just mark the first segment index/delta where it occurs
        assert firstSegmentIndex < segmentOrds.length;
        firstSegments.add(firstSegmentIndex);
        globalOrdDeltas.add(globalOrdDelta);
        globalOrd++;
      }
      this.firstSegments = firstSegments.build();
      this.globalOrdDeltas = globalOrdDeltas.build();
      // ordDeltas is typically the bottleneck, so let's see what we can do to make it faster
      segmentToGlobalOrds = new LongValues[subs.length];
      long ramBytesUsed = BASE_RAM_BYTES_USED + this.globalOrdDeltas.ramBytesUsed()
          + this.firstSegments.ramBytesUsed() + RamUsageEstimator.shallowSizeOf(segmentToGlobalOrds)
          + segmentMap.ramBytesUsed();
      for (int i = 0; i < ordDeltas.length; ++i) {
        final PackedLongValues deltas = ordDeltas[i].build();
        if (ordDeltaBits[i] == 0L) {
          // segment ords perfectly match global ordinals
          // likely in case of low cardinalities and large segments
          segmentToGlobalOrds[i] = LongValues.IDENTITY;
        } else {
          final int bitsRequired = ordDeltaBits[i] < 0 ? 64 : PackedInts.bitsRequired(ordDeltaBits[i]);
          final long monotonicBits = deltas.ramBytesUsed() * 8;
          final long packedBits = bitsRequired * deltas.size();
          if (deltas.size() <= Integer.MAX_VALUE
              && packedBits <= monotonicBits * (1 + acceptableOverheadRatio)) {
            // monotonic compression mostly adds overhead, let's keep the mapping in plain packed ints
            final int size = (int) deltas.size();
            final PackedInts.Mutable newDeltas = PackedInts.getMutable(size, bitsRequired, acceptableOverheadRatio);
            final PackedLongValues.Iterator it = deltas.iterator();
            for (int ord = 0; ord < size; ++ord) {
              newDeltas.set(ord, it.next());
            }
            segmentToGlobalOrds[i] = new LongValues() {
              @Override
              public long get(long ord) {
                return ord + newDeltas.get((int) ord);
              }
            };
            ramBytesUsed += newDeltas.ramBytesUsed();
          } else {
            segmentToGlobalOrds[i] = new LongValues() {
              @Override
              public long get(long ord) {
                return ord + deltas.get(ord);
              }
            };
            ramBytesUsed += deltas.ramBytesUsed();
          }
          ramBytesUsed += RamUsageEstimator.shallowSizeOf(segmentToGlobalOrds[i]);
        }
      }
      this.ramBytesUsed = ramBytesUsed;
    }

 

以上这段代码逻辑确实有点复杂,内部不去细究,总的思路是遍历每个子segment上的Terms然后构建一个全局的ordinal数据结构,这里的算法负责度是OtermsCount)它的执行时间是随着terms的数量决定的。因此,对于对于散列的字段类型特别要注意,比如一些主键字段类似“用户id”(几乎每条记录的主键值都不相同),如果是一枚举类型的字段就没有问题,比如“用户类型”全部数据集合上也就几十种类型。

 

因此每当一次softcommit之后,内部的SlowCompositeReaderWrapper.cachedOrdMaps中保存的OrdinalMap对象就会失效,从而需要构建一个新的OrdinalMap,而构建OrdinalMap的时间是依赖于field对应的term的多少来确定的。

如何解决

问题已经明确,那么现在就可以对症下药了,解决这个问题有三个办法:

1.看看真的是否有必要使用string类型的字段,是否可以将字段类型换成数字类型比如long,因为NumericDocValues没有getOrd这样的方法,它直接通过docid取对应field的值,没有构建OrdinalMap对象的过程。

2.如果真的要使用string类型的字段,考虑是否可以最终在处理结果集的时候不在根reader上操作进行操作。就拿facet查询,最终要得到的结果就是统计根据最终值统计该字段值的个数(基数统计)。如果在最终命中结果数可控的情况下,比如最终命中在几千个的情况下可以考虑自己构建一个SearchComponentprocess方法中定义一个collector去遍历索引命中的dociddocid都是子reader上的),所以就可以避免使用全局docvalue,因此也可以避免构建OrdinalMap了。

3.如果真的没有办法去改变已有的数据机构,还有一招也可以起到立竿见影的功效,那就是在每次softcommit之后在新的indexReader生效之前,先对docValue进行预热,写一个AbstractSolrEventListener,代码如下:

public class FieldWarmupEventListener extends AbstractSolrEventListener {

 

 public FieldWarmupEventListener(SolrCore core) {

        super(core);

    }

 

    @Override

    public void newSearcher(SolrIndexSearcher newSearcher, SolrIndexSearcher currentSearcher) {

        try {

            DocValues.getSorted(newSearcher.getLeafReader(), order_id);    

        } catch (IOException e) {

            throw new IllegalStateException(e);

        }

    }

}

solrconfig中添加一个listener

<listener event="newSearcher" class="com.sit.solrextend.FieldWarmupEventListener"/>

 

无论使用以上哪种方法,都可以解决使用docvalue引起的性能问题。祝君玩得愉快

  • 大小: 38.8 KB
0
1
分享到:
评论

相关推荐

    lucene,solr的使用

    lucene,solr的使用lucene,solr的使用lucene,solr的使用lucene,solr的使用lucene,solr的使用lucene,solr的使用lucene,solr的使用lucene,solr的使用lucene,solr的使用lucene,solr的使用lucene,solr的使用

    solr/ext/ 里面的jar包

    For more information, see: http://wiki.apache.org/solr/SolrLogging 原因,可能是你的solr服务器版本问题, 1、下载最新的solr包,比如:solr-5.3.1.zip 2、解压后找到,ext文件夹,把这个文件夹下面的所有jar...

    apache lucene solr 官网历史版本 免费下载地址

    http://archive.apache.org/dist/lucene/java/ 这个是lucene的历史版本 http://archive.apache.org/dist/lucene/solr/ 这个是solr的历史版本

    01.lucene&solr;-day01-v2.0.doc

    lucene和solr笔记

    配置好的solr启动环境

    完全配置好的solr容器,直接修改web.xml设置一下solr core路劲即可

    solr_solr_lucene_

    Solr是一个高性能,采用Java开发,基于Lucene的全文搜索服务器。同时对其进行了扩展,提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展并对查询性能进行了优化,并且提供了一个完善的功能管理界面,是一...

    paoding-webx3-solr-lucene

    paoding-webx3-solr-lucene

    Solr Elasticsearch lucene 搜索引擎

    Solr Elasticsearch lucene 搜索引擎

    solr5.4.0完整包

    Solr 依存于Lucene,因为Solr底层的核心技术是使用Lucene 来实现的,Solr和Lucene的本质区别有以下三点:搜索服务器,企业级和管理。Lucene本质上是搜索库,不是独立的应用程序,而Solr是。Lucene专注于搜索底层的...

    LoremIpsumSearch:包含与 lucene 和 solr 一起使用的搜索算法

    LoremIpsum搜索 包含与 lucene 和 solr 一起使用的搜索算法... export CLASSPATH="&lt;lucene&gt;/lucene/replicator/lib/*:&lt;nutch&gt;/build/*:&lt;nutch&gt;/build/lib/*:&lt;lucene&gt;/solr/dist/*:&lt;lucene&gt;/solr/ dist/solrj-lib/*:*:.

    Apache Solr lucene 搜索模块设计实现

    Apache Solr lucene 搜索模块设计实现 Solr 模块 架构 lucene 搜索

    IKAnalyzer2012FF_u1.jar

    3.把IKAnalyzer2012FF_u1.jar添加到/opt/cloudera/parcels/CDH/lib/solr/webapps/solr/WEB-INF/lib目录 4.修改 /opt/cdhsolr/fuser/conf/schema.xml文件,在其中添加 &lt;!--配置IK分词器--&gt; 引用 ...

    solr -8.11.1.zip 文件

    solr -8.11.1.zip 文件

    solr_lucene3.5_lukeall-3.5.0.jar.zip

    solr_lucene3.5_lukeall-3.5.0.jar.zip

    Solr Enterprise Search Server

    solr是基于Lucene Java搜索库的企业级全文搜索引擎,目前是apache的一个项目。它的官方网址在http://lucene.apache.org/solr/ 。solr需要运行在一个servlet 容器里,例如tomcat5.5。solr在lucene的上层提供了一个...

    lucene&solr原理分析

    lucene&solr原理分析,lucene搜索引擎和solr搜索服务器原理分析。

    Apache+Solr+Reference+Guide 2018.pdf

    Solr is free to download from http://lucene.apache.org/solr/. Designed to provide high-level documentation, this guide is intended to be more encyclopedic and less of a cookbook. It is structured to ...

    xmljava系统源码-IKAnalyzer2017_6_6_0:IK中文分词,兼容solr/lucene6.6.0,优化数字和英文搜索

    IK中文分词对于数字和英文的分词方式是:一个英文单词为一个语汇单元,一个数值为一个语汇单元。 比如:"2017 IK Analyzer是一个中文分词开源工具包。"这个句话使用IK中文分词后的结果为: 所以针对数值和英文单词的...

    solr-5.2.1-src.tgz源码

    Solr源码在MyEclipse下的... &lt;Environment name="solr/home" type="java.lang.String" value="E:\Solr" override="true" /&gt; &lt;/Context&gt; 注:value对应地址即你创建的solr/home目录地址 4. 部署到tomcat,开始Solr

Global site tag (gtag.js) - Google Analytics