当前位置: 首页 > news >正文

Lucene原理

详细理解lucene存储结构

存储结构

索引(Index) 

一个目录一个索引,在Lucene中一个索引是放在一个文件夹中的。

(Segment)

一个索引(逻辑索引)由多个段组成, 多个段可以合并, 以减少读取内容时候的磁盘IO。

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

文档(Document) 

文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。
新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。

(Field) 

一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,描述等,都可以保存在不
同的域里。
不同域的索引方式可以不同。
词是索引的最小单位,是经过词法分析和语言处理后的字符串。

词典的构建

为何Lucene大数据量搜索快, 要分两部分来看,一点是因为底层的倒排索引存储结构。另一点就是查询关键字的时候速度快, 因为词典的索引结构。

词典数据结构对比

倒排索引中的词典位于内存,其结构尤为重要,有很多种词典结构,各有各的优缺点,最简单如排序数 组,通过二分查找来检索数据,更快的有哈希表,磁盘查找有B树、B+树,但一个能支持TB级数据的倒 排索引结构需要在时间和空间上有个平衡,下图列了一些常见词典的优缺点:
跳跃表 ,占用内存小,且可调,但是对模糊查询支持不好;
排序列表Array/List,使用二分法查找,不平衡;
字典树,查询效率跟字符串长度有关,但只适合英文词典;
哈希表,性能高,内存消耗大,几乎是原始数据的三倍;
双数组字典树,适合做中文词典,内存占用小,很多分词工具均采用此种算法;
Finite State Transducers (FST),一种有限状态转移机,Lucene 4有开源实现,并大量使用;
B树,磁盘索引,更新方便,但检索速度慢,多用于数据库。

跳跃表原理

点 :结构简单、跳跃间隔、级数可控,Lucene3.0之前使用的也是跳跃表结构,但跳跃表在
Lucene其他地方还有应用如倒排表合并和文档号索引。 缺点 :模糊查询支持不好。
单链表 :单链表中查询一个元素即使是有序的,我们也不能通过二分查找法的方式缩减查询时间。通俗的讲也就是按照链表顺序一个一个找.
举例: 查找85这个节点, 需要查找7.
跳跃表:
举例: 查询85这个节点, 一共需要查询6.
1. level3, 查询3, 查询到1结尾, 退回到37节点
2. level2, 37节点开始查询, 查询2, 查询到1结尾, 退回到71节点
3. level1, 71节点开始查询, 查询1, 查询到85节点.

FST原理简析

ucene现在采用的数据结构为FST,它的特点就是:优点:内存占用率低,压缩率一般在3~20倍之间、模糊查询支持好、查询快。缺点:结构复杂、输入要求有序、更新不易
已知FST要求输入有序,所以Lucene会将解析出来的文档单词预先排序,然后构建FST,我们假设输入为abd,abe,acf,acg,那么整个构建过程如下:

Lucene优化

解决大量磁盘IO

config.setMaxBufferedDocs(100000); 控制写入一个新的segment前内存中保存的document
数目,设置较大的数目可以加快建索引速度。数值越大索引速度越快, 但是会消耗更多的内存
indexWriter.forceMerge(文档数量); 设置N个文档合并为一个段。数值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快更高的值意味着索引期间更低的段合并开销,但同时也意味着更慢的搜索速度,因为此时的索引通常会包含更多的段。如果该值设置的过高,能获得更高的索引性能。但若在最后进行索引优化,那么较低的值会带来更快的搜索速度,因为在索引操作期间程序会利用并发机制完成段合并操作。故建议对程序分别进行高低多种值的测试,利用计算机的实际性能来告诉你最优值。
实例代码
public void createIndexTest() throws Exception {// 1. 采集数据SkuDao skuDao = new SkuDaoImpl();List<Sku> skuList = skuDao.querySkuList();// 2. 创建Document文档对象List<Document> documents = new ArrayList<Document>();for (Sku sku : skuList) {Document document = new Document();// Document文档中添加Field域// 商品Id, 不分词,索引,存储document.add(new StringField("id", sku.getId(), Field.Store.YES));// 商品名称, 分词, 索引, 存储document.add(new TextField("name", sku.getName(), Field.Store.YES));// 商品价格, 分词,索引,不存储, 不排序document.add(new FloatPoint("price", sku.getPrice()));//添加价格存储支持document.add(new StoredField("price", sku.getPrice()));//添加价格排序支持document.add(new NumericDocValuesField("price",sku.getPrice()));// 品牌名称, 不分词, 索引, 存储document.add(new StringField("brandName", sku.getBrandName(),Field.Store.YES));// 分类名称, 不分词, 索引, 存储document.add(new StringField("categoryName", sku.getCategoryName(),Field.Store.YES));// 图片地址, 不分词,不索引,存储document.add(new StoredField("image", sku.getImage()));// 把Document放到list中documents.add(document);}long startTime = System.currentTimeMillis();// 3. 创建Analyzer分词器,分析文档,对文档进行分词Analyzer analyzer = new IKAnalyzer();// 4. 创建Directory对象,声明索引库的位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 5. 创建IndexWriteConfig对象,写入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);//控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度。config.setMaxBufferedDocs(100000);// 6.创建IndexWriter写入对象IndexWriter indexWriter = new IndexWriter(directory, config);//设置100000个文档合并为一个段indexWriter.forceMerge(100000);// 7.写入到索引库,通过IndexWriter添加文档对象documentfor (Document doc : documents) {indexWriter.addDocument(doc);}// 8.释放资源indexWriter.close();long endTime = System.currentTimeMillis();System.out.println("======运行时间为:===" + (endTime - startTime) + "ms");}

选择合适的分词器

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

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

SimpleFSDirectory

类: SimpleFSDirectory
写操作:  java.io.RandomAccessFile
读操作: java.io.RandomAccessFile
特点: 简单实现,并发 能力差

NIOFSDirectory

类: NIOFSDirectory
写操作: java.nio.FileChannel
读操作: FSDirectory.FSIndexOutput
特点: 并发能力强, windows平台下 有重大bug

MMapDirectory

类: MMapDirectory
写操作: 内存映射
读操作:FSDirectory.FSIndexOutput
特点: 读取操作基于内存
测试代码修改:
Directory directory = MMapDirectory.open(Paths.get("E:\\dir"));

搜索api的选择

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

Lucene相关度排序

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

如何打分

Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:
1. 计算出词(Term)的权重。
2. 根据词的权重值,计算文档相关度得分。
明确索引的最小单位是一个Term(索引词典中的一个词),搜索也是要从Term中搜索,再根据Term找到文档,Term对文档的重要性称为权重,影响Term权重有两个因素:
Term Frequency (tf): 指此Term在此文档中出现了多少次。tf 越大说明越重要。 词(Term)在文档
中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数
很多,说明该文档主要就是讲Lucene技术的。
Document Frequency (df): 指有多少文档包含次Termdf 越大说明越不重要。 比如,在一篇英
语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明
此词(Term)太普通,不足以区分这些文档,因而重要性越低。

怎样影响相关度排序

boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。
在索引时对某个文档中的field设置加权值高,在搜索时匹配到这个文档就可能排在前边。
在搜索时对某个域进行加权,在进行组合域查询时,匹配到加权值高的域最后计算的相关度得分就
高。
设置boost是给域(field)或者Document设置的。

人为影响相关度排序实例

public void testIndexSearch() throws Exception {long startTime = System.currentTimeMillis();// 1. 创建Query搜索对象// 创建分词器Analyzer analyzer = new IKAnalyzer();//查询的域名String[] fields = {"name","brandName","categoryName"};//设置权重Map<String, Float> boots = new HashMap<>();boots.put("categoryName", 10000000f);// 根据多个域进行搜索MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, analyzer, boots);// 创建搜索对象Query query = queryParser.parse("手机");// 2. 创建Directory流对象,声明索引库位置Directory directory = MMapDirectory.open(Paths.get("E:\\dir"));// 3. 创建索引读取对象IndexReaderIndexReader reader = DirectoryReader.open(directory);// 4. 创建索引搜索对象IndexSearcher searcher = new IndexSearcher(reader);// 5. 使用索引搜索对象,执行搜索,返回结果集TopDocs// 第一个参数:搜索对象,第二个参数:返回的数据条数,指定查询结果最顶部的n条数据返回TopDocs topDocs = searcher.search(query, 50);System.out.println("查询到的数据总条数是:" + topDocs.totalHits);// 获取查询结果集ScoreDoc[] docs = topDocs.scoreDocs;// 6. 解析结果集for (ScoreDoc scoreDoc : docs) {// 获取文档int docID = scoreDoc.doc;Document doc = searcher.doc(docID);System.out.println("=============================");System.out.println("docID:" + docID);System.out.println("id:" + doc.get("id"));System.out.println("name:" + doc.get("name"));System.out.println("price:" + doc.get("price"));System.out.println("brandName:" + doc.get("brandName"));System.out.println("image:" + doc.get("image"));}// 7. 释放资源reader.close();long endTime = System.currentTimeMillis();System.out.println("==========消耗时间:============" + (startTime - endTime)+ "ms");}

Lucene使用注意事项

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

相关文章:

  • Android自定义View的事件分发流程
  • (33)记录描述窗体组件属性的枚举量 enum Qt :: WidgetAttribute, 简记为 WA_
  • Java结构型模式---外观模式
  • 和 *,以及 -> 和 .
  • C语言基础知识--柔性数组
  • 串口学习和蓝牙通信HC05(第八天)
  • LlamaIndex 检索器 Retriever
  • 题目V^V
  • 008_Claude_Code开发工具
  • 自注意力机制及其与早期注意力机制的区别
  • C++高频知识点(十)
  • Android 响应式编程完整指南:StateFlow、SharedFlow、LiveData 详解
  • 封装---统一封装处理页面标题
  • 关于 java:11. 项目结构、Maven、Gradle 构建系统
  • DAY02:【ML 第一弹】KNN算法
  • Datawhale AI夏令营——用AI预测新增用户学习笔记
  • 【VLLM】大模型本地化部署
  • 【图片识别内容改名】用图片的内容改图片文件的名字,批量OCR识别图片上的文字并同时进行批量改名的操作步骤和注意事项
  • 深入了解JAVA中Synchronized
  • MD5算法深度剖析与可视化解析
  • Kubernetes集群安装
  • Codeforces Round 1032 (Div. 3)(A-G)
  • 嵌入式 Linux开发环境构建之安装 Samba
  • Wireshark的安装和基本使用
  • C语言---自定义类型(上)(结构体类型)
  • Vue Router 完全指南:从入门到实战,高效管理前端路由
  • C++高频知识点(十二)
  • 【LeetCode数据结构】单链表的应用——反转链表问题、链表的中间节点问题详解
  • 通信原理与USRP :PSK的调制解调(BPSK、QPSK、16PSK) 文本、图片
  • Struts2框架对重定向URL处理不当导致的OGNL注入漏洞(s2-057)