Lucene 实现全文检索
前言:本文是衔接上一篇文章来写的,详情看上一篇 【Lucene】全文检索技术介绍
2. Lucene 实现全文检索的流程
2.1.索引和搜索流程图
- 绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:确定原始内容即要搜索的内容 -> 采集文档 -> 创建文档 -> 分析文档 ->索引文档
- 红色表示搜索过程,从索引库中搜索内容,搜索过程包括:用户通过搜索界面 -> 创建查询 -> 执行搜索,从索引库搜索 -> 渲染搜索结果
2.2.创建索引
步骤:
- 获得文档
- 原始文档:要基于那些数据来进行搜索,那么这些数据就是原始文档。
- 搜索引擎:使用爬虫获得原始文档
- 站内搜索:数据库中的数据。
- 案例:直接使用 io 流读取磁盘上的文件。
- 构建文档对象
- 对应每个原始文档创建一个 Document 对象
- 每个 document 对象中包含多个域(field)
- 域中保存的就是原始文档的数据
- 域的名称、域的值
- 每个文档都有一个唯一的编号,就是文档id。
注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)
- 分析文档
- 就是分词的过程
- 根据空格进行字符串拆分,得到一个单词列表
- 把单词统一转换成小写。
- 去除标点符号。
- 去除停用词(无意义的词)
- 每个关键词都封装成一个 Term 对象中
- Term 中包含两部分内容:
- 关键词所在的域
- 关键词本身
- 不同的域中拆分出来的相同的关键词是不同的 Term 。
- 创建索引
- 基于关键词列表创建一个索引。保存到索引库中。
- 索引库中:
- 索引
- document 对象
- 关键词和文档的对应关系
- 通过词语找文档,这种索引的结构叫倒排索引结构。如下图:
倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。
2.3.查询索引
- 用户查询接口
用户输入查询条件的地方
例如:百度的搜索框 - 把关键词封装成一个查询对象(创建查询)
- 要查询的域
- 要搜索的关键词
- 执行查询
- 根据要查询的关键词到对应的域上进行搜索。
- 找到关键词,根据关键词找到 对应的文档
- 渲染结果
- 根据文档的 id 找到文档的对象
- 对关键词进行高亮显示
- 分页处理
- 最终展示给用户看。
3.入门程序
3.1.配置开发环境
Lucene 下载
Lucene是开发全文检索功能的工具包,从官方网站下载lucene-7.4.0,并解压。
- 官方网站:http://lucene.apache.org/
- 版本:lucene-7.4.0
- Jdk要求:1.8以上
使用的jar包
lucene-core-7.4.0.jar
lucene-analyzers-common-7.4.0.jar
3.2.需求
实现一个文件的搜索功能,通过关键字搜索文件,凡是文件名或文件内容包括关键字的文件都需要找出来。还可以根据中文词语进行查询,并且需要支持多个条件查询。
本案例中的原始内容就是磁盘上的文件,如下图:
3.3.创建索引
实现步骤:
第一步:创建一个java工程,并导入jar包。
第二步:创建一个indexwriter对象。
1)指定索引库的存放位置Directory对象
2)指定一个IndexWriterConfig对象。
第二步:创建document对象。
第三步:创建field对象,将field添加到document对象中。
第四步:使用indexwriter对象将document对象写入索引库,此过程进行索引创建。并将索引和document对象写入索引库。
第五步:关闭IndexWriter对象。
代码实现:
/**
* @Auther: lss
* @Date: 2019/5/7 17:27
* @Description:
*/
public class LuceneFirst {
@Test
public void createIndex() throws IOException {
// 创建一个 Directory 对象,指定索引库保存的位置
// 把索引库保存在内存中
// Directory directory = new RAMDirectory();
// 把索引库保存在磁盘中
Directory directory = FSDirectory.open(new File("D:\\IDEA1\\lelucene\\index").toPath());
// 基于 Directory 对象创建一个 IndexWriter 对象
IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig());
// 读取磁盘上的文件,对应每个文件创建一个文档对象。
File dir = new File("D:\\searchsource");
File[] files = dir.listFiles();
for (File file : files) {
// 取文件名
String fileName = file.getName();
// 文件的路径
String filePath = file.getPath();
// 文件的路径
String fileContent = FileUtils.readFileToString(file, "utf-8");
// 文件的大小
long fileSize = FileUtils.sizeOf(file);
// 创建 Field
Field fieldName = new TextField("name", fileName, Field.Store.YES);
Field fieldPath = new TextField("path", filePath, Field.Store.YES);
Field fieldContent = new TextField("content", fileContent, Field.Store.YES);
Field fieldSize = new TextField("size", fileSize + "", Field.Store.YES);
// 创建文档对象
Document document = new Document();
// 向文档对象中添加域
document.add(fieldName);
document.add(fieldPath);
document.add(fieldContent);
document.add(fieldSize);
// 把文档对象写入索引库
writer.addDocument(document);
}
// 关闭 indexWriter 对象
writer.close();
}
}
执行测试: 生成的索引库
这里生成的了一堆看不懂的文件,这些文件没办法用普通的文本编辑器查看,这该怎么办,有个小软件 luck 可以查看索引库。我会把文件上传到下载上。下载使用 luke 查看索引库中的内容
我们使用的luke的版本是luke-7.4.0,跟lucene的版本对应的。可以打开7.4.0版本的lucene创建的索引库。需要注意的是此版本的Luke是jdk9编译的,所以要想运行此工具还需要jdk9才可以(PS:jdk 1.8貌似也是可以的)。
3.4.查询索引
实现步骤:
第一步:创建一个Directory对象,也就是索引库存放的位置。
第二步:创建一个indexReader对象,需要指定Directory对象。
第三步:创建一个indexsearcher对象,需要指定IndexReader对象
第四步:创建一个TermQuery对象,指定查询的域和查询的关键词。
第五步:执行查询。
第六步:返回查询结果。遍历查询结果并输出。
第七步:关闭IndexReader对象
代码实现:
@Test
public void searchIndex() throws Exception {
// 创建一个 Directory 对象,指定索引库的位置
Directory directory = FSDirectory.open(new File("D:\\IDEA1\\lelucene\\index").toPath());
// 创建一个 IndexReader 对象
IndexReader indexReader = DirectoryReader.open(directory);
// 创建一个Indexsearcher 对象,构造方法中的参数 indexReader 对象。
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 创建一个 Query 对象,TermQuery
Query query = new TermQuery(new Term("name", "spring"));
// 执行查询,得到一个 TopDocs 对象
// 参数1:查询对象 参数2:查询结果返回的最大记录数
TopDocs topDocs = indexSearcher.search(query, 10);
// 取查询结果的总记录数
System.out.println("查询总记录数:" + topDocs.totalHits);
// 取文档列表
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
// 打印文档中的内容
for (ScoreDoc doc : scoreDocs) {
// 取文档 id
int docId = doc.doc;
// 根据 id 取文档对象
Document document = indexSearcher.doc(docId);
System.out.println(document.get("name"));
System.out.println(document.get("path"));
// System.out.println(document.get("content"));
System.out.println(document.get("size"));
System.out.println("------------------华丽的分割线");
}
// 关闭 IndexReader 对象
indexReader.close();
}
执行测试:
这里没有打印内容,太多了不好截屏,同学们可以自行测试。
我们使用 luck 查看分词的结果,发现对于中文是不友好的,英文以按照单词分,而中文只能是一个字一个字的,
搜索英文是没有问题的,中文就不行了,单个字搜索还可以。这就有了问题,这就引入了一个分析器。
4.分析器
4.1.分析器的分词效果
代码实现:
@Test
public void testTokenStream() throws Exception {
// 创建一个标准分析器对象
Analyzer analyzer = new StandardAnalyzer();
// 获得tokenStream对象
// 第一个参数:域名,可以随便给一个
// 第二个参数:要分析的文本内容
TokenStream tokenStream = analyzer.tokenStream("", "The Spring Framework provides a comprehensive programming and configuration model.");
// TokenStream tokenStream = analyzer.tokenStream("", "单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”");
// 添加一个引用,可以获得每个关键词
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
// 添加一个偏移量的引用,记录了关键词的开始位置以及结束位置
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
// 将指针调整到列表的头部
tokenStream.reset();
// 遍历关键词列表,通过incrementToken方法判断列表是否结束
while (tokenStream.incrementToken()) {
// 关键词的起始位置
// System.out.println("start->" + offsetAttribute.startOffset());
// 取关键词
System.out.println(charTermAttribute);
// 结束位置
// System.out.println("end->" + offsetAttribute.endOffset());
}
// 关闭 tokenStream 对象
tokenStream.close();
}
**执行测试:**英文
中文:
对中文不友好,这不行啊,我们查看源码发现 创建索引库我们没有指定分词器,使用的是默认的分词器 StandardAnalyzer
下面介绍中文分析器
4.2.中文分析器
4.2.1.Lucene 自带中文分词器
- StandardAnalyzer:
单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”,
效果:“我”、“爱”、“中”、“国”。 - SmartChineseAnalyzer:
对中文支持较好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理。
4.2.2.IKAnalyzer
使用方法:
第一步:把jar包添加到工程中
第二步:把配置文件和扩展词典和停用词词典添加到classpath下(hotword.dic 和 stopword.dic,配文件 IKAnalyzer.cfg.xml)
注意:hotword.dic和ext_stopword.dic文件的格式为UTF-8,注意是无BOM 的UTF-8 编码也就是说禁止使用windows记事本编辑扩展词典文件
使用EditPlus.exe保存为无BOM 的UTF-8 编码格式,如下图:
-**扩展词典:**添加一些新词
**停用词词典:**无意义的词或者是敏感词汇
4.3.使用自定义分析器
代码实现:
@Test
public void addDocument() throws Exception {
// 创建一个 IndexWriter 对象,需要使用 IKAnalyzer 作为分析器
Directory directory = FSDirectory.open(new File("D:\\IDEA1\\lelucene\\index").toPath());
// 创建一个 IndexWriter 对象,需要使用 IKAnalyzer 作为分析器
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new IKAnalyzer());
IndexWriter writer = new IndexWriter(directory, indexWriterConfig);
// 创建一个 document 对象
Document document = new Document();
// 向 document 对象添加域
document.add(new TextField("name", "新添加的文件", Field.Store.YES));
document.add(new TextField("content", "新添加的文件内容", Field.Store.NO));
document.add(new StoredField("path", "C:/temp/hello"));
// 把文档添写入索引库
writer.addDocument(document);
// 关闭索引库
writer.close();
}
使用上面分词效果的代码测试:
以后我们在创建索引库时,使用 IKAnalyzer 就可以了。
先到这里了,下一节介绍索引库的维护。
【Lucene】系列文章
【Lucene】全文检索技术介绍【Lucene】实现全文检索【Lucene】索引库的维护【Lucene】索引库的查询