应用背景
诸多业务场景下,都有使用kv型式存储数据供快速查询的需求。正常的做法有使用HashMap存入内存,或者存入外部的nosql KV数据库/缓存。
- 使用HashMap做KV存储,速度快,但是如果数据量达到百万及至千万级时,HashMap必将占用大量的java堆内存,给应用带来极大的内存回收压力。
- 外部kv存储,以堆外(offHeap)存储的方式让我们的应用免于内存回收之忧,但其查询性能往往低于内存map。假设采用外部db的方式作kv存储,就会引入服务之间的通信成本,以基于LR(逻辑回归)实现推荐系统的打分服务为例,每次打分,须执行近求成百上万次kv查询(lr参数的查询),如此的查询量对性能的要求是极高的,如果每一次查询都要查询外部服务,那么网络io势必占用大量的时间。
此外,在工作中会发现很多算法问题,都会被转换为一种追求效率的搜索问题,高效的内嵌式kv存储就会显得更有价值。
需求与方案选择
综上所述,本人想要的KV存储,应当满足如下五点要求:
- 内嵌的形式,做为内部存储。这样做的好处在于,能够大大减少通讯成本与性能消耗。而且很多时候,我们所需的kv存储并未达到需要做到分布式的数据量级。
- 采用堆外的方式存储数据。
- 接近于内存Map的查询速度。
- 最好具有硬盘执久化的能力,避免每次重启全量构建。
- 适用于java开发环境。由于我们的各种引擎构建以java为主要开发语言,所以我需要的是一种适用于java的内嵌式kv存储,最好这种存储就是java实现的。
鉴于上述要求,本文选择lucene的mmap存储方式。如果不考虑第五点需求,鼎鼎大名的leveldb是一个很好的选择。leveldb与lucene都可以视作为lsm-tree实现的存储library。原生的leveldb 是用c++实现的。如果以java方式调用,必然会涉及一定量的改造工作。leveldb也有存java实现的版本,据说性能并不逊于lucene,未敢用过。
基于上述要求,我采用lucene4.5.1做kv存储(本文是对之前工作的整体,请不要吐槽我的版本太老),以mmap方式加载lucene索引。由于lucene本身是为搜索设计的,其索引结构远比正常的map形式kv索引复杂得多,存在很多的简化与性能优化的空间。下文描述就是基于lucene MMap实现KV索引,并作不同尝试过程。
以下所有测试采用长度为20的string作为key,int作为value,索引构建条目数为200w。
测试代码地址:https://github.com/quentinxxz/LuceneKVTest
以HashMap为性能参考
参考代码:HashMapTest
首先,以HashMap存储的方式。
每百万次查询,结果如下:
百万次查询平均用时,仅用0.14s,可见HashMap查询的速度相当惊人。
方法1:store存储value,默认压缩
参考代码:LuceneSotreTest.java
value值以lucene store的方式存储。Value field的定义如下:
protected static class ValueField extends Field {
/**
* Type for numeric DocValues.
*/
public static final FieldType TYPE = new FieldType();
static {
// TYPE.setIndexed(true);
TYPE.setStored(true);
// TYPE.setDocValueType(DocValuesType.NUMERIC);
TYPE.setNumericType(NumericType.INT);// 需要支持范围查询,NumbericType会自动建Trie结构
TYPE.setOmitNorms(true);
TYPE.setIndexOptions(IndexOptions.DOCS_ONLY);
TYPE.freeze();
}
public ValueField(String name, int value){
super(name, TYPE);
fieldsData = value;
}
}
查询时,并没有直接使用TermQuery进行查询。TermQuery的内部操作较复杂,我们只作KV查询可以使用更底层的接口进行查询。
首先初始化termsEnumList,避免每次查询时又重新初始化,而且注意termsEnumList不能多线程共享,否则会有线程问题:
IndexReader indexReader = DirectoryReader.open(new MMapDirectory(indexPath));
List<TermsEnum> termsEnumList = new ArrayList<TermsEnum>();// 事先初始化termsEnumList,会有多线程问题,当多线程查询时,请用ThreadLocal封装
for (AtomicReaderContext context : indexReader.leaves()) {
termsEnumList.add(context.reader().terms("key").iterator(null));
}
百万次查询代码如下:
// mmap方式查询
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
for (int i = 0; i < 10; i++) {
start = System.currentTimeMillis();
keys.stream().limit(1000000).forEachOrdered(key -> {
Term term = new Term("key", key);
for (int l = 0; l < termsEnumList.size(); l++) {
try {
TermsEnum termsEnum = termsEnumList.get(l);
// TermsEnum termsEnum = ctx.reader().terms(term.field()).iterator(null);
if (termsEnum.seekExact(term.bytes()) == false) continue;
DocsEnum docs = termsEnum.docs(null, null);
int docId = docs.nextDoc();
Document d = indexSearcher.doc(docId);
int result = (Integer) d.getField("value").numericValue();
// System.out.println(result);
return;
} catch (IOException e) {
}
}
System.out.println("not found");
});
百万次查询用的时测试结果如下:
在200w条记录的情况下,百万次查询,平均用时3.4s。
方法2:store存储value,去压缩
参考代码:LuceneUnCompressedSotreTest.java
UnCompressedLucene45Codec.java
利用jvisualvm监控方法1查询过程,发现主要的cpu消耗,在于vaule信息读取解压缩的过程。所以接下来的一步优化在于去掉store存储的压缩。具体做法需要自定义lucene的Codec,对UnCompressedLucene45Codec稍作修改:
public class UnCompressedLucene45Codec extends Codec {
// private final StoredFieldsFormat fieldsFormat = new Lucene41StoredFieldsFormat();
private final StoredFieldsFormat fieldsFormat = new Lucene40StoredFieldsFormat();
private final TermVectorsFormat vectorsFormat = new Lucene42TermVectorsFormat();
private final FieldInfosFormat fieldInfosFormat = new Lucene42FieldInfosFormat();
private final SegmentInfoFormat infosFormat = new Lucene40SegmentInfoFormat();
private final LiveDocsFormat liveDocsFormat = new Lucene40LiveDocsFormat();
......
将第一个成员fieldFormat由 Lucene41StoredFieldsFormat,换成Lucene40StoredFieldFormat()。要使得自定义的Codec生效还如其步骤,可参考 http://www.romseysoftware.co.uk/2012/07/04/writing-a-new-lucene-codec/
再次进行测试,得到百万次查询用时约1.8s。
方法3:使用DocValues存储vaule
参考代码:LuceneDocValuesTest.java
DocValues存储的结构比store方式存储更为简单,理应效率更高。这里对Value field的定义再作修改,如下:
protected static class ValueField extends Field {
/**
* Type for numeric DocValues.
*/
public static final FieldType TYPE = new FieldType();
static {
TYPE.setIndexed(true);
TYPE.setStored(false);
TYPE.setDocValueType(DocValuesType.NUMERIC);
TYPE.setNumericType(NumericType.INT);
TYPE.setOmitNorms(true);
TYPE.setIndexOptions(IndexOptions.DOCS_ONLY);
TYPE.freeze();
}
public ValueField(String name, long value){
super(name, TYPE);
fieldsData = value;
}
}
具体搜索方法请参考代码。
再次测试,得到平均百万次查询用时约为1.5s。
方法4:使用Payload存储vaule
参考代码:LucenePayloadTest.java
IntPayloadTokenizer.java
Payload 是 Lucene 一个允许在索引中为词条储存元数据信息。这边自定义Payload内容是采用重写Tokenizer实现的,具体过程不再赘述,请参考代码。
测试结果为,百万次查询用时约3.6s,较慢。
方法5: lucene FSA存储value
参考代码:
lucene FSA(有限自动机) 或称为FST。它是lucene4开始大量使用的一种索引结构。该算法可以利用索引词的前缀与后缀信息做加压缩,类似Double Array Trie Tree需要查询复杂度为O(n),n为key的字符长度。演示代码是使用lucene FSA的底层api实现。
本人使用时遇到两个明显的问题:
- 条目添加时必须事先排序,即必须所有key按字典序排好,再作插入操作,索引查询结果不正确。
- FST的构建本身是发生heap中的,构建结束再一次序列化到磁盘,也就是说如果KV的量很大的话,构建过程会消耗大量的heap空间。
所以当数据量较大时,直接使用并不是一个很好的选择。为了解决上速问题,可以尝试类似lucene普通索引构建一样,分多个小的segment,在内存上构建一个较小的FST结构再写入磁盘,然可以对多个segment作merge操作。而我在阅读lucene代码BlockTreeTremWriter时,发现,lucene并不是直接使用FST,而是先用FST index+ Block(相同前缀的key分到一个块)分块的方案。
百万次查询平均用时1.3s。
方法5:Freq存储value信息
最外,还尝试使用Freq存储value信息,由于Freq为int类型,只能存储4个byte,无法存储double类型,所以只做性能测试。具体实现方法较为复杂,不作赘述。 查询时,获取Freq值的方法如下:
if(termsEnum.seekExact(term.bytes())==false) return null;
DocsEnum docs = termsEnum.docs(null,null);
docs.nextDoc();
return docs.freq()
之前的测试代码找不到后,以后有机会再作补充。了解lucene索存结构的读者,应该能猜到这种方式是最快的。(以前得到的结论,具体多快得补充代码后测试才可知)
总结
本人目前kv存储一般采用DocValues存储,mmap读,即方法3。主要两点考虑,一是为了达到性能上的要求,另一点允许任意长度的value值(代码稍作修改,读者可自行探索)。
20161023首发于3dobe.com http://3dobe.com/archives/247/
本站链接 http://quentinXXZ.iteye.com/blog/2332914
相关推荐
文章主要研究和应用了基于Lucene的搜索引擎,其特点是利用开源网络爬虫工具抓取互联网信息,并通过Lucene的API对特定信息进行索引和搜索。下面详细介绍相关知识点。 1. Lucene基础 Lucene是由Apache软件基金会提供...
《基于Lucene的小型搜索引擎构建详解》 在信息爆炸的时代,如何快速、准确地找到所需信息成为了一项挑战。搜索引擎作为解决这一问题的关键工具,其技术实现也引起了广泛关注。本篇将详细介绍一个基于Apache Lucene...
**基于Lucene技术的增量索引** 在信息技术领域,全文搜索引擎是处理大量数据查询的关键工具。Apache Lucene是一个开源的全文检索库,被广泛应用于构建高效、可扩展的搜索功能。本文将深入探讨如何利用Lucene实现...
**基于Lucene的搜索引擎** Lucene是一个开源的全文检索库,由Apache软件基金会开发并维护。它是Java编写的一个高性能、可扩展的信息检索库,为开发者提供了构建搜索功能的基础框架。这个课程设计创建了一个简单的...
**基于Lucene的中型搜索引擎(C#)** 在IT领域,搜索引擎是不可或缺的一部分,它们能够高效地处理海量数据,帮助用户快速找到所需信息。本文将深入探讨一个基于Apache Lucene的中型搜索引擎实现,该实现是由...
**基于Lucene的全文检索系统** Lucene是一个高性能、全文本搜索库,由Apache软件基金会开发,被广泛应用于各种搜索引擎的构建。它提供了一个简单但功能强大的API,可以帮助开发者快速地在大量文档中实现高效的全文...
【标题】:“基于Lucene的全文检索系统” 【描述】提到的“在学校教育网上搜的 不知道帮助大不大 看看吧 也许会用得到 好多个 可能有重复”暗示了资源可能包含多个关于Lucene全文检索系统的PDF文档,这些文档可能...
【标题】:“基于Lucene的Web站内信息搜索系统” 【描述】:Lucene是一个高性能、全文本搜索引擎库,由Apache软件基金会开发并维护。它为开发者提供了在Java应用程序中实现全文检索功能的工具包。当构建一个Web站内...
3. 索引存储:索引会被存储在磁盘上,可以是单一文件或多个文件,LUCENE提供了多种存储格式供选择,如SimpleFSDirectory、MMapDirectory等。 四、LUCENE搜索机制 1. 查询解析:用户输入的查询字符串被转换为Query...
java代码,基于Lucene和mysql的简单的字符串匹配分词系统
### 一种基于Lucene检索引擎的全文数据库的研究与实现 #### 1. 引言 随着信息技术的飞速发展和互联网的普及,大量的文本信息被数字化存储,这为信息检索带来了前所未有的挑战和机遇。传统的数据库管理系统(DBMS)...
### 基于Lucene的全文检索系统研究与开发 #### 摘要与背景介绍 本文探讨了一种基于Jakarta Lucene构建的全文检索系统模型。相较于Google的站内检索及传统数据库检索方法,该模型展现出显著的优势,特别是在关键字...
**基于Lucene的桌面搜索引擎** 在信息技术飞速发展的今天,数据量激增,高效的信息检索变得至关重要。桌面搜索引擎作为个人信息管理的重要工具,可以帮助用户快速、准确地定位存储在本地计算机中的文件和信息。本...
**基于Lucene的文件检索系统详解** Lucene是一款开源的全文搜索引擎库,由Apache软件基金会维护,被广泛应用于各种搜索引擎的开发。它提供了一个高效、可扩展的框架,用于索引和搜索大量文本数据。本篇文章将深入...
### 基于Lucene的WEB站内搜索引擎研究与实现 #### 一、搜索引擎基本原理与Lucene概述 搜索引擎的基本原理涉及对大量文档或网页进行分析、索引和检索的过程。这一过程通常包括数据采集(爬虫)、预处理(如分词、...