You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Spring Boot中用Hibernate Search基于子实体日期范围筛选父实体

问题:基于Hibernate Search实现带有效价格筛选的文章搜索

背景环境

  • Spring Boot整合Hibernate Search,底层数据库为MySQL
  • 主实体Article包含子实体集合Set<Price>
  • Price关联Country、Store实体,拥有数值类型amount字段,可选dateBegin(生效开始)、dateEnd(生效结束)字段,用于标记限时折扣

需求说明

能否为Article建立索引,实现仅返回拥有有效价格的文章的搜索?有效价格定义:

  • 无日期限制的永久价格
  • 匹配当前/指定日期范围的限时价格

示例场景

Article #1
    Price: US, $100(永久有效)
    Price: DE, €100,生效时间2023-12-25至2023-12-31
    Price: IT, €100,生效时间从2024-01-01开始

Article #2
    Price: DE, €50,生效截止到2023-12-31

当以当前日期查询「针对DE国家有有效价格的文章」时,预期仅返回Article #2——因为Article #1的DE价格生效时间在未来,不符合条件。

选择Lucene的核心原因:业务存在大量复杂搜索条件(如多标签、多语言名称、辅助关键词searchAid、地址信息等),用SQL会生成极度复杂的巨型查询,性能极差。此前用Hibernate+QueryDSL实现时,仅100条测试数据就耗时10秒以上,对应的SQL查询如下:

SELECT 
    JSON_EXTRACT(art.name, '$.hr_HR'), 
    pri.amount, 
    cur.java_currency, 
    cou.alpha2,
    JSON_EXTRACT(cit.name, '$.hr_HR'),
    pri.local_date_time_valid_from, 
    pri.local_date_time_valid_to, 
    pri.zone_id_valid_from_to
FROM article art
LEFT JOIN price_info      pri ON pri.article_id = art.id
LEFT JOIN provider        pro ON pro.id         = pri.provider_id 
LEFT JOIN city            cit ON cit.id         = pro.city_id
LEFT JOIN country         cou ON cou.id         = cit.country_id
LEFT JOIN custom_currency cur ON cur.id         = pri.currency_id
WHERE 
    cou.alpha2 = 'DE'
    AND
    pri.amount IS NOT NULL
    AND (
        pri.local_date_time_valid_from IS NULL
        OR
        pri.local_date_time_valid_from >= STR_TO_DATE('2021-01-01','%Y-%m-%d')
    )
    AND (
        pri.local_date_time_valid_to IS NULL
        OR
        pri.local_date_time_valid_to <= STR_TO_DATE('2022-01-01','%Y-%m-%d')
    );

实现方案

1. 实体索引配置

通过Hibernate Search注解,将Price及其关联实体的字段嵌入到Article的索引中,避免索引查询时的跨实体关联开销。

Article实体配置

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded;

@Entity
@Indexed // 标记该实体需建立全文索引
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 辅助搜索关键词字段
    private String searchAid;
    // 关联标签集合,嵌入到文章索引
    @ManyToMany
    @IndexedEmbedded
    private Set<Tag> tags;
    // 关联分类,嵌入到文章索引
    @ManyToOne
    @IndexedEmbedded
    private Category category;
    // 关联价格集合,嵌入价格及关联的国家信息
    @OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
    @IndexedEmbedded(depth = 2)
    private Set<Price> prices;

    // getter、setter方法
}

Price实体配置

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded;

@Entity
public class Price {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @GenericField // 索引金额字段
    private BigDecimal amount;

    @GenericField // 索引生效开始日期
    private LocalDateTime dateBegin;

    @GenericField // 索引生效结束日期
    private LocalDateTime dateEnd;

    // 关联国家,嵌入国家编码到索引
    @ManyToOne
    @IndexedEmbedded
    private Country country;

    @ManyToOne
    private Store store;

    @ManyToOne
    private Article article;

    // getter、setter方法
}

Country实体配置

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;

@Entity
public class Country {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @GenericField // 索引国家alpha2编码(如DE、US)
    private String alpha2;

    // 其他字段及getter、setter方法
}

2. 构建搜索查询

通过Hibernate Search的API构建布尔查询,精准筛选符合条件的文章:

import org.hibernate.search.engine.search.query.SearchResult;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.session.SearchSession;

import jakarta.persistence.EntityManager;
import java.time.LocalDateTime;
import java.util.List;

public class ArticleSearchService {
    private final EntityManager entityManager;

    public ArticleSearchService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public List<Article> findArticlesWithValidPriceForCountry(String countryAlpha2, LocalDateTime queryDateTime) {
        SearchSession searchSession = Search.session(entityManager);

        return searchSession.search(Article.class)
                .where(f -> f.bool(b -> {
                    // 筛选指定国家的价格
                    b.must(f.match().field("prices.country.alpha2").matching(countryAlpha2));
                    // 价格金额非空
                    b.must(f.match().field("prices.amount").nonNull());
                    // 生效日期条件:无开始日期 或 开始日期≤查询日期
                    b.must(f.bool()
                            .should(f.match().field("prices.dateBegin").isNull())
                            .should(f.range().field("prices.dateBegin").atMost(queryDateTime))
                            .minimumShouldMatch(1));
                    // 截止日期条件:无结束日期 或 结束日期≥查询日期
                    b.must(f.bool()
                            .should(f.match().field("prices.dateEnd").isNull())
                            .should(f.range().field("prices.dateEnd").atLeast(queryDateTime))
                            .minimumShouldMatch(1));
                }))
                .fetchHits(20); // 按需设置分页参数
    }

    // 针对当前日期的查询重载方法
    public List<Article> findArticlesWithValidPriceForCountry(String countryAlpha2) {
        return findArticlesWithValidPriceForCountry(countryAlpha2, LocalDateTime.now());
    }
}

3. 关键说明

  • 索引嵌入:通过@IndexedEmbedded将关联实体字段直接写入Article的索引,避免查询时的跨实体关联操作,大幅提升性能。
  • 布尔查询组合:用must确保必选条件(指定国家、金额非空),用should组合日期的可选条件,确保至少满足「永久有效」或「在指定日期范围内」的其中一个规则。
  • 性能优势:所有筛选逻辑直接在Lucene索引上完成,无需关联数据库表,完美适配多标签、多语言、地址搜索等复杂业务场景。

内容的提问来源于stack exchange,提问作者user1034461

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 10:00:53