Lucene:一、入门
1 Lucene简介
Lucene是apache下的一个开源的全文检索引擎工具包。
1.1 全文检索(Full-text Search)
1.1.1 定义
全文检索就是先分词创建索引,再执行搜索的过程。
分词:就是将一段文字分成一个个单词
全文检索就将一段文字分成一个个单词去查询数据!!!
1.1.2 应用场景
1.1.2.1 搜索引擎(了解)
搜索引擎是一个基于全文检索、能独立运行、提供搜索服务的软件系统。
1.2 Lucene实现全文检索的流程
全文检索的流程分为两大部分:索引流程、搜索流程。
索引流程:采集数据--->构建文档对象--->创建索引(将文档写入索引库)。
搜索流程:创建查询--->执行搜索--->渲染搜索结果。
2 入门示例
2.1 需求
使用Lucene实现电商项目中图书类商品的索引和搜索功能。
2.2 配置步骤说明
(1)搭建环境(先下载Lucene)
(2)创建索引库
(3)搜索索引库
2.3 配置步骤
2.3.1 第一部分:搭建环境(创建项目,导入包)
前提:已经创建好了数据库(直接导入book.sql文件)
2.3.1.1 第一步:下载Lucene
Lucene是开发全文检索功能的工具包,使用时从官方网站下载,并解压。
官方网站:http://lucene.apache.org/
下载地址:http://archive.apache.org/dist/lucene/java/
下载版本:4.10.3(要求:jdk1.7及以上)
核心包lucene-core-4.10.3.jar(附常用API)
2.3.1.2 第二步:创建项目,导入包
mysql5.1驱动包:mysql-connector-java-5.1.7-bin.jar 核心包:lucene-core-4.10.3.jar 分析器通用包:lucene-analyzers-common-4.10.3.jar 查询解析器包:lucene-queryparser-4.10.3.jar |
项目结构如下:
2.3.2 第二部分:创建索引
步骤说明:
(1)采集数据
(2)将数据转换成Lucene文档
(3)将文档写入索引库,创建索引
2.3.2.1 第一步:采集数据
Lucene全文检索,不是直接查询数据库,所以需要先将数据采集出来。
(1)创建Book类
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Book { 2 3 private Integer bookId; // 图书ID 4 5 private String name; // 图书名称 6 7 private Float price; // 图书价格 8 9 private String pic; // 图书图片 10 11 private String description; // 图书描述 12 13 // 补全get\set方法 14 15 }
public class Book { private Integer bookId; // 图书ID private String name; // 图书名称 private Float price; // 图书价格 private String pic; // 图书图片 private String description; // 图书描述 // 补全get\set方法 } |
(2)创建一个BookDao类
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package cn.gzsxt.lucene.dao; 2 3 import java.sql.Connection; 4 5 import java.sql.DriverManager; 6 7 import java.sql.PreparedStatement; 8 9 import java.sql.ResultSet; 10 11 import java.sql.SQLException; 12 13 import java.util.ArrayList; 14 15 import java.util.List; 16 17 import cn.gzsxt.lucene.pojo.Book; 18 19 public class BookDao { 20 21 public List<Book> getAll() { 22 23 // 数据库链接 24 25 Connection connection = null; 26 27 // 预编译statement 28 29 PreparedStatement preparedStatement = null; 30 31 // 结果集 32 33 ResultSet resultSet = null; 34 35 // 图书列表 36 37 List<Book> list = new ArrayList<Book>(); 38 39 try { 40 41 // 加载数据库驱动 42 43 Class.forName("com.mysql.jdbc.Driver"); 44 45 // 连接数据库 46 47 connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/lucene", "root", "gzsxt"); 48 49 // SQL语句 50 51 String sql = "SELECT * FROM book"; 52 53 // 创建preparedStatement 54 55 preparedStatement = connection.prepareStatement(sql); 56 57 // 获取结果集 58 59 resultSet = preparedStatement.executeQuery(); 60 61 // 结果集解析 62 63 while (resultSet.next()) { 64 65 Book book = new Book(); 66 67 book.setBookId(resultSet.getInt("id")); 68 69 book.setName(resultSet.getString("name")); 70 71 book.setPrice(resultSet.getFloat("price")); 72 73 book.setPic(resultSet.getString("pic")); 74 75 book.setDescription(resultSet.getString("description")); 76 77 list.add(book); 78 79 } 80 81 } catch (Exception e) { 82 83 e.printStackTrace(); 84 85 }finally { 86 87 if(null!=resultSet){ 88 89 try { 90 91 resultSet.close(); 92 93 } catch (SQLException e) { 94 95 // TODO Auto-generated catch block 96 97 e.printStackTrace(); 98 99 } 100 101 } 102 103 if(null!=preparedStatement){ 104 105 try { 106 107 preparedStatement.close(); 108 109 } catch (SQLException e) { 110 111 // TODO Auto-generated catch block 112 113 e.printStackTrace(); 114 115 } 116 117 } 118 119 if(null!=connection){ 120 121 try { 122 123 connection.close(); 124 125 } catch (SQLException e) { 126 127 // TODO Auto-generated catch block 128 129 e.printStackTrace(); 130 131 } 132 133 } 134 135 } 136 137 return list; 138 139 } 140 141 }
package cn.gzsxt.lucene.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import cn.gzsxt.lucene.pojo.Book; public class BookDao { public List<Book> getAll() { // 数据库链接 Connection connection = null; // 预编译statement PreparedStatement preparedStatement = null; // 结果集 ResultSet resultSet = null; // 图书列表 List<Book> list = new ArrayList<Book>(); try { // 加载数据库驱动 Class.forName("com.mysql.jdbc.Driver"); // 连接数据库 connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/lucene", "root", "gzsxt"); // SQL语句 String sql = "SELECT * FROM book"; // 创建preparedStatement preparedStatement = connection.prepareStatement(sql); // 获取结果集 resultSet = preparedStatement.executeQuery(); // 结果集解析 while (resultSet.next()) { Book book = new Book(); book.setBookId(resultSet.getInt("id")); book.setName(resultSet.getString("name")); book.setPrice(resultSet.getFloat("price")); book.setPic(resultSet.getString("pic")); book.setDescription(resultSet.getString("description")); list.add(book); } } catch (Exception e) { e.printStackTrace(); }finally { if(null!=resultSet){ try { resultSet.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(null!=preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(null!=connection){ try { connection.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } return list; } } |
(3)创建一个测试类BookDaoTest
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package cn.gzsxt.lucene.test; 2 3 import java.util.List; 4 5 import org.junit.Test; 6 7 import cn.gzsxt.lucene.dao.BookDao; 8 9 import cn.gzsxt.lucene.pojo.Book; 10 11 public class BookDaoTest { 12 13 @Test 14 15 public void getAll(){ 16 17 BookDao dao = new BookDao(); 18 19 List<Book> books = dao.getAll(); 20 21 for (Book book : books) { 22 23 System.out.println("图书id:"+book.getBookId()+",图书名称:"+book.getName()); 24 25 } 26 27 } 28 29 }
package cn.gzsxt.lucene.test; import java.util.List; import org.junit.Test; import cn.gzsxt.lucene.dao.BookDao; import cn.gzsxt.lucene.pojo.Book; public class BookDaoTest { @Test public void getAll(){ BookDao dao = new BookDao(); List<Book> books = dao.getAll(); for (Book book : books) { System.out.println("图书id:"+book.getBookId()+",图书名称:"+book.getName()); } } } |
(4)测试结果,采集数据成功
2.3.2.2 第二步:将数据转换成Lucene文档
Lucene是使用文档类型来封装数据的,所有需要先将采集的数据转换成文档类型。其格式为:
修改BookDao,新增一个方法,转换数据
public List<Document> getDocuments(List<Book> books){ // Document对象集合 List<Document> docList = new ArrayList<Document>(); // Document对象 Document doc = null; for (Book book : books) { // 创建Document对象,同时要创建field对象 doc = new Document(); // 根据需求创建不同的Field Field id = new TextField("id", book.getBookId().toString(), Store.YES); Field name = new TextField("name", book.getName(), Store.YES); Field price = new TextField("price", book.getPrice().toString(),Store.YES); Field pic = new TextField("pic", book.getPic(), Store.YES); Field desc = new TextField("description", book.getDescription(), Store.YES); // 把域(Field)添加到文档(Document)中 doc.add(id); doc.add(name); doc.add(price); doc.add(pic); doc.add(desc); docList.add(doc); } return docList; } |
2.3.2.3 第三步:创建索引库
说明:Lucene是在将文档写入索引库的过程中,自动完成分词、创建索引的。因此创建索引库,从形式上看,就是将文档写入索引库!
修改测试类,新增createIndex方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 @Test 2 3 public void createIndex(){ 4 5 try { 6 7 BookDao dao = new BookDao(); 8 9 // 分析文档,对文档中的field域进行分词 10 11 Analyzer analyzer = new StandardAnalyzer(); 12 13 // 创建索引 14 15 // 1) 创建索引库目录 16 17 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719")); 18 19 // 2) 创建IndexWriterConfig对象 20 21 IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, analyzer); 22 23 // 3) 创建IndexWriter对象 24 25 IndexWriter writer = new IndexWriter(directory, cfg); 26 27 // 4) 通过IndexWriter对象添加文档对象(document) 28 29 writer.addDocuments(dao.getDocuments(dao.getAll())); 30 31 // 5) 关闭IndexWriter 32 33 writer.close(); 34 35 System.out.println("创建索引库成功"); 36 37 } catch (Exception e) { 38 39 e.printStackTrace(); 40 41 } 42 43 }
@Test public void createIndex(){ try { BookDao dao = new BookDao(); // 分析文档,对文档中的field域进行分词 Analyzer analyzer = new StandardAnalyzer(); // 创建索引 // 1) 创建索引库目录 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719")); // 2) 创建IndexWriterConfig对象 IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, analyzer); // 3) 创建IndexWriter对象 IndexWriter writer = new IndexWriter(directory, cfg); // 4) 通过IndexWriter对象添加文档对象(document) writer.addDocuments(dao.getDocuments(dao.getAll())); // 5) 关闭IndexWriter writer.close(); System.out.println("创建索引库成功"); } catch (Exception e) { e.printStackTrace(); } } |
测试结果,创建成功!!!
2.3.3 第三部分:搜索索引
2.3.3.1 说明
搜索的时候,需要指定搜索哪一个域(也就是字段),并且,还要对搜索的关键词做分词处理。
2.3.3.2 执行搜索
修改测试类,新增searchDocumentByIndex方法
1 @Test 2 3 public void searchDocumentByIndex(){ 4 5 try { 6 7 // 1、 创建查询(Query对象) 8 9 // 创建分析器 10 11 Analyzer analyzer = new StandardAnalyzer(); 12 13 QueryParser queryParser = new QueryParser("name", analyzer); 14 15 Query query = queryParser.parse("name:java教程"); 16 17 // 2、 执行搜索 18 19 // a) 指定索引库目录 20 21 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719")); 22 23 // b) 创建IndexReader对象 24 25 IndexReader reader = DirectoryReader.open(directory); 26 27 // c) 创建IndexSearcher对象 28 29 IndexSearcher searcher = new IndexSearcher(reader); 30 31 // d) 通过IndexSearcher对象执行查询索引库,返回TopDocs对象 32 33 // 第一个参数:查询对象 34 35 // 第二个参数:最大的n条记录 36 37 TopDocs topDocs = searcher.search(query, 10); 38 39 // e) 提取TopDocs对象中前n条记录 40 41 ScoreDoc[] scoreDocs = topDocs.scoreDocs; 42 43 System.out.println("查询出文档个数为:" + topDocs.totalHits); 44 45 for (ScoreDoc scoreDoc : scoreDocs) { 46 47 // 文档对象ID 48 49 int docId = scoreDoc.doc; 50 51 Document doc = searcher.doc(docId); 52 53 // f) 输出文档内容 54 55 System.out.println("==============================="); 56 57 System.out.println("文档id:" + docId); 58 59 System.out.println("图书id:" + doc.get("id")); 60 61 System.out.println("图书name:" + doc.get("name")); 62 63 System.out.println("图书price:" + doc.get("price")); 64 65 System.out.println("图书pic:" + doc.get("pic")); 66 67 System.out.println("图书description:" + doc.get("description")); 68 69 } 70 71 // g) 关闭IndexReader 72 73 reader.close(); 74 75 } catch (Exception e) { 76 77 // TODO Auto-generated catch block 78 79 e.printStackTrace(); 80 81 } 82 83 }
@Test public void searchDocumentByIndex(){ try { // 1、 创建查询(Query对象) // 创建分析器 Analyzer analyzer = new StandardAnalyzer(); QueryParser queryParser = new QueryParser("name", analyzer); Query query = queryParser.parse("name:java教程"); // 2、 执行搜索 // a) 指定索引库目录 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719")); // b) 创建IndexReader对象 IndexReader reader = DirectoryReader.open(directory); // c) 创建IndexSearcher对象 IndexSearcher searcher = new IndexSearcher(reader); // d) 通过IndexSearcher对象执行查询索引库,返回TopDocs对象 // 第一个参数:查询对象 // 第二个参数:最大的n条记录 TopDocs topDocs = searcher.search(query, 10); // e) 提取TopDocs对象中前n条记录 ScoreDoc[] scoreDocs = topDocs.scoreDocs; System.out.println("查询出文档个数为:" + topDocs.totalHits); for (ScoreDoc scoreDoc : scoreDocs) { // 文档对象ID int docId = scoreDoc.doc; Document doc = searcher.doc(docId); // f) 输出文档内容 System.out.println("==============================="); System.out.println("文档id:" + docId); System.out.println("图书id:" + doc.get("id")); System.out.println("图书name:" + doc.get("name")); System.out.println("图书price:" + doc.get("price")); System.out.println("图书pic:" + doc.get("pic")); System.out.println("图书description:" + doc.get("description")); } // g) 关闭IndexReader reader.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } |
测试结果,非常成功!!!
2.4 小结
Lucene全文检索,确实可以实现对关键词做分词、再执行搜索功能。并且结果更精确。
3 分词
3.1 重要性
分词是全文检索的核心。
所谓的分词,就是将一段文本,根据一定的规则,拆分成一个一个词。
Lucene是根据分析器实现分词的。针对不同的语言提供了不同的分析器。并且提供了一个通用的标准分析器StandardAnalyzer
3.2 分词过程
--说明:我们通过分析StandardAnalyzer核心源码来分析分词过程
1 @Override 2 3 protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) { 4 5 final StandardTokenizer src = new StandardTokenizer(getVersion(), reader); 6 7 src.setMaxTokenLength(maxTokenLength); 8 9 TokenStream tok = new StandardFilter(getVersion(), src); 10 11 tok = new LowerCaseFilter(getVersion(), tok); 12 13 tok = new StopFilter(getVersion(), tok, stopwords); 14 15 return new TokenStreamComponents(src, tok) { 16 17 @Override 18 19 protected void setReader(final Reader reader) throws IOException { 20 21 src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength); 22 23 super.setReader(reader); 24 25 } 26 27 }; 28 29 }
@Override protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) { final StandardTokenizer src = new src.setMaxTokenLength(maxTokenLength); TokenStream tok = new tok = new tok = new return new TokenStreamComponents(src, tok) { @Override protected void setReader(final Reader reader) throws IOException { src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength); super.setReader(reader); } }; } |
对应Lucene分词的过程,我们可以做如下总结:
(1)分词的时候,是以域为单位的。不同的域,相互独立。
同一个域中,拆分出来相同的词,视为同一个词(Term)
不同的域中,拆分出来相同的词,不是同一个词。由下图可以看出,在name域和desc域中都有java。
其中,Term是Lucene最小的语汇单元,不可再细分。
(2)分词的时候经历了一系列的过滤器。如大小写转换、去除停用词等。
3.3 分词后索引库结构
我们这里借助前面的示例来说明
从上图中,我们发现:
(1)索引库中有两个区域:索引区、文档区。
(2)文档区存放的是文档。Lucene给每一个文档自动加上一个文档编号docID。
(3)索引区存放的是索引。注意:
索引是以域为单位的,不同的域,彼此相互独立。
索引是根据分词规则创建出来的,根据索引就能找到对应的文档。
3.4 Luke客户端连接索引库
Luke作为Lucene工具包中的一个工具(http://www.getopt.org/luke/),可以通过可视化界面,连接操作索引库。
3.4.1 启动方法
(1)双击start.bat启动!
(2)连接索引库
3.4.2 验证分词效果
4 Field域
问题:我们已经知道,Lucene是在写入文档时,完成分词、索引的。那Lucene是怎么知道如何分词的呢?
答:Lucene是根据文档中的域的属性,来确定是否要分词、创建索引的。所以,我们必须搞清楚域有哪些属性。
4.1 域的属性
4.1.1 三大属性
4.1.1.1 是否分词(tokenized)
只有设置了分词属性为true,lucene才会对这个域进行分词处理。
在实际的开发中,有一些字段是不需要分词的,比如商品id,商品图片等。
而有一些字段是必须分词的,比如商品名称,描述信息等。
4.1.1.2 是否索引(indexed)
只有设置了索引属性为true,lucene才为这个域的Term词创建索引。
在实际的开发中,有一些字段是不需要创建索引的,比如商品的图片等。我们只需要对参与搜索的字段做索引处理。
4.1.1.3 是否存储(stored)
只有设置了存储属性为true,在查找的时候,才能从文档中获取这个域的值。
在实际开发中,有一些字段是不需要存储的。比如:商品的描述信息。
因为商品描述信息,通常都是大文本数据,读的时候会造成巨大的IO开销。而描述信息是不需要经常查询的字段,这样的话就白白浪费了cpu的资源了。
因此,像这种不需要经常查询,又是大文本的字段,通常不会存储到索引库。
4.1.2 特点
(1)三大属性彼此独立。
(2)通常分词是为了创建索引。
(3)不存储这个域文本内容,也可以对这个域先分词、创建索引。
4.2 Field常用类型
域的常用类型有很多,每一个类都有自己默认的三大属性。如下:
Field类 |
数据类型 |
Analyzed 是否分词 |
Indexed 是否索引 |
Stored 是否存储 |
StringField(FieldName, FieldValue,Store.YES)) |
字符串 |
N |
Y |
Y或N |
LongField(FieldName, FieldValue,Store.YES) |
Long型 |
Y |
Y |
Y或N |
FloatField(FieldName, FieldValue,Store.YES) |
Float型 |
Y |
Y |
Y或N |
StoredField(FieldName, FieldValue) |
重载方法,支持多种类型 |
N |
N |
Y |
TextField(FieldName, FieldValue, Store.NO) |
字符串 |
Y |
Y |
Y或N |
(不单单是这些,还有像DoubleField等等)
4.3 改造入门示例中的域类型
4.3.1 分析
(1)图书id:
是否分词:不用分词,因为不会根据商品id来搜索商品
是否索引:不索引,因为不需要根据图书ID进行搜索
是否存储:要存储,因为查询结果页面需要使用id这个值。
(2)图书名称:
是否分词:要分词,因为要将图书的名称内容分词索引,根据关键搜索图书名称抽取的词。
是否索引:要索引。
是否存储:要存储。
(3)图书价格:
是否分词:要分词,lucene对数字型的值只要有搜索需求的都要分词和索引,因为lucene对数字型的内容要特殊分词处理,本例子可能要根据价格范围搜索, 需要分词和索引。
是否索引:要索引
是否存储:要存储
(4)图书图片地址:
是否分词:不分词
是否索引:不索引
是否存储:要存储
(5)图书描述:
是否分词:要分词
是否索引:要索引
是否存储:因为图书描述内容量大,不在查询结果页面直接显示,不存储。
不存储是来不在lucene的索引文件中记录,节省lucene的索引文件空间,如果要在详情页面显示描述,思路:
从lucene中取出图书的id,根据图书的id查询关系数据库中book表得到描述信息。
4.3.2 代码修改
修改BookDao的getDocument方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public List<Document> getDocuments(List<Book> books){ 2 3 // Document对象集合 4 5 List<Document> docList = new ArrayList<Document>(); 6 7 // Document对象 8 9 Document doc = null; 10 11 for (Book book : books) { 12 13 // 创建Document对象,同时要创建field对象 14 15 doc = new Document(); 16 17 // 图书ID 18 19 // 参数:域名、域中存储的内容、是否存储 20 21 // 不分词、索引、要存储 22 23 // Field id = new TextField("id", book.getId().toString(),Store.YES); 24 25 Field id = new StoredField("id", book.getBookId().toString()); 26 27 // 图书名称 28 29 // 分词、索引、存储 30 31 Field name = new TextField("name", book.getName(),Store.YES); 32 33 // 图书价格 34 35 // 分词、索引、存储 36 37 Field price = new FloatField("price", book.getPrice(), Store.YES); 38 39 // 图书图片 40 41 // 不分词、不索引、要存储 42 43 Field pic = new StoredField("pic", book.getPic()); 44 45 // 图书描述 46 47 // 分词、索引、不存储 48 49 Field desc = new TextField("description",book.getDescription(), Store.NO); 50 51 // 把域(Field)添加到文档(Document)中 52 53 doc.add(id); 54 55 doc.add(name); 56 57 doc.add(price); 58 59 doc.add(pic); 60 61 doc.add(desc); 62 63 docList.add(doc); 64 65 } 66 67 return docList; 68 69 }
public List<Document> getDocuments(List<Book> books){ // Document对象集合 List<Document> docList = new ArrayList<Document>(); // Document对象 Document doc = null; for (Book book : books) { // 创建Document对象,同时要创建field对象 doc = new Document(); // 图书ID // 参数:域名、域中存储的内容、是否存储 // 不分词、索引、要存储 // Field id = new TextField("id", book.getId().toString(),Store.YES); Field id = new StoredField("id", book.getBookId().toString()); // 图书名称 // 分词、索引、存储 Field name = new TextField("name", book.getName(),Store.YES); // 图书价格 // 分词、索引、存储 Field price = new FloatField("price", book.getPrice(), Store.YES); // 图书图片 // 不分词、不索引、要存储 Field pic = new StoredField("pic", book.getPic()); // 图书描述 // 分词、索引、不存储 Field desc = new TextField("description",book.getDescription(), Store.NO); // 把域(Field)添加到文档(Document)中 doc.add(id); doc.add(name); doc.add(price); doc.add(pic); doc.add(desc); docList.add(doc); } return docList; } |
4.3.3 测试
(1)去索引库目录中,手动清空索引库。
(2)重新创建索引库。
(3)使用Luke验证分词、索引效果。
改造成功!!!
5 索引库维护
在第4节,我们需要重新创建索引的时候,是去索引库目录下,手动删除的。
而在实际的开发中,我们可能压根就不知道索引库在哪,就算知道,我们也不可能每次都去手动删除,非常之麻烦!!!
所以,我们必须学习如何维护索引库,使用程序来操作索引库。
需要注意的是,索引是与文档紧密相连的,因此对索引的维护,实际上就是对文档的增删改。
5.1 添加索引(文档)
5.1.1 需求
数据库中新上架了图书,必须把这些图书也添加到索引库中,不然就搜不到该新上架的图书了。
5.1.2 代码实现
调用 indexWriter.addDocument(doc)添加索引。
参考入门示例中的创建索引。
5.2 删除索引(文档)
5.2.1 需求
某些图书不再出版销售了,我们需要从索引库中移除该图书。
5.2.2 代码实现
1 @Test 2 3 public void deleteIndex() throws Exception { 4 5 // 1、指定索引库目录 6 7 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719")); 8 9 // 2、创建IndexWriterConfig 10 11 IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, 12 13 new StandardAnalyzer()); 14 15 // 3、 创建IndexWriter 16 17 IndexWriter writer = new IndexWriter(directory, cfg); 18 19 // 4、通过IndexWriter来删除索引 20 21 // 删除指定索引 22 23 writer.deleteDocuments(new Term("name", "apache")); 24 25 // 5、关闭IndexWriter 26 27 writer.close(); 28 29 System.out.println("删除成功"); 30 31 }
@Test public void deleteIndex() throws Exception { // 1、指定索引库目录 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719")); // 2、创建IndexWriterConfig IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, new StandardAnalyzer()); // 3、 创建IndexWriter IndexWriter writer = new IndexWriter(directory, cfg); // 4、通过IndexWriter来删除索引 // 删除指定索引 writer.deleteDocuments(new Term("name", "apache")); // 5、关闭IndexWriter writer.close(); System.out.println("删除成功"); } |
5.2.3 清空索引库
1 @Test 2 3 public void deleteIndex() throws Exception { 4 5 // 1、指定索引库目录 6 7 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719")); 8 9 // 2、创建IndexWriterConfig 10 11 IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, 12 13 new StandardAnalyzer()); 14 15 // 3、 创建IndexWriter 16 17 IndexWriter writer = new IndexWriter(directory, cfg); 18 19 // 4、通过IndexWriter来删除索引 20 21 // 删除指定索引 22 23 writer.deleteAll(); 24 25 // 5、关闭IndexWriter 26 27 writer.close(); 28 29 System.out.println("清空索引库成功"); 30 31 }
@Test public void deleteIndex() throws Exception { // 1、指定索引库目录 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719")); // 2、创建IndexWriterConfig IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, new StandardAnalyzer()); // 3、 创建IndexWriter IndexWriter writer = new IndexWriter(directory, cfg); // 4、通过IndexWriter来删除索引 // 删除指定索引 writer.deleteAll(); // 5、关闭IndexWriter writer.close(); System.out.println("清空索引库成功"); } |
5.3 更新索引(文档)
5.3.1 说明
Lucene更新索引比较特殊,是先删除满足条件的索引,再添加新的索引。
5.3.2 代码实现
1 // 修改索引
2
3 @Test
4
5 public void updateIndex() throws Exception {
6
7 // 1、指定索引库目录
8
9 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719"));
10
11 // 2、创建IndexWriterConfig
12
13 IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST,
14
15 new StandardAnalyzer());
16
17 // 3、 创建IndexWriter
18
19 IndexWriter writer = new IndexWriter(directory, cfg);
20
21 // 4、通过IndexWriter来修改索引
22
23 // a)、创建修改后的文档对象
24
25 Document document = new Document();
26
27 // 文件名称
28
29 Field filenameField = new StringField("name", "updateIndex", Store.YES);
30
31 document.add(filenameField);
32
33 // 修改指定索引为新的索引
34
35 writer.updateDocument(new Term("name", "apache"), document);
36
37 // 5、关闭IndexWriter
38
39 writer.close();
40
41 System.out.println("更新成功");
42
43 }
// 修改索引 @Test public void updateIndex() throws Exception { // 1、指定索引库目录 Directory directory = FSDirectory.open(new File("F:\\lucene\\0719")); // 2、创建IndexWriterConfig IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, new StandardAnalyzer()); // 3、 创建IndexWriter IndexWriter writer = new IndexWriter(directory, cfg); // 4、通过IndexWriter来修改索引 // a)、创建修改后的文档对象 Document document = new Document(); // 文件名称 Field filenameField = new StringField("name", "updateIndex", Store.YES); document.add(filenameField); // 修改指定索引为新的索引 writer.updateDocument(new Term("name", "apache"), document); // 5、关闭IndexWriter writer.close(); System.out.println("更新成功"); } |
6 搜索
问题:我们在入门示例中,已经知道Lucene是通过IndexSearcher对象,来执行搜索的。那我们为什么还要继续学习Lucene的查询呢?
答:因为在实际的开发中,我们的查询的业务是相对复杂的,比如我们在通过关键词查找的时候,往往进行价格、商品类别的过滤。
而Lucene提供了一套查询方案,供我们实现复杂的查询。
6.1 创建查询的两种方法
执行查询之前,必须创建一个查询Query查询对象。
Query自身是一个抽象类,不能实例化,必须通过其它的方式来实现初始化。
在这里,Lucene提供了两种初始化Query查询对象的方式。
6.1.1 使用Lucene提供Query子类
Query是一个抽象类,lucene提供了很多查询对象,比如TermQuery项精确查询,NumericRangeQuery数字范围查询等。
使用TermQuery实例化
Query query = new TermQuery(new Term("name", "lucene")); |
6.1.2 使用QueryParse解析查询表达式
QueryParser会将用户输入的查询表达式解析成Query对象实例。如下代码:
QueryParser queryParser = new QueryParser("name", new IKAnalyzer()); Query query = queryParser.parse("name:lucene"); |
6.2 常用的Query子类搜索
6.2.1 TermQuery
特点:查询的关键词不会再做分词处理,作为整体来搜索。代码如下:
1 /**
2
3 * Query子类查询之 TermQuery
4
5 *
6
7 * 特点:不会再对查询的关键词做分词处理。
8
9 *
10
11 * 需要:查询书名与java教程相关书。
12
13 */
14
15 @Test
16
17 public void queryByTermQuery(){
18
19 //1、获取一个查询对象
20
21 Query query = new TermQuery(new Term("name", "编程思想"));
22
23 doSearch(query);
24
25 }
26
27 private void doSearch(Query query) {
28
29 try {
30
31 //2、创建一个查询的执行对象
32
33 //指定索引库的目录
34
35 Directory d = FSDirectory.open(new File("F:\\lucene\\0719"));
36
37 //创建流对象
38
39 IndexReader reader = DirectoryReader.open(d);
40
41 //创建搜索执行对象
42
43 IndexSearcher searcher = new IndexSearcher(reader);
44
45 //3、执行搜索
46
47 TopDocs result = searcher.search(query, 10);
48
49 //4、提出结果集,获取图书的信息
50
51 int totalHits = result.totalHits;
52
53 System.out.println("共查询到"+totalHits+"条满足条件的数据!");
54
55 System.out.println("-----------------------------------------");
56
57 //提取图书信息。
58
59 //score即相关度。即搜索的关键词和 图书名称的相关度,用来做排序处理
60
61 ScoreDoc[] scoreDocs = result.scoreDocs;
62
63 for (ScoreDoc scoreDoc : scoreDocs) {
64
65 /**
66
67 * scoreDoc.doc的返回值,是文档的id, 即 将文档写入索引库的时候,lucene自动给这份文档做的一个编号。
68
69 *
70
71 * 获取到这个文档id之后,即可以根据这个id,找到这份文档。
72
73 */
74
75 int docId = scoreDoc.doc;
76
77 System.out.println("文档在索引库中的编号:"+docId);
78
79 //从文档中提取图书的信息
80
81 Document doc = searcher.doc(docId);
82
83 System.out.println("图书id:"+doc.get("id"));
84
85 System.out.println("图书name:"+doc.get("name"));
86
87 System.out.println("图书price:"+doc.get("price"));
88
89 System.out.println("图书pic:"+doc.get("pic"));
90
91 System.out.println("图书description:"+doc.get("description"));
92
93 System.out.println();
94
95 System.out.println("------------------------------------");
96
97 }
98
99 //关闭连接,释放资源
100
101 if(null!=reader){
102
103 reader.close();
104
105 }
106
107 } catch (Exception e) {
108
109 e.printStackTrace();
110
111 }
112
113 }
/** * Query子类查询之 TermQuery * * 特点:不会再对查询的关键词做分词处理。 * * 需要:查询书名与java教程相关书。 */ @Test public void queryByTermQuery(){ //1、获取一个查询对象 Query query = new TermQuery(new Term("name", "编程思想")); doSearch(query); } private void doSearch(Query query) { try { //2、创建一个查询的执行对象 //指定索引库的目录 Directory d = FSDirectory.open(new File("F:\\lucene\\0719")); //创建流对象 IndexReader reader = DirectoryReader.open(d); //创建搜索执行对象 IndexSearcher searcher = new IndexSearcher(reader); //3、执行搜索 TopDocs result = searcher.search(query, 10); //4、提出结果集,获取图书的信息 int totalHits = result.totalHits; System.out.println("共查询到"+totalHits+"条满足条件的数据!"); System.out.println("-----------------------------------------"); //提取图书信息。 //score即相关度。即搜索的关键词和 图书名称的相关度,用来做排序处理 ScoreDoc[] scoreDocs = result.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { /** * scoreDoc.doc的返回值,是文档的id, 即 将文档写入索引库的时候,lucene自动给这份文档做的一个编号。 * * 获取到这个文档id之后,即可以根据这个id,找到这份文档。 */ int docId = scoreDoc.doc; System.out.println("文档在索引库中的编号:"+docId); //从文档中提取图书的信息 Document doc = searcher.doc(docId); System.out.println("图书id:"+doc.get("id")); System.out.println("图书name:"+doc.get("name")); System.out.println("图书price:"+doc.get("price")); System.out.println("图书pic:"+doc.get("pic")); System.out.println("图书description:"+doc.get("description")); System.out.println(); System.out.println("------------------------------------"); } //关闭连接,释放资源 if(null!=reader){ reader.close(); } } catch (Exception e) { e.printStackTrace(); } } |
6.2.2 NumericRangeQuery
指定数字范围查询.(创建field类型时,注意与之对应)
1 /**
2
3 * Query子类查询 之 NumricRangeQuery
4
5 * 需求:查询所有价格在[60,80)之间的书
6
7 * @param query
8
9 */
10
11 @Test
12
13 public void queryByNumricRangeQuery(){
14
15 /**
16
17 * 第一个参数:要搜索的域
18
19 * 第二个参数:最小值
20
21 * 第三个参数:最大值
22
23 * 第四个参数:是否包含最小值
24
25 * 第五个参数:是否包含最大值
26
27 */
28
29 Query query = NumericRangeQuery.newFloatRange("price", 60.0f, 80.0f, true, false);
30
31 doSearch(query);
32
33 }
/** * Query子类查询 之 NumricRangeQuery * 需求:查询所有价格在[60,80)之间的书 * @param query */ @Test public void queryByNumricRangeQuery(){ /** * 第一个参数:要搜索的域 * 第二个参数:最小值 * 第三个参数:最大值 * 第四个参数:是否包含最小值 * 第五个参数:是否包含最大值 */ Query query = NumericRangeQuery.newFloatRange("price", 60.0f, 80.0f, true, false); doSearch(query); } |
6.2.3 BooleanQuery
BooleanQuery,布尔查询,实现组合条件查询。
1 /**
2
3 * Query子类查询 之 BooelanQuery查询 组合条件查询
4
5 *
6
7 * 需求:查询书名包含java,并且价格区间在[60,80)之间的书。
8
9 */
10
11 @Test
12
13 public void queryBooleanQuery(){
14
15 //1、要使用BooelanQuery查询,首先要把单个创建出来,然后再通过BooelanQuery组合
16
17 Query price = NumericRangeQuery.newFloatRange("price", 60.0f, 80.0f, true, false);
18
19 Query name = new TermQuery(new Term("name", "java"));
20
21 //2、创建BooleanQuery实例对象
22
23 BooleanQuery query = new BooleanQuery();
24
25 query.add(name, Occur.MUST_NOT);
26
27 query.add(price, Occur.MUST);
28
29 /**
30
31 * MSUT 表示必须满足 对应的是 +
32
33 * MSUT_NOT 必须不满足 应对的是 -
34
35 * SHOULD 可以满足也可以不满足 没有符号
36
37 *
38
39 * SHOULD 与MUST、MUST_NOT组合的时候,SHOULD就没有意义了。
40
41 */
42
43 doSearch(query);
44
45 }
/** * Query子类查询 之 BooelanQuery查询 组合条件查询 * * 需求:查询书名包含java,并且价格区间在[60,80)之间的书。 */ @Test public void queryBooleanQuery(){ //1、要使用BooelanQuery查询,首先要把单个创建出来,然后再通过BooelanQuery组合 Query price = NumericRangeQuery.newFloatRange("price", 60.0f, 80.0f, true, false); Query name = new TermQuery(new Term("name", "java")); //2、创建BooleanQuery实例对象 BooleanQuery query = new BooleanQuery(); query.add(name, Occur.MUST_NOT); query.add(price, Occur.MUST); /** * MSUT 表示必须满足 对应的是 + * MSUT_NOT 必须不满足 应对的是 - * SHOULD 可以满足也可以不满足 没有符号 * * SHOULD 与MUST、MUST_NOT组合的时候,SHOULD就没有意义了。 */ doSearch(query); } |
6.3 通过QueryParser搜索
6.3.1 特点
对搜索的关键词,做分词处理。
6.3.2 语法
6.3.2.1 基础语法
域名:关键字 实例:name:java |
6.3.2.2 组合条件语法
条件1 AND 条件2 条件1 OR 条件2 条件1 NOT 条件2 |
6.3.3 QueryParser
6.3.3.1 代码实现
1 /**
2
3 * 查询解析器查询 之 QueryParser查询
4
5 */
6
7 @Test
8
9 public void queryByQueryParser(){
10
11 try {
12
13 //1、加载分词器
14
15 Analyzer analyzer = new StandardAnalyzer();
16
17 /**
18
19 * 2、创建查询解析器实例对象
20
21 * 第一个参数:默认搜索的域。
22
23 * 如果在搜索的时候,没有特别指定搜索的域,则按照默认的域进行搜索
24
25 * 如何在搜索的时候指定搜索域呢?
26
27 * 答:格式 域名:关键词 即 name:java教程
28
29 *
30
31 * 第二个参数:分词器 ,对关键词做分词处理
32
33 */
34
35 QueryParser parser = new QueryParser("description", analyzer);
36
37 // 设置组合条件查询
38
39 // Query query = queryParser.parse("name:java教程 or description:apache");
40
41 // Query query = queryParser.parse("name:java教程 or apache");
42
43 Query query = parser.parse("name:java教程");
44
45 doSearch(query);
46
47 } catch (Exception e) {
48
49 e.printStackTrace();
50
51 }
52
53 }
/** * 查询解析器查询 之 QueryParser查询 */ @Test public void queryByQueryParser(){ try { //1、加载分词器 Analyzer analyzer = new StandardAnalyzer(); /** * 2、创建查询解析器实例对象 * 第一个参数:默认搜索的域。 * 如果在搜索的时候,没有特别指定搜索的域,则按照默认的域进行搜索 * 如何在搜索的时候指定搜索域呢? * 答:格式 域名:关键词 即 name:java教程 * * 第二个参数:分词器 ,对关键词做分词处理 */ QueryParser parser = new QueryParser("description", analyzer); // 设置组合条件查询 // Query query = queryParser.parse("name:java教程 or description:apache"); // Query query = queryParser.parse("name:java教程 or apache"); Query query = parser.parse("name:java教程"); doSearch(query); } catch (Exception e) { e.printStackTrace(); } } |
6.3.4 MultiFieldQueryParser
通过MulitFieldQueryParse对多个域查询。
1 /**
2
3 * 查询解析器查询 之 MultiFieldQueryParser查询
4
5 *
6
7 * 特点:同时指定多个搜索域,并且对关键做分词处理
8
9 */
10
11 @Test
12
13 public void queryByMultiFieldQueryParser(){
14
15 try {
16
17 //1、定义多个搜索的 name、description
18
19 String[] fields = {"name","description"};
20
21 //2、加载分词器
22
23 Analyzer analyzer = new StandardAnalyzer();
24
25 //3、创建 MultiFieldQueryParser实例对象
26
27 MultiFieldQueryParser mParser = new MultiFieldQueryParser(fields, analyzer);
28
29 Query query = mParser.parse("lucene教程");
30
31 doSearch(query);
32
33 } catch (Exception e) {
34
35 e.printStackTrace();
36
37 }
38
39 }
/** * 查询解析器查询 之 MultiFieldQueryParser查询 * * 特点:同时指定多个搜索域,并且对关键做分词处理 */ @Test public void queryByMultiFieldQueryParser(){ try { //1、定义多个搜索的 name、description String[] fields = {"name","description"}; //2、加载分词器 Analyzer analyzer = new StandardAnalyzer(); //3、创建 MultiFieldQueryParser实例对象 MultiFieldQueryParser mParser = new MultiFieldQueryParser(fields, analyzer); Query query = mParser.parse("lucene教程"); doSearch(query); } catch (Exception e) { e.printStackTrace(); } } |