在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
相关产品推荐
相关产品推荐

