1、Lucene介绍
1.1、什么是lucene
- Lucene是Apache的一个
全文检索引擎工具包
,通过lucene可以让程序员快速开发一个全文检索功能。- 引擎:核心组件
- 工具包:jar包、类库
1.2、全文检索的应用场景
1.2.1、搜索引擎
1.2.2、站内搜索(关注)
1.2.3、文件系统的搜索
1.2.4、总结
- Lucene和搜索引擎不是一回事!
- Lucene是一个工具包,它
不能独立运行
,不能单独对外提供服务
。 - 搜索引擎可以独立运行对外提供搜索服务。
1.3、全文检索的定义
- 全文检索首先对要搜索的文档进行
分词
,然后形成索引,通过查询索引来查询文档。这种先创建索引
,然后根据索引来进行搜索
的过程,就叫全文检索。 - 比如:字典
- 字典的偏旁部首页,就类似于luence的索引。
- 字典的具体内容,就类似于luence的文档内容。
2、Lucene实现全文检索的流程
详解如下:
全文检索的流程:创建索引流程、搜索索引流程 创建索引流程:采集数据 --> 文档处理存储到索引库中 搜索索引流程:输入查询条件 --> 通过lucene的查询器查询索引 --> 从索引库中取出结果 --> 视图渲染 注意:Lucene本身不能进行视图渲染。
3、Lucene入门程序
3.1、需求
- 使用Lucene实现电商项目中图书类商品的索引和搜索功能。
3.2、环境准备
- Jdk环境:1.7.0_80
- Ide环境:Eclipse Oxygen
- 数据库环境:mysql 5.X
- Lucene:4.10.3(从4.8版本以后,必须使用jdk1.7及以上)
3.2.1、数据库脚本初始化
内容如下:
代码语言:javascript复制drop table if exists book;
create table book
(
id int(11) not null,
name varchar(192),
privce double,
pic varchar(96),
description text,
primary key (id)
);
3.2.2、Lucene下载安装
如下所示:
Lucene是开发全文检索功能的工具包,使用时从官方网站下载,并解压。 官方网站:http://lucene.apache.org/ 目前最新版本:7.5.0 下载地址:http://archive.apache.org/dist/lucene/java/ 下载版本:4.10.3(学习使用版本) JDK要求:1.7以上(从版本4.8开始,不支持1.7以下)
要学习的3个文件夹
3.3、工程搭建(两步)
3.3.1、第一步:创建普通的java工程
3.3.2、第二步:添加jar包
如下所示:
入门程序只需要添加以下jar包: 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 junit包(非必须):junit-4.9.jar
3.4、创建索引的流程
- 对文档索引的过程,就是将用户要搜索的文档内容进行索引,然后把索引存储在索引库(index)中。
3.4.1、为什么要采集数据
详解如下:
为什么要采集数据? 全文检索要搜索的数据信息格式多种多样,拿搜索引擎(百度、google)来说,通过搜索引擎网站能搜索互联网站上的网页(html)、互联网上的音乐(mp3)、视频(avi)、pdf电子书等。对于这种格式不同的数据,需要先将他们采集到本地,然后统一封装到
lucene的文档对象
中,也就是说需要将存储的内容进行统一后才能对它进行查询。 全文检索搜索的这些数据称为非结构化数据。 什么是非结构化数据? 结构化数据:指具有固定格式或有限长度的数据,如数据库、元数据等。 非结构化数据:指不定长或无固定格式的数据,如邮件、word文档等。 如何对结构化数据进行搜索? 由于结构化数据是固定格式,所以就可以针对固定格式的数据设计算法来搜索,比如数据库like查询,like查询采用顺序扫描法,使用关键字匹配内容,对于内容量大的like查询速度慢。 如何对非结构化数据进行搜索? 需要将所有要搜索的非结构化数据通过技术手段采集到一个固定的地方,将这些非结构化的数据想办法组成结构化的数据,再以一定的算法去搜索。
3.4.2、如何采集数据
详解如下:
采集数据技术有哪些? 1、对于互联网上网页采用http将网页抓取到本地生成html文件。(网页采集:使用爬虫工具(http工具)将网页爬取到本地) 2、
如果数据在数据库中就连接数据库读取表中的数据。(数据库采集:使用jdbc程序进行数据采集)
3、如果数据是文件系统中的某个文件,就通过文件系统读取文件的内容。(文件系统采集:使用io流采集)
(1)网页采集(了解) 详解如下:
因为目前
搜索引擎
主要搜索数据的来源是互联网,搜索引擎使用一种爬虫程序
抓取网页(通过http抓取html网页信息),以下是一些爬虫
项目: Solr(http://lucene.apache.org/solr),solr 是apache的一个子项目,支持从关系数据库、xml文档中
提取原始数据。 Nutch(http://lucene.apache.org/nutch),Nutch 是apache的一个子项目,包括大规模爬虫工具
,能够抓取和分辨web网站数据。 jsoup(http://jsoup.org/),jsoup 是一款java的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API
,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。 heritrix(http://sourceforge.net/projects/archive-crawler/files/),Heritrix 是一个由java开发的、开源的网络爬虫,用户可以使用它来从网上抓取想要的资源。其最出色之处在于它良好的可扩展性
,方便用户实现自己的抓取逻辑。
(2)数据库采集(掌握)
- 针对电商站内搜索功能,全文检索的数据源在数据库中,需要通过jdbc访问数据库中book表的内容。
po类:Book.java
代码语言:javascript复制package com.itheima.lucene.po;
public class Book {
// 图书ID
private Integer id;
// 图书名称
private String name;
// 图书价格
private Float price;
// 图书图片
private String pic;
// 图书描述
private String description;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Float getPrice() {
return price;
}
public void setPrice(Float price) {
this.price = price;
}
public String getPic() {
return pic;
}
public void setPic(String pic) {
this.pic = pic;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
dao:BookDao.java
代码语言:javascript复制public interface BookDao {
// 图书查询
public List<Book> queryBookList();
}
dao:BookDaoImpl.java
代码语言:javascript复制public class BookDaoImpl implements BookDao {
@Override
public List<Book> queryBookList() {
// 数据库连接
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/solr", "root", "root");
// SQL语句
String sql = "SELECT * FROM book";
// 创建preparedStatement
preparedStatement = connection.prepareStatement(sql);
// 获取结果集
resultSet = preparedStatement.executeQuery();
// 结果集解析
while (resultSet.next()) {
Book book = new Book();
book.setId(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();
}
return list;
}
可以进行单元测试一下。这里省略啦!
3.4.3、索引文件的逻辑结构
详解如下:
文档域 文档域存储的信息就是采集到的信息,通过Document对象来存储,具体说是通过Document对象中Field域来存储数据。对于非结构化的数据统一格式为Document文档格式,
一个文档有多个Field域
,不同的文档其Field的个数可以不同,建议相同类型的文档包括相同的Field
。 比如:数据库中一条记录会存储一个一个Document对象,数据库中一列会存储成Document中一个field域。 文档域中,Document对象之间是没有关系的。而且每个Document中的field域也不一定一样。 本例子一个Document对应一 条 Book表的记录。 索引域 用于搜索
,搜索程序将从索引域中搜索一个一个词,根据词找到对应的文档
。 将Document中的Field的内容进行分词,将分好的词创建索引,索引=Field域名:词
。 索引域主要是为了搜索使用的。索引域内容是经过lucene分词之后存储的
。 倒排索引表 传统方法是先找到文件,如何在文件中找内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法
,数据量大就搜索慢。 倒排索引结构是根据内容(词语)找文档,倒排索引结构
也叫反向索引结构
,包括索引和文档两部分,索引即词汇表
,它是在索引中匹配搜索关键字
,由于索引内容量有限并且采用固定优化算法
搜索速度很快,找到了索引中的词汇,词汇与文档关联
,从而最终找到了文档。
3.4.4、创建索引
创建索引流程图解:
详解如下:
IndexWriter:是索引过程的核心组件,通过IndexWriter可以
创建新索引
、更新索引
、删除索引
操作。IndexWriter需要通过Directory
对索引进行存储操作。 Directory:描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。它是一个抽象类,它的子类常用的包括FSDirectory
(在文件系统存储索引)、RAMDirectory
(在内存存储索引)。
(1)创建Document
采集数据的目的是为了索引,在索引前需要将原始内容创建成
文档(Document)
,文档(Document)中包括一个一个的域(Field)
。
(2)分词
在对Docuemnt中的内容创建索引之前需要使用分词器进行分词,主要过程就是分词、过滤两步。 分词:就是将采集到的文档内容切分成一个一个的词,具体应该说是将Document中Field的value值切分成一个一个的词。 过滤:包括去除标点符号、去除停用词(的、是、a、an、the等)、大写转小写、词的形还原(复数形式转成单数形参、过去式转成现在式)等。 什么是停用词?停用词是为节省存储空间和提高搜索效率,搜索引擎在索引页面或处理搜索请求时会自动忽略某些字或词,这些字或词即被称为Stop Words(停用词)。比如语气助词、副词、介词、连接词等,通常自身并无明确的意义,只有将其放入一个完整的句子中才有一定作用,如常见的“的”、“在”、“是”、“啊”等。 示例: 要分词的内容:Lucene is a Java full-text search engine. 分词: Lucene is a Java full - text search engine . 过滤: 去掉标点符号 Lucene is a Java full text search engine 去掉停用词 Lucene Java full text search engine 大写转小写 lucene java full text search engine
Lucene作为了一个工具包提供不同国家的分词器,如下图所示:
注意由于语言不同分析器的切分规则也不同,本例子使用StandardAnalyzer,它可以对用英文进行分词。 如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源码:
代码语言:javascript复制@Override
protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) {
final StandardTokenizer src = new StandardTokenizer(getVersion(), reader);
src.setMaxTokenLength(maxTokenLength);
TokenStream tok = new StandardFilter(getVersion(), src);
tok = new LowerCaseFilter(getVersion(), tok);
tok = new StopFilter(getVersion(), tok, stopwords);
return new TokenStreamComponents(src, tok) {
@Override
protected void setReader(final Reader reader) throws IOException {
src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);
super.setReader(reader);
}
};
}
详解如下:
Tokenizer是分词器
,负责将reader转换为语汇单元即进行分词,Lucene提供了很多的分词器,也可以使用第三方的分词,比如:IKAnalyzer是一个中文分词器
。tokenFilter是分词过滤器
,负责对语汇单元进行过滤,tokenFilter可以是一个过滤器链儿
,Lucene提供了很多的分词器过滤器,比如:大小写转换、去除停用词等。
如下图是语汇单元的生成过程:
从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词器,经过三个TokenFilter生成语汇单元Token
。
同一个域中相同的语汇单元(Token)对应同一个Term(词)
,Term(词)记录了语汇单元的内容及所在域的域名等,还包括了该token出现的频率及位置。- 不同的域中拆分出来的相同的单词对应不同的term。
- 相同的域中拆分出来的相同的单词对应相同的term。
- 例如:图书信息里面,图书名称中的java和图书描述中的java对应不同的term。
代码实现如下:
代码语言:javascript复制/**
* 将采集到的数据list集合封装到Document对象中,创建索引库库
* @author Bruce
*
*/
public class IndexManager {
/**
* 创建索引
* @throws Exception
*/
@Test
public void createIndex() throws Exception {
// 1、采集数据
BookDao dao = new BookDaoImpl();
List<Book> list = dao.queryBookList();
// 2、将采集到的数据list封装到Document对象中
// 先创建Document对象集合
List<Document> docList = new ArrayList<>();
Document document = null; // 开发时建议这么做,因为这样每次地址指向是同一片内存,省内存
for (Book book : list) {
// 创建Document对象,同时要创建field对象
document = new Document();
Field id = new TextField("id", book.getId().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 description = new TextField("description", book.getDescription(), Store.YES);
// 把域(Field)添加到文档(Document)中
document.add(id);
document.add(name);
document.add(price);
document.add(pic);
document.add(description);
docList.add(document);
}
// 3、创建分词器对象:标准分词器
Analyzer analyzer = new StandardAnalyzer();
// 4、创建索引写对象:IndexWriter
// 指定索引库的地址
File indexFile = new File("E:\index\hm19");
// 创建索引目录流对象:Directory
Directory directory = FSDirectory.open(indexFile);
IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
IndexWriter indexWriter = new IndexWriter(directory, cfg);
// 5、通过索引写对象:IndexWriter,将Document写入到索引库中
for (Document doc : docList) {
indexWriter.addDocument(doc);
}
// 6、关闭索引写对象:IndexWriter
indexWriter.close();
}
}
3.4.5、使用Luke查看索引
Luke作为Lucene工具包中的一个工具(http://www.getopt.org/luke/),可以
通过界面
来进行索引文件的查询、修改。
打开Luke方法: 命令运行:cmd运行:java -jar lukeall-4.10.3.jar 手动执行:双击lukeall-4.10.3.jar Luke的界面:
连接索引库成功的界面:
3.5、搜索索引的流程
搜索流程图:
详解如下:
1、Query 用户定义查询语句,用户确定查询什么内容(输入什么关键字) 指定查询语法,相当于sql语句。 2、
IndexSearcher 索引搜索对象,定义了很多搜索方法,程序员调用此方法搜索
。 3、IndexReader 索引读取对象,它对应的索引维护对象IndexWriter,IndexSearcher通过IndexReader读取索引目录中的索引文件。 4、Directory 索引流对象,IndexReader需要Directory读取索引库,使用FSDirectory文件系统流对象。 5、IndexSearcher 搜索完成,返回一个TopDocs(匹配度高的前边的一些记录
)。
3.5.1、输入查询语句
详解如下:
同数据库的sql一样,lucene全文检索也有固定的语法: 最基本的有比如:AND, OR, NOT 等 举个例子,用户想找一个description中包括java关键字和spring关键字的文档。 它对应的查询语句:
description:java AND spring
如下是使用Luke搜索的例子:
3.5.2、搜索分词
详解如下:
和索引过程的分词一样,这里要对用户输入的关键字进行分词,一般情况索引和搜索使用的分词器一致。 比如:输入搜索关键字“java培训”,分词后为java和培训两个词,与java和培训有关的内容都搜索出来了,如下:
3.5.3、搜索索引
详解如下:
根据关键字从索引中找到对应的索引信息,即词term。term与document相关联,找到了term就找到了关联的document,从document取出Field中的信息即是要搜索的信息。
代码实现:
代码语言:javascript复制/**
* 搜索索引
* @author Bruce
*
*/
public class IndexSearch {
@Test
public void searchIndex() throws Exception {
// 1、 创建查询对象(Query对象)
// 使用QueryParser搜索时,需要指定分词器,搜索索引时使用的分词器要和创建索引时使用的分词器一致
// 创建分析器对象
Analyzer analyzer = new StandardAnalyzer();
QueryParser queryParser = new QueryParser("description", analyzer); // 第一个参数:是默认搜索的域的名称
// 通过QueryParser来创建Query对象
Query query = queryParser.parse("description:java AND spring"); // 参数:输入的是lucene的查询语句(注意:关键字一定要大写)
// 2、创建IndexSearcher(索引搜索对象)
File indexFile = new File("E:\index\hm19\");
Directory directory = FSDirectory.open(indexFile);
IndexReader indexReader = DirectoryReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader );
// 3、通过IndexSearcher(索引搜索对象)来搜索索引库
TopDocs topDocs = indexSearcher.search(query, 10); // 第二个参数:指定需要显示的顶部记录的N条
int totalHits = topDocs.totalHits; // 根据查询条件匹配出的记录总数
System.out.println("匹配出的记录总数:" totalHits);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;// 根据查询条件匹配出的记录
for (ScoreDoc scoreDoc : scoreDocs) {
int docId = scoreDoc.doc; // 获取文档的ID
Document document = indexSearcher.doc(docId); // 通过ID获取文档
System.out.println("商品ID:" document.get("id"));
System.out.println("商品名称:" document.get("name"));
System.out.println("商品价格:" document.get("price"));
System.out.println("商品图片地址:" document.get("pic"));
System.out.println("商品描述:" document.get("description"));
}
// 关闭IndexReader
indexReader.close();
}
}
4、Field域
4.1、Field的属性
Field是文档中的域,包括
Field名
和Field值
两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。 是否进行分词(tokenized) 是:作分词处理,即将Field值进行分词,分词的目的是为了索引
。 比如:商品名称、商品价格、商品简介等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后后作为语汇单元索引
。 否:不作分词处理。不分词,不代表不索引,而是将整个内容进行索引。 比如:商品id、订单号、身份证号等 。 是否进行索引(indexed) 是:进行索引。将Field分词后的词或整个Field值进行索引,索引的目的是为了搜索
。 比如:商品名称、商品价格、商品简介分词后进行索引,商品id、订单号、身份证号不用分词但也要索引
,这些将来整个内容都要作为查询条件。 否:不索引。该域的内容无法搜索到。 比如:文件路径、图片路径等,不用作为查询条件的不用索引。 是否进行存储(stored) 是:将Field值存储在文档中,存储在文档中的Field才可以从Document中获取。存储的目的是为了搜索页面显示取值用的
。 比如:商品名称、商品价格、订单号、商品图片地址,凡是将来要从Document中获取的Field都要存储
。 否:不存储Field值,不存储的Field无法通过Document获取。 比如:商品简介,由于商品描述在搜索页面中不需要显示,且商品描述的内容较大不用存储。如果需要商品描述,则根据搜索出的商品ID去数据库中查询,然后显示出商品描述信息即可。
4.2、Field的常用类型
下边列出了开发中常用的Filed类型,注意Field的属性,根据需求选择:
4.3、修改入门程序Field的代码
详解如下:
代码语言:javascript复制图书id:
是否分词:不分词。
是否索引:要索引,因为需要根据图书id进行搜索。
是否存储:要存储,因为查询结果页面需要使用id这个值。
图书名称:
是否分词:要分词,因为要将图书的名称内容分词索引,根据关键搜索图书名称抽取的词。
是否索引:要索引。
是否存储:要存储。
图书价格:
是否分词:要分词,`lucene对数字型的值只要有搜索需求的都要分词和索引`,因为`lucene对数字型的内容要特殊分词处`理,本例子可能要根据价格范围搜索,需要分词和索引。
是否索引:要索引。
是否存储:要存储。
图书图片地址:
是否分词:不分词。
是否索引:不索引。
是否存储:要存储。
图书描述:
是否分词:要分词。
是否索引:要索引。
是否存储:不存储,因为图书描述内容量大,不在查询结果页面直接显示。
`不存储是用来不在lucene的索引文件中记录`,`节省lucene的索引文件空间`,如果要在详情页面显示描述:
思路:从lucene中取出图书的id,根据图书的id查询关系数据库中book表得到描述信息。
代码如下图所示:
我们需要重新生成索引库,重新执行生成索引库代码,注意:执行之前我们先删除掉原先的索引库
。
5、索引的维护
5.1、需求
- 管理人员通过电商系统更改图书信息,这时更新的是数据库,如果使用lucene搜索图书信息需要在数据库表book信息变化时及时更新lucene索引库。
5.2、添加索引
调用 indexWriter.addDocument(doc); 添加索引。 参考入门程序的创建索引。
5.3、删除索引
- 注意:增删改操作,都是需要通过IndexWriter对象来操作的。
5.3.1、删除指定索引
Term是索引域中最小的单位。根据条件删除时,建议根据唯一键来进行删除。在solr中就是根据ID来进行删除和修改操作的。 根据Term项删除索引,满足条件的将全部删除。 示例代码如下:
代码语言:javascript复制 /**
* 删除指定索引
* @throws Exception
*/
@Test
public void deleteIndex() throws Exception {
// 4、创建索引写对象:IndexWriter
// 指定索引库的地址
File indexFile = new File("E:\index\hm19");
// 创建索引目录流对象:Directory
Directory directory = FSDirectory.open(indexFile);
IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, new StandardAnalyzer());
IndexWriter indexWriter = new IndexWriter(directory, cfg);
// 通过IndexWriter来删除指定索引
indexWriter.deleteDocuments(new Term("id", "1"));
// 关闭索引写对象:IndexWriter
indexWriter.close();
}
5.3.2、删除全部索引(慎用)
将索引目录的索引信息全部删除,直接彻底删除,无法恢复。慎用!!! 示例代码如下:
代码语言:javascript复制 /**
* 删除全部索引(慎用)
* @throws Exception
*/
@Test
public void deleteAllIndex() throws Exception {
// 4、创建索引写对象:IndexWriter
// 指定索引库的地址
File indexFile = new File("E:\index\hm19");
// 创建索引目录流对象:Directory
Directory directory = FSDirectory.open(indexFile);
IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, new StandardAnalyzer());
IndexWriter indexWriter = new IndexWriter(directory, cfg);
// 通过IndexWriter来删除全部索引(慎用)
indexWriter.deleteAll();;
// 关闭索引写对象:IndexWriter
indexWriter.close();
}
建议参照关系数据库基于主键删除方式,所以在创建索引时需要创建一个主键Field,删除时根据此主键Field删除。 索引删除后将放在Lucene的回收站中,Lucene3.X版本可以恢复删除的文档,3.X之后无法恢复。
5.4、修改索引
更新索引是根据查询条件,可以查询出结果,则将以前的删掉,然后覆盖新的Document对象,如果没有查询出结果,则新增一个Document 修改流程即:先查询,再删除,再添加。 示例代码如下:
代码语言:javascript复制 /**
* 修改索引
* @throws Exception
*/
@Test
public void updateIndex() throws Exception {
// 4、创建索引写对象:IndexWriter
// 指定索引库的地址
File indexFile = new File("E:\index\hm19");
// 创建索引目录流对象:Directory
Directory directory = FSDirectory.open(indexFile);
IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, new StandardAnalyzer());
IndexWriter indexWriter = new IndexWriter(directory, cfg);
// 创建修改后的文档对象
Document document = new Document();
Field name = new TextField("name", "黑泽", Store.YES); // 文件名称
document.add(name);
// 通过IndexWriter来修改索引
// 第一个参数:指定的查询条件
// 第二个参数:修改之后的对象
// 修改时如果根据查询条件,可以查询出结果,则将以前的删掉,然后覆盖新的Document对象,如果没有查询出结果,则新增一个Document
// 修改流程即:先查询,再删除,再添加
indexWriter.updateDocument(new Term("name", "晓艺"), document);
// 关闭索引写对象:IndexWriter
indexWriter.close();
}
6、搜索
6.1、创建查询对象的两种方法
对要搜索的信息创建Query查询对象,Lucene会根据Query查询对象生成最终的查询语法。类似关系数据库Sql语法一样,Lucene也有自己的查询语法,比如:"name:lucene" 表示查询Field域的name值为"lucene"的文档信息。
可通过两种方法创建查询对象:
代码语言:javascript复制1)使用Lucene提供的Query子类,不能输入lucene的查询语法,不需要指定分词器
Query是一个抽象类,lucene提供了很多查询对象,比如:TermQuery精确词项查询、NumericRangeQuery数字范围查询、BooleanQuery布尔查询(实现组合查询)等。
如下代码:
Query query = new TermQuery(new Term("name", "lucene"));
2)使用QueryParse解析查询表达式(常用)、MultiFieldQueryParser多域查询,可以输入lucene的查询语法、需要指定分词器
QueryParser会将用户输入的查询表达式解析成Query对象实例。
如下代码:
QueryParser queryParser = new QueryParser("name", new IKAnalyzer());
Query query = queryParser.parse("name:lucene");
6.2、通过Query的子类创建查询对象
6.2.1、TermQuery
- TermQuery项查询,TermQuery不需要使用分析器,搜索关键词作为整体来匹配Field域中的词进行查询,比如:订单号、分类ID号等。
示例代码如下:
代码语言:javascript复制/**
* 搜索索引
* @author Bruce
*
*/
public class IndexSearch {
/**
* 优化代码,抽取成通用搜索方法
* @param query
*/
private void doSearch(Query query) {
IndexReader indexReader = null; // 好的编程习惯
try {
// 2、创建IndexSearcher(索引搜索对象)
File indexFile = new File("E:\index\hm19\");
Directory directory = FSDirectory.open(indexFile);
indexReader = DirectoryReader.open(directory);
IndexSearcher indexSearcher = new IndexSearcher(indexReader );
// 3、通过IndexSearcher(索引搜索对象)来搜索索引库
TopDocs topDocs = indexSearcher.search(query, 10); // 第二个参数:指定需要显示的顶部记录的N条
int totalHits = topDocs.totalHits; // 根据查询条件匹配出的记录总数
System.out.println("匹配出的记录总数:" totalHits);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;// 根据查询条件匹配出的记录
Document document; // 好的编程习惯
for (ScoreDoc scoreDoc : scoreDocs) {
int docId = scoreDoc.doc; // 获取文档的ID
document = indexSearcher.doc(docId); // 通过ID获取文档
System.out.println("商品ID:" document.get("id"));
System.out.println("商品名称:" document.get("name"));
System.out.println("商品价格:" document.get("price"));
System.out.println("商品图片地址:" document.get("pic"));
System.out.println("商品描述:" document.get("description"));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (indexReader != null) { // 好的编程习惯
try { // 好的编程习惯
// 关闭IndexReader
indexReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 使用QueryParse解析查询表达式(常用),可以输入lucene的查询语法、需要指定分词器
* @throws Exception
*/
@Test
public void searchIndex() throws Exception {
// 1、 创建查询对象(Query对象)
// 使用QueryParser搜索时,需要指定分词器,搜索索引时使用的分词器要和创建索引时使用的分词器一致
// 创建分析器对象
Analyzer analyzer = new StandardAnalyzer();
QueryParser queryParser = new QueryParser("description", analyzer); // 第一个参数:是默认搜索的域的名称
// 通过QueryParser来创建Query对象
Query query = queryParser.parse("description:java AND spring"); // 参数:输入的是lucene的查询语句(注意:关键字一定要大写)
// 这里优化代码,抽取成通用搜索方法了
// 执行搜索
doSearch(query);
}
/**
* TermQuery 精确项查询,TermQuery 不需要指定分析器
*/
@Test
public void testTermQuery() {
// 1、 创建查询对象(Query对象)
Query query = new TermQuery(new Term("description", "java"));
// 2、执行搜索
doSearch(query);
}
}
控制台输出结果如下:
通过Luck测试的结果如下:
二者得到的结果一样!
6.2.2、NumericRangeQuery
- NumericRangeQuery,指定数字范围查询。
示例代码如下:
代码语言:javascript复制 /**
* NumericRangeQuery 指定数字范围查询,NumericRangeQuery不使用指定分析器
*/
@Test
public void testNumericRangeQuery() {
// 1、 创建查询对象(NumericRangeQuery对象)
// 第一个参数:域名
// 第二个参数:最小值
// 第三个参数:最大值
// 第四个参数:是否包含最小值
// 第五个参数:是否包含最大值
Query query = NumericRangeQuery.newFloatRange("price",50f, 70f, true, true);
// 2、执行搜索
doSearch(query);
}
6.2.3、BooleanQuery
- BooleanQuery,布尔查询,实现组合条件查询。
示例代码如下:
代码语言:javascript复制 /**
* BooleanQuery 布尔查询,实现组合条件查询。BooleanQuery不使用指定分析器
*/
@Test
public void testBooleanQuery() {
// 1、 创建查询对象(BooleanQuery对象)
BooleanQuery query = new BooleanQuery();
Query query1 = new TermQuery(new Term("description", "java"));
Query query2 = NumericRangeQuery.newFloatRange("price", 50f, 70f, true, true);
// MUST:查询条件必须满足,相当于AND
// SHOULD:查询条件可选,相当于OR
// MUST_NOT:查询条件不能满足,相当于NOT非
// 组合关系代表的意思如下:
// 1、MUST和MUST 表示“与”的关系,即“交集”。
// 2、MUST和MUST_NOT 前者包含后者不包含。
// 3、MUST_NOT和MUST_NOT 没意义。
// 4、SHOULD和MUST 表示MUST,SHOULD失去意义。
// 5、SHOUlD和MUST_NOT 相当于MUST与MUST_NOT。
// 6、SHOULD和SHOULD 表示“或”的关系,即“并集”。
query.add(query1, Occur.MUST);
query.add(query2, Occur.SHOULD);
System.out.println(query);
// 2、执行搜索
doSearch(query);
}
组合关系代表的意思如下: 1、MUST和MUST 表示“与”的关系,即“交集”。 2、MUST和MUST_NOT 前者包含后者不包含。 3、MUST_NOT和MUST_NOT 没意义。 4、SHOULD和MUST 表示MUST,SHOULD失去意义。 5、SHOUlD和MUST_NOT 相当于MUST与MUST_NOT。 6、SHOULD和SHOULD 表示“或”的关系,即“并集”。
6.3、通过QueryParser创建查询对象
6.3.1、QueryParser
- 通过QueryParser也可以创建Query,QueryParser提供一个Parse()方法,此方法可以直接根据查询语法来查询。
- Query对象执行的查询语法可以通过 System.out.println(query); 来查看。
- 通过QueryParser来创建query对象,需要指定分词器,
注意:搜索索引时使用的分词器要和创建索引时使用的分词器一致
。还可以输入查询语句。
参考我们入门程序代码即可。 示例代码如下:
代码语言:javascript复制 /**
* 使用QueryParse解析查询表达式(常用),可以输入lucene的查询语法、需要指定分词器
* @throws Exception
*/
@Test
public void searchIndex() throws Exception {
// 1、 创建查询对象(Query对象)
// 使用QueryParser搜索时,需要指定分词器,搜索索引时使用的分词器要和创建索引时使用的分词器一致
// 创建分析器对象
Analyzer analyzer = new StandardAnalyzer();
QueryParser queryParser = new QueryParser("description", analyzer); // 第一个参数:是默认搜索的域的名称
// 通过QueryParser来创建Query对象
Query query = queryParser.parse("description:java AND spring"); // 参数:输入的是lucene的查询语句(注意:关键字一定要大写)
// 这里优化代码,抽取成通用搜索方法了
// 执行搜索
doSearch(query);
}
6.3.2、MultiFieldQueryParser
- 通过MuliFieldQueryParse对多个域查询。
示例代码如下:
代码语言:javascript复制 /**
* 使用MultiFieldQueryParser 多域查询,解析查询表达式,可以输入lucene的查询语法、需要指定分词器
* @throws Exception
*/
@Test
public void testMultiFieldQueryParser() throws Exception {
// 1、 创建查询对象(MultiFieldQueryParser对象)
// 可以指定默认搜索的域是多个
String[] fields = {"name", "description"}; // 或的关系:两个条件满足其一即可。
// 创建分析器对象
Analyzer analyzer = new StandardAnalyzer();
// 创建一个MulitFiledQueryParser对象
QueryParser queryParser = new MultiFieldQueryParser(fields, analyzer);
Query query = queryParser.parse("java");
System.out.println(query);
// 执行搜索
doSearch(query);
}
6.3.3、查询语法
详解如下:
代码语言:javascript复制1、基础的查询语法,关键词查询:
域名 ":" 搜索的关键字
例如:description:java
2、范围查询
域名 ":" [最小值 TO 最大值]
例如:price:[1 TO 1000]
`注意`:QueryParser不支持对数字范围的搜索,它支持字符串范围。
数字范围搜索建议使用NumericRangeQuery。
3、组合条件查询
第一种写法:
Occur.MUST 查询条件必须满足,相当于and (加号)
Occur.SHOULD 查询条件可选,相当于or 空(不用符号)
Occur.MUST_NOT 查询条件不能满足,相当于not非 -(减号)
1) 条件1 条件2:两个条件之间是并且的关系and
例如: name:java description:java
2) 条件1 条件2:必须满足第一个条件,忽略第二个条件
例如: name:java description:java
3)条件1 条件2:两个条件满足其一即可。
例如:name:java description:java
4)-条件1 条件2:必须不满足条件1,要满足条件2
例如:-name:java description:java
第二种写法:
条件1 AND 条件2
条件1 OR 条件2
条件1 NOT 条件2
6.4、TopDocs
Lucene搜索结果可通过TopDocs遍历,TopDocs类提供了少量的属性,如下:
注意: Search方法需要指定匹配记录数量n:indexSearcher.search(query, n); topDocs.totalHits; // 表示匹配索引库中所有记录的数量。 topDocs.scoreDocs; // 表示匹配相关度高的前边记录数组,scoreDocs的长度小于等于search方法指定的参数n。
7、相关度排序
7.1、什么是相关度排序
相关度排序是查询结果按照与查询关键字的相关性进行排序
,越相关的越靠前。比如:搜索“java”关键字,与该关键字最相关的文章应该排在前边。Lucene是通过打分
来进行相关度排序的。
7.2、相关度打分
详解如下:
Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。如何打分呢? Lucene是在
用户进行检索时实时根据搜索的关键字计算出来的
,分两步: 1)计算出词(Term)的权重。 2)根据词的权重值,计算文档相关度得分。 什么是词的权重? 通过索引部分的学习,明确索引的最小单位是一个Term(索引词典中的一个词),搜索也是要从Term中搜索,再根据Term找到文档,Term对文档的重要性称为权重
,影响Term权重有两个因素:Term Frequency (tf):
指此Term在同一文档中出现了多少次,即词在同一个文档中出现的频率。tf 越大说明越重要。 词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,比如:“java”这个词,在文档中出现的次数很多,说明该文档主要就是讲java技术的。Document Frequency (df):
指有多少文档包含次Term,即词在多个文档中出现的频率。df 越大说明越不重要。 比如:在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term),说明此词(Term)太普通,不足以区分这些文档,因而重要性越低。 以上是自然打分的规则。
7.3、设置boost值影响相关度排序
boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。 在创建索引时对
某个文档
中的field设置加权值高,在搜索时匹配到这个文档就可能排在前边。 在搜索索引时对某个域
进行加权,在进行组合域查询时,匹配到加权值高的域最后计算的相关度得分就高。 即:设置加权值可以在创建索引时设置,也可以在查询索引时设置。 设置boost是给域(field)或者Document设置的。
7.3.1、在创建索引时设置boost值
如果希望某些文档更重要,当此文档中包含所要查询的词则应该得分较高,这样相关度排序可以排在前边,可以在创建索引时设定文档中某些域(Field)的boost值来实现,如果不进行设定,则Field Boost默认为1.0f。一旦设定,除非删除此文档,否则无法改变。
代码实现:
7.3.2、在搜索索引时设置boost值
在MultiFieldQueryParser创建时设置boost值。
代码实现:
8、中文分词器
8.1、什么是中文分词器
学过英文的都知道,
英文是以单词为单位的
,单词与单词之间以空格或者逗号句号隔开。而中文则以字为单位
,字又组成词,字和词再组成句子。所以对于英文,我们可以简单以空格判断某个字符串是否为一个单词,比如:I love China,love和China很容易被程序区分开来;但中文“我爱中国”就不一样了,电脑不知道“中国”是一个词语还是“爱中”是一个词语。把中文的句子切分成有意义的词,就是中文分词
,也称切词
。我爱中国,分词的结果是:我 爱 中国。
8.2、Lucene自带的中文分词器
StandardAnalyzer:
单字分词
:就是按照中文一个字一个字地进行分词。 比如:“我爱中国” 效果:“我”、“爱”、“中”、“国” CJKAnalyzer二分法分词
:按两个字进行切分。 比如:“我是中国人” 效果:“我是”、“是中”、“中国”、“国人” 上边两个分词器无法满足需求。
8.3、第三方中文分词器
paoding:庖丁解牛最新版在 https://code.google.com/p/paoding/ 中最多支持Lucene 3.0,且最新提交的代码在 2008-06-03,在svn中最新也是2010年提交,已经过时,不予考虑。 mmseg4j:最新版已从 https://code.google.com/p/mmseg4j/ 移至 https://github.com/chenlb/mmseg4j-solr,支持Lucene 4.10,且在github中最新提交代码是2014年6月,从09年~14年一共有:18个版本,也就是一年几乎有3个大小版本,有较大的活跃度,用了
mmseg算法
。 IK-analyzer:最新版在https://code.google.com/p/ik-analyzer/上,支持Lucene 4.10,从2006年12月推出1.0版开始,IKAnalyzer已经推出了4个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词
和文法分析算法
的中文分词组件。从3.0版本开始,IK发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。在2012版本中,IK实现了简单的分词歧义排除算法
,标志着IK分词器从单纯的词典分词向模拟语义分词衍化。 但是也就是2012年12月后没有在更新。 ansj_seg:最新版本在 https://github.com/NLPchina/ansj_seg tags/,仅有1.1版本,从2012年到2014年更新了大小6次,但是作者本人在2014年10月10日说明:“可能我以后没有精力来维护ansj_seg了”,现在由”nlp_china”管理。2014年11月有更新。并未说明是否支持Lucene,是一个由CRF(条件随机场)算法所做的分词算法。 imdict-chinese-analyzer:最新版在 https://code.google.com/p/imdict-chinese-analyzer/,最新更新也在2009年5月,下载源码,不支持Lucene 4.10 。是利用HMM(隐马尔科夫链)算法
。 cseg:最新版本在git.oschina.net/lionsoul/jcseg,支持Lucene 4.10,作者有较高的活跃度。利用mmseg算法
。
8.4、使用中文分词器IKAnalyzer
IKAnalyzer继承Lucene的Analyzer抽象类,使用IKAnalyzer和Lucene自带的分词器方法一样,将Analyzer测试代码改为IKAnalyzer测试中文分词效果。 如果使用中文分词器ik-analyzer,就在
创建索引
和搜索程序
中使用一致的分词器ik-analyzer
。
8.4.1、添加jar包至项目中
8.4.2、修改分词器代码
修改创建索引时的分词器代码:
修改搜索索引时的分词器代码:
8.5、扩展中文词库
将以下文件拷贝到config目录下:
从ikanalyzer包中拷贝配置文件到classpath下。
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!-- 用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">dicdata/mydict.dic</entry>
<!-- 用户可以在这里配置自己的扩展停用词字典 -->
<entry key="ext_stopwords">dicdata/ext_stopword.dic</entry>
</properties>
如果想配置扩展词和停用词,就创建扩展词的文件和停用词的文件,文件的编码要是utf-8。
注意:不要用记事本保存扩展词文件和停用词文件,那样的话,格式中是含有bom的。
8.6、使用luke测试中文分词效果
第一步:将ikanalyzer的jar包,拷贝到luke工具的目录
第二步:使用命令行方式打开luke工具 运行lukeall,如果需要加载第三方分词器,需通过-Djava.ext.dirs加载jar包, 可简单的将第三方分词器和lukeall放在一块儿,cmd下运行: 命令:java -Djava.ext.dirs=. -jar lukeall-4.10.3.jar 之后指定第三方分词器的类路径