发现问题
最近在使用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_id在schema设置上必须要开启docValue=true属性,如下:
<field name="class_id" type="string" stored="true" indexed="true" docValues="true"/> |
docValue功能说明:
docValue是一个正排索引,的数据结构就是一个超级大map。Lucene实现方式利用了Linux系统虚拟内存机制,key为docid,value为某列的值。功能是当需要对某列进行排序操作,或者在命中结果集上进行基数统计,分组操作。这些操作都需要利用正排索引通过docid取fieldvalue,像上面说到的按照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的时序图:
从调用的时序图可以看出,最后真正返回docValue为MultiSortedDocValues。它是聚合了各个子reader的聚合代理类。MultiSortedDocValues 的构造函数之一的OrdinalMap这个参数是很重要的,简单说明一下该类的作用,需要先说明一下Lucene的IndexReader类型,实际运行时提供给上层搜索器使用的是一个两层树状结构的数据结构,上层是一个根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上都有一个字段值“美丽”,在第一个Reader上doc1的排序是n1,在第二个Reader上doc2的排序为n2,那么在根Reader对应的docValue上(base1 +doc1)和(base2+doc2)所对应顺序应该是多少呢?毫无疑问这个顺序值是同一个,这就需要有一个映射函数帮助了。这时候OrdinalMap就该上场啦,它的作用就是帮子docValue和Root 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数据结构,这里的算法负责度是O(termsCount)它的执行时间是随着terms的数量决定的。因此,对于对于散列的字段类型特别要注意,比如一些主键字段类似“用户id”(几乎每条记录的主键值都不相同),如果是一枚举类型的字段就没有问题,比如“用户类型”全部数据集合上也就几十种类型。
因此每当一次softcommit之后,内部的SlowCompositeReaderWrapper.cachedOrdMaps中保存的OrdinalMap对象就会失效,从而需要构建一个新的OrdinalMap,而构建OrdinalMap的时间是依赖于field对应的term的多少来确定的。
如何解决
问题已经明确,那么现在就可以对症下药了,解决这个问题有三个办法:
1.看看真的是否有必要使用string类型的字段,是否可以将字段类型换成数字类型比如long,因为NumericDocValues没有getOrd这样的方法,它直接通过docid取对应field的值,没有构建OrdinalMap对象的过程。
2.如果真的要使用string类型的字段,考虑是否可以最终在处理结果集的时候不在根reader上操作进行操作。就拿facet查询,最终要得到的结果就是统计根据最终值统计该字段值的个数(基数统计)。如果在最终命中结果数可控的情况下,比如最终命中在几千个的情况下可以考虑自己构建一个SearchComponent在process方法中定义一个collector去遍历索引命中的docid(docid都是子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引起的性能问题。祝君玩得愉快
相关推荐
Solr 基于 Lucene 库,提供了一个高度可配置和可扩展的平台,用于处理和索引大量数据,支持多种数据源,如文件、数据库等。其主要特性包括: 1. **全文搜索**:Solr 可以对文本进行分词和索引,实现高效的模糊匹配...
- **使用方式**:Lucene通常作为库直接集成到应用程序中,而Solr则作为一个独立的服务运行。 - **功能丰富程度**:Solr相对于Lucene来说,提供了更多高级功能,如分面搜索、自动完成等。 - **集群支持**:Solr支持...
For more information, see: http://wiki.apache.org/solr/SolrLogging 原因,可能是你的solr服务器版本问题, 1、下载最新的solr包,比如:solr-5.3.1.zip 2、解压后找到,ext文件夹,把这个文件夹下面的所有jar...
"paoding-webx3-solr-lucene"是一个专注于搜索引擎构建的项目,它结合了Webx3、Paoding(分词库)和Solr(企业级搜索平台)的优势,为中文搜索提供了一套强大的解决方案。本文将深入探讨这个项目的各个组件以及它们...
http://archive.apache.org/dist/lucene/java/ 这个是lucene的历史版本 http://archive.apache.org/dist/lucene/solr/ 这个是solr的历史版本
Solr 使用Lucene作为其核心搜索引擎库,提供了一个分布式、可扩展且高度可用的搜索和分析服务。 **Lucene**: Apache Lucene 是一个高性能、全功能的文本搜索库,也是Solr的核心。它提供了强大的索引和搜索功能,...
Lucene是一个强大的全文搜索引擎库,由Doug Cutting创建并维护,自2001年起成为Apache软件基金会的一部分,归属其Jakarta项目。作为Java编写的一款开源工具,Lucene被广泛应用于各种需要高效检索功能的系统中,如...
Lucene是Java开发的一个全文检索库,而Solr则是基于Lucene构建的企业级搜索平台,提供了更高级的功能和管理界面。 **Lucene简介** Lucene是Apache软件基金会的一个项目,它提供了一个强大的文本分析和索引框架,...
Solr是一个高性能的全文搜索引擎,基于Apache Lucene开发,使用Java 5编写。它不仅继承了Lucene的强大功能,还提供了更丰富的查询语言以及更好的性能优化。Solr具备高度可配置性和可扩展性,支持通过HTTP请求提交XML...
在这个“配置好的solr启动环境”中,我们有一个预先配置好的Solr运行环境,尤其适合快速部署和测试。 该压缩包文件名为“solrtomcat”,暗示了Solr是通过Tomcat这样的Servlet容器来运行的。Tomcat是一个轻量级的...
Solr是Apache Lucene项目的一个子项目,是一个高性能、基于Java的企业级全文搜索引擎服务器。当你在尝试启动Solr时遇到404错误,这通常意味着Solr服务没有正确地启动或者配置文件设置不正确。404错误表示“未找到”...
**Solr** 是 Apache 下的一个顶级开源项目,它基于 **Lucene** 进行构建,提供了强大的全文搜索能力。相较于 Lucene,Solr 提供了更为丰富的查询语言支持,并且具备高度可配置性和可扩展性,针对索引和搜索性能进行...
在Solr,一个基于Lucene的全文搜索服务器,配置`SOLR_HOME`是至关重要的步骤,因为它决定了Solr实例的数据存储位置。本篇将详细解释三种不同的`SOLR_HOME`配置方式。 首先,我们来看第一种配置方法,即**基于当前...
Elasticsearch同样基于Lucene,但它是一个完整的分布式搜索和分析引擎,提供了更高级的API和更友好的使用体验。Elasticsearch不仅支持全文搜索,还支持结构化和半结构化数据的搜索,如日志分析、监控、图谱分析等。...
- 在`D:/solr/apache-solr-3.5.0/example/solr`目录下创建一个名为`dic`的文件夹。 - 将解压后的`data`目录中的`words.dic`文件复制到`D:/solr/apache-solr-3.5.0/example/solr/dic`目录下。 5. **配置Schema文件...
- 访问 Apache 官方镜像站点 [http://mirror.bjtu.edu.cn/apache/lucene/solr/](http://mirror.bjtu.edu.cn/apache//lucene/solr/) 下载最新版本的 Solr,目前推荐使用的是 Solr 3.5.0 版本。 ##### 2.3 安装与启动...