搜索是网络的支柱之一,而全文搜索是每个网站都需要的强制性功能之一。但是实现这样一个特性是复杂的,许多有经验的工程师已经对这个问题进行了深入的思考。因此,让我们不要重新发明轮子,而是使用经过严格测试过的 Hibernate Search
库。
spring init --dependencies=web,data-jpa,h2,lombok,validation spring-boot-hibernate-search
REST API
的 web 依赖 spring Data JPA
,它使用 hibernate 作为默认的对象关系映射工具。与许多库一样,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。
我们以植物为例,其中包含植物的通用名称、学名、家族和创建日期。
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 注解我们想要搜索的字段。此注解仅适用于字符串字段,其他注解适用于不同类型的字段。
我们现在需要定义数据层来处理与数据库的交互。
我们使用 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 {
}