Lucene高级

Lucene底层储存结

lucene存储结构

存储结构
在这里插入图片描述
索引(Index)
一个目录一个索引,在Lucene中一个索引是放在一个文件夹中的。
段(Segment)
一个索引(逻辑索引)由多个段组成, 多个段可以合并, 以减少读取内容时候的磁盘IO.

Lucene中的数据写入会先写内存的一个Buffer,当Buffer内数据到一定量后会被flush成一个 Segment,每个Segment有自己独立的索引,可独立被查询,但数据永远不能被更改。这种模式 避免了随机写,数据写入都是批量追加,能达到很高的吞吐量。Segment中写入的文档不可被修 改,但可被删除,删除的方式也不是在文件内部原地更改,而是会由另外一个文件保存需要被删除 的文档的DocID,保证数据文件不可被修改。Index的查询需要对多个Segment进行查询并对结果 进行合并,还需要处理被删除的文档,为了对查询进行优化,Lucene会有策略对多个Segment进 行合并。
文档(Document)
文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。

新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。
域(Field)
一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,描述等,都可以保存在不 同的域里。

不同域的索引方式可以不同。

词是索引的小单位,是经过词法分析和语言处理后的字符串。

索引库物理文件

在这里插入图片描述

索引库文件扩展名对照表

Segments Filesegments_N保存了一个提交点(a commit point)的信息
Lock Filewrite.lock防止多个IndexWriter同时写到一份索引文件中
Segment Info.si保存了索引段的元数据信息
Compound File.cfs,.cfe一个可选的虚拟文件,把所有索引信息都存储到复合索 引文件中
Fields.fnm保存fields的相关信息
Field Index.fdx保存指向field data的指针
Field Data.fdt文档存储的字段的值
Term Dictionary.timterm词典,存储term信息
Term Index.tip到Term Dictionary的索引
Frequencies.doc由包含每个term以及频率的docs列表组成
Positions.pos存储出现在索引中的term的位置信息
Payloads.pay存储额外的per-position元数据信息,例如字符偏移和 用户payloads
Norms.nvd,.nvm.nvm文件保存索引字段加权因子的元数据,.nvd文件保 存索引字段加权数据
Per-Document Values.dvd,.dvm.dvm文件保存索引文档评分因子的元数据,.dvd文件保 存索引文档评分数据
Term Vector Index.tvx将偏移存储到文档数据文件中
Term Vector Documents.tvd包含有term vectors的每个文档信息
Term Vector Fields.tvf字段级别有关term vectors的信息
Live Documents.liv哪些是有效文件的信息
Point values.dii,.dim保留索引点,如果有的话

词典的构建

为何Lucene大数据量搜索快, 要分两部分来看

  • 一点是因为底层的倒排索引存储结构.
  • 另一点就是查询关键字的时候速度快, 因为词典的索引结构.
词典数据结构对比

倒排索引中的词典位于内存,其结构尤为重要,有很多种词典结构,各有各的优缺点,简单如排序数 组,通过二分查找来检索数据,更快的有哈希表,磁盘查找有B树、B+树,但一个能支持TB级数据的倒 排索引结构需要在时间和空间上有个平衡,下图列了一些常见词典的优缺点:

跳跃表占用内存小,且可调,但是对模糊查询支持不好
排序列表Array/List使用二分法查找,不平衡
字典树查询效率跟字符串长度有关,但只适合英文词典
哈希表性能高,内存消耗大,几乎是原始数据的三倍
双数组字典树适合做中文词典,内存占用小,很多分词工具均采用此种算法
Finite State Transducers (FST)一种有限状态转移机,Lucene 4有开源实现,并大量使用
B树磁盘索引,更新方便,但检索速度慢,多用于数据库

Lucene3.0之前使用的也是跳跃表结构,后换成了FST,但跳跃表在Lucene其他地方还有应用如倒排表 合并和文档号索引

跳跃表原理

Lucene3.0版本之前使用的跳跃表结构后换成了FST结构
优点 :结构简单、跳跃间隔、级数可控,Lucene3.0之前使用的也是跳跃表结构,,但跳跃表在 Lucene其他地方还有应用如倒排表合并和文档号索引。
缺点 :模糊查询支持不好.
单链表 :单链表中查询一个元素即使是有序的,我们也不能通过二分查找法的方式缩减查询时间。
通俗的讲也就是按照链表顺序一个一个找.
举例: 查找85这个节点, 需要查找7次.
在这里插入图片描述
跳跃表:
举例: 查询85这个节点, 一共需要查询6次.

  1. 在level3层, 查询3次, 查询到1结尾, 退回到37节点
  2. 在level2层, 从37节点开始查询, 查询2次, 查询到1结尾, 退回到71节点
  3. 在level1层, 从71节点开始查询, 查询1次, 查询到85节点.
    在这里插入图片描述
FST原理简析

已知FST要求输入有序,所以Lucene会将解析出来的文档单词预先排序,然后构建FST,我们假设输入 为abd,abe,acf,acg,那么整个构建过程如下:
在这里插入图片描述
输入数据:

String inputValues[] = {"hei","ma","cheng","xu","yuan","good"}; 
long outputValues[] = {0,1,2,3,4,5}; 

输入的数据如下:
hei/0 ma/1 cheng/2 xu/3 yuan/4 good/5

Lucene优化

解决大量磁盘IO

  • config.setMaxBufferedDocs(100000); 控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度。数值越大索引速度越快, 但是会消耗更多的内存

  • indexWriter.forceMerge(文档数量); 设置N个文档合并为一个段数值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快 更高的值意味着索引期间更低的段合并开销,但同时也意味着更慢的搜索速度,因为此时的索引通 常会包含更多的段。如果该值设置的过高,能获得更高的索引性能。但若在后进行索引优化,那 么较低的值会带来更快的搜索速度,因为在索引操作期间程序会利用并发机制完成段合并操作。故 建议对程序分别进行高低多种值的测试,利用计算机的实际性能来告诉你优值。

@Test
//创建索引代码优化测试
public void testCreateIndex() throws Exception {
	// 1. 采集数据
	SkuDao skuDao = new SkuDaoImpl();
	List<Sku> skuList = skuDao.querySkuList();

	// 文档集合
	List<Document> docList = new ArrayList<>();

	for (Sku sku : skuList) {
		// 2. 创建文档对象
		Document document = new Document();
		document.add(new StringField("id", sku.getId(), Field.Store.YES));
		document.add(new TextField("name", sku.getName(), Field.Store.YES));
		document.add(new IntPoint("price", sku.getPrice()));
		document.add(new StoredField("price", sku.getPrice()));
		document.add(new StoredField("image", sku.getImage()));
		document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));
		document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));

		// 将文档对象放入到文档集合中
		docList.add(document);
	}

	long start = System.currentTimeMillis();

	// 3. 创建分词器, StandardAnalyzer标准分词器, 对英文分词效果好, 对中文是单字分词, 也就是一个字就认为是一个词.
	Analyzer analyzer = new StandardAnalyzer();
	// 4. 创建Directory目录对象, 目录对象表示索引库的位置
	Directory dir = FSDirectory.open(Paths.get("E:\\temp\\index"));
	// 5. 创建IndexWriterConfig对象, 这个对象中指定切分词使用的分词器
	IndexWriterConfig config = new IndexWriterConfig(analyzer);
	// 设置在内存中多少个文档向磁盘中批量写入一次数据
	// 如果设置的数字过大, 会过多消耗内存, 但是会提升写入磁盘的速度
	 config.setMaxBufferedDocs(500000);
	// 6. 创建IndexWriter输出流对象, 指定输出的位置和使用的config初始化对象
	IndexWriter indexWriter = new IndexWriter(dir, config);
	// 设置多少给文档合并成一个段文件,数值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快
	indexWriter.forceMerge(1000000);
	// 7. 写入文档到索引库
	for (Document doc : docList) {
		indexWriter.addDocument(doc);
	}
	// 8. 释放资源
	indexWriter.close();
	long end = System.currentTimeMillis();
	System.out.println("=====消耗的时间为:==========" + (end - start) + "ms");
}

选择合适的分词器

不同的分词器分词效果不同, 所用时间也不同
虽然StandardAnalyzer切分词速度快过IKAnalyzer, 但是由于StandardAnalyzer对中文支持不好, 所 以为了追求好的分词效果, 为了追求查询时的准确率, 也只能用IKAnalyzer分词器, IKAnalyzer支持停 用词典和扩展词典, 可以通过调整两个词典中的内容, 来提升查询匹配的精度

选择合适的位置存放索引库

写操作读操作特点
SimpleFSDirectoryjava.io.RandomAccessFilejava.io.RandomAccessFile简单实现,并发能力差
NIOFSDirectoryjava.nio.FileChannelFSDirectory.FSIndexOutput并发能力强, windows平台下 有重大bug
MMapDirectory内存映射FSDirectory.FSIndexOutput读取操作基于内存

测试代码修改:

Directory directory = MMapDirectory.open(Paths.get("E:\\dir"));

搜索api的选择

  1. 尽量使用TermQuery代替QueryParser
  2. 尽量避免大范围的日期查询

Lucene相关度排序

什么是相关度排序

Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。

如何打分

Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:

  1. 计算出词(Term)的权重
  2. 根据词的权重值,计算文档相关度得分。

明确索引的小单位是一个Term(索引词典中的一个词),搜索也是要从Term中搜索,再根据Term找到 文档,Term对文档的重要性称为权重,影响Term权重有两个因素:

  • Term Frequency (tf): 指此Term在此文档中出现了多少次。tf 越大说明越重要。 词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数很多,说明该文档主要就是讲Lucene技术的。
  • Document Frequency (df): 指有多少文档包含次Term。df 越大说明越不重要。 比如,在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低。
怎样影响相关度排序

boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。

  • 在索引时对某个文档中的field设置加权值高,在搜索时匹配到这个文档就可能排在前边。

  • 在搜索时对某个域进行加权,在进行组合域查询时,匹配到加权值高的域后计算的相关度得分就 高。

设置boost是给域(field)或者Document设置的。

人为影响相关度排序

查询的时候, 通过设置查询域的权重, 可以人为影响查询结果.

@Test
public void testIndexSearch() throws Exception {
	// 1. 创建分词器(对搜索的关键词进行分词使用)
	// 注意: 分词器要和创建索引的时候使用的分词器一模一样
	Analyzer analyzer = new IKAnalyzer();
	// 需求: 不管是名称域还是品牌域或者是分类域有关于手机关键字的查询出来
	// 查询的多个域名
	String[] fields = { "name", "categoryName", "brandName" };
	// 设置影响排序的权重, 这里设置域的权重
	Map<String, Float> boots = new HashMap<>();
	boots.put("categoryName", 10000000000f);
	// 从多个域查询对象
	MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, analyzer, boots);
	// 设置查询的关键词
	Query query = multiFieldQueryParser.parse("手机");
	// 4. 创建Directory目录对象, 指定索引库的位置
	Directory dir = FSDirectory.open(Paths.get("E:\\temp\\index"));
	// 5. 创建输入流对象
	IndexReader indexReader = DirectoryReader.open(dir);
	// 6. 创建搜索对象
	IndexSearcher indexSearcher = new IndexSearcher(indexReader);
	// 7. 搜索, 并返回结果
	// 第二个参数: 是返回多少条数据用于展示, 分页使用
	TopDocs topDocs = indexSearcher.search(query, 10);
	// 获取查询到的结果集的总数, 打印
	System.out.println("=======count=======" + topDocs.totalHits);
	// 8. 获取结果集
	ScoreDoc[] scoreDocs = topDocs.scoreDocs;
	// 9. 遍历结果集
	if (scoreDocs != null) {
		for (ScoreDoc scoreDoc : scoreDocs) {
			// 获取查询到的文档唯一标识, 文档id, 这个id是lucene在创建文档的时候自动分配的
			int docID = scoreDoc.doc;
			// 通过文档id, 读取文档
			Document doc = indexSearcher.doc(docID);
			System.out.println("==================================================");
			// 通过域名, 从文档中获取域值
			System.out.println("===id==" + doc.get("id"));
			System.out.println("===name==" + doc.get("name"));
			System.out.println("===price==" + doc.get("price"));
			System.out.println("===image==" + doc.get("image"));
			System.out.println("===brandName==" + doc.get("brandName"));
			System.out.println("===categoryName==" + doc.get("categoryName"));
		}
	}
	// 10. 关闭流
}

Lucene使用注意事项

  • 关键词区分大小写 OR AND TO等关键词是区分大小写的,lucene只认大写的,小写的当做普通单 词。
  • 读写互斥性 同一时刻只能有一个对索引的写操作,在写的同时可以进行搜索
  • 文件锁 在写索引的过程中强行退出将在tmp目录留下一个lock文件,使以后的写操作无法进行, 可以将其手工删除
  • 时间格式 lucene只支持一种时间格式yyMMddHHmmss,所以你传一个yy-MM-dd HH:mm:ss的
    时间给lucene它是不会当作时间来处理的
  • 设置boost 有些时候在搜索时某个字段的权重需要大一些,例如你可能认为标题中出现关键词的文
    章比正文中出现关键词的文章更有价值,你可以把标题的boost设置的更大,那么搜索结果会优先 显示标题中出现关键词的文章.
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值