`
hz_chenwenbiao
  • 浏览: 996051 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

Web开发教程12-Hibernate Search(转)

阅读更多

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技术细节及查询语法全部讲透。希望您通过本系列文章的入门讲解,进行更深入的学习。

分享到:
评论
2 楼 anybyb 2011-11-28  
请在Maven的pom.xml配置文件中添加下述dependency:

一定要这么做吗?!Maven是什么啊!这个不会是不是就不能用Hibernate Search了?有关于Maven入门点的文章吗?
1 楼 anybyb 2011-11-28  
真要寻找这方面的资料:好文章!学习了

相关推荐

Global site tag (gtag.js) - Google Analytics