You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

Spring Boot集成Hibernate Search 6无法返回部分匹配查询结果的问题求助

Spring Boot集成Hibernate Search 6无法返回部分匹配查询结果的问题求助

你好呀!我来帮你拆解下这个问题的核心原因,再给出针对性的解决方案~

问题根源

你现在遇到的问题本质是字段的索引逻辑和查询逻辑不匹配

你用@GenericField标注的字段,Hibernate Search默认会使用KeywordAnalyzer——也就是把整个字符串当成一个完整的词来索引,不会做任何分词拆分。所以当你调用f.match().matching(terms)时,Hibernate Search会尝试匹配完整的词:

  • 搜“AP”能找到匹配结果,因为“AP”本身就是area_zone字段的完整词;
  • 但搜“Pun”找不到“Punjab”,因为“Punjab”是一个完整的词,“Pun”只是它的前缀片段,自然匹配不到;
  • 手机号同理,“987”只是11位手机号的前缀片段,不是完整的词,所以也匹配不到。

针对性解决方案

根据你需要的匹配场景,我给你两种常用的解决思路:


场景1:只需要前缀/后缀匹配(类似SQL的xxx%%xxx

这种场景不需要修改实体的字段注解,只需要调整查询语句即可:

前缀匹配(比如搜手机号开头的“987123”)

把原来的match查询改成prefix查询,或者给match开启前缀模式:

// 方式1:用prefix查询
return Search.session(em).search(PhoneNumber.class)
        .where(f -> f.bool()
                .should(f.prefix().field("phonenumber").matching(terms))
                .should(f.prefix().field("area_zone").matching(terms))
                .should(f.prefix().field("state").matching(terms))
        )
        .fetchHits(offset, limit);

// 方式2:给match开启前缀模式
return Search.session(em).search(PhoneNumber.class)
        .where(f -> f.match()
                .fields("phonenumber", "area_zone", "state")
                .matching(terms)
                .prefix() // 启动前缀匹配,等价于SQL的xxx%
        )
        .fetchHits(offset, limit);

后缀匹配(比如搜手机号结尾的“4567”)

suffix查询即可:

return Search.session(em).search(PhoneNumber.class)
        .where(f -> f.bool()
                .should(f.suffix().field("phonenumber").matching(terms))
                .should(f.suffix().field("area_zone").matching(terms))
                .should(f.suffix().field("state").matching(terms))
        )
        .fetchHits(offset, limit);

场景2:需要任意位置的部分匹配(类似SQL的%xxx%

如果要支持任意位置的片段匹配(比如搜“456”能找到所有包含这个片段的手机号),就需要同时修改索引配置和查询逻辑,因为要在索引阶段把字符串拆分成小的片段(ngram),这样任意位置的片段都能被匹配到。

步骤1:定义Ngram分析器

先在Spring Boot配置类里创建一个ngram分析器,用来把字符串拆分成指定长度的小片段:

@Configuration
public class HibernateSearchConfig {

    @Bean
    public LuceneAnalysisConfigurationProvider luceneAnalysisConfigurationProvider() {
        return context -> {
            context.analyzer("ngram_analyzer")
                    .custom()
                    .tokenizer("standard")
                    .tokenFilter("lowercase") // 可选:统一转小写,实现大小写不敏感匹配
                    .tokenFilter("ngram")
                    .param("minGramSize", 3) // 最小片段长度,比如3个字符
                    .param("maxGramSize", 5); // 最大片段长度,可根据你的需求调整
        };
    }
}

步骤2:修改实体字段注解

把需要支持任意部分匹配的字段,改成@FullTextField并指定刚才的分析器,同时保留@GenericField来支持排序(因为FullTextField默认不支持排序):

@Entity
@Table(name="PHONENUMBER", uniqueConstraints = {@UniqueConstraint(columnNames = "phonenumber")})
@Indexed
public class PhoneNumber {
    @Id
    @GeneratedValue
    private int id;

    @FullTextField(analyzer = "ngram_analyzer", searchAnalyzer = "ngram_analyzer")
    @GenericField(sortable= Sortable.YES)
    private String phonenumber;
    
    @FullTextField(analyzer = "ngram_analyzer", searchAnalyzer = "ngram_analyzer")
    @GenericField(sortable=Sortable.YES)
    private String area_zone;
    
    @FullTextField(analyzer = "ngram_analyzer", searchAnalyzer = "ngram_analyzer")
    @GenericField(sortable= Sortable.YES)
    private String state;
}

步骤3:调整查询语句

现在用普通的match查询就能实现任意位置的部分匹配了:

return Search.session(em).search(PhoneNumber.class)
        .where(f -> f.match()
                .fields("phonenumber", "area_zone", "state")
                .matching(terms))
        .fetchHits(offset, limit);

步骤4:重建索引

修改完字段注解后,需要重新生成索引才能生效,你可以在启动时执行这段代码:

// 重建PhoneNumber表的索引,可放在启动类或者手动触发
@Bean
ApplicationRunner initIndex(EntityManager em) {
    return args -> {
        Search.session(em).massIndexer(PhoneNumber.class)
                .batchSizeToLoadObjects(100)
                .startAndWait();
    };
}

应急方案:通配符查询(不推荐大数据量)

如果不想修改索引配置,也可以直接用通配符查询模拟%xxx%,但这种方式性能很差(通配符开头的查询无法利用索引,会做全表扫描),只适合小数据量场景:

return Search.session(em).search(PhoneNumber.class)
        .where(f -> f.bool()
                .should(f.wildcard().field("phonenumber").matching("*" + terms + "*"))
                .should(f.wildcard().field("area_zone").matching("*" + terms + "*"))
                .should(f.wildcard().field("state").matching("*" + terms + "*"))
        )
        .fetchHits(offset, limit);

说明:*表示任意长度的字符序列,?表示单个字符。

备注:内容来源于stack exchange,提问作者Manchanda. P

火山引擎 最新活动