Lucene
文章目录
一、 全文检索
-
数据的分类和查询方法
-
数据的分类
-
结构化数据
例如数据库中的数据。
格式固定、长度固定、数据类型固定。
-
非结构化数据
word 文档、pdf 文档、邮件、html、txt
格式不固定、长度不固定、数据类型不固定。
-
-
数据的查询
-
结构化数据的查询
SQL 语句,查询结构化数据的方法。简单、速度快。
-
非结构化数据的查询
例:从文本文件中找出包含 Spring 单词的文件:
-
目测法。
-
使用程序把文档读取到内存中,然后匹配字符串,顺序扫描。
-
把非结构化数据变成结构化数据:
先根据空格进行字符串拆分,得到一个单词列表,基于单词列表创建一个索引,然后查询索引,根据单词和文档的对应关系找到文档列表。这个过程就叫做全文检索。
-
-
-
-
全文检索定义
先创建索引然后查询索引的过程就叫做全文检索。索引一次创建可以多次使用,表现为每次查询速度很快。
-
全文检索应用场景
-
搜索引擎
百度、360搜索、谷歌、搜狗。
-
站内搜索
论坛内搜索、微博搜索、文章搜索、淘宝搜索、京东搜索。
-
只要是有搜索的地方就可以使用全文检索技术。
-
-
Lucene
Lucene 是一个基于 Java 开发全文检索工具包。
二、 Lucene 实现全文检索的流程
-
索引和搜索流程图
2.1 创建索引
-
获得文档
原始文档:要基于哪些数据来进行搜索,那么这些数据就是原始文档。
搜索引擎:使用爬虫获取原始文档。
站内搜索:数据库中的数据。
-
构建文档对象
对应每个原始文档创建一个 Document 对象,每个 Document 对象中包含多个域 (field),域中保存的就是原始文档数据。域的名称和域的值构成一对键值对。
每个文档都有一个唯一的编号,也就是文档的 id。
-
分析文档
就是分词过程
-
根据空格进行字符串拆分,得到一个单词列表。
-
把单词统一转换成小(或大)写。
-
去除标点符号。
-
去除停用词(也就是无意义的词)。
每个关键字都封装成一个 Term 对象。Term 对象中包含两部分内容:一是关键字所在的域、二是关键词本身,不同的域中拆分出来的相同的关键词是不同的 Term。
-
-
创建索引
基于关键词列表创建一个索引,保存到索引库中。
索引库中包含:
-
索引
-
Document 对象
-
关键词和文档的对应关系
通过词语找文档,这种索引的结构叫做倒排索引结构。
-
2.2 查询索引
-
用户查询接口
用户输入查询条件的地方。例如:百度的搜索框。
-
把关键词封装成一个查询对象
查询对象包含:要查询的域,要搜索的关键字。
-
执行查询
根据要查询的关键词到对应的域上进行搜索。找到关键词,根据关键词找到对应的文档。
-
渲染结果
根据文档的 id 找到文档的对象,对关键词进行高亮显示、分页处理。
三、 入门案例(简单使用 Lucene)
3.1 导入依赖 jar 包
* lucene-analyzer-commom-7.4.0.jar
* lucene-core-7.4.0.jar
* commons-io.jar
3.2 建立索引库
-
创建一个 Directory 对象;
-
基于 Directory 对象创建一个 IndexWriter 对象;
-
读取磁盘上的文件,对应每一个文件创建一个文档对象;
-
向文档对象中添加域;
-
把文档对象写入索引库;
-
关闭 IndexWriter 对象。
/** * 1. 建立索引库,并把文档映射到索引 * */ @Test public void buildIndexStore() throws IOException { // 1. 创建一个 Directory 对象,指定索引库的位置 Directory directory = FSDirectory.open(new File("E:\\java\\15_Lucene_IndexStore").toPath()); // 2. 创建 写索引 对象 IndexWriterConfig config = new IndexWriterConfig(); IndexWriter indexWriter = new IndexWriter(directory, config); // 3. 读取磁盘上的文件,对应每个文件创建一个文档对象。 File file = new File("E:\\java\\15_Lucene\\01_lucene_demo\\searchsource"); File[] files = file.listFiles(); for (File f: files) { // 获取文件名 String fileName = f.getName(); System.out.println(fileName); // 获取文件路径 String filePath = f.getPath(); // 获取文件内容 String fileContent = FileUtils.readFileToString(f, "utf-8"); // 获取文件大小 long fileSize = FileUtils.sizeOf(f); // 参数1:域的名称,参数2:域的内容,参数3:是否存储 Field fieldName = new TextField("fileName", fileName, Field.Store.YES); Field fieldPath = new TextField("filePath", filePath, Field.Store.YES); Field fieldContent = new TextField("fileContent", fileContent, Field.Store.YES); Field fieldSize = new TextField("fileSize", String.valueOf(fileSize), Field.Store.YES); // 4. 创建文档对象 Document doc = new Document(); // 向文档对象中添加索引库 doc.add(fieldName); doc.add(fieldPath); doc.add(fieldContent); doc.add(fieldSize); // 5. 把文档对象写入索引库 indexWriter.addDocument(doc); } // 6. 关闭 IndexWriter 对象 indexWriter.close(); }
注:可以使用 luke 查看索引库中的内容。
3.3 查询索引库
- 创建一个 Directory 对象,指定索引库的位置;
- 创建一个 IndexReader 对象;
- 创建一个 IndexSearcher 对象,构造方法中的参数为 IndexReader 对象;
- 创建一个 Query 对象,TermQuery;
- 执行查询,得到一个 TopDocs 对象;
- 取查询结果的总记录数;
- 取文档列表;
- 打印文档中的内容;
- 关闭 IndexReader 对象。
/**
* 2. 从索引库中查找文档
* */
@Test
public void searchIndex() throws IOException {
// 1. 创建一个 Directory 对象,指定索引库的位置
Directory directory = FSDirectory.open(new File("E:\\java\\15_Lucene_IndexStore").toPath());
// 2. 创建一个 IndexReader 对象
IndexReader indexReader = DirectoryReader.open(directory);
// 3. 创建一个 IndexSearcher 对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 4. 创建一个 Query 对象,TermQuery
Query query = new TermQuery(new Term("fileContent", "spring"));
// 5. 执行查询,得到一个TopDocs对象
// 传入查询条件并设置最多返回 10 条结果
TopDocs topDocs = indexSearcher.search(query, 10);
System.out.println("总的记录条数为:" + topDocs.totalHits);
// 6. 获取文档列表
ScoreDoc[] docs = topDocs.scoreDocs;
// 7. 打印文档中的内容
for (ScoreDoc d :
docs ) {
// 获取文档的 id
int doc = d.doc;
Document document = indexSearcher.doc(doc);
System.out.println(document.get("fileName"));
System.out.println(document.get("filePath"));
System.out.println(document.get("fileSize"));
System.out.println("-------------------------------");
}
// 8. 关闭 indexReader 对象
indexReader.close();
}
四、 分析器
默认情况下使用的是标准分析器 StandardAnalyzer,但是 StandardAnalyzer 对中文分词的支持不是很友好。为了实现对中文的友好支持,可以使用 IKAnalyzer 类。
-
查看标准分析器 StandardAnalyzer 的分析效果
使用 Analyzer 对象的 tokenStream 方法返回一个 tokenStream 对象。此对象中包含了最终的分词结果。
实现步骤:
- 创建一个 Analyzer 对象,StandardAnalyzer 对象;
- 使用分析器对象的 tokenStream 方法获得一个 TokenStream 对象;
- 向 TokenStream 对象中设置一个引用,相当于是一个指针;
- 调用 TokenStream 对象的 reset 方法,如果不调用则会抛出异常;
- 使用 while 循环遍历 TokenStream 对象;
- 关闭 TokenStream 对象。
代码演示:
/** * 3. 测试使用 Analyzer * */ @Test public void testIKAnalyzer() throws IOException { // 1. 创建一个Analyzer对象,StandardAnalyzer 对象,主要是对英文进行分析 // Analyzer analyzer = new StandardAnalyzer(); // IKAnalyzer 主要是对中文进行分析 IKAnalyzer analyzer = new IKAnalyzer(); // 2. 使用分析器对象的 tokenStream 方法获得一个 TokenStream 对象 // TokenStream tokenStream = analyzer.tokenStream("", "Learn how to create a web page with Spring MVC."); TokenStream tokenStream = analyzer.tokenStream("","今天是个好日子啊,真的是个好日子啊,白日依山尽"); // 3. 向 TokenStream 对象中设置一个引用,相当于是一个指针 CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class); // 4. 调用 TokenStream 对象的 reset 方法。如果不调用抛异常 tokenStream.reset(); // 5. 使用while循环遍历TokenStream对象 while(tokenStream.incrementToken()){ System.out.println(charTermAttribute.toString()); } // 6. 关闭 tokenStream 对象 tokenStream.close(); }
-
IKAnalyzer 的使用方法
-
把 IKAnalyzer 的 jar 包添加到工程中;
* IK-Analyzer-1.0-SNAPSHOT.jar
-
把配置文件和扩展词典添加到工程的 classpath 下。
* hotword.dic * IKAnalyzer.cfg.xml * stopword.dic
注意:扩展词严禁使用 windows 自带的记事本编辑,主要是为了保证编码格式为 “UTF-8”。
- 扩展词典:添加一些新词;
- 停用词词典:无意义的词或者是敏感词汇。
代码演示:
// 创建 写索引 对象时,把 IKAnalyzer 对象添加进去配置对象即可 IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
-
五、 索引库维护
-
添加文档
/** * 1. 添加文档 * */ @Test public void addDocument() throws IOException { Document document = new Document(); document.add(new TextField("name", "文章标题", Field.Store.YES)); // 是否存储的标准:是否要将内容展示给用户 document.add(new TextField("content", "这是文章的内容哦,弟中弟", Field.Store.NO)); // 不分析,不索引,但要Field存储在文档中 document.add(new StoredField("path", "c:/temp/hello")); // 把 document 对象存储到索引域中 indexWriter.addDocument(document); indexWriter.close(); }
-
删除文档
-
删除全部文档
/** * 2. 删除所有文档 * */ @Test public void deleteAllDocuments() throws IOException { indexWriter.deleteAll(); indexWriter.close(); }
-
根据查询、关键词查询文档
/** * 3. 删除指定文档 * */ @Test public void deleteDocumentsByTerm() throws Exception{ // 删除域名为 name 域中 包含 “文章” 关键字的文档 indexWriter.deleteDocuments(new Term("name", "文章")); indexWriter.close(); }
-
-
更新文档
/** * 4. 更新文档 * */ @Test public void updateDocument() throws Exception{ Document document = new Document(); document.add(new TextField("name", "更新后,这是第一个标题", Field.Store.YES)); document.add(new TextField("name1", "更新后,哈哈,介系第二个嘛", Field.Store.YES)); document.add(new TextField("name2", "这是更新后的第三个标题", Field.Store.YES)); indexWriter.updateDocument(new Term("name", "标题"), document); indexWriter.close(); }
六、索引库查询
-
使用 Query 的子类
-
TermQuery
根据关键词进行查询,需要指定要查询的域以及关键词。
public void testTermQuery() throws IOException { // 1. 创建一个 Directory 对象,指定索引库的位置 Directory directory = FSDirectory.open(new File("E:\\java\\15_Lucene_IndexStore").toPath()); // 2. 创建一个 IndexReader 对象 IndexReader indexReader = DirectoryReader.open(directory); // 3. 创建一个 IndexSearcher 对象 IndexSearcher indexSearcher = new IndexSearcher(indexReader); // 4. 创建一个 Query 对象,TermQuery Query query = new TermQuery(new Term("fileContent", "spring")); // 5. 执行查询,得到一个TopDocs对象 // 传入查询条件并设置最多返回 10 条结果 TopDocs topDocs = indexSearcher.search(query, 10); System.out.println("总的记录条数为:" + topDocs.totalHits); // 6. 获取文档列表 ScoreDoc[] docs = topDocs.scoreDocs; // 7. 打印文档中的内容 for (ScoreDoc d : docs ) { // 获取文档的 id int doc = d.doc; Document document = indexSearcher.doc(doc); System.out.println(document.get("fileName")); System.out.println(document.get("filePath")); System.out.println(document.get("fileSize")); System.out.println("-------------------------------"); } // 8. 关闭 indexReader 对象 indexReader.close(); }
-
RangeQuery
范围查询。
代码演示:
/** * 限定文件大小范围查找 * */ @Test public void testRangeQuery() throws Exception{ // 查找条件为:域名为 size,且文件大小在 1~1000 字节之间 Query query = LongPoint.newRangeQuery("fileSize", 1, 1000); // 自己写的查询方法,具体怎么写,自己去上面查琢磨琢磨 printQuery(query); }
-
-
使用 QueryParser 进行查询
可以先对要查询的内容先分词,然后基于分词的结果进行查询。
需要添加如下 jar 包:
* lucene-queryparser-7.4.0.jar
代码演示:
/** * 对搜索的内容进行分析,然后查找 * */ @Test public void testQueryParser () throws Exception{ // 创建 queryParser 对象:要搜索哪个域,要对搜索的词使用什么分析器 QueryParser queryParser = new QueryParser("fileName", new IKAnalyzer()); // 要搜索的内容,并且对内容进行分析 Query query = queryParser.parse("lucene是一个Java开发的全文检索工具包"); // 执行查询 printQuery(query); }