通过 Hibernate 将全文搜索集成到 Spring MVC
在本文中,我将向您展示如何将 Hibernate Search 集成到一个简单但功能完整的 Web 应用程序中,该应用程序启用了 Spring MVC 和 Hibernate。
引言
最近,在我开发我的第一个博客引擎(抱歉这里有令人尴尬的自我推销)时,在非常后期,我发现了一个问题——这个博客引擎没有搜索功能。我把它部署在 Digital Ocean 上作为我的个人摄影网站。但我后悔了当初没有在应用程序中添加搜索功能的决定。
为了我自己,我决定研究一个解决方案来为 Web 应用程序添加全文搜索。事实证明有几种可行的方案。对我来说,最明显的两个是:
- 为 Hibernate 添加自定义 SQL 方言,并利用 MySQL(或其他 SQL DB)的全文搜索功能。
- 使用 Hibernate Search 并与现有的 Spring MVC 应用程序集成。
我强烈建议不要尝试第一种方法,因为它不具有可移植性。事情是这样的,如果你采用这种方法,你就会将应用程序与特定的关系数据库紧密耦合。如果更换到不同的数据库,你必须实现一个新的方言。
基于这个推理,我选择了第二种方法,根据我在 Google 上搜索到的结果,这是一种相当流行的方法。无论采用哪种方法,我都发现很难找到关于我所做的那种集成的良好文档。本文旨在记录所有信息,并为读者提供一份我所做工作的良好工作副本。
所用技术背景
在我详细介绍实现细节之前,我想简要讨论一下我为此使用的技术。该项目是一个基于 Spring MVC 的 Web 应用程序。我使用了 Spring 3 组件。构建使用了 maven 2。持久层使用了 Hibernate 4。后端数据存储是 MySQL。所有一切都通过 Spring 集成。
附带的 zip 文件将包含您运行 Web 应用程序所需的所有内容。您需要做的是:
- 安装 Java 1.6
- 安装 Apache Maven 2 或 3
- 安装 MySQL 并安装数据库和表。
- 设置 JAVA_HOME 和 M2_HOME 的环境变量,以及两者的 PATH 变量。
- 运行 mvn clean install。一旦 war 文件被创建,将其部署到您的机器并运行。
或者,您可以使用 Maven 生成 Eclipse 项目文件,然后将项目导入 Eclipse 进行编辑。我甚至在 Eclipse 中运行服务器进行故障排除。本文的重点是描述基于 SQL 的数据库上的全文搜索实现如何工作,因此我不会花费时间解释如何为它设置开发环境。有大量的资源可以帮助您解决这个问题。
理解代码
我将详细讨论项目架构和代码结构。首先,让我描述一下这个应用程序做什么。它是一个简单的 Web 应用程序。它允许用户输入书籍的详细信息:标题、描述和作者。它还允许用户通过 1 个简单的关键字搜索已输入的信息。正如您所看到的,它非常简单,而且功能不多。这是故意的。我只想向您展示如何设置整个流程。如何使用 Hibernate Search 实现全文搜索功能取决于您自己。让我们一步步进行设置这个 Java 项目。
步骤 1:创建数据库用户和表
在 zip 文件中,有一个名为“DB”的文件夹,其中包含 2 个脚本。第一个脚本创建一个数据库用户。内容如下:
CREATE DATABASE fulltextsearch;
CREATE USER 'ftuser1'@'localhost' IDENTIFIED BY '123test321';
GRANT ALL PRIVILEGES ON fulltextsearch.* TO 'ftuser1'@'localhost';
FLUSH PRIVILEGES;
使用 root 用户在 MySQL 上运行上面的脚本,您将为该项目创建一个新用户和新数据库。
还包含另一个 SQL 脚本,它将在新数据库中创建一个新表。内容如下:
use fulltextsearch;
DROP TABLE IF EXISTS book;
CREATE TABLE book (
id VARCHAR(37) NOT NULL PRIMARY KEY,
title VARCHAR(128) NOT NULL,
description VARCHAR(256) NOT NULL,
author VARCHAR(64) NOT NULL,
createdate DATETIME NOT NULL,
updatedate DATETIME NOT NULL
);
再次,我不会解释如何运行这两个脚本。网上应该有很多资源可以帮助您。
步骤 2:了解 POM 文件
最后,我切换到使用 Maven 来构建 Java 应用程序。那是 3 年前。那时,我在这里发布了一篇文章,只使用了 Eclipse。它并没有得到很好的反响(只有 5000 次浏览)。我怀疑的问题是,我无法将所有依赖的 jar 包包含在源代码中供人们下载,这使得之前的文章有点难跟进。
无论如何,对于本文来说,打包所有依赖的 jar 包供人们下载仍然是一个糟糕的主意。使用 Maven,这个问题将不再是问题。只要您正确设置了开发环境,您就可以下载依赖的 jar 包,在 1 个自动化过程中完成编译、打包 war 文件。
对于这个项目,事实证明,我需要包含许多 jar 包,这使得这个 POM 文件看起来相当可怕。所以让我把它贴出来,吓跑任何新手程序员,然后对于留下来的任何人,我将解释这个 POM 文件的关键元素。下面是它:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.hanbo.spring.app</groupId>
<artifactId>SampleSpringApp</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>Sample Spring REST App</name>
<url>http://maven.apache.org</url>
<properties>
<spring.version>3.2.6.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>antlr</groupId>
<artifactId>antlr</artifactId>
<version>2.7.7</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>3.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.2.15.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-search-engine</artifactId>
<version>4.3.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-search-orm</artifactId>
<version>4.3.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-search-orm</artifactId>
<version>4.3.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
<version>1.0.1.Final</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.1.4.GA</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.29</version>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0.2</version>
</dependency>
</dependencies>
<build>
<finalName>SpringMVC</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
请忽略此文件中的任何不规范之处。我使用了之前的项目并添加了一些依赖项。说实话,这个文件很容易阅读。它有两个主要部分,第一部分是依赖项。对于每个依赖项,您都有 `groupId`、`artifactId` 和 `version`。在此部分顶部,有一堆关于 Spring、Spring Data 和 Spring MVC 的依赖项。所有这些的版本都是 3.2.6.RELEASE。
此部分的下一部分是 Hibernate 依赖项。这里有一些您必须注意的陷阱:
- 我只能使用 4.2.15.Final 版本的 hibernate-core 来让整个项目工作。我尝试过使用更高版本的 hibernate-core。其中一个依赖类已从这些版本的 hibernate-core 中移除。我在初始化时遇到了一个异常,无法解决。网上没有任何解决方案。这让我猜测没有人关注这个问题,可能没有人使用 hibernate search 或其他东西。或者我错过了一些简单的东西可以解决这个问题。
- 我一直在努力寻找正确的 `hibernate-search.jar`,因为所有这些不同版本的 jar 包都不包含类。显然,您不需要这个 jar。相反,只需包含 hibernate-search-engine 和 hibernate-search-orm。然后您就可以开始了。我选择了 4.3.0.Final。这是一个较早的稳定版本,似乎很多人都在使用。
- 还有另一个陷阱。我稍后会告诉您。
最后一部分是所需的 `misc. jars`,例如:JSTL、persistence-api 等。您应该注意的一个有趣之处是,不知何故,hibernate 依赖于 jboss-logging,我还没有研究如何摆脱它。与其对抗,我只是把它包含在依赖列表中。
另一部分是用于编译项目和生成最终 war 文件的编译器插件。我将编译器版本设置为 1.6,这样就不用处理 Java 1.7 了。
步骤 3:项目目录结构
这是目录结构:
SampleSpringFullTextSearch1
|
---- DB
|
---- tables.sql
|
---- user.sql
|
---- Index (used for storing the indexes for full tgext search)
|
---- src
|
---- main
|
---- java
|
---- org / hanbo / mvc / controller (MVC controller classes)
|
---- org / hanbo / mvc / models (MVC data models used for display)
|
---- org / hanbo / mvc / entities (data persistence classes)
|
---- resources
|
---- webapp
|
---- META-INF
|
---- MANIFEST.MF
|
---- WEB-INF
|
---- lib
|
---- pages
|
---- A number of JSP pages, used as MVC views
|
---- mvc-dispatcher-servlet.xml
|
---- web.xml
我不想过多的深入细节,上面的项目目录结构是创建 Spring MVC 项目和使用 Maven 创建可部署 war 文件的基本结构。这是基于配置已正确完成的假设,这将在接下来解释。
步骤 4:Web.xml
`web.xml` 是部署描述符。J2EE 容器必须查看它,并弄清楚如何正确地将 Web 应用程序连接起来供用户交互。对于这个应用程序,`web.xml` 看起来就像一个典型的 Spring Web 应用程序的 `web.xml`。这个文件内容如下:
<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>Spring Web MVC Application</display-name>
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
正如我所描述的,这个文件没有什么特别的。第一部分描述了 servlet 的名称。它使用 Spring Web 的 `DispatchServlet` 来处理传入的请求。注意 servlet 的名称是 `mvc-dispatcher`。这个名称很重要。Spring Web 在初始化时,会查找具有此命名约定的应用程序上下文:`
第二部分基本上表示任何 `/` 请求都将由 Spring Web 的 `DispatcherServlet` 处理。第四部分也是最后一部分基本上是注册一个事件监听器来加载上下文,当事件监听器收到上下文加载事件的通知时,它会加载指定的应用程序上下文配置。没什么特别的。有关 Spring 配置的更多信息,请在线搜索。接下来,我们将查看应用程序上下文的 `config` 文件。这是这个应用程序的核心。
步骤 5:应用程序上下文配置文件的内容
与使用 Hibernate 进行持久化的典型 Web 应用程序相比,此应用程序的应用程序上下文 `config` 文件几乎相同,只有一些修改。首先让我们看看这个文件的完整内容,名为 `mvc-dispatcher-servlet.xml`。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="org.hanbo.mvc.controller" />
<context:component-scan base-package="org.hanbo.mvc.entities" />
<mvc:resources mapping="/assets/**" location="/assets/" />
<mvc:annotation-driven />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix">
<value>/WEB-INF/pages/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/fulltextsearch?autoReconnect=true"/>
<property name="username" value="ftuser1"/>
<property name="password" value="123test321"/>
<property name="maxActive" value="8"/>
<property name="maxIdle" value="4"/>
<property name="maxWait" value="900000"/>
<property name="validationQuery" value="SELECT 1" />
<property name="testOnBorrow" value="true" />
</bean>
<bean id="mySessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref="myDataSource"/>
<property name="packagesToScan">
<array>
<value>org.hanbo.mvc.entities</value>
</array>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.cache.provider_class">org.hibernate.cache.NoCacheProvider</prop>
<prop key="hibernate.search.default.directory_provider">
org.hibernate.search.store.impl.FSDirectoryProvider</prop>
<prop key="hibernate.search.default.indexBase">
C:/Users/hanbo/workspace-ee/SampleSpringFullTextSearch/indexes</prop>
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
现在,让我们分解并详细讨论每个部分。第一部分是设置组件扫描包。其思想是允许 Spring 扫描指定的包以查找被注释为 bean 的类(无论是为了自动装配还是为了让 Spring 组件知道它们是什么以及如何使用它们)。
<context:component-scan base-package="org.hanbo.mvc.controller" />
<context:component-scan base-package="org.hanbo.mvc.entities" />
... ...
<mvc:annotation-driven />
下一部分配置 Spring MVC 以查找 JSP 视图文件。如以下代码片段所示,代码将 war 文件内的文件夹 `/WEB-INF/pages` 设置为所有视图页面的容器,并告知 Spring MVC 视图页面的后缀为 `.jsp`。
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix">
<value>/WEB-INF/pages/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
下一部分是此应用程序的数据源配置。这里没有什么特别的。如果您想尝试此应用程序,请确保配置对您有效。如果对您无效,您可能需要调整这些值。到目前为止,还没有配置 Hibernate Search。
<bean id="myDataSource"
class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/fulltextsearch?autoReconnect=true"/>
<property name="username" value="ftuser1"/>
<property name="password" value="123test321"/>
<property name="maxActive" value="8"/>
<property name="maxIdle" value="4"/>
<property name="maxWait" value="900000"/>
<property name="validationQuery" value="SELECT 1" />
<property name="testOnBorrow" value="true" />
</bean>
下一部分变得有趣起来。所以让我向您展示 Hibernate 持久化配置所需的内容。
<bean id="mySessionFactory"
class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
<property name="dataSource" ref="myDataSource"/>
<property name="packagesToScan">
<array>
<value>org.hanbo.mvc.entities</value>
</array>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.cache.provider_class">org.hibernate.cache.NoCacheProvider</prop>
... ...
</props>
</property>
</bean>
如您所见,所有不是“... ...”的代码都只是 Hibernate 持久化特定的配置。现在要填补 Hibernate 特定配置的空白,它相当简单,您可以使用以下方法:
<prop key="hibernate.search.default.directory_provider">
org.hibernate.search.store.impl.FSDirectoryProvider</prop>
<prop key="hibernate.search.default.indexBase">
C:/Users/hsun/workspace-ee/SampleSpringFullTextSearch/indexes</prop>
正如我之前提到的,有一个陷阱。就在这里。Google 搜索结果中有许多页面显示“hibernate.search.default.directory_provider
”的值应为“org.hibernate.search.store.FSDirectoryProvider
”。这已不再有效。该类在新版本中已移至不同的包。我花了一些时间才找到这个 `FSDirectoryProvider`。正如您所猜到的,它有效。
配置到此结束。接下来,是乐趣所在——Java 类。
Java 类
在我们深入研究 Java 类之前,我需要解释一下这个应用程序是如何工作的。这是一个简单的 Web 应用程序,允许用户输入一本新书。它允许用户通过一个关键字来搜索书籍,并根据书籍的标题、作者和描述进行匹配(一个关键字匹配三个不同的字段)。还有一个附加功能允许应用程序重新索引所有记录。
下面是显示添加书籍页面的屏幕截图,URL 是 https://:8080/SampleSpringFullTextSearch/addBook
这是显示搜索书籍页面的屏幕截图,URL 是 https://:8080/SampleSpringFullTextSearch/search
正如您所看到的,演示应用程序非常简单,您可以将一些书籍信息输入到数据库中,然后使用一些关键字搜索来查看全文搜索功能是否有效。要使此应用程序运行起来,我们首先必须在同一个控制器中定义 3 个不同的操作:
- 一个演示重新索引所有表记录的能力的操作;
- 一个允许将书籍信息插入后端数据库的操作;
- 一个执行简单全文搜索并列出结果的操作。
在开始处理控制器和这三个操作的有趣部分之前,让我们先看看实体类(这很有趣)。
实体类
还记得我们在本文开头创建的数据库表吗?为了使用 Hibernate,我们需要一个相应的实体类。这就是 Hibernate Search 发挥作用的地方。
您会在 `org\hanbo\mvc\entities` 目录中找到实体类“Book.java”。在同一目录下,还有存储库类“BookRepository.java”。
package org.hanbo.mvc.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.Store;
import java.util.Date;
@Entity
@Indexed
@Table(name = "book")
public class Book
{
@Id
@Column(name = "id")
private String id;
@Column(name = "title", nullable= false, length = 128)
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String title;
@Column(name = "description", nullable= false, length = 256)
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String description;
@Column(name = "author", nullable= false, length = 64)
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String author;
@Column(name = "createdate", nullable= false)
private Date createDate;
@Column(name = "updatedate", nullable= false)
private Date updateDate;
public String getId()
{
return id;
}
public void setId(String id)
{
this.id = id;
}
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
public String getDescription()
{
return description;
}
public void setDescription(String description)
{
this.description = description;
}
public String getAuthor()
{
return author;
}
public void setAuthor(String author)
{
this.author = author;
}
public Date getCreateDate()
{
return createDate;
}
public void setCreateDate(Date createDate)
{
this.createDate = createDate;
}
public Date getUpdateDate()
{
return updateDate;
}
public void setUpdateDate(Date updateDate)
{
this.updateDate = updateDate;
}
}
我的实体类使用 JPA 注解将类与表关联,并将类的实例成员与表列关联。这些对于 Hibernate 来说都是相当标准的操作。Hibernate Search 酷炫之处在于它提供了一些注解,因此您可以将类的实例成员(即表列)标记为可索引字段。它们是:
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.Store;
要使表列可使用 Hibernate Search 进行索引,请参见以下代码:
@Column(name = "title", nullable= false, length = 128)
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String title;
@Column(name = "description", nullable= false, length = 256)
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String description;
@Column(name = "author", nullable= false, length = 64)
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String author;
注解 `@Field` 及其参数基本上使表列“title
”、“description
”和“author
”可索引,并在索引操作期间分析列的内容。根据 Hibernate Search 的教程,“分析是指将句子分解成单个单词,将它们转换为小写,并可能排除像‘a’或‘the’这样的常见单词。” 无论如何,这 3 个 `@Field` 注解使用了默认设置。`store` 参数设置为 `No`,意味着我不想将内容存储在全文搜索索引存储中。我这样做是为了节省空间(不确定我是否能证明这一点)。
存储库类
这样,我们就完成了最终产品的大约四分之一。接下来我想展示的是 `BookRepository` 的 CRUD 操作。这是代码:
package org.hanbo.mvc.entities;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
@SuppressWarnings("unchecked")
public class BookRepository
{
@Autowired
private SessionFactory mySessionFactory;
@Transactional
public void indexBooks() throws Exception
{
try
{
Session session = mySessionFactory.getCurrentSession();
FullTextSession fullTextSession = Search.getFullTextSession(session);
fullTextSession.createIndexer().startAndWait();
}
catch(Exception e)
{
throw e;
}
}
@Transactional
public void addBookToDB(String bookTitle, String bookDescription, String bookAuthor)
{
Session session = mySessionFactory.getCurrentSession();
Book book = new Book();
UUID x = UUID.randomUUID();
Date dateNow = new Date();
book.setId(x.toString());
book.setAuthor(bookAuthor);
book.setDescription(bookDescription);
book.setTitle(bookTitle);
book.setCreateDate(dateNow);
book.setUpdateDate(dateNow);
session.saveOrUpdate(book);
}
@Transactional
public List<Book> searchForBook(String searchText) throws Exception
{
try
{
Session session = mySessionFactory.getCurrentSession();
FullTextSession fullTextSession = Search.getFullTextSession(session);
QueryBuilder qb = fullTextSession.getSearchFactory()
.buildQueryBuilder().forEntity(Book.class).get();
org.apache.lucene.search.Query query = qb
.keyword().onFields("description", "title", "author")
.matching(searchText)
.createQuery();
org.hibernate.Query hibQuery =
fullTextSession.createFullTextQuery(query, Book.class);
List<Book> results = hibQuery.list();
return results;
}
catch(Exception e)
{
throw e;
}
}
}
这个类确实是乐趣所在,我首先想展示的是记录插入操作,即这部分:
@Transactional
public void addBookToDB(String bookTitle, String bookDescription, String bookAuthor)
{
Session session = mySessionFactory.getCurrentSession();
Book book = new Book();
UUID x = UUID.randomUUID();
Date dateNow = new Date();
book.setId(x.toString());
book.setAuthor(bookAuthor);
book.setDescription(bookDescription);
book.setTitle(bookTitle);
book.setCreateDate(dateNow);
book.setUpdateDate(dateNow);
session.saveOrUpdate(book);
}
正如您从代码中可以看到的,这是 Hibernate 将实体持久化到数据库中的一个相当标准的方法。这里没有揭示的是,在后台,Hibernate Search 也会索引新实体的标题、作者和描述属性。这很酷!
您可能会问,如果我有一个现有的数据库,并且我将前端改造为具有全文搜索功能,我该如何将现有记录添加到搜索索引中?这是可以做到的;而且相当容易。有一个陷阱,但是,根据文档,这是一项昂贵的操作,如果有很多表并且每个表都有大量数据,其性能会更差。基本上发生的情况是,整个索引被清除,并且每个表中的每个记录都将被重新处理到新索引中。如果您拥有的数据量较少,它会很快完成。如果您拥有的数据量很大,这将花费很长时间才能完成。所以您最好的选择是:
- 您不必重新索引,只需创建空表,并在插入记录时索引这些记录。
- 您只需要进行一次重新索引,仅在维护时进行一次。总之,在上面的类中,这是执行重新索引的代码:
@Transactional
public void indexBooks() throws Exception
{
try
{
Session session = mySessionFactory.getCurrentSession();
FullTextSession fullTextSession = Search.getFullTextSession(session);
fullTextSession.createIndexer().startAndWait();
}
catch(Exception e)
{
throw e;
}
}
最后,让我们检查执行全文搜索的逻辑:
@Transactional
public List<Book> searchForBook(String searchText) throws Exception
{
try
{
Session session = mySessionFactory.getCurrentSession();
FullTextSession fullTextSession = Search.getFullTextSession(session);
QueryBuilder qb = fullTextSession.getSearchFactory()
.buildQueryBuilder().forEntity(Book.class).get();
org.apache.lucene.search.Query query = qb
.keyword().onFields("description", "title", "author")
.matching(searchText)
.createQuery();
org.hibernate.Query hibQuery =
fullTextSession.createFullTextQuery(query, Book.class);
List<Book> results = hibQuery.list();
return results;
}
catch(Exception e)
{
throw e;
}
}
上面的代码执行以下操作:
- 获取当前 Hibernate 会话。
- 获取全文搜索会话。
- 创建对 `Book` 实体的查询,并在“
title
”、“description
”和“author
”字段上进行搜索。 - 执行搜索并返回结果列表。
请注意,此搜索功能非常有限。您输入一个词,只要任何实体包含该词,它就会出现在结果列表中。这只是演示了应用程序已设置为全文搜索。
控制器类
控制器类是解开谜题的最后一块。控制器类是懒惰实现的。它直接使用存储库对象,而不是使用服务层对象。此控制器公开的操作是:
- URL:/welcome - 此操作将强制重新索引所有全文索引。实际上,这不应该暴露给最终用户,但在本例中,您可以使用此操作来观察重新索引的实际情况。
- URL:/addBookToDB - 此操作将接收来自书籍输入页面的 POST 数据,并将书籍详细信息插入数据库中的 Book 表。
- URL:/doSearch - 此操作将执行全文搜索并列出所有相关结果。
除了这些操作之外,还有两个会显示初始页面:
- URL:/addBook - 此操作将显示允许用户输入书籍详细信息的页面。
- URL:/search - 此操作将显示允许用户搜索书籍的页面。
在开始进行这些操作之前,让我们先看看代码:
package org.hanbo.mvc.controller;
import java.util.ArrayList;
import java.util.List;
import org.hanbo.mvc.entities.Book;
import org.hanbo.mvc.entities.BookRepository;
import org.hanbo.mvc.models.BookModel;
import org.jboss.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloWorld
{
private static Logger _logger = Logger.getLogger(HelloWorld.class);
@Autowired
private BookRepository _repo;
@RequestMapping(value = "/welcome", method = RequestMethod.GET)
public ModelAndView welcome() throws Exception
{
_repo.indexBooks();
ModelAndView mav = new ModelAndView("hello");
mav.addObject("message", "Hello World!");
return mav;
}
@RequestMapping(value = "/addBook", method = RequestMethod.GET)
public ModelAndView addBookPage()
{
ModelAndView mav = new ModelAndView("addBook", "command", new BookModel());
return mav;
}
@RequestMapping(value = "/addBookToDB", method = RequestMethod.POST)
public ModelAndView addBookToDB(
@ModelAttribute("BookModel")
BookModel bookInfo
) throws Exception
{
_logger.info(bookInfo.getBookTitle());
_logger.info(bookInfo.getBookDescription());
_logger.info(bookInfo.getBookAuthor());
_repo.addBookToDB(
bookInfo.getBookTitle(),
bookInfo.getBookDescription(),
bookInfo.getBookAuthor()
);
ModelAndView mav = new ModelAndView("done");
mav.addObject("message", "Add book to DB successfully");
return mav;
}
@RequestMapping(value = "/search", method = RequestMethod.GET)
public ModelAndView searchPage()
{
ModelAndView mav = new ModelAndView("search");
return mav;
}
@RequestMapping(value = "/doSearch", method = RequestMethod.POST)
public ModelAndView search(
@RequestParam("searchText")
String searchText
) throws Exception
{
List<Book> allFound = _repo.searchForBook(searchText);
List<BookModel> bookModels = new ArrayList<BookModel>();
for (Book b : allFound)
{
BookModel bm = new BookModel();
bm.setBookAuthor(b.getAuthor());
bm.setBookDescription(b.getDescription());
bm.setBookTitle(b.getTitle());
bookModels.add(bm);
}
ModelAndView mav = new ModelAndView("foundBooks");
mav.addObject("foundBooks", bookModels);
return mav;
}
}
让我们逐一来看。首先是关于最不令人兴奋的部分,页面显示操作。要显示添加书籍页面,这是执行此操作的代码:
@RequestMapping(value = "/addBook", method = RequestMethod.GET)
public ModelAndView addBookPage()
{
ModelAndView mav = new ModelAndView("addBook", "command", new BookModel());
return mav;
}
要显示搜索书籍页面,这是执行此操作的代码:
@RequestMapping(value = "/addBook", method = RequestMethod.GET)
public ModelAndView addBookPage()
{
ModelAndView mav = new ModelAndView("addBook", "command", new BookModel());
return mav;
}
现在是更有趣的部分,添加一本新书,这段代码就是这样做的:
@RequestMapping(value = "/addBookToDB", method = RequestMethod.POST)
public ModelAndView addBookToDB(
@ModelAttribute("BookModel")
BookModel bookInfo
) throws Exception
{
_logger.info(bookInfo.getBookTitle());
_logger.info(bookInfo.getBookDescription());
_logger.info(bookInfo.getBookAuthor());
_repo.addBookToDB(
bookInfo.getBookTitle(),
bookInfo.getBookDescription(),
bookInfo.getBookAuthor()
);
ModelAndView mav = new ModelAndView("done");
mav.addObject("message", "Add book to DB successfully");
return mav;
}
我现在所做的,只是使用 `_repo` 对象并将新创建的书籍对象保存到数据库中。数据来自 `BookModel` 对象。`BookModel` 是与显示相关的数据模型,包含书籍标题、描述和作者的值。您可以在 `org.hanbo.mvc.models.BookModel` 中找到它。
最后,执行书籍搜索的代码,使用 Hibernate Search:
@RequestMapping(value = "/doSearch", method = RequestMethod.POST)
public ModelAndView search(
@RequestParam("searchText")
String searchText
) throws Exception
{
List<Book> allFound = _repo.searchForBook(searchText);
List<BookModel> bookModels = new ArrayList<BookModel>();
for (Book b : allFound)
{
BookModel bm = new BookModel();
bm.setBookAuthor(b.getAuthor());
bm.setBookDescription(b.getDescription());
bm.setBookTitle(b.getTitle());
bookModels.add(bm);
}
ModelAndView mav = new ModelAndView("foundBooks");
mav.addObject("foundBooks", bookModels);
return mav;
}
这段代码再次不那么有趣,因为实际的搜索是在存储库对象中执行的。这段代码将获取返回结果列表,然后将其转换为 `BookModel` 对象,放入列表中并返回页面进行显示。
在 `webapp/WEB-INF/pages` 中有许多 JSP 页面。您可以查看它们。Spring MVC 控制器的做法是,对于操作方法,它们返回页面的名称字符串(不带扩展名),Spring MVC 使用 `InternalResourceViewResolver` 对象来获取 `webapp/WEB-INF/pages` 中的 JSP 文件。
基本上就这些了。玩得开心,尽情使用演示应用程序。别忘了查看 我的博客页面。
历史
- 2014/10/16 - 完成初稿