Java | 使用 Hibernate Search 构建一个带有全文搜索的 Spring Boot REST API (一)

杨彦君
2023-12-01

1 概述

搜索是网络的支柱之一,而全文搜索是每个网站都需要的强制性功能之一。但是实现这样一个特性是复杂的,许多有经验的工程师已经对这个问题进行了深入的思考。因此,让我们不要重新发明轮子,而是使用经过严格测试过的 Hibernate Search 库。

2 项目设置

2.1 第一步是使用 spring initializr 生成 spring boot 项目。

spring init --dependencies=web,data-jpa,h2,lombok,validation spring-boot-hibernate-search

2.2 我们将以下依赖项打包:

  • 针对 REST API 的 web 依赖
  • 数据访问层 spring Data JPA,它使用 hibernate 作为默认的对象关系映射工具。
  • H2库提供了一个易于使用的内存嵌入式数据库。这种类型的数据库适合于小型非生产项目。
  • Lombok 通过注解生成代码片段。
  • Validation 是遵循 JSR 380规范的验证 API 的 Hibernate 实现。它使用注解对 bean 进行验证。

3 安装 Hibernate Search

与许多库一样,Spring Boot 提供了安装 Hibernate Search 的简单方法。我们只需要将所需的依赖项添加到 pom.xml 文件中。

<properties>
	<hibernate.search.version>6.1.1.Final</hibernate.search.version>
</properties>

...

<dependencies>
	...
	<dependency>
		<groupId>org.hibernate.search</groupId>
		<artifactId>hibernate-search-mapper-orm</artifactId>
		<version>${hibernate.search.version}</version>
	</dependency>
	<dependency>
		<groupId>org.hibernate.search</groupId>
		<artifactId>hibernate-search-backend-lucene</artifactId>
		<version>${hibernate.search.version}</version>
	</dependency>
</dependencies>

我们使用的是 Hibernate Search 6,是迄今为止最新的版本,Lucene 作为后端。Lucene 是一个开源的索引和搜索引擎库,是 Hibernate Search 使用的默认实现。我们也可以使用不同的实现,比如 ElasticSearch 或 OpenSearch。

4 定义数据模型

4.1 第一步是定义将要进行搜索的实体的模型。

我们以植物为例,其中包含植物的通用名称、学名、家族和创建日期。

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.NaturalId;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;

import javax.persistence.*;
import java.time.Instant;

@Indexed
@Entity
@Table(name = "plant")
@Getter
@Setter
@ToString
@EqualsAndHashCode
public class Plant {

    public Plant() {
        this.createdAt = Instant.now();
    }

    public Plant(String name, String scientificName, String family) {
        this.name = name;
        this.scientificName = scientificName;
        this.family = family;
        this.createdAt = Instant.now();
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @FullTextField()
    @NaturalId()
    private String name;

    @FullTextField()
    @NaturalId()
    private String scientificName;

    @FullTextField()
    private String family;

    private Instant createdAt ;
}

让我们忽略 JPA 和 Lombok 注解,重点关注与 Hibernate search 相关的注解。

首先,@index 注解向 Hibernate Search 表明,我们希望对这个实体进行索引,以便对其应用搜索操作。

其次,我们使用 @FullTextField 注解我们想要搜索的字段。此注解仅适用于字符串字段,其他注解适用于不同类型的字段。

5 定义数据层

我们现在需要定义数据层来处理与数据库的交互。

我们使用 Spring Data 仓库,它围绕 JPA 的 Hibernate 实现构建了一个抽象。它是在前面添加的 spring-boot-starter-data-jpa 依赖项中提供的。

对于只需要 CRUD 操作的基本用例,我们可以为 Plant 实体定义一个简单的存储库,并直接扩展 JpaRepository 接口。

但这对于全文搜索来说是不够的。在我们的例子中,我们希望将搜索特性添加到我们定义的所有存储库中。为此,我们需要将自定义方法添加到 JpaRepository 接口,或者任何继承 Repository 接口的接口。

这样,我们只声明这些方法一次,并使它们应用于项目的每个实体的存储库。

  • 首先,我们需要创建一个新的通用接口来继承JpaRepository 接口。

    @NoRepositoryBean
    public interface SearchRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
    
        List<T> searchBy(String text, int limit, String... fields);
    }
    

    这里,我们声明了一个将用于全文搜索操作的 searchBy 函数。

    @ norepositorybean 注解告诉 spring,这个存储库接口不应该被实例化。

    我们使用这个注解是因为这个接口不能被直接使用,而是由存储库实现。

  • 我们还需要为这个接口创建实现。

    @Transactional
    public class SearchRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
            implements SearchRepository<T, ID> {
    
        private final EntityManager entityManager;
    
        public SearchRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
            super(domainClass, entityManager);
            this.entityManager = entityManager;
        }
    
        public SearchRepositoryImpl(
                JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
            super(entityInformation, entityManager);
            this.entityManager = entityManager;
        }
    
        @Override
        public List<T> searchBy(String text, int limit, String... fields) {
    
            SearchResult<T> result = getSearchResult(text, limit, fields);
    
            return result.hits();
        }
    
        private SearchResult<T> getSearchResult(String text, int limit, String[] fields) {
            SearchSession searchSession = Search.session(entityManager);
    
            SearchResult<T> result =
                    searchSession
                            .search(getDomainClass())
                            .where(f -> f.match().fields(fields).matching(text).fuzzy(2))
                            .fetch(limit);
            return result;
        }
    }
    

    searchBy 方法实现是使用 Hibernate Search 的地方。

    我们使用 java varargs 来传递我们想要搜索的所有字段。

    从现在开始,需要全文本搜索的存储库只需要实现 SearchRepository 接口,而不需要 Spring 提供的标准 JpaRepository 接口。

    这正是我们为植物实体所做的。

    package com.mozen.springboothibernatesearch.repository;
    
    import com.mozen.springboothibernatesearch.model.Plant;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public interface PlantRepository extends SearchRepository<Plant, Long> {
    }
    

    正如您所看到的,所有的实现都已经完成,我们只需要实现先前创建的 SearchRepository 接口,以获得对 SearchRepositoryImpl 类中定义的实现的访问权。

    最后一步是让 Spring 使用 SearchRepositoryImpl 作为基类来检测 Jpa 存储库。

    package com.mozen.springboothibernatesearch;
    
    import com.mozen.springboothibernatesearch.repository.SearchRepositoryImpl;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    
    @Configuration
    @EnableJpaRepositories(repositoryBaseClass = SearchRepositoryImpl.class)
    public class ApplicationConfiguration {
    }
    
 类似资料: