全文检索技术Lucene
一. Lucene 简介
1. Lucene 是什么
Lucene 是基于开放源代码构建的一个全文检索工具包,在其架构设计上虽然包含了全面的查询功能和索引机制,并具备部分文本分析能力;然而从本质上讲它只是一个信息检索程序库,并非直接的应用产品;因此,并非像百度或谷歌那样方便易用;相反地……
2 . Lucene 能做什么
为了解答这个问题
3 . Lucene 速度测试
以下提供了一些测试数据供参考使用:
如果你愿意接受这些参数设置,则可以选择相应的配置方案进行运行。
具体来说:
- 测试一:包含约25百万条记录的数据集,在约三亿字节的文本内容基础上生成了约三亿八千一百十万字节的索引文件;在运行时采用八核心处理器的情况下(即八线程),平均完成一次任务所需的时间约为三毛零四分秒。
- 测试二:针对约三七千条记录的数据集,在基于varchar字段设计的索引结构上实现了相应的查询优化;对应的索引文件大小约为二点六MB;同样采用八核心处理器的情况下(即八线程),完成一次查询操作的时间仅为一点五毫秒。
二. 深入lucene
1. 为什么 lucene 这么快
1、倒排索引
2、压缩算法
3、二元搜索
代码解读
2. 倒排序索引
它是根据属性的值来查找记录。
这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址由于不是由记录来确定属性值,
而是由属性值来确定记录的位置,因而称为倒排索引(invertedindex) 如下简单的例子
代码解读

3. 工作方式
Lucene 提供的服务主要包含两个方面:一是添加到索引中的一系列操作(Insert),二是移除索引中的数据(Delete)。所谓添加到索引中的一系列操作(Insert),即为将您提供的源(本质上是字符串)添加到索引中或从索引中删除这些数据;而向用户提供全文搜索服务(Search),则允许用户通过关键词定位到这些源信息。
4. 写入流程
- 源字符串通过使用analyzer进行处理,并涉及步骤包括对源字符串进行分词处理以及去除停用词(可选)。
- 将源中的关键信息分配到Document的各种信息域中,并对需要进行索引的字段执行索引操作的同时,也需完成那些需存储的数据字段的记录。
- 将生成的索引结果存储到磁盘上。
5. 读出流程
用户提交一组搜索关键词。
经过分析器处理后,
对其进行索引查找,
找到对应的文档.
用户则可从中提取所需的信息字段.
6. Docement
用户所使用的数据源通常是一系列记录(包括文本文件、字符串以及数据库表中的单个记录等)。经过索引处理后,在索引文件中每个原始文档被转换为Document对象;当用户发起搜索时,系统会返回一个包含这些Document对象的列表。
7. Field
一个Document可能包含多个InformationDomain(即信息领域),例如一篇文章可能拥有'标题'、'正文'以及'最后修改时间'等多个InformationDomain。这些InformationDomain被定义为Field在Document中所存储的内容。每个Field都具有两个可选属性:存储属性与索引属性。通过设置存储属性你可以决定是否对这个Field进行数据存储;而设置索引属性则决定了该Field是否会被用于快速搜索(即索引)。这种设计看似有些多余(因为它们的存在并不直接提升数据存储效率),但实际上对这两个属性的正确配置至关重要。
8. 实现原理
文本倒排处理:

Lucene 整体使用如图所示:

9. 环境配置
9-1 下载jar包的方式
下载 lucene jar
官网:https://lucene.apache.org/
导入 jar 到项目中
我下载的为7-5-0版本的zip,架包引入下面依赖中的jar就行,但是org.apache.commons.io
必须从依赖库里面引入,本人建议你可以建个maven项目,然后只引入org.apache.commons.io
依赖,其他的几个直接把你下载的zip解压,找到里面的对应架包引入就行,因为maven库里面的版本较低
代码解读
9-2或者加入依赖,相关依赖如下:
<dependencies>
<!--测试环境-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.directory.studio/org.apache.commons.io -->
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.io</artifactId>
<version>2.4</version>
</dependency>
<!-- http://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-common -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>4.10.3</version>
</dependency>
<!-- http://mvnrepository.com/artifact/org.apache.lucene/lucene-queryparser -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>4.10.3</version>
</dependency>
<!-- http://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>4.10.3</version>
</dependency>
</dependencies>
代码解读
10. 创建索引
Lucene的核心工作流程是将需要查询的数据进行Lucene索引构建,并且这种操作能够显著提升搜索速度。通过这一步骤实现数据的Lucene化处理,并简化了后续操作流程。 luceneIndex = new LuceneIndex(); lucenseIndex.add(data); $ lucenseIndex.save(); 为了提高效率和方便性,在编写代码时应遵循这一操作规范。注释内容较为冗杂。
两个成员变量:
//要搜索的目录路径
String pathSearch = "C://Users//威威//Desktop//课堂内容//第15周//day 5";
//索引要存放的路径
String pathIndex = "C://Users//威威//Desktop//课堂内容//第16周//day 5//testLucene";
代码解读
正式的建索引代码:
@Test
public void createIndex() throws IOException {
//索引存放的目录文件夹
File indexRepositoryFile = new File(pathIndex);
//得到目录的文件路径(不能直接用上面的path,不然会报错)
Path directoryPath = indexRepositoryFile.toPath();
//lucene进行搜索的目录
Directory directory = FSDirectory.open(directoryPath);
//准备你想要搜索的目录文件
File searchFiles = new File(pathSearch);
//获取一个标准分词器
Analyzer analyzer = new StandardAnalyzer();
//配置indexWriterConfig
//IndexWriterConfig indexWConfig = new IndexWriterConfig();
IndexWriterConfig indexWConfig = new IndexWriterConfig(analyzer);
//指定索引写入的模式
indexWConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
//通过索引目录与配置信息得到writer
IndexWriter writer = new IndexWriter(directory, indexWConfig);
//遍历读取文件目录pathSearch里的所有文件,非常重要,如果直接遍历杜会报错(文件存在,但它是个目录)
Collection<File> files = FileUtils.listFiles(searchFiles, TrueFileFilter.INSTANCE,TrueFileFilter.INSTANCE);
//遍历读取目录里的所有文件
for(File file : files){
//得到文件名
String fileName = file.getName();
//文件内容
String fileContent = FileUtils.readFileToString(file);
//文件路径
String filePath = file.getPath();
//文件大小
Long fileSize = FileUtils.sizeOf(file);
//创建一个document对象
Document document = new Document();
// 向Document对象中添加域信息
// 参数:1、域的名称;2、域的值;3、是否存储;
Field nameField = new TextField("name",fileName,Store.YES );
Field contentField = new TextField("content", fileContent, Store.YES);
// storedFiled默认存储
Field pathField = new StoredField("path",filePath );
Field sizeField = new StoredField("size",fileSize );
// 将域添加到document对象中
document.add(nameField);
document.add(contentField);
document.add(pathField);
document.add(sizeField);
//将信息写入到检索库中
writer.addDocument(document);
}
//关闭indexWriter
writer.close();
}
代码解读
点击运行会在你索引目录生成索引文件:

11. 查询索引
上面索引已经完成建立,建议您进行全面搜索,即只需对那些已经建立好索引的目录进行操作,而无需在源文件中进行目录索引
正式搜索代码:
@Test
public void search() throws IOException, ParseException {
//指定索引的目录并打开,路径不能直接给,必须转化一下
File file = new File(pathIndex);
Path path = file.toPath();
Directory directory = FSDirectory.open(path);
//得到一个基础分词器,查询也需要分词操作,假如用户输入的内容很长
Analyzer analyzer = new StandardAnalyzer();
IndexReader indexReader = DirectoryReader.open(directory);
//创建IndexSearch对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//创建一个分析器
QueryParser parser = new QueryParser("content",analyzer);
//要查询的东西
Query query = parser.parse("用");
/*// 创建一个查询对象
TermQuery termQuery = new TermQuery(new Term("name", "crm"));*/
// 执行查询
// 返回的最大值,在分页的时候使用
TopDocs topDocs = indexSearcher.search(query, 10);
// 取查询结果总数量
System.out.println("总共的查询结果:" + topDocs.totalHits);
// 查询结果,就是documentID列表
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 取对象document的对象id
int docID = scoreDoc.doc;
// 相关度得分
float score = scoreDoc.score;
// 根据ID去document对象
Document document = indexSearcher.doc(docID);
/*System.out.println("相关度得分:" + score);
System.out.println("");
System.out.println("文件的名字: "+document.get("name"));
System.out.println("");*/
// 另外的一种使用方法
System.out.println(document.getField("content").stringValue());
System.out.println(document.get("path"));
System.out.println();
System.out.println("=======================");
}
indexReader.close();
}
代码解读
12. Lucene的其他功能
12-1 分词器
Lucene 的 StandardAnalyzer 分析器仅适用于英文文本。在中文处理时采用了基于单字的分词方法(例如'我'是'中'国人),由此可观察到该方法在中文处理上效果不佳。特别推荐采用其来进行中文分词。
12-2 停用词
停用词即为在信息检索过程中为节省存储空间并提高搜索效率,在处理自然语言数据(或文本)之前自动被筛选出去的某些词汇。
这些词汇通常被称为 Stop Words(停用词),例如中文中的"了"、"么"等。
一些意义不大且在一篇文章中出现频率又很高的词语(如"呢""的"),例如英文中的"for""in""it""a""or"等词语,在使用IKAnalyzer分词器的过程中,在其配置文件...里进行相关设置。如上图所示:
<?xml version="1.0" encoding="UTF-8"?>
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户在这可以配置自己的扩展字典-->
<entry key="ext_dict">ext_dict;</entry>
<!--用户可以在这里配置自己的停止词字典-->
<entry key="ext_stopwords">stopword.dic;chinese_stopword.dic</entry>
</properties>
代码解读
12-2 高亮-Highlighter
高亮是什么?来看一下百度就知道了

红色标识词实现了高亮显示的效果,可以通过引入内部提供的highlighting jar包即可完成功能实现,用户可以根据需求自行配置.
三. Lucene相关概念补充
1. Field属性
在文档中定义的域即为 Field,在其基础上分为 Field 名称与对应的 Field 值两部分,在这种情况下单个文档通常包含若干个不同的 Fields 项;其中 Document 仅作为承载所有 Fields 的容器存在;而该 Fields 值既是需要进行索引的关键信息也是实际搜索时的重要依据
在处理文本数据时是否需要将其进行分词?
是:将字段值实施分词操作以便提高信息检索效率。
例如,在用户搜索时通常会涉及如商品名称和商品简介等详细信息。
此外,在数据量大且复杂的情况下需要对这些内容进行分类和组织。
否:不作分词处理如用于标识产品或订单的唯一标识符。
-
是否需要执行索引操作(indexing)
-
如果是的话,则需执行以下操作:
- 对 Field 字段中的分词结果或完整值进行存储和归档
- 以便在后续查询时使用这些信息
- 其中一些字段如商品名称和商品简介则需分词后存储
- 而订单号以及身份证号码虽然无需分词处理但仍然需要创建对应字段的访问权限
- 这些字段的数据将在未来的查询中起到重要作用
-
如果不需要,则无需执行任何操作
-
该域的内容无法通过当前系统实现快速检索
-
这些字段如商品 id 和文件路径等数据类型不具备支持快速检索的能力
-
因此无需创建相关字段的访问权限
-
是否存储(stored)
- 是:那么将 Field 的值存放在文档中. 存放于文档中的 Fields 才能从Document中被访问. 例如: 商品名称, 订单号等其他相关信息都应该先以某种形式保存到Document里以便后续查询. 所有未来预期从中提取的Fields都需要事先进行保存.
-
否:避免存储 Field 数据项时,则该 Field 就无法从 Document 中调用。
例如,在实际应用中:
较大的内容无需进行数据持久化存储。
可以通过系统的关系型数据库查询接口来访问商品简介信息。
当检索到对应的商品 ID 时,
可以通过检索对应的商品 ID 来触发相应的数据加载流程。
2. Field 常用类型
开发中常用 的 Filed 类型,注意 Field 的属性,根据需求选择:

3. 例子
-
图书的标识符为:
-
是否需要对图书进行分词?答案是不需要。原因在于搜索时不会基于商品 ID 来检索内容。
-
是否需要对图书进行索引?答案是否定的。原因在于搜索时不会依赖于图书的 ID。
-
是否需要将图书信息存储到数据库中?答案是肯定的。这是因为查询结果页面会直接引用图书的 ID 值。
-
图书名称:
- 是否分词:作为分词处理的功能之一
- 是否索引:建立专门的索引结构
- 是否存储:将图书信息存储到数据库中
-
图书的价格
-
是否需要对数值进行分词?答案是肯定的。Lucene会对所有数字型数据进行特殊处理以提高搜索效率,在本例中可能需要根据不同的价格范围来调整搜索策略。
-
必须进行索引处理
-
必须执行存储操作
- 图书图片地址:
- 是否分词:不分词
- 是否索引:不索引
- 是否存储:要存储
- 图书图片地址:
-
书籍详细描述:
- 是否进行分词处理:要进行分词处理
- 是否构建索引:要构建索引
- 是否存储数据:由于书籍详细描述内容丰富,在查询结果页面直接展示会导致加载时间延长并占用大量服务器资源。
避免占用过多的存储空间和提高查询效率的前提下,
我们可以选择将数据存放在Lucene的索引文件中。
这样一来,
可以从而节省 Lucene 索引文件的空间。
如果需要在书籍详情页面展示其描述信息,
则可以通过以下步骤操作:
首先从 Lucene 指典中获取书籍 ID;
然后根据该 ID 查询关系型数据库中的 book 表获取具体的描述内容。
这样既能保证数据高效管理,
又能在用户浏览时快速返回所需信息。
