一、全文检索概述
1. 什么是全文检索
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。
常见的文件检索:Windows中的搜索功能、互联网搜索。
2. 数据分类
数据通常情况可以分为结构化数据和非结构化数据。
(1)结构化数据:具有固定的格式或有限长度的数据。例:存储在数据库中的数据、元数据。
(2)非结构化数据:没有固定格式或不定长度的数据。例:Word文档、磁盘中的文件、邮件。
3. 数据查询
数据查询通常也分为结构化数据查询和非结构化数据查询。
(1)结构化数据查询:通常使用 SQL 语句进行查询,而且能够快速得到查询结果。
结构化数据查询比较容易的原因:结构化数据的存储是有规律的,有行有列、格式固定、
长度固定。
(2)非机构化数据查询:通常分为顺序扫描和全文检索。
顺序扫描(Serial Scanning)
假设要查找内容包含某些字符串的文件,就是一个文档接着一个文档的看,对
于每一个文档,从头看到尾,如果该文档包含指定的字符串,则该文档即为要查找的文件。之后,接着看下一个文档,直到扫描完所有的文件。
例:使用 Windows 的搜索功能查找文件内容,速度相当慢。
全文检索(Full-text Search)
将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对有一
定结构的数据进行搜索,从而达到搜索相对较快目的。这部分从非结构化数据中提取出并
重新组织的信息,称为索引。
先建立索引,再对索引进行搜索的过程称为全文检索。
二、什么是Lucene
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。就其本身而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。人们经常提到信息检索程序库,虽然与搜索引擎有关,但不应该将信息检索程序库与搜索引擎相混淆。
二、Lucene实现全文检索流程
1. 索引和搜索流程图
绿色框表示创建索引,对要搜索的原始内容进行索引构建一个索引库,索引的过程:
确定原始内容(要所搜的内容)—》采集文档—》创建文档—》分析文档—》创建索引。
红色框表示查询索引,从索引库中搜索内容,搜索过程包括:
用户通过搜索界面—》创建查询—》执行查询(从索引库中搜索)—》渲染搜索结果。
2. 创建索引
对文档索引的过程,将用户需要搜索的内容进行索引,索引存储在索引库(index)中。
(1)获取原始文档
原始文档指的是索引和搜索的内容。
原始文档包含:
搜索引擎:互联网上的网页
站内搜索:数据库中的数据
文件系统:磁盘中的文件
从互联网、数据库、文件系统中获取需要搜索的原始信息,这个过程称为信息采集。新宁夏采集的目的就是为了对原始文档进行索引。
(2)创建文档对象
在索引前需要将原始文档创建成文档对象(Document),文档中包含多个域(Field),域中存储原始文档数据。
Document中包含的域有file_name(文件名称)、file_path(文件路径)、file_content(文件内容)、file_size(文件大小)等。
注意:(1)每个Document可以有多个Field
(2)不同的Document可以有不同的Field
(3)同一个Document可以有相同的Field(域名和域值都相同)
(4)每个文档都有一个唯一的编号,就是文档id。
(3)分析文档
分析文档的过程就是分词的过程。
将原始内容创建为包含多个域的文档,需要再对域中的内容进行解析,分析的过程是经过对原始文档去单词、将字母转为小写、去除标点符号、去除停用词等过程生成最终的词汇单元(单词列表),可以理解为一个一个的单词。
(4)创建索引
对文档分析所得出的词汇单元进行索引,索引的目的是为了搜索,最终要实现只需搜索被索引的词汇单元从而找到文档。
注意:(1)创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构。
(2)传统方法是根据文件找到该文件的内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大、搜索慢。
(3)倒排索引结构是根据内容(词语)找文档
3. 查询索引
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找得到要搜索的内容。
(1)用户查询接口
全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结构。
如:百度搜索
注意:Lucene不提供制作用户搜索界面的功能,需要根据自己的需求开发搜索界面。
(2)创建查询
用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要搜索的Filed文档域、查询关键字等,查询对象会生成具体的查询语法。
例如:语法“fileName:lucene”表示要搜索 Field 域的内容为“lucene”的文档。
(3)执行查询
搜索索引过程:
根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。
例如:语法“fileName:lucene”表示要搜索处 fileName 域中包含 Lucene 的文档。其搜索过 程是在索引上查找域为 fileName 且关键在为 Lucene 的 Term,并根据 Term 找到文档 id 列表。
(4)渲染效果
以友好的界面将查询结果展示给用户,用户根据搜索结构找自己想要的信息,为了帮助用户能够找到自己所需的结果,提供了很多展示的效果。
例如在搜索结果中将关键字高亮显示,百度提供的百度快照、分页处理等。
注意:搜索结果默认按照相关度排序,相关度高的排在最前面,一般情况下,官方网站排在最前面。
三、Lucene入门
1. 搭建开发环境
下载lucene:
官网:https://lucene.apache.org/
下载lucene-8.5.2.zip并解压。
2. 入门案例
需求:实现本地文件搜索功能。
通过关键字搜索文件,所有文件名或文件内容中包含关键字的文件都需要搜索出来。
创建工程:
(1)创建索引
编码步骤:
1. 创建一个Director对象,指定保存索引库的位置。
2. 基于Director对象创建一个IndexWriter对象。
3. 读取磁盘上的文件,对应每一个文件创建一个Document对象。
4. 向文档对象中添加域。
5. 将文档对象写入索引库。
6. 关闭IndexWriter对象。
//创建索引
public void testCreateIndex() throws IOException{
//指定索引库的存放位置Directory对象
Directory directory = FSDirectory.open(new File("E:\\programme\\test"));
//索引库还可以存放到内存中
//Directory directory = new RAMDirectory();
//指定一个标准分析器,对文档内容进行分析
Analyzer analyzer = new StandardAnalyzer();
//创建indexwriterCofig对象
//第一个参数: Lucene的版本信息,可以选择对应的lucene版本也可以使用LATEST
//第二根参数:分析器对象
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);
//创建一个indexwriter对象
IndexWriter indexWriter = new IndexWriter(directory, config);
//原始文档的路径
File file = new File("E:\\programme\\searchsource");
File[] fileList = file.listFiles();
for (File file2 : fileList) {
//创建document对象
Document document = new Document();
//创建field对象,将field添加到document对象中
//文件名称
String fileName = file2.getName();
//创建文件名域
//第一个参数:域的名称
//第二个参数:域的内容
//第三个参数:是否存储
Field fileNameField = new TextField("fileName", fileName, Store.YES);
//文件的大小
long fileSize = FileUtils.sizeOf(file2);
//文件大小域
Field fileSizeField = new LongField("fileSize", fileSize, Store.YES);
//文件路径
String filePath = file2.getPath();
//文件路径域(不分析、不索引、只存储)
Field filePathField = new StoredField("filePath", filePath);
//文件内容
String fileContent = FileUtils.readFileToString(file2, "utf-8");
//文件内容域
Field fileContentField = new TextField("fileContent", fileContent, Store.YES);
document.add(fileNameField);
document.add(fileSizeField);
document.add(filePathField);
document.add(fileContentField);
//使用indexwriter对象将document对象写入索引库,此过程进行索引创建。并将索引和document对象写入索引库。
indexWriter.addDocument(document);
}
//关闭IndexWriter对象。
indexWriter.close();
}
(2)查询索引
编码步骤:
1. 创建一个Directory对象,指定索引库的位置。
2. 创建一个IndexReader对象。
3. 创建一个IndexSearcher对象。
4. 创建一个Query对象,使用TermQuery创建。
5. 执行查询,获取TopDocs对象,使用TermQuery创建。
a) 获取查询结果的总记录数
b) 获取文档列表
6. 打印文档内容。
7. 关闭IndexReader对象。
//查询索引
@Test
public void testSearchIndex() throws IOException{
//创建一个Directory对象,指定索引库存放的路径
Directory directory = FSDirectory.open(new File("E:\\programme\\test"));
//创建IndexReader对象,需要指定Directory对象
IndexReader indexReader = DirectoryReader.open(directory);
//创建Indexsearcher对象,需要指定IndexReader对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//创建一个TermQuery(精准查询)对象,指定查询的域与查询的关键词
//创建查询
Query query = new TermQuery(new Term("fileName", "apache"));
//执行查询
//第一个参数是查询对象,第二个参数是查询结果返回的最大值
TopDocs topDocs = indexSearcher.search(query, 10);
//查询结果的总条数
System.out.println("查询结果的总条数:"+ topDocs.totalHits);
//遍历查询结果
//topDocs.scoreDocs存储了document对象的id
//ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
//scoreDoc.doc属性就是document对象的id
//int doc = scoreDoc.doc;
//根据document的id找到document对象
Document document = indexSearcher.doc(scoreDoc.doc);
//文件名称
System.out.println(document.get("fileName"));
//文件内容
System.out.println(document.get("fileContent"));
//文件大小
System.out.println(document.get("fileSize"));
//文件路径
System.out.println(document.get("filePath"));
System.out.println("----------------------------------");
}
//关闭indexreader对象
indexReader.close();
}
}
四、分词器
1.英文分词器
特点:
(1)切分关键词
(2)去除停用词
(3)转为小写(搜索时不区分大小写,因为分词器进行了自动转化)
package com.demo;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.wltea.analyzer.lucene.IKAnalyzer;
/**
* @author liu
* @create 2020-07-01
*/
public class AnalyzerTest {
/**
* 查询英文分词器的效果
* 使用默认的标准分词器StandardAnalyzer
* @throws Exception
*/
public void testEnAnalyzer() throws Exception{
//1.创建一个Analyzer对象,StandardAnalyzer对象
Analyzer analyzer = new StandardAnalyzer();
//2.使用分词器对象的tokenStream()方法获取TokenStream对象
TokenStream tokenStream = analyzer.tokenStream("contentField","Lucene Core is a Java library providing powerful indexing and search features.");
//3.向TokenStream对象中设置一个引用
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
//4.调用TokenStream对象的reset()方法
tokenStream.reset();
//5.遍历TokenStream对象
while(tokenStream.incrementToken()){
System.out.println(charTermAttribute.toString());
}
//6.关闭TokenStream对象
tokenStream.close();
}
public static void main(String[] args) {
AnalyzerTest test = new AnalyzerTest();
try {
test.testEnAnalyzer();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.中文分词器
(1)StandardAnalyzer
单字分词:按照中文一个字一个字地进行分词。
例如:“中文分词器”,分词后的结果:“中”、“文”、“分”、“词”、“器”
(2)SmartChineseAnalyzer
对中文支持较好,但是其扩展性较差:扩展词库、停用词库和同义词库不好处理。
(3)IK Analyzer
IK Analyzer 是一个开源的,基于 java 语言开发的轻量级的中文分词工具包。从 2006 年 12 月推出 1.0 版开始, IKAnalyzer 已经推出了 4 个大版本。最初,它是以开源项目 Luence 为应用主体的,结合词典分词和文法分析算法的中文分词组件。从 3.0 版本开始,IK 发展为面向 Java 的公用分词组件,独立于 Lucene项目,同时提供了对 Lucene 的默认优化实现。在 2012 版本中,IK 实现了简单的分词歧义排除算法,标志着 IK 分词器从单纯的词典分词向模拟语义分词衍化。
IK Analyzer 2012 特性:
采用了特有的“正向迭代最细粒度切分算法“,支持细粒度和智能分词两种切分模式;
在系统环境:Core2 i7 3.4G 双核,4G 内存,window 7 64 位, Sun JDK 1.6_29 64 位 普通 pc 环境测试,
IK2012 具有 160 万字/秒(3000KB/S)的高速处理能力。
2012 版本的智能分词模式支持简单的分词排歧义处理和数量词合并输出。
采用了多子处理器分析模式,支持:英文字母、数字、中文词汇等分词处理,兼容韩文、日文字符优化的词典存储,更小的内存占用。支持用户词典扩展定义。特别的,在 2012 版本,词典支持中文,英文,数字混合词语。
IKAnalyzer 使用步骤:
- 将 IKIKAnalyzer 的 jar 包添加到工程中。
- 将配置文件、扩展词典和停用词典添加到项目的 classpath 下。
注意:扩展词典和停用词词典文件的编码格式是 UTF-8,严禁使用 Windows 记事本编辑,因为
Windows 记事本编辑后得到的文件编码格式为 UTF-8+BOM。 - 使用 IKAnalyzer 进行分词。
package com.demo;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.wltea.analyzer.lucene.IKAnalyzer;
/**
* @author liu
* @create 2020-07-01
*/
public class AnalyzerTest {
/**
* 查询中文分词器的效果
* @throws Exception
*/
public void testEnAnalyzer() throws Exception{
//1.创建一个Analyzer对象,IKAnalyzer对象
Analyzer analyzer = new IKAnalyzer();
TokenStream tokenStream = analyzer.tokenStream("contentField", "Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供");
//3.向TokenStream对象中设置一个引用
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
//4.调用TokenStream对象的reset()方法
tokenStream.reset();
//5.遍历TokenStream对象
while(tokenStream.incrementToken()){
System.out.println(charTermAttribute.toString());
}
//6.关闭TokenStream对象
tokenStream.close();
}
public static void main(String[] args) {
AnalyzerTest test = new AnalyzerTest();
try {
test.testEnAnalyzer();
} catch (Exception e) {
e.printStackTrace();
}
}
}
五、索引库操作
常用的 Field 域:
Field类 | 数据类型 | Analyzed是否分区 | Indexed是否索引 | Stored是否存储 | 说明 |
---|---|---|---|---|---|
StringField(FiledName, FieldValue, Store.YES ) | 字符串 | N | Y | Y/N | 该 Field 用来创建一个字符串Field,但是不进 行分词,会将整个字符串存储在索引库中(姓名、身份证号、订单号) |
LongPoint(String name, long… point) | Long | Y | Y | N | 可以使用LongPoint、IntPoint 等类型存 储数值类型的数据。使得数值数据进行索引,但是不 能存储数据,若要存储,需要使用StoredField。 |
StoredField(FiledName, FieldValue) | 支持多种类型 | N | N | Y | 该 Field 用来创建不同类型 Field,不分词,不索引,只存储。 |
TextField( FiledName, FieldValue, Store.NO)/TextField(FiledName,reader) | 字符串/字符流 | Y | Y | Y/N | 若是一个 Reader,Lucene 猜测内容比较多,会采用Unstored 策略。 |
Field 域的属性
是否分词:是否对域的内容进行分词处理。
前提条件:对域的内容进行查询。
是否索引:将 Field 分析后的词或整个 Field 值进行索引,只有索引才可以搜索到。
例如:商品名称、商品简介分词后进行索引,姓名、身份证号、订单号不用分词但要索
引,作为查询条件。
是否存储:将 Field 值存储在文档中,存储在文档中的 Field 才能从 Document 中获取。
例如:商品名称、订单号,凡是后来要从 Document 中获取的 Field 都要存储。
标准:内容是否要展示给用户。
CRUD文档
package com.demo;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.junit.Before;
import org.junit.Test;
import org.wltea.analyzer.lucene.IKAnalyzer;
import java.io.File;
/**
* 索引库操作:CRUD
* 1.添加文档
* 2.删除文档
* 3.修改文档
* 4.查询文档
*
* @author liu
* @create 2020-07-02
*/
public class IndexOperation {
private IndexWriter indexWriter;
private IndexReader indexReader;
private IndexSearcher indexSearcher;
@Before
public void init() throws Exception{
//创建Directory对象,索引库存放路径
Directory directory = FSDirectory.open(new File("D:\\lucene\\index").toPath());
//创建IndexWriterConfig对象,指定IKAnalyzer对象
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new IKAnalyzer());
//创建IndexWriter对象
indexWriter = new IndexWriter(directory,indexWriterConfig);
//创建IndexReader对象
indexReader = DirectoryReader.open(directory);
//创建IndexSearcher对象
indexSearcher = new IndexSearcher(indexReader);
}
/**
* 添加文档
* @throws Exception
*/
@Test
public void addDoc() throws Exception{
//创建Document对象
Document document = new Document();
//向文档中添加域
document.add(new TextField("nameField","新建文本文档", Field.Store.YES));
document.add(new TextField("contentField","新建文本文档的内容", Field.Store.YES));
document.add(new TextField("pathField","D:\\\\lucene\\\\source", Field.Store.YES));
//将文档写入索引库
indexWriter.addDocument(document);
//关闭索引库
indexWriter.close();
}
/**
* 删除文档
* 1.删除所有文档
* 2.根据查询删除文档
* @throws Exception
*/
@Test
public void deleteAllDoc() throws Exception{
//删除所有文档
indexWriter.deleteAll();
//关闭索引库
indexWriter.close();
}
@Test
public void deleteDocByQuery() throws Exception{
//根据查询删除文档
indexWriter.deleteDocuments(new Term("nameField","lucene"));
//关闭索引库
indexWriter.close();
}
/**
* 修改文档
* @throws Exception
*/
@Test
public void updateDoc() throws Exception{
//创建新的文档对象
Document document = new Document();
//向文档中添加域
document.add(new TextField("nameField", "修改后的文本文档", Field.Store.YES));
document.add(new TextField("nameField1", "修改后的文本文档1", Field.Store.YES));
document.add(new TextField("nameField2", "修改后的文本文档2", Field.Store.YES));
//修改文档
indexWriter.updateDocument(new Term("nameField","lucene"),document);
//关闭索引库
indexWriter.close();
}
/**
* 查询文档
* 使用 Query 的子类查询
* (1) TermQuery:根据关键词进行查询,需要指定查询的域及查询的关键字
* (2) RangeQuery:范围查询
* @throws Exception
*/
@Test
public void rangeQuery() throws Exception{
//创建Query对象
Query query = LongPoint.newRangeQuery("sizeField",0L,1000L);
this.printResult(query);
}
private void printResult(Query query) throws Exception{
//执行查询
TopDocs topDocs = indexSearcher.search(query,10);
System.out.println("查询总记录数:"+topDocs.totalHits);
ScoreDoc[] docs = topDocs.scoreDocs;
for (ScoreDoc doc : docs) {
//获取文档id
int docId = doc.doc;
//根据文档id获取文档对象
Document document = indexSearcher.doc(docId);
System.out.println("文件名称:" + document.get("nameField"));
System.out.println("文件路径:" + document.get("pathField"));
System.out.println("文件内容:" + document.get("contentField"));
System.out.println("文件大小:" + document.get("sizeField"));
System.out.println("---------------------------------------------------------");
}
indexReader.close();
}
/**
* 查询文档
* 使用 QueryParser 查询
* 可以对查询语句进行分词,然后基于分词的结果进行查询。
* 需要依赖 lucene-queryparser-8.5.2.jar
* @throws Exception
*/
@Test
public void queryParser() throws Exception{
/*
* 创建QueryParser对象
* 参数f:默认搜索域
* 参数a:分词器对象
*/
QueryParser queryParser = new QueryParser("nameField",new IKAnalyzer());
//创建Query对象
Query query = queryParser.parse("demo");
//执行查询,打印结果
this.printResult(query);
}
}
六、Lucene核心API
1. IndexWriter
(1)利用这个类可以对索引库进行增、删、改操作。
(2)利用构造方法可以构造一个IndexWriter的对象
IndexWriter indexWriter =
new IndexWriter(directory,LuceneConfig.analyzer,MaxFieldLength.LIMITED)
(3)addDocument 向索引库中添加一个Document
(4)updateDocument 更新一个Document
(5)deleteDocuments 删除一个指定的Document
(6)deleteAll 删除索引库中所有的Document
2. Directory
指向索引库的位置,有两种Directory
FSDirectory
(1)通过FSDirectory.open(new File("D:\lucene\index"))
建立的这个index文件夹,就是索引库存放的位置。
(2)通过这种方法建立索引库时如果index文件夹不存在,程序将自动创建一个,如果存在就用原本的这个。
(3)“优点:” 通过这个类可以知道所建立的索引库在磁盘上,能永久性的保存数据。
(4)“缺点”: 因为程序要访问磁盘上的数据,这个操作可能引发大量的IO操作,会降低性能。
RAMDirectory
(1)通过构造函数的形式Directory ramdirectory = new RAMDirectory(fsdirectory)
可以建立RAMDirectory。
(2)这种方法建立的索引库会在内存中开辟一定的空间,通过构造函数的形式吧FSDirectory移动到内存中。
(3)“缺点:”这种方法索引库中的数据是暂时的,只要内存的数据消失,这个索引库也就跟着消失了。
(4)“优点:”程序是在内存中跟索引库交互,因此通过该方式创建的索引的好处就在效率比较高,访问速度较快。
3. Document
(1)通过无参的构造函数可以创建一个Document对象。Document doc = new Document();
(2)一个Directory是由很多Document组成的。用户从客户端输入要搜索的关键内容被服务器端包装成JavaBean,然后再转化为Document。
4. Field
(1)Field相当于JavaBean的属性。
(2)Field的用法为:new Field(“title”,article.getTitle(),Store.YES,Index.ANALYZED)
第一个参数:属性;
第二个参数:属性值;
第三个参数:是否往索引库里存储;
第四个参数:是否更新索引;
a)NO 不进行索引
b)ANALYZED 进行分词索引
c)NOT_ANALYZED 进行索引,把整个输入作为一个词对待。