上一篇blog介绍了全文检索的实现思路,这一篇呢主要介绍开源的搜索引擎Lucene是如何基于这样的思路来进行具体的实现的。
Lucene基本概念
Lucene是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。在Java开发环境里Lucene是一个成熟的免费开源工具。一句话概括,就是一组实现全文检索的Jar包。
Lucene环境搭建
首先需要从官网 下载Lucene,Java需要1.8以上的版本支持,,内容组成如下,实际上我们这里用到的就是以下的5个jar包:
其中commons-io是为了进行文件的读写,junit是为了进行单元测试。首先我们提供如下待检索文件:
然后使用lucene建立倒排索引,来通过关键字快速检索文档。按照上一篇blog提到的需求我们来做一下。
索引的基本使用
接下来我们看看如何创建索引、查询索引,完成全流程
创建索引
创建一个java工程,并导入jar包:
Field的几个属性
创建Field的时候,有几种重载方法可以选择:其实string就是索引不分词、textfield就是索引分词。
代码执行
然后在程序里读取文件转为lucene使用到的相关对象:
package com.company; import org.apache.commons.io.FileUtils; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.junit.Test; import java.io.File; public class Main { //创建索引 @Test public void createIndex() throws Exception { //创建indexwriter对象 Directory directory = FSDirectory.open(new File("F:\\lucene-index").toPath()); IndexWriterConfig config = new IndexWriterConfig(); IndexWriter indexWriter = new IndexWriter(directory, config); //原始文档的路径 File dir = new File("F:\\lucene-file"); for (File f : dir.listFiles()) { String fileName = f.getName(); String fileContent = FileUtils.readFileToString(f); String filePath = f.getPath(); long fileSize = FileUtils.sizeOf(f); //创建文件名域 //第一个参数:域的名称 //第二个参数:域的内容 //第三个参数:是否存储 //(分析、索引、存储)【索引分词存储】 Field fileNameField = new TextField("filename", fileName, Field.Store.YES); //文件大小域(分析、索引、不存储)【索引分词不存储】 Field fileSizeField = new LongPoint("size", fileSize); //文件路径域(不分析、索引、存储)【索引不分词存储】 Field filePathField = new StringField("path", filePath, Field.Store.YES); //文件内容域(不分析、不索引、存储)【不索引不分词存储】 Field fileContentField = new StoredField("content",fileContent); //创建document对象 Document document = new Document(); document.add(fileNameField); document.add(fileContentField); document.add(filePathField); document.add(fileSizeField); //创建索引,并写入索引库 indexWriter.addDocument(document); } //关闭indexwriter indexWriter.close(); }
创建完成后的索引文件夹查看:
显然是看不了的,所以需要使用工具去看,就是luke,一定要注意,luke的版本一定要和lucene一毛一样,否则就出问题了,例如我这里使用的都是7.4.0版本。打开luke可以看到每个域拆分的关键字:
以及可以看到所有的文档,如果有存储,对应域上会有数据:
查询索引
查询索引的代码内容如下所示:
//查询索引 @Test public void searchIndex() throws Exception { //第一步:创建一个Directory对象,也就是索引库存放的位置。 Directory directory = FSDirectory.open(new File("F:\\lucene-index").toPath()); //第二步:创建一个indexReader对象,需要指定Directory对象。 IndexReader indexReader= DirectoryReader.open(directory); //第三步:创建一个indexsearcher对象,需要指定IndexReader对象 IndexSearcher indexSearcher=new IndexSearcher(indexReader); //第四步:创建一个TermQuery对象,指定查询的域和查询的关键词。 TermQuery termQuery=new TermQuery(new Term("filename","丑")); //第五步:执行查询。 TopDocs topDocs = indexSearcher.search(termQuery, 5); //查询参数和返回最大数 System.out.println("总命中数为"+topDocs.totalHits); //实际总命中数,不被返回数限制,就是真实的命中数 //第六步:返回查询结果。遍历查询结果并输出。 for (ScoreDoc scoreDoc:topDocs.scoreDocs) { Document document = indexSearcher.doc(scoreDoc.doc); System.out.println(document.get("filename")); System.out.println(document.get("path")); System.out.println("-------------------------"); } //第七步:关闭IndexReader对象 indexReader.close(); }
查询结果如下,共命中了两篇文档:
对于索引不分词的查询,只能输入全名,例如对路径查询,替换下terms:
//第四步:创建一个TermQuery对象,指定查询的域和查询的关键词。 TermQuery termQuery=new TermQuery(new Term("path","F:\\lucene-file\\tml一般帅.txt"));
只搜帅或者tml这些关键词是搜不出来的,因为对于path域没有分过词:
中文分词器的使用
Lucene标准的StandardAnalyzer只支持单个的中文字。例如:
所以,如果查询很丑一定是搜索不出来的,因为根本就还没索引到文档。
这个时候为了符合中国人的使用习惯,我们使用第三方的IK分词器。
首先需要把Jar包导入到项目中,然后需要注意hotword.dic和ext_stopword.dic文件的格式为UTF-8,注意是无BOM 的UTF-8 编码。
调整下使用的分词器后,我们重建索引,写索引的时候使用IK分词器进行索引写入:
这个时候我们再看下分词效果,中文的词组就出来了。
那么为什么会这样呢?因为这些词都存在字典里,我们拿【很丑】举例:
然后我们再来跑一遍程序,看看查询的时候是分词是否生效:
这个时候搜【很丑】就能命中文件了。