1.什么是全文检索
我们生活中的数据总体分为两种:结构化数据和非结构化数据。
结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等磁盘上的文件
1.2.非结构化数据查询方法
(1)顺序扫描法(Serial Scanning)
所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对
于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。如利用windows的搜索也可以搜索文件内容,只是相当的慢。
(2)全文检索(Full-text Search)
将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。
这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。
可以使用Lucene实现全文检索。Lucene是apache下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。
2Lucien实现全文检索的流程
1、绿色表示索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
确定原始内容即要搜索的内容采集文档创建文档分析文档索引文档
2、红色表示搜索过程,从索引库中搜索内容,搜索过程包括:
用户通过搜索界面创建查询执行搜索,从索引库搜索渲染搜索结果
2.1获得原始文档
原始文档是指要索引和搜索的内容。原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。
2.2创建文档对象
获取原始内容的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容。
这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Field(file_name文件名称、file_path文件路径、file_size文件大小、file_content文件内容)
注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)
每个文档都有一个唯一的编号,就是文档id。
2.3分析文档
将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的语汇单元,可以将语汇单元理解为一个一个的单词。
2.4创建索引
对所有文档分析得出的语汇单元进行索引,索引的目的是为了搜索,最终要实现只搜索被索引的语汇单元从而找到Document(文档)。
注意:创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构。
传统方法是根据文件找到该文件的内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大、搜索慢。
倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。
2.5查询索引
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容(这里指磁盘上的文件)。
2.6用户查询接口
全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果。
Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面。
2.7创建查询
用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要搜索的Field文档域、查询关键字等,查询对象会生成具体的查询语法,
2.8执行查询
搜索索引过程:
根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。
2.9渲染结果
Lucene使用步骤:
1)Lucene下载
Lucene是开发全文检索功能的工具包,从官方网站下载lucene-9.3.0,并解压。
官网地址:https://lucene.apache.org/
JDK要求:1.8以上。
Maven坐标引入
<dependencies>
<!-- lucene核心库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>8.11.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
创建索引库代码
//创建索引
@Test
public void demo01() throws IOException {
//指定索引存放的路径 E:\work\lucene
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
//索引库还可以存放到内存中 不推荐使用
//Directory directory = new RAMDirectory();
//创建indexwriterConfig对象
IndexWriterConfig indexWriterConfig = new IndexWriterConfig();
//创建indexwriter对象
IndexWriter indexWriter = new IndexWriter(directory,indexWriterConfig);
//原始文档的路径
File dir = new File("E:\\BaiduNetdiskDownload\\searchsource");
for (File f :dir.listFiles()){
//文件名
String fileName = f.getName();
//文件内容
String fileContent = FileUtils.readFileToString(f, "UTF-8");
//文件路径
String filePath = f.getPath();
//文件大小
long fileSize = FileUtils.sizeOf(f);
//创建文件域(字段)
//第一参数,域的名称
//第二参数,域的内容
//第三参数,是否存储
Field fileNameField = new TextField("filename",fileName,Field.Store.YES);
//文件内容域
Field fileContextField = new TextField("content",fileContent,Field.Store.YES);
//文件路径域(不分析,不索引,只存储)
Field filePathField = new TextField("path",filePath,Field.Store.YES);
//文件大小域
Field fileSizeField = new TextField("size",fileSize+"",Field.Store.YES);
//创建document对象
Document document = new Document();
document.add(fileNameField);
document.add(fileContextField);
document.add(filePathField);
document.add(fileSizeField);
//创建索引 ,并写入索引库
indexWriter.addDocument(document);
}
//关闭indexWriter
indexWriter.close();
}
运行代码后查看索引库
使用Luke工具查看索引文件(注意不要用windows文本编辑器打开)
使用的luke的版本是luke-7.4.0,跟lucene的版本对应的。可以打开7.4.0版本的lucene创建的索引库。需要注意的是此版本的Luke是jdk9编译的,所以要想运行此工具还需要jdk9才可以。
2)查询索引代码
//查询索引库
@Test
public void demo02() throws IOException {
//指定索引库存放的路径
//指定索引存放的路径 E:\work\lucene
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
//创建indexReader对象
IndexReader indexReader = DirectoryReader.open(directory);
//创建indexSearcher对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//创建查询 第一参数域的名称 需要查询的内容
Query query = new TermQuery(new Term("filename","apache"));
//执行查询
//第一参数是查询对象,第二个参数是查询结果返回的最大值
TopDocs topDocs = indexSearcher.search(query, 10);
//查询结果的总条数
System.out.println("查询结果的总记录数:"+topDocs.totalHits);
//遍历查询的结果,打印出来
for (ScoreDoc scoreDoc :topDocs.scoreDocs){
//scoreDoc.doc属性就是document对象的id
//根据document的id 找到document对象
Document document = indexSearcher.doc(scoreDoc.doc);
System.out.println("文档对象域的文件名称"+document.get("filename"));
// System.out.println("文档对象域的文件内容"+document.get("content"));
System.out.println("文档对象域的文件路径"+document.get("content"));
System.out.println("文档对象域的文件大小"+document.get("size"));
System.out.println("*******************************************************");
}
indexReader.close();
}
运行结果显示
3)分析器
代码测试lucene默认的分词器
//测试分词器效果
@Test
public void demo03() throws IOException {
//创建一个标准的分析器对象
Analyzer analyzer = new StandardAnalyzer();
//获得tokenStream对象
//第一个参数,域名 可以随便设置
//第二个参数, 要分析的文本内容
TokenStream tokenStream = analyzer.tokenStream("demo", "I first came here, there are many aspects of knowledge need to everybody to learn, but also hope in the later work we can comments!");
//添加一个引用,可以获得每个关键词
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
//添加一个偏移量的引用,记录关键词的开始位置和结束位置
OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
//将指针调整到列表的头部
tokenStream.reset();
//遍历关键词列表,通过incrementToken方法判断列表是否结束
while (tokenStream.incrementToken()){
//关键词的起始位置
System.out.println("开始位置"+offsetAttribute.startOffset());
//取关键词
System.out.println("关键词"+charTermAttribute);
//关键词的结束位置
System.out.println("结束位置:"+offsetAttribute.endOffset());
}
tokenStream.close();
}
运行后效果
4)中文分析器
4.1Lucene自带中文分词器
- StandardAnalyzer
单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”,
效果:“我”、“爱”、“中”、“国”。 - SmartChineseAnalyzer
对中文支持较好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理
推荐使用第三方中文分词器(IKAnalyze)
Maven依赖引入
<!--IKAnalyzer中文分词器-->
<dependency>
<groupId>com.jianggujin</groupId>
<artifactId>IKAnalyzer-lucene</artifactId>
<version>8.0.0</version>
</dependency>
引入自定义的分析器(在创建索引的代码中进行修改)
//指定索引存放的路径 E:\work\lucene
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
//索引库还可以存放到内存中 不推荐使用
//Directory directory = new RAMDirectory();
//创建indexwriterConfig对象 加入自定义的中文分词器
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new IKAnalyzer());
//创建indexwriter对象
IndexWriter indexWriter = new IndexWriter(directory,indexWriterConfig);
索引库的维护
1.Field域的属性
1)是否分析:是否对域的内容进行分词处理。前提是我们要对域的内容进行查询。
2)是否索引:将Field分析后的词或整个Field值进行索引,只有索引方可搜索到。
比如:商品名称、商品简介分析后进行索引,订单号、身份证号不用分析但也要索引,这些将来都要作为查询条件。
3)是否存储:将Field值存储在文档中,存储在文档中的Field才可以从Document中获取。
比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。
是否存储的标准:是否要将内容展示给用户
添加文档代码的实现
//添加索引
@Test
public void Demo04() throws IOException {
//索引库存放路径
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
//使用自定义的IK中文分词器
IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
//创建一个indexWriter对象
IndexWriter indexWriter = new IndexWriter(directory,config);
//创建一个document对象
Document document = new Document();
//向document对象中添加域
//不同的document可以有不同的域,同一个document可以有相同的域
document.add(new TextField("filename","新添加的文档",Field.Store.YES));
document.add(new TextField("content","新添加的文档内容",Field.Store.NO));
//LongPoiint创建索引
document.add(new LongPoint("size",1000));
//StoreField存储数据
document.add(new StoredField("size",1000));
//不需要创建索引的就使用StoredField存储
document.add(new StoredField("path","D:\\QMDownload"));
//添加文档到索引库
indexWriter.addDocument(document);
indexWriter.close();
}
索引库的删除
//删除全部索引
@Test
public void demo05() throws IOException {
//索引库存放路径
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
//使用自定义的IK中文分词器
IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
//创建一个indexWriter对象
IndexWriter indexWriter = new IndexWriter(directory,config);
//删除全部索引
indexWriter.deleteAll();
indexWriter.close();
}
说明:将索引目录的索引信息全部删除,直接彻底删除,无法恢复。(此方法慎用!!)
指定查询条件删除
//指定查询条件删除
@Test
public void demo06() throws IOException {
//索引库存放路径
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
//使用自定义的IK中文分词器
IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
//创建一个indexWriter对象
IndexWriter indexWriter = new IndexWriter(directory,config);
//创建一个查询条件
Query query = new TermQuery(new Term("filename","spring"));
//根据查询条件删除
indexWriter.deleteDocuments(query);
indexWriter.close();
}
索引库的修改操作(原理就是先删除后添加)
//索引库的修改
@Test
public void demo07() throws IOException {
//索引库存放路径
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
//使用自定义的IK中文分词器
IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
//创建一个indexWriter对象
IndexWriter indexWriter = new IndexWriter(directory,config);
//创建一个document对象
Document document = new Document();
//向document对象中添加域
//不同的document可以有不同的域,同一个documet可以有相同的域
document.add(new TextField("filename","要更新的文档",Field.Store.YES));
document.add(new TextField("content","there are many aspects of knowledge need to everybody",Field.Store.YES));
//把内容为java更新成there are many aspects of knowledge need to everybody
indexWriter.updateDocument(new Term("content","java"),document);
indexWriter.close();
}
Lucene索引库查询
对要搜索的信息创建Query查询对象,Lucene会根据Query查询对象生成最终的查询语法,类似关系数据库Sql语法一样Lucene也有自己的查询语法,比如:“name:lucene”表示查询Field的name为“lucene”的文档信息。
- 使用Lucene提供Query子类
- 使用QueryParse解析查询表达式
1.TermQuery
TermQuery,通过项查询,TermQuery不使用分析器所以建议匹配不分词的Field域查询,比如订单号、分类ID号等。
//使用Termquery查询
@Test
public void demo08() throws IOException {
//索引库存放路径
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
IndexReader indexReader = DirectoryReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//创建查询对象 查询content域里面的lucene关键词的
Query query = new TermQuery(new Term("content","lucene"));
//执行查询 查询10条
TopDocs topDocs = indexSearcher.search(query, 10);
//共查询到的lucene的总记录数
System.out.println("查询到的总记录数:"+topDocs.totalHits);
//遍历查询结果
for (ScoreDoc scoreDoc :topDocs.scoreDocs){
//scoreDoc.doc属性就是document对象的id
//根据document的id 找到document对象
Document document = indexSearcher.doc(scoreDoc.doc);
System.out.println("文档对象域的文件名称"+document.get("filename"));
// System.out.println("文档对象域的文件内容"+document.get("content"));
System.out.println("文档对象域的文件路径"+document.get("content"));
System.out.println("文档对象域的文件大小"+document.get("size"));
System.out.println("*******************************************************");
}
indexSearcher.getIndexReader().close();
}
数值范围查询
@Test
public void demo09() throws IOException {
//索引库存放路径
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
IndexReader indexReader = DirectoryReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
Query query = LongPoint.newRangeQuery("size",0,1000);
TopDocs topDocs = indexSearcher.search(query,10);
//共查询到的lucene的总记录数
System.out.println("查询到的总记录数:"+topDocs.totalHits);
//遍历查询结果
for (ScoreDoc scoreDoc :topDocs.scoreDocs){
//scoreDoc.doc属性就是document对象的id
//根据document的id 找到document对象
Document document = indexSearcher.doc(scoreDoc.doc);
System.out.println("文档对象域的文件名称"+document.get("filename"));
// System.out.println("文档对象域的文件内容"+document.get("content"));
System.out.println("文档对象域的文件路径"+document.get("content"));
System.out.println("文档对象域的文件大小"+document.get("size"));
System.out.println("*******************************************************");
}
indexSearcher.getIndexReader().close();
}
2.使用Queryparser查询
通过QueryParser也可以创建Query,QueryParser提供一个Parse方法,此方法可以直接根据查询语法来查询。Query对象执行的查询语法可通过System.out.println(query);查询。
需要使用到分析器。建议创建索引时使用的分析器和查询索引时使用的分析器要一致。
需要加入queryParser依赖的jar包。
Maven依赖引入
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>8.9.0</version>
</dependency>
@Test
public void demo10() throws Exception {
//索引库存放路径
Directory directory = FSDirectory.open(new File("E:\\work\\lucene").toPath());
IndexReader indexReader = DirectoryReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//创建queryparser对象
//第一个参数默认搜索的域
//第二个参数就是分析器对象
QueryParser queryParser = new QueryParser("content",new IKAnalyzer());
Query query = queryParser.parse("Lucen是用java语言开发的");
//执行查询
TopDocs topDocs = indexSearcher.search(query,10);
//共查询到的lucene的总记录数
System.out.println("查询到的总记录数:"+topDocs.totalHits);
//遍历查询结果
for (ScoreDoc scoreDoc :topDocs.scoreDocs){
//scoreDoc.doc属性就是document对象的id
//根据document的id 找到document对象
Document document = indexSearcher.doc(scoreDoc.doc);
System.out.println("文档对象域的文件名称"+document.get("filename"));
// System.out.println("文档对象域的文件内容"+document.get("content"));
System.out.println("文档对象域的文件路径"+document.get("content"));
System.out.println("文档对象域的文件大小"+document.get("size"));
System.out.println("*******************************************************");
}
indexSearcher.getIndexReader().close();
}
运行结果