Hibernate Search是Hibernate的子项目,把数据库全文检索能力引入到项目中,并通过"透明"(不影响既有系统)的配置,提供一套标准的全文检索接口。这一章我们就来学习这块内容。
全文检索的概念
在进入正文之前,有必要介绍一下全文检索的概念。简单来说,Google就是一个全文检索引擎。全文检索允许用户输入一些关键字,从数据层中查找到所需要的信息。此外全文检索和数据库"LIKE"语句相比,没有数据库开销或是数据库的开销非常小,因为检索过程全部从通过检索文件完成,因此效率非常高。此外,全文检索引擎可以提供的还远不止"LIKE"语句这么多。在全文检索领域,用户输入的搜索信息叫做关键字,而全文检索系统把海量信息按照这些关键字进行结构化处理,把文章打散成段落、文字,最后,按关键字对文章的数据进行分类。这个处理后的数据文本叫做检索文件,检索文件往往比实际数据小得多,但它的数据所包含的信息量损失却非常小。当用户输入一个关键字时,全文检索引擎可以很快地定位到相关文本。
什么是Lucene
Lucene是一个开源的全文检索引擎,目前已经成为了Apache基金会赞助项目。Lucene是Java社区非常流行的全文检索引擎,功能强大。它不仅可以检索一般的数据文本,还可以检索PDF、HTML及微软的Word文件等。此外,Lucene成功的原因之一是它开放的框架,几乎框架的每一部分都可以扩展。它的文本分析器可以定制,检索文件存储方式可以定制,查询引擎也有不同的可选方案,如果愿意,还可以自已定制。此外,它提供一套非常强大的API接口,使客户用起来很方便。此外,Lucene除支持非结构化检索\footnote{用户输入一个关键字,全文检索引擎去匹配任何字段包含该关键字的数据条目。}外,还支持结构化检索(用户可以指定具体搜索的model类、字段名以及搜索条件)。这章的重点不是Lucene,但做为Hibernate Search的核心,您有必要对它的基本概念有所了解。下面介绍一些Lucene中的基本概念:
- Document:在Lucene中,一个Document即一个搜索单元。举例来说:如果对一个用户表做检索,那么每条用户信息就是一个Document。
- Field:每一个Document都包含一或多个Field,每一个Field都是key-value数据对。
- Analyzer:分析器/断字器。这是全文检索引擎的心脏,如何将一篇文章打散成一些关键字,并能够不丢失信息量,这是一门单独的学科。Lucene提供多种Analyzer,并提供开放的接口让社区的专家提供新的Analyzer。
- Index:系统生成的检索信息,这里面存储了Document。
- IndexSearcher:IndexSearcher负责检索Index内容负责给出检索结果。
- IndexWriter:IndexWriter负责调用Analyzer,分析后生成Index。
Lucene、Hibernate Search及Hibernate的联系
如果在本项目中直接使用Lucene,将不得不面临一些问题。因为本项目是基于数据库的,因此,当数据库中的数据发生变化时,就必须手工触发Lucene,让它随之更新检索文件中的内容,使之与数据库中的实际数据保持一致。这也就意味着dao中的每一个函数都要插入一段Lucene的代码,这样做有违OCP原则,这一层面应被提取到单独的逻辑层。此外model类别如何映射到全文检索引擎中,这也是一个问题,必须要手工处理这种映射关系,这样使用Lucene的代价就大大增加了。为了解决这些使用上的问题,Hibernate Search应运而生。
那么,Lucene、Hibernate Search及Hibernate三者之间是什么样的关系呢?请见下图:
如图所示,Hibernate+Hibernate Search位于全文检索数据目录及实际数据库中间。一方面,Hibernate处理与数据库相关的事宜,另一方面Hibernate Search会根据数据库中实际数据的情况,自动触发更新全文检索数据目录。此外Hibernate Search自动完成model层数据类对Lucene检索文件结构的映射。理论总是很枯躁,接下来依然拿报名系统来展示具体使用方法。
安装Hibernate Search
如果需要在项目中使用Hibernate Search功能,请在Maven的pom.xml配置文件中添加下述dependency:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search</artifactId> <version>3.0.0.GA</version> </dependency>
报名系统全文检索功能说明
假设现在针对报名系统有一个业务需要:希望可以使用全文检索的方式查找报名者的姓名。从技术角度上来说,希望能够通过Registration的username检索到Registration数据。方案之一是使用数据库的LIKE语句。但是LIKE语句需要在数据库中进行查询,并且开销比较大,虽然有些数据库本身具有全文检索引擎(如PostgreSQL),但是使用某个数据库本身的特定功能,将造成系统的可扩展性降低。因此决定采用Hibernate Search全文检索引擎制作这一项目。整体设计如图\ref{fig:reg-search-design}所示。
设计依然严格遵守MVC设计模式。在Model层,系统提供全文检索数据,并在DAO模块中提供全文接索的接口功能。在View层,系统一方面接收用户的关键字搜索输入,另一方面,把系统得到的搜索结果返回给用户。Controller层把Model层与View层连接在一起,实现整个功能。我们在一下节,从model模块讲起,通过Hibernate Search框架来实现这一功能。
重温model
为了使系统支持全文检索,首先需要做的是在Registration中加一些Annotation。我们来看看如何使用Hibernate Search的标记达到这一目的:
package model;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Store;
@Entity
@Indexed
public class Registration {
@Id
@DocumentId
@GeneratedValue(generator = "hibernate-uuid.hex")
@GenericGenerator(name = "hibernate-uuid.hex", strategy = "uuid.hex")
private String id;
@Field(index = Index.TOKENIZED, store = Store.NO)
private String username;
private Date createdAt;
private Date updatedAt;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "clazz_id", nullable = false)
private Clazz clazz;
}
可以看到Registration中多了一些Hibernate Search的标记,下面一一进行讲解:
- 第20行的@Indexed标记声明此数据类将被纳入全文检索。
- 第24行的@DocumentId对数据的主键进行声明,在全文检索文件中,每一条数据也应该有一个主键,保证检索的可靠性。
- 第29行中需要说明的有三部分:
- @Field声明username为被检索的字段。当用户输入keyword时,username将被纳入检索范围。
- Index.TOKENIZED告诉底层的Lucene引擎,这块数据将使用默认机制被断字、a, the等没有实际意义的词将被省略掉1。
1 默认的断字引擎对英文优化的很好,但对于中文就很傻了,只能单字断开。但Lucene聪明之处在于它的框架很开放,提供很多种断字器及分析器,其中也有对中文做出优化的工具,有兴趣的读者可以自己做深入研究,如果您是全文检索方面的专家,也可以尝试制作自己的分析器。
- Store.NO则保证username的实际数据不被保存在检索文件中(仅保存断字后的数据)。这样做将导致keyword对数据的命中率达不到100%。如果用户对此字段的数据检索要求可靠性,需要设置成Store.Yes。请注意,被声明为@DocumentId的字段永远是被系统强制设置成Store.YES的。
通过添加以上仅仅三行的Annotation,编码工作实际上已经完成了,model层已经具备了全文检索的能力。下面是制作dao层中的全文检索功能。
在dao层添加全文检索功能
model层已经具备了全文检索能力,现在要做的是在DAO中提供相关的功能支持。请看一下全文检索功能的具体实现:
package model.dao;
import java.util.Date;
import java.util.List;
import javax.persistence.EntityManager;
import model.Registration;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.springframework.orm.jpa.JpaCallback;
import org.springframework.orm.jpa.support.JpaDaoSupport;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
public class JpaRegistrationDaoImpl extends JpaDaoSupport implements
RegistrationDao {
...
@Transactional(readOnly = true)
public List search(final String keyword) {
return getJpaTemplate().executeFind(new JpaCallback() {
public Object doInJpa(EntityManager entityManager) {
FullTextEntityManager fullTextEntityManager =
org.hibernate.search.jpa.Search
.createFullTextEntityManager(entityManager);
MultiFieldQueryParser parser = new MultiFieldQueryParser(
new String[] { "id", "username" },
new StandardAnalyzer());
org.apache.lucene.search.Query query;
try {
query = parser.parse(keyword);
return fullTextEntityManager.
createFullTextQuery(
query,Registration.class).
getResultList();
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
});
}
}
这个查询功能看起来有点复杂,请仔细看一下它是如何工作的:
- 第25行第一次使用了Spring的Jpa模版的Callback机制JpaCallback2。对于一般的功能,如查询和存储等,Spring的JpaTemplate提供预设的方法,如前一章用到的merge和find等功能。但对于各种各样复杂的功能,Spring不能进行预设,但又需要用到JpaTemplate的会话联接及事务管理功能,需要特定功能在模版内部运行,这时就可以利用Callback机制,定制自己的功能,并在模版内部运行。
2 Callback机制看起来复杂,实际上原理很简单,就是向函数中传入一个类,函数内部会执行这个类中的特定方法,而这个特定方法的具体逻辑由您自己定义,这样封装后,各功能是在容器内部执行的,所有的其它工作在函数内完成。函数经常接收类,如: func(Class c)
。只不过一般情况下,这些类已经被创建,并且函数也仅使用一下。但Callback函数是在传参时创建类: func(new Class())
,并且在func内部使用这个类的特定功能,并且还用这个传进来的类的返回值做为函数本身的返回值。有关Callback机制,在网上有很多介绍,如果有兴趣则可以简单学习一下。
- 第29行中,创建Hibernate Search提供的FullTextEntityManager(与Hibernate JPA标准中的EntityManager做类似理解,只不过FullTextEntityManager操作的是全文检索文件,而不是数据库)。
- 第30行创建一个Lucene的多字段搜索处理器MultiFieldQueryParser,基于两个字段进行关字搜索:id及username,并且使用Lucene的标准分析器StandardAnalyzer,请注意这个分析器对中文的处理并不是最好的。
- 第35行及第37行的功能很明白,进行实际的搜索并返回搜索结果。
系统的心脏已经完成,但不能硬生生地把一个API丢给用户去使用。我们还必须要给用户一个可视化的操作页面。在下一节中将完成控制层及视图层的功能。
控制层及视图层
通过前面Spring MVC的学习,相信您对这里的功能应该会觉得很简单了。接收用户的keyword输入,返回结果。
请先来看看视图层的页面文件:
<%@ include file="/common/includes.jsp"%>
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title><fmt:message key="title.search" />
</title>
</head>
<body>
<form method="post">
<table>
<fmt:message key="keyword" />
-
<input id="keyword" name="keyword" type="text" />
<input type="submit" />
</table>
</form>
<c:if test="${! empty results}">
匹配结果:<br>
<table border="1">
<tr>
<td>
<fmt:message key="username" />
</td>
<td>
<fmt:message key="number" />
</td>
</tr>
<c:forEach items="${results}" var="result">
<tr>
<td>
${result.username}
</td>
<td>
${result.clazz.number}
</td>
</tr>
</c:forEach>
</table>
</c:if>
</body>
</html>
对这个页面文件,有以下几点需要说明:
- 由于这个页面功能非常简单,并不需要使用Spring标签
<form:form modelAttribute=...>
去绑定任何model层的数据模型。因此在第10行仅使用一般的html标签,请注意指定提交方式为POST。 - 在第14行定义了一个一般的html input标签,接收用户的keyword输入。请注意id=`keyword’是必须的,否则在控制层将无法使用@ModelAttribute绑定这个参数。
- 这个页面实际上有两个功能,一方面接收用户输入,另一方面显示搜索结果。控制层在接收完用户输入,并在底层进行搜索后,还会把结果返回给此页面进行显示。结果被封装在叫`results’的数组中。因此,在第19行判断HTTP Session中是否存在叫results的数据,如果存在,说明控制层已经完成了查找,那么就把结果显示在页面上。
下面请看一下控制层的代码:
package web;
import java.util.List;
import model.dao.RegistrationDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/search.html")
public class Search {
@RequestMapping(method = RequestMethod.POST)
public String processSubmit(@RequestParam("keyword")
String keyword, Model model) {
List l = dao.search(keyword);
model.addAttribute("results", l);
return "search";
}
@RequestMapping(method = RequestMethod.GET)
public String showPage() {
return "search";
}
@Autowired
private RegistrationDao dao;
}
功能非常简单,接收用户的keyword输入,调用dao层面的全文检索功能,把结果放在results中并返回至视图层页面。
装配
做完了编码工作,为了使Hibernate Search正确工作,还需要做一些简单的配置工作。说简单是名符其实,因为只需要加6行配置。在databaseContext.xml中,添加配置如下:
<bean id="entityManagerFactory" ...> ... <property name="jpaProperties"> <props> ... <prop key="hibernate.search.default.directory_provider"> org.hibernate.search.store.FSDirectoryProvider </prop> <prop key="hibernate.search.default.indexBase"> /usr/local/demo </prop> </props> </property> </bean>
- 第6行指定全文检索数据的存储方式,使用FSDirectoryProvider将检索数据以文件的形式存入文件系统。
- 第9行指定存储位置,将检索文件保存在
/usr/local/demo
目录中。
以上装配工作完成了。接下来启动系统看看效果。首先可以看到检索文件在指定的位置被自动生成了3:
ls /usr/local/demo/
model.Registration
ls是linux列出目录内容的命令,和Windows下的dir类似
文件名为 model.Registration
,可以看到是按类的结构进行存储的,这个文件名称是Hibernate Search自动映射而成的,不是我们自己命名的。Hibernate Search会按照持久化类的名字组织存放全文检索文件。此时文件初始大小为
du -h /usr/local/demo/
8.0K /usr/local/demo/model.Registration
du是linux查看文件大小的命令
下面添加一个新的报名数据"李四",然后再看检索文件:
du -h /usr/local/demo/
12K /usr/local/demo//model.Registration
至此完成了对报名数据按报名者姓名进行全文检索的业务需求。
多表检索
学习完了前面的内容,可能您觉得还不满足:如果Hibernate Search能够提供给我们的检索只能针对一张表中的某个数据,是不是太朴素了?确实是这样,如果Hibernate Search只能够提供给我们这样简单的功能,就无法满足更复杂的业务需要。因此在这节,向您介绍用Hibernate Search进行多表关联检索。假设我们对报名系统增加一个业务需要:通过全文检索,根据报名数据所属课程的名称,查找报名数据。
为了实现这一要求,我们首先要在Registration中添加一个标记:
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "clazz_id", nullable = false)
@IndexedEmbedded(prefix="inClazz_")
private Clazz clazz;
您看到了,我们在clazz成员上方添加了一个@IndexEmbedded标记。这就告诉检索引擎,这个成员是可检索项,并且它是与另一个数据类相关的项,而prefix属性则指定指定clazz查询条件时需要使用的前缀。用个例子来说您会更明白点,比如我们现在要查课程名为"clazz1"的下属报名情况,那么查询语法就是:
inClazz_clazzName:clazz1
这样,全文检索就会去找registration所包含的clazz的name属性为clazz1的报名数据。但是我们的工作刚做了一半,为了使Registration中的标记能正常工作。Clazz也必须要纳入到全文检索引擎的视线范围之中,我们需要在Clazz中添加一些标记:
...
@Entity
@Indexed
public class Clazz {
...
@Id
@DocumentId
@GeneratedValue(generator = "hibernate-uuid.hex")
@GenericGenerator(name = "hibernate-uuid.hex", strategy = "uuid.hex")
private String id;
@Field(index = Index.TOKENIZED, store = Store.YES)
private String clazzName;
@OneToMany(mappedBy = "clazz", fetch = FetchType.EAGER)
@OrderBy(clause = "username asc")
@ContainedIn
private Set<Registration> registrations = new HashSet<Registration>();
...
在clazzName字段,我们将其声明为可检索的。为了保证结果的准确性,指定Store类型为YES,即全部信息不丢失地保存。在registrations上方,我们添加了@ContainedIn标记,声明clazz包含在registration的检索信息当中。这样,我们的工作就全部完成了,您现在即可以使用Lucene查询语法,进行这样的多表关系结构化检索。
小结
您在这章中学习使用了Hibernate Search,并使用它为项目添加了全文检索功能。实际上Hibernate Search是一个非常强大的框架,它支持多表关联式查询、结构化搜索、分布式群集部署等很多企业级项目所必需的功能。由于本系列文章篇幅有限,并不能将Hibernate Search的全部功能及底层的Lucene技术细节及查询语法全部讲透。希望您通过本系列文章的入门讲解,进行更深入的学习。