课程计划:
1. 什么是全文检索,如何实现全文检索
2. Lucene实现全文检索的流程
* 创建索引
* 查询索引
3. 配置开发环境
4. 入门程序
5. 分析器的分析过程
1. 测试分析器的分析效果
2. 第三方中文分析器
6. 索引库维护
1. 添加文档
2. 删除文档
3. 修改文档
7. 索引库查询
1. 使用Query子类查询
2. 使用QueryParser查询
全文检索概述
1. 数据分类:
1. 结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。 [数据类型固定]
2. 非结构化数据:指不定长或无固定格式的数据。如邮件,word文档等磁盘上的文件 [数据类型不固定]
2. 结构化数据搜索:
1. 常见的结构化数据也就是数据库中的数据。 使用sql语句进行查询,很快得到结果
2. 为什么数据库搜索很容易?
* 因为数据库中的数据存储是有规律的,有行有列而且数据格式、数据长度都是固定的。
* [简单,速度快]
3. 非结构化数据查询方法:
1. 顺序扫描法:
* 所谓的顺序扫描,比如要找内容包含一个字符串,就是一个文档一个文档的看,如果此文档包含此字符串则为我们要找的文件,借着看下一个文件,直到扫描所有的文件。利用windows的搜索也可以搜索文件内容,只是相当的慢。
2. 全文检索 [把非结构化数据变成结构化数据]:
1. 概述:
* 将结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此结构进行搜索,从而达到搜索相对较快的目的。
2. 索引:
* 这部分从非结构化数据中提取出的然后重新组织的信息,我们称之为索引。
3. 分析:
* 先根据空格进行字符串拆分,得到一个单词列表,基于单词列表创建一个索引。然后查询索引,根据单词和文档的对应关系找到文档列表,这个过程就叫做全文检索。
* 查询的时候,先查询索引表,通过关键字与坐标对应找到该关键字的坐标,再通过坐标查询存有关键字的表,速度很快。
* 树形结构;
4. 总结:
* 这种先创建索引然后查询索引的过程就叫做全文检索。
* 索引一次创建,多次使用,表现为每次查询速度很快;
* 以id为主键,[唯一]使用主键作为查询条件速度很快。
5. 注意:
1. 并不是所有的表都需要建立索引,尤其是有增删改;
2. 只查询的数据可以用来建立索引;
4. 如何实现全文检索:
* 可以使用 Lucene 实现全文检索。Lucene 是 apache 下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。
5. 应用场景:
* 对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,比如百度、Google 等搜索引擎、论坛站内搜索、电商网站站内搜索等。
* 举例:
1. 搜索引擎: 百度,360搜索,谷歌,搜狗
2. 站内搜索: 论坛搜索,微博,文章搜索
3. 电商搜索: 淘宝搜索,京东搜索
6. 种类:
1. lucene
* 它是已经被淘汰的全文检索技术,学习它是为了学习全文检索原理;
2. ElasticSearch
* 它在学成在线项目中有讲解,它是最新的技术,使用率日益增加;
3. solr
* 它在品优购项目中有讲解
lucene
1. 什么是Lucene?
* Lucene是一个基于java开发全文检索工具包。
2. Lucene实现全文检索的流程
1. 创建索引
1. 获得文档
* 原始文档:要基于那些数据来进行搜索,那么这些数据就是原始文档。
* 原始文档是指要索引和搜索的内容。原始内容包括互联网上的网页、数据库中的数据、磁盘上的文件等。
* 搜索引擎:使用爬虫获得原始文档。
* 目的:信息采集的目的是为了对原始内容进行索引。
* 站内搜索:数据库中的数据。
* 我们要获取磁盘上文件的内容,可以通过文件流来读取文本文件的内容,对于pdf、doc、xls 等文件可通过第三方提供的解析工具读取文件内容,比如 Apache POI 读取 doc和 xls 的文件内容。
* 案例:直接使用io流读取磁盘上的文件,直接获得原始文档
2. 构建文档对象:
1. 获取原始内容的目的是为了索引,在索引前需要将原始内容建成文档(Document),文档中包括一个一个的域(Field),域中储存内容。
2. 这里我们可以将磁盘上的一个文件当成一个document,Document中包括一些Filed(file_name文件名称,file_path文件路径,file_size文件大小,file_content文件内容);
3. 域中保存就是原始文档数据
* 域的名称
* 域的值
4. 注意:每个Document可以有多个Field,同一个Document可以有相同的Field(域名和域值都相同),每个文档都有一个唯一的编号,就是文档id;
3. 分析文档:
1. 就是分词的过程
1. 根据空格进行字符串拆分,得到一个单词列表
2. 把单词统一转换为小写
3. 去除标点符号
4. 去除停用词
* 停用词: 无意义的词;
* 每个关键词都封装成一个Term对象中
* Term中包含两部分内容
* 关键词所在的域
* 关键词本身
* 不同的域中拆分出来的相同的关键词是不同的Term。
4. 创建索引:
1. 基于关键词列表创建一个索引。保存到索引库中;
2. 索引库中:
1. 索引
2. document对象
3. 关键词和文档的对应关系
3. 倒排索引结构:通过词语找文档
2. 查询索引:
1. 用户查询接口
1. 用户输入查询条件的地方
2. 例如:百度的搜索框
2. 把关键词封装成一个查询对象;
1. 要查询的域
2. 要搜索的关键词
3. 执行查询
1. 根据要查询的关键词到对应的域上进行搜索。
2. 找到关键词,根据关键词找到对应的文档。
4. 渲染结果
1. 根据文档的id找到文档对象
2. 对关键词进行高亮显示
3. 分页处理
4. 最终展示给用户看
3. 过程:
1. 索引过程:对搜索的原始内容进行索引构建一个索引库,索引过程包括:
* 确定原始内容即要搜索的内容->采集文档->创建文档->分析文档->索引文档
2. 搜索过程:从索引库中搜索内容,搜索过程包括:
* 用户通过搜索界面->创建查询->执行搜索,从索引库搜索->渲染搜索结果
入门程序
1. 创建索引
* 环境:
1. 下载
* http://lucene.apache.org/
* 最低要求:JDK1.8
2. 解压缩
3. 文件含义:
1. core文件夹: 核心
2. analysis->lucene-analyzers-common-7.4.0.jar : 分析
4. 搭建:
1. 创建一个java工程
2. 添加jar:
* lucene-analyzers-common-7.4.0.jar
* lucene-core-7.4.0.jar
* commons-io.jar
5. 开始环境搭建:
1. 创建一个java空项目
* 创建一个Modules模块,选择"JAVA" ; [JDK必须在1.8以上]
2. 新建一个目录:lib
* 添加上面的jar包
* Dependencies->添加导入依赖
* 步骤:
1. 创建一个Director对象,指定索引库保存的位置
2. 基于Directory对象创建一个IndexWriter对象
3. 读取磁盘上的文件,对应每个文件创建一个文档对象
4. 向文档对象中添加域
5. 把文档对象写入索引库
6. 关闭indexWriter对象
2. 开始
1. 创建一个LuceneFirst类:
public class LuceneFirst {
@Test
public void createIndex() throws Exception {
//1、创建一个Director对象,指定索引库保存的位置。
//把索引库保存在内存中
//Directory directory = new RAMDirectory();
//把索引库保存在磁盘
Directory directory = FSDirectory.open(new File("C:\\temp\\index").toPath());
//2、基于Directory对象创建一个IndexWriter对象
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig());
//3、读取磁盘上的文件,对应每个文件创建一个文档对象。
File dir = new File("C:\\A0.lucene2018\\05.参考资料\\searchsource");
File[] files = dir.listFiles();
for (File f :
files) {
//取文件名
String fileName = f.getName();
//文件的路径
String filePath = f.getPath();
//文件的内容
String fileContent = FileUtils.readFileToString(f, "utf-8");
//文件的大小
long fileSize = FileUtils.sizeOf(f);
//创建Field
//参数1:域的名称,参数2:域的内容,参数3:是否存储
Field fieldName = new TextField("name", fileName, Field.Store.YES);
//Field fieldPath = new TextField("path", filePath, Field.Store.YES);
Field fieldPath = new StoredField("path", filePath);
Field fieldContent = new TextField("content", fileContent, Field.Store.YES);
//Field fieldSize = new TextField("size", fileSize + "", Field.Store.YES);
Field fieldSizeValue = new LongPoint("size", fileSize);
Field fieldSizeStore = new StoredField("size", fileSize);
//创建文档对象
Document document = new Document();
//向文档对象中添加域
document.add(fieldName);
document.add(fieldPath);
document.add(fieldContent);
//document.add(fieldSize);
document.add(fieldSizeValue);
document.add(fieldSizeStore);
//5、把文档对象写入索引库
indexWriter.addDocument(document);
}
//6、关闭indexwriter对象
indexWriter.close();
}
2. 使用luke查看索引库中的内容
1. 资料->luke-javafx-7.4.0-luke-reale
2. 解压后双击打开 [最低运行版本:jdk1.9]
3. 在name中不同类中的分词出来的词即便是一样的,其类型还是不一样。比如标题名叫apache和内容中包含apache,这里的两个apache不一样;
4. 查询:
1. *:* ,它表示查询全部;
5. 使用此工具用于测试和查看,实际工作中我们还是需要用程序来完成这些操作,接下来继续我们的入门程序;
3. 查询索引库
1. 步骤:
1. 创建一个Director对象,指定索引库的位置;
2. 创建一个IndexReader对象
3. 创建一个IndexSearcher对象,构造方法中的参数就是indexReader对象。
4. 创建一个Query对象,TermQuery [它的意思就是根据关键词查询]
5. 执行查询,得到一个TopDocs对象
6. 取查询结果的总记录数。
7. 取文档列表
8. 打印文档中的内容
9. 关闭indexReader对象
2. 实现:
* 继续在LuceneFirst类中写:
* public class LuceneFirst {
* ....
@Test
public void searchIndex() throws Exception {
//1、创建一个Director对象,指定索引库的位置
Directory directory = FSDirectory.open(new File("C:\\temp\\index").toPath());
//2、创建一个IndexReader对象
IndexReader indexReader = DirectoryReader.open(directory);
//3、创建一个IndexSearcher对象,构造方法中的参数indexReader对象。
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//4、创建一个Query对象,TermQuery
Query query = new TermQuery(new Term("name", "spring"));
//5、执行查询,得到一个TopDocs对象
//参数1:查询对象 参数2:查询结果返回的最大记录数
TopDocs topDocs = indexSearcher.search(query, 10);
//6、取查询结果的总记录数
System.out.println("查询总记录数:" + topDocs.totalHits);
//7、取文档列表
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
//8、打印文档中的内容
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("size"));
//System.out.println(document.get("content"));
System.out.println("-----------------寂寞的分割线");
}
//9、关闭IndexReader对象
indexReader.close();
}
4.分析器
1. 默认使用的数标准分析器StandardAnalyzer
2. 查看分析器的分析效果
1. 使用Analyzer对象的tokenStream方法返回一个TokenStream对象,词对象中包含了最终分词结果。
2. 实现步骤:
1. 创建一个Analyzer对象,StandarAnalyzer对象
2. 使用分析器对象的tokenStream方法获得一个TokenStream对象
3. 向TokenStream对象中设置一个引用,相当于数一个指针;
4. 调用TokenStream对象的rest方法,如果不调用抛异常
5. 使用while循环遍历TokenStream对象
6. 关闭TokenStream对象
3. 实现:
* 继续在LuceneFirst类中写:
* public class LuceneFirst {
* ....
* Test
@Test
public void testTokenStream() throws Exception {
//1)创建一个Analyzer对象,StandardAnalyzer对象
Analyzer analyzer = new StandardAnalyzer();
//2)使用分析器对象的tokenStream方法获得一个TokenStream对象
TokenStream tokenStream = analyzer.tokenStream("", "2017年12月14日 - 传智播客Lucene概述公安局Lucene是一款高性能的、可扩展的信息检索(IR)工具库。信息检索是指文档搜索、文档内信息搜索或者文档相关的元数据搜索等操作。");
//3)向TokenStream对象中设置一个引用,相当于数一个指针
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
//4)调用TokenStream对象的rest方法。如果不调用抛异常
tokenStream.reset();
//5)使用while循环遍历TokenStream对象
while(tokenStream.incrementToken()) {
System.out.println(charTermAttribute.toString());
}
//6)关闭TokenStream对象
tokenStream.close();
中文分析器
1. 标准分析器的缺点:
1. 默认分词器是美国的一个程序员开发的,分词为英文的时候,以空格为分隔点,然后拆分成各个单词,中文只支持一个汉字一个关键字,准确度并不高,所以不适用;所以我们必须要使用中文分析器对其进行分析。
2. IK分词器
* 在词语被分词的时候,它被分词成几个部分
* 在搜索的时候,分词器拿到value值,并将其也分成几个部分
* 然后几个部分找几个部分,按照关联度高地进行排列;
3. IKAnalyze的使用方法
1. 把IKAnalyzer的jar包添加到工程中
2. 把配置文件和扩展词典添加到工程的classpath下;
3. 注意:
* 扩展词典严禁使用windows记事本编辑,否则会使扩展词典不生效;
* 必须保证编写的格式是utf-8 [windows记事本的utf-8不是标准的utf-8,它是utf-8+BOM,不是纯的utf-8];
* 可以使用notepad++等编辑.
4. 扩展词典[hotword.dic]: 添加一些日新月异的新词/公司名称等...
5. 停用词词典[stopword.dic]: 无意义的词/敏感词等
4. 演示中文分析器的使用方法:
1. 将IK-Analyzer...jar包导入
2. 将配置文件,扩展词典,停用词典放入该包下:com.itheima.lucene
3. 实现:
* 继续在LuceneFirst类中写:
* public class LuceneFirst {
* ....
* Test
@Test
public void testTokenStream() throws Exception {
//1)创建一个Analyzer对象,StandardAnalyzer对象
// Analyzer analyzer = new StandardAnalyzer(); 英文分析器
Analyzer analyzer = new IKAnalyzer(); 中文分析器
//2)使用分析器对象的tokenStream方法获得一个TokenStream对象
TokenStream tokenStream = analyzer.tokenStream("", "2017年12月14日 - 传智播客Lucene概述公安局Lucene是一款高性能的、可扩展的信息检索(IR)工具库。信息检索是指文档搜索、文档内信息搜索或者文档相关的元数据搜索等操作。");
//3)向TokenStream对象中设置一个引用,相当于数一个指针
CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
//4)调用TokenStream对象的rest方法。如果不调用抛异常
tokenStream.reset();
//5)使用while循环遍历TokenStream对象
while(tokenStream.incrementToken()) {
System.out.println(charTermAttribute.toString());
}
//6)关闭TokenStream对象
tokenStream.close();
5. 在代码中使用分析器
1. 实现:
public class LuceneFirst {
@Test
public void createIndex() throws Exception {
//1、创建一个Director对象,指定索引库保存的位置。
//把索引库保存在内存中
//Directory directory = new RAMDirectory();
//把索引库保存在磁盘
Directory directory = FSDirectory.open(new File("C:\\temp\\index").toPath());
//2、基于Directory对象创建一个IndexWriter对象
IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
IndexWriter indexWriter = new IndexWriter(directory, config);
//3、读取磁盘上的文件,对应每个文件创建一个文档对象。
File dir = new File("C:\\A0.lucene2018\\05.参考资料\\searchsource");
File[] files = dir.listFiles();
for (File f :
files) {
//取文件名
String fileName = f.getName();
//文件的路径
String filePath = f.getPath();
//文件的内容
String fileContent = FileUtils.readFileToString(f, "utf-8");
//文件的大小
long fileSize = FileUtils.sizeOf(f);
//创建Field
//参数1:域的名称,参数2:域的内容,参数3:是否存储
Field fieldName = new TextField("name", fileName, Field.Store.YES);
//Field fieldPath = new TextField("path", filePath, Field.Store.YES);
Field fieldPath = new StoredField("path", filePath);
Field fieldContent = new TextField("content", fileContent, Field.Store.YES);
//Field fieldSize = new TextField("size", fileSize + "", Field.Store.YES);
Field fieldSizeValue = new LongPoint("size", fileSize);
Field fieldSizeStore = new StoredField("size", fileSize);
//创建文档对象
Document document = new Document();
//向文档对象中添加域
document.add(fieldName);
document.add(fieldPath);
document.add(fieldContent);
//document.add(fieldSize);
document.add(fieldSizeValue);
document.add(fieldSizeStore);
//5、把文档对象写入索引库
indexWriter.addDocument(document);
}
//6、关闭indexwriter对象
indexWriter.close();
}
索引库维护
1. 域的类型:
1. StringField(FieldName,FieldValue,Store.YES):
* 这个用来构造Field,不分析,会将整个储存在索引中,比如姓名,身份证,订单编号等不需要分词;
* 不分词,建索引,存储/不存储
* [有些可能不需要存储,注意:不存储不代表不建索引库,搜整条数据依然可以找到它]
2. LongPoint(String name,long...Point):
* 可以使用LongPoint/IntPoint等类型存储数据类型的数据
* 分词,建索引,不存储
* 如果需要储存数据还需要使用StoreField
3. StoreField(FieldName,FieldValue):
* 这个Field用来构建不同类型Field
* 不分析,不索引,但要Field要储存在文档中
4. TextField(FieldName,FieldValue,Store.No)或TextField(FieldName,reader)
* 如果是一个Reader,lucene猜测内容比较多,会采用Unstored的策略;
5. 注意:
* 如果没有建索引,有储存,那么可以查询到该内容,但是内容中没有该关键词数据;
2. 添加文档,改造:
Field fieldName = new TextField("name", fileName, Field.Store.YES);
Field fieldPath = new StoredField("path", filePath);
Field fieldContent = new TextField("content", fileContent, Field.Store.YES);
Field fieldSizeValue = new LongPoint("size", fileSize);
Field fieldSizeStore = new StoredField("size", fileSize);
添加域:
document.add(fieldName);
document.add(fieldPath);
document.add(fieldContent);
document.add(fieldSizeValue);
document.add(fieldSizeStore);
3. 索引库维护-添加
1. 新建一个类:IndexManger
* public class IndexManger{
*
// 添加索引
@Test
public void addDocument() throws Exception {
// 索引库存放路径
Directory directory = FSDirectory.open(new File("D:\\temp\\index").toPath());
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));
//LongPoint 创建索引
document.add(new LongPoint("size", 1000l));
//StoreField 存储数据
document.add(new StoredField("size", 1000l));
// 不需要创建索引的就使用 StoreField 存储
document.add(new StoredField("path", "d:/temp/1.txt"));
// 添加文档到索引库
indexWriter.addDocument(document);
// 关闭 indexwriter
indexWriter.close();
}
4. 索引库维护-删除
* 删除文档有两种方式:
1. 删除全部
2. 根据查询,修改
* 示例:
* 继续上面的添加类...
* @Test
* public void deleteAllDocument() throws Exception{
* //删除全部文档
* indexWriter.deleteAll();
* //indexWriter.close();
* }
* @Test
* public void deleteDocumentByQuery() throws Exception{
* //如果删除了索引库,需要先建立索引库再查询
* indexWriter.deleteDocuments(new Term("name","apache"))
* indexWriter.close();
* }
5. 索引库维护-更新
* 特点:先删除原来的,然后再进行添加操作;
* 示例:
* public void updateDocument() throws Exception{
* //创建一个新的文档对象
* Document document =new Document();
* //向文档对象中添加域
* document.add(new TextField("name","更新之后的文档",Field.Store.YES));
* document.add(new TextField("name1","更新之后的文档1",Field.Store.YES));
* document.add(new TextField("name2","更新之后的文档2",Field.Store.YES));
* //更新索引库
* indexWriter.updateDocument(new Term("name","spring"),document);
* indexWriter.close();
* }
6. 数值范围查询
1. TermQuery:
1. 根据关键词查询,需要指定要查询的域及要查询的关键词;
2. 范围进行查询
2. 使用QueryPaser进行查询
1. 可以对要查询的内容先进行分词,然后基于分词的结果进行查询;
2. 需要导入jar包:lucene-queryparser-7.4.0.jar
3. 范围查询和QueryParser查询的代码整合如下:
public class SearchIndex {
private IndexReader indexReader;
private IndexSearcher indexSearcher;
@Before
public void init() throws Exception {
indexReader = DirectoryReader.open(FSDirectory.open(new File("C:\\temp\\index").toPath()));
indexSearcher = new IndexSearcher(indexReader);
}
@Test
public void testRangeQuery() throws Exception {
//创建一个Query对象
Query query = LongPoint.newRangeQuery("size", 0l, 100l);
printResult(query);
}
private void printResult(Query query) throws Exception {
//执行查询
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("size"));
//System.out.println(document.get("content"));
System.out.println("-----------------寂寞的分割线");
}
indexReader.close();
}
@Test
public void testQueryParser() throws Exception {
//创建一个QueryPaser对象,两个参数
QueryParser queryParser = new QueryParser("name", new IKAnalyzer());
//参数1:默认搜索域,参数2:分析器对象
//使用QueryPaser对象创建一个Query对象
Query query = queryParser.parse("lucene是一个Java开发的全文检索工具包");
//执行查询
printResult(query);
}
}