目录
Lucene
Lucene介绍
全文检索引擎工具包,实现从海量数据中快速查询数据。
方案对比
-
传统方案
-
优点:使用简单,简单的SQL语句即可。
-
缺点:1、数据量大时查询性能差(不会通过索引查询);2、查询的结果不够全面。
-
-
全文检索方案
-
缺点:使用相对复杂,通过提供的API进行操作。
-
优点:1、数据量大时查询性能好;2、查询的结果全面
-
全文检索是什么?
-
索引流程:通过索引程序切分数据的内容成一个个词语,建立词语与每一条数据的对应关系
-
检索流程:通过检索程序,根据搜索关键词,在索引库查找目标内容
Lucene是什么?
Lucene是apache软件基金会下的一个子项目。 是一个免费、开放源代码的全文检索引擎工具包。
Lucene官方网站
官方网站:http://lucene.apache.org/
全文检索流程介绍
索引和检索流程图
索引流程详细介绍
- 采集数据
-
通过JDBC操作获取到关系数据库中的业务数据
-
通过IO流获取文件上的数据
-
通过爬虫(蜘蛛)程序获取网络上的网页数据
-
- 创建文档对象
-
说明:文档对象(Document),一个文档对象包含有多个域(Field)。一个文档对象就相当于关系数据库表中的一条记录,一个域就相当于一个字段。
-
-
分析文档对象
-
把原始数据,转换成文档对象后,使用分词器把文档中域的内容切分成一个个词语,为后续建立索引做准备。
-
-
建立倒排索引
-
建立词语和文档的对应关系,词语在什么文档出现,出现了几次,在什么位置出现(倒排索引),并且保存到索引库。
-
倒排索引
样例:
-
文档0(编号0): we like java java java
-
文档1(编号1): we like lucene lucene lucene
(Term 词条) | (Doc 文档,Freq 频率) | (Pos 位置) |
---|---|---|
we | (0,1) (1,1) | (0)(0) |
like | (0,1) (1,1) | (1)(1) |
java | (0,3) | (2,3,4) |
lucene | (1,3) | (2,3,4) |
存储格式:词条/词语在哪个文档出现(文档编号)、出现的频率(次数)、出现的位置(下标)
数据库&索引库结构对比
数据库 | 表 | 行 | 列 |
---|---|---|---|
Databases实例 | Tables | Rows | column |
索引库的存储目录 | 无 | Document文档 | Field域 |
分词器
分词器介绍
-
在对文档(Document)中的内容进行存储之前,需要使用分词器对域(Field)中的内容进行分词,目的是为了建立倒排索引,过程是先分词,再过滤。
-
分词:将Document中Field域的值根据分词器内部的词典切分成一个一个的单词,具体的切分方式根据使用的分词器而不同。
-
过滤:分词完毕后,通过过滤器排除一些标点符号以及一些停用词,也会将词条的大写转换小写。
-
停用词说明:停用词是指为了节省存储空间和提高搜索效率,在对field内容或搜索关键字分词时会自动排除的字或词,这些字或词被称为“stop words”。如语气助词、副词、介词、连接词等等(的、在、是、啊)
-
注:索引流程和检索流程使用分词器要一致
IK分词器使用
IK分词器本身就是对Lucene提供的分词器Analyzer扩展实现,使用方式与Lucene的分词器一致
扩展中文词库
- 在企业项目中,有一些词语根据业务需要不需要分词,需要作为一个整体,比如:蓝瘦香菇
特性与种类
Field的特性
Document(文档)是Field(域)的承载体,一个Document是由多个Field组成。Field由名称和值两部分组成,Field的值是要索引的内容,也是要搜索的内容。
Field的种类
Field种类 | 数据类型 | 是否 分词 | 是否 索引 | 是否 存储 | 说明 |
---|---|---|---|---|---|
StringField(FieldName, FieldValue,Store.YES) | 字符串 | N | Y | Y或N | 字符串类型Field,不分词,作为一个整体进行索引(比如:身份证号,订单编号),是否需要存储根据Store.YES或Store.NO决定 |
LongField(FieldName, FieldValue,Store.YES)、 DoubleField(FieldName,FieldValue,Store.YES)... | 数值型代表 | Y | Y | Y或N | 数值型Field代表,分词并且索引(比如:价格),是否需要存储根据Store.YES或Store.NO决定 |
StoredField(FieldName, FieldValue) | 重载方法,支持多种类型 | N | N | Y | 不分词,不索引,默认存储(比如:商品图片路径) |
TextField(FieldName, FieldValue,Store.NO) | 文本类型 | Y | Y | Y或N | 文本类型Field,分词并且索引,是否需要存储根据Store.YES或Store.NO决定 |
Lucene基本使用
环境搭建
pom依赖
<properties>
<mysql.version>5.1.46</mysql.version>
<lucene.version>4.10.3</lucene.version>
</properties>
<dependencies>
<!--mysql连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--ik分词器-->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
<!--查询解析器-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
</dependencies>
实体类
@Data
public class Book {
// 图书id
private int id;
// 图书名称
private String bookName;
// 图书价格
private float bookPrice;
// 图书图片
private String bookPic;
// 图书描述
private String bookDesc;
}
DAO实现类
public class BookDaoImpl implements BookDao {
//查询所有数据
@Override
public List<Book> findAll() {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql:///test", "root", "root");
preparedStatement = connection.prepareStatement("select * from book");
resultSet = preparedStatement.executeQuery();
List<Book> bookList = new ArrayList<>();
while (resultSet.next()) {
Book book = new Book();
book.setId(resultSet.getInt("id"));
book.setBookName(resultSet.getString("bookname"));
book.setBookPrice(resultSet.getFloat("price"));
book.setBookPic(resultSet.getString("pic"));
book.setBookDesc(resultSet.getString("bookdesc"));
bookList.add(book);
}
return bookList;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (connection != null) {
connection.close();
}
if (preparedStatement != null) {
preparedStatement.close();
}
if (resultSet != null) {
resultSet.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
索引流程实现
public class IndexTest {
/**
* 索引流程
*/
@Test
public void createIndex() throws Exception {
BookDao bookDao = new BookDaoImpl();
List<Book> list = bookDao.findAll();
// 1 创建分词器(Analyzer),用于分词
Analyzer analyzer = new IKAnalyzer();
// 2.创建索引配置对象(IndexWriterConfig),配置索引库
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LUCENE_47, analyzer);
/**
* 2.1 设置一个打开模式
* CREATE-每次都覆盖之前的索引库,创建新的索引库
* APPEND-追加,如果存在索引库,直接追加数据,否则报错
* CREATE_OR_APPEND-创建或追加,如果存在就追加,没有就创建
*/
indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
// 3.创建索引目录对象(Directory),指定索引库的位置
Directory directory = FSDirectory.open(new File("e:/luceneIndex"));
// 4.创建索引写入对象(IndexWriter)
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
for (Book book : list) {
//创建文档对象
Document document = new Document();
/**
* TextField参数:
* 参数一:域的名称
* 参数二:域的值
* 参数三:指定是否把域值存储到文档对象中,最终保存在磁盘上
*/
document.add(new StringField("id", book.getId()+"", Field.Store.YES));
document.add(new TextField("bookName", book.getBookName(), Field.Store.YES));
document.add(new DoubleField("bookPrice", book.getBookPrice(), Field.Store.YES));
document.add(new StoredField("bookPic", book.getBookPic()));
document.add(new TextField("bookDesc", book.getBookDesc(), Field.Store.NO));
//把文档对象写入索引库
indexWriter.addDocument(document);
}
// 5.提交事务
indexWriter.commit();
// 6.关闭资源
indexWriter.close();
}
}
运行结果:
luke查看索引数据
检索流程实现
/**
* 检索流程
*/
@Test
public void searchIndex() throws Exception {
// 1.创建分词器对象(Analyzer),用于对关键字分词
Analyzer analyzer = new IKAnalyzer();
// 2.创建查询对象(Query)
// 2.1 创建查询解析器
QueryParser queryParser = new QueryParser("", analyzer);
Query query = queryParser.parse("bookName:java");
// 3.创建索引目录对象(Directory),指定索引库的位置
FSDirectory directory = FSDirectory.open(new File("e:/luceneIndex"));
// 4.创建索引读取对象(IndexReader),把索引数据读取到内存中
IndexReader indexReader = DirectoryReader.open(directory);
// 5.创建索引查询对象(IndexSearcher),执行搜索
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
// 6.执行搜索,返回结果集(TopDocs)
TopDocs topDocs = indexSearcher.search(query, 10);
System.out.println("总命中数:" + topDocs.totalHits);
// 7.处理结果集
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
//scoreDoc.doc:索引
Document doc = indexSearcher.doc(scoreDoc.doc);
System.out.println("图书id = " + doc.get("id"));
System.out.println("图书名称 = " + doc.get("bookName"));
System.out.println("图书价格 = " + doc.get("bookPrice"));
System.out.println("图书图片 = " + doc.get("bookPic"));
System.out.println("图书描述 = " + doc.get("bookDesc"));
System.out.println("===========================================");
}
// 8.释放资源
indexReader.close();
}
运行结果:
高级搜索
公共搜索方法:
//公共搜索方法
public void search(Query query) throws Exception{
//1.创建索引目录对象
Directory directory = FSDirectory.open(new File("e:/luceneIndex"));
//2.创建索引读取对象
DirectoryReader indexReader = DirectoryReader.open(directory);
//3.创建索引查询对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//4.执行搜索
TopDocs topDocs = indexSearcher.search(query, 5);
// 6.处理返回结果
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
System.out.println("文档分数 = " + scoreDoc.score + ",文档id = " + scoreDoc.doc);
Document doc = indexSearcher.doc(scoreDoc.doc);
System.out.println("图书id = " + doc.get("id"));
System.out.println("图书名称 = " + doc.get("bookName"));
System.out.println("图书价格 = " + doc.get("bookPrice"));
System.out.println("图书图片 = " + doc.get("bookPic"));
System.out.println("图书描述 = " + doc.get("bookDesc"));
System.out.println("=========================================");
}
// 7.关闭资源
indexReader.close();
}
词条搜索
匹配的结果更加精确,不会对关键字进行分词
//查询图书名称域中包含有java的图书
@Test
public void queryByTerm() throws Exception {
Query query = new TermQuery(new Term("bookName","java"));
search(query);
}
范围查询
//搜索图书价格大于80,小于100的图书
@Test
public void queryByRange() throws Exception {
Query query = NumericRangeQuery.newDoubleRange("bookPrice", 80d, 100d,
false, false);
search(query);
}
组合查询
//组合查询:查询图书名称中包含有lucene,并且80<=图书价格<=100之间的图书
@Test
public void queryByBool() throws Exception {
BooleanQuery boolQuery = new BooleanQuery();
Query termQuery = new TermQuery(new Term("bookName","lucene"));
Query rangeQuery = NumericRangeQuery.newDoubleRange("bookPrice", 80d, 100d,
true, true);
/**
* 布尔查询
* MUST:必须满足,相当于and
* MUST_NOT:不能满足,相当于not
* SHOULD:表示或,相当于or
*/
boolQuery.add(termQuery, BooleanClause.Occur.MUST);
boolQuery.add(rangeQuery, BooleanClause.Occur.MUST);
search(boolQuery);
}
条件表达式
使用逻辑运算符表达式(必须大写)
- AND:与
- OR:或
- NOT:非
//查询图书名称中包含有java,并且包含lucene的图书
@Test
public void queryByExpress() throws Exception {
QueryParser queryParser = new QueryParser("", new IKAnalyzer());
Query query = queryParser.parse("bookName:java AND bookName:lucene");
search(query);
}
分页和排序
//查询图书名称包含java或lucene的图书,根据价格降序排序,每页显示2条,查询第2页的数据
@Test
public void queryByPage() throws Exception{
Analyzer analyzer = new IKAnalyzer();
QueryParser queryParser = new QueryParser("", analyzer);
Query query = queryParser.parse("bookName:java OR bookName:lucene");
/**
* SoftField字段排序对象
* 参数一:排序字段
* 参数二:字段类型
* 参数三:是否降序排序
*/
SortField sortField = new SortField("bookPrice", SortField.Type.DOUBLE, false);
Sort sort = new Sort(sortField);
//1.创建索引目录对象
Directory directory = FSDirectory.open(new File("e:/luceneIndex"));
//2.创建索引读取对象
DirectoryReader indexReader = DirectoryReader.open(directory);
//3.创建索引查询对象
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
//4.执行搜索
int pageNum = 2;
int pageSize = 2;
TopDocs topDocs = indexSearcher.search(query, pageNum * pageSize, sort);
//5.处理返回结果
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (int i=(pageNum - 1)*pageSize; i<pageNum*pageSize; i++) {
ScoreDoc scoreDoc = scoreDocs[i];
System.out.println("文档分数 = " + scoreDoc.score + ",文档id = " + scoreDoc.doc);
Document doc = indexSearcher.doc(scoreDoc.doc);
System.out.println("图书id = " + doc.get("id"));
System.out.println("图书名称 = " + doc.get("bookName"));
System.out.println("图书价格 = " + doc.get("bookPrice"));
System.out.println("图书图片 = " + doc.get("bookPic"));
System.out.println("图书描述 = " + doc.get("bookDesc"));
System.out.println("=========================================");
}
//6.关闭资源
indexReader.close();
}