最近在网上找视频学习了Lucene的相关知识,根据视频内容以及其他资料简单总结下Lucene的相关知识点。
Lucene是apache软件基金会的一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。
一、全文检索的定义
全文检索首先对要搜索的文档进行分词,然后形成索引,通过查询索引来查询文档。通俗点理解就是先创建索引,然后根据索引来搜索的全过程叫做全文检索。
生活中的字典就是一个很好的全文检索的例子:字典的偏旁部首页,就类似于Lucene的索引。而字典的具体内容就类似于Lucene的文档内容。
二、Lucene全文检索的流程
从图中可以看出来,全文检索主要分为两大块:索引流程和搜索流程。
索引流程:采集数据-->文档处理-->存储到索引库中
搜索流程:输入查询条件-->通过lucene的查询器查询索引-->从索引库中取出结果-->视图渲染
在这里对几个概念的词汇进行说明:
1、采集数据:全文检索搜索的内容的格式有多种,比如:视频、mp3、图片、文档,需要先将它们采集到本地,然后统一封装到lucene的文档对象中,也就是说需要将存储的内容进行统一才能对它进行查询。
采集数据的方式:对于互联网中的数据,使用爬虫工具(http工具)将网页爬取到本地;
对于数据库中的数据,使用jdbc程序进行数据采集;
对于文件系统的数据,使用io流采集。
2、索引文件的逻辑结构
文档域存储的信息就是采集到的信息,通过Document对象来存储,具体说是通过Document对象中field域来存储数据。
索引域主要是为了搜索使用的。索引域内容是经过lucene分词之后存储的。
3、分词
Lucene中分词主要分为两个步骤:分词和过滤。
分词:将field域中的内容一个个的分开;
过滤:将分好的词进行过滤,比如去掉标点符号、大写转小写、词的型还原(复数转单数、过去式转成现在式)、停用词过滤等;
停用词:单独应用没有特殊意义的词。比如的、啊、等,英文中的this is a the等;
中文分词需要借助第三方插件:中文分词器:IK-analyzer。
三、入门案例
1、需求说明
使用Lucene完成对数据库中图书信息的索引和搜索功能。
2、环境搭建
JDK:1.7以上版本 、Lucene:4.10、数据库:mysql
3、Lucene工具包下载
官方网站:http://lucene.apache.org/
4、数据库脚本初始化(省略)
5、工程搭建
5.1 导入jar包
MySql驱动包、Analysis包、Core包、QueryParser包、JUnit(测试用)。
5.2 编写索引流程代码
package com.zju.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;
}
}
5.2.2 采集数据
package com.zju.lucene.dao;
import java.util.List;
import com.zju.lucene.po.Book;
public interface BookDao {
//创建查询书的接口
public List<Book> queryBooks();
}
package com.zju.lucene.dao.impl;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import com.zju.lucene.dao.BookDao;
import com.zju.lucene.po.Book;
public class BookDaoImpl implements BookDao {
@Override
public List<Book> queryBooks() {
//数据库连接
Connection con = null;
//预编译statement
PreparedStatement pst = null;
//结果集
ResultSet rs = null;
//创建图书列表
List<Book> list = new ArrayList<Book>();
try{
//加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
//连接数据库
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/lucene", "root" , "123456");
//编写SQL语句
String sql = "SELECT * FROM book";
//创建preparedStatement
pst = con.prepareStatement(sql);
//获取结果集
rs = pst.executeQuery();
//结果解析
while(rs.next()){
Book book = new Book();
book.setId(rs.getInt("id"));
book.setName(rs.getString("name"));
book.setPrice(rs.getFloat("price"));
book.setPic(rs.getString("pic"));
book.setDescription(rs.getString("description"));
list.add(book);
}
}catch(Exception e){
e.printStackTrace();
}
return list;
}
}
5.2.3 创建索引
创建索引的流程:文档(Document)-->分词器(Analyzer)-->索引写对象(IndexWriter)-->索引目录流对象(Directory)-->索引库
IndexWriter是索引过程的核心组件,通过IndexWriter可以创建新索引、更新索引、删除索引操作。IndexWriter需要通过Directory对索引进行存储操作。
package com.zju.lucene.practice;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.FloatField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.Test;
import com.zju.lucene.dao.BookDao;
import com.zju.lucene.dao.impl.BookDaoImpl;
import com.zju.lucene.po.Book;
public class IndexManager {
@Test
public void createIndex() throws IOException{
//采集数据
BookDao bookDao = new BookDaoImpl();
List<Book> bookList = bookDao.queryBooks();
//将采集到的数据封装到Document对象中
List<Document> docList = new ArrayList<>();
Document document;
for(Book book : bookList){
document = new Document();
//store.YES:存储到文档域中
/*
* Field域的相关说明:
* Field域的三个属性:
* 1、是否分词(Tokenized):分词的目的就是为了索引;
* 2、是否索引(Indexed):索引的目的是为了搜索;
* 3、是否存储(Stored):存储的目的是为了搜索页面显示取值用的。
*
* Field常用类型:
* StringField(FieldName,FieldValue,Store.YES):不分词、索引、存储/不存储;
* LongField(FieldName,FieldValue,Store.YES):分词、索引、存储/不存储;
* StoredField(FieldName,FieldValue):不分词、不索引、存储;
* TextField(FieldName,FieldValue,Store.NO):分词、索引、不存储;
* TextField(FieldName,reader):分词、索引、存储;
*/
//图书ID
Field id = new StringField("id", book.getId().toString(), Store.YES);
//图书名称
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 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);
}
//创建分词器
Analyzer analyzer = new StandardAnalyzer();
//创建IndexWriter
IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
//指定索引库的地址
File indexFile = new File("D:\\lucene\\indexManager\\");
/*
* Directory描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。
* 它是一个抽象类,它的子类常用的包括FSDirectory(在文件系统存储索引)、RAMDirectory(在内存存储索引)。
* FSDirectory比较常用。
*/
Directory dir = FSDirectory.open(indexFile);
IndexWriter writer = new IndexWriter(dir, cfg);
//通过IndexWriter对象将Document写入到索引库中
for(Document doc : docList){
writer.addDocument(doc);
}
//关闭writer
writer.close();
}
}
5.3 编写搜索流程代码
搜索流程示意图:
package com.zju.lucene.practice;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.junit.Test;
public class IndexSearch {
private void doSearch(Query query) {
// 创建IndexSearcher
// 指定索引库的地址
try {
File indexFile = new File("D:\\lucene\\indexManager\\");
Directory directory = FSDirectory.open(indexFile);
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
// 通过searcher来搜索索引库
// 第二个参数:指定需要显示的顶部记录的N条
TopDocs topDocs = searcher.search(query, 10);
// 根据查询条件匹配出的记录总数
int count = topDocs.totalHits;
System.out.println("匹配出的记录总数:" + count);
// 根据查询条件匹配出的记录
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
// 获取文档的ID
int docId = scoreDoc.doc;
// 通过ID获取文档
Document doc = searcher.doc(docId);
System.out.println("商品ID:" + doc.get("id"));
System.out.println("商品名称:" + doc.get("name"));
System.out.println("商品价格:" + doc.get("price"));
System.out.println("商品图片地址:" + doc.get("pic"));
}
// 关闭资源
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void indexSearch() throws Exception {
// 创建query对象
// 使用QueryParser搜索时,需要指定分词器,搜索时的分词器要和索引时的分词器一致
// 第一个参数:默认搜索的域的名称
QueryParser parser = new QueryParser("description",
new StandardAnalyzer());
// 通过queryparser来创建query对象
// 参数:输入的lucene的查询语句(关键字一定要大写)
Query query = parser.parse("description:java AND lucene");
doSearch(query);
}
@Test
public void termQuery() {
// 创建TermQuery对象
Query query = new TermQuery(new Term("description", "java"));
doSearch(query);
}
@Test
public void numericRangeQuery() {
// 创建NumericRangeQuery对象
// 参数:域的名称、最小值、最大值、是否包含最小值、是否包含最大值
Query query = NumericRangeQuery.newFloatRange("price", 55f, 60f, true, false);
doSearch(query);
}
@Test
public void booleanQuery() {
// 创建BooleanQuery
BooleanQuery query = new BooleanQuery();
// 创建TermQuery对象
Query q1 = new TermQuery(new Term("description", "lucene"));
// 创建NumericRangeQuery对象
// 参数:域的名称、最小值、最大值、是否包含最小值、是否包含最大值
Query q2 = NumericRangeQuery.newFloatRange("price", 55f, 60f, true, false);
// 组合关系代表的意思如下:
// 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(q1, Occur.MUST_NOT);
query.add(q2, Occur.MUST_NOT);
doSearch(query);
}
@Test
public void multiFieldQueryParser() throws Exception {
// 默认搜索的多个域的域名
String[] fields = { "name", "description" };
Analyzer analyzer = new StandardAnalyzer();
Map<String, Float> boosts = new HashMap<String, Float>();
boosts.put("name", 200f);
MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, analyzer, boosts);
// Query query = parser.parse("name:lucene OR description:lucene");
Query query = parser.parse("java");
System.out.println(query);
doSearch(query);
}
}
补充:使用Luke工具查看索引.......未完待续