Spring Boot整合Hibernate Search实现精准前缀/后缀匹配问题
嘿,我来帮你搞定这个精准匹配的需求!你当前遇到的问题,核心原因是你用的分词器(StandardTokenizer)会把字段内容拆分成多个词,而wildcard查询是在分词后的单个词上进行匹配,不是针对整个字段的完整内容。咱们一步步来解决:
1. 先调整实体类:新增专用匹配字段
为了实现整字段的精准前缀/后缀匹配,我们需要给establishmentNameEn新增两个专用字段:一个用于整字段的小写匹配,另一个用于反向字符串的匹配(处理后缀)。同时定义对应的分析器:
@AnalyzerDef(name = "english", tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class), filters = { @TokenFilterDef(factory = StandardFilterFactory.class), @TokenFilterDef(factory = LowerCaseFilterFactory.class), }) // 新增不分词的小写分析器:把整个字符串作为一个词,同时转成小写 @AnalyzerDef(name = "exact_lowercase", tokenizer = @TokenizerDef(factory = KeywordTokenizerFactory.class), filters = { @TokenFilterDef(factory = LowerCaseFilterFactory.class) }) @Indexed @Entity @Table(name = "DIRECTORY") public class DirectoryEntity { @Analyzer(definition = "english") @Field(store = Store.YES) @Column(name = "ESTABLISHMENT_NAME_EN") private String establishmentNameEn; // 用于整字段前缀/后缀匹配的字段(不分词、小写) @Field(name = "establishmentNameEn_exact", analyzer = @Analyzer(definition = "exact_lowercase")) private String establishmentNameEn; // 用于后缀匹配的反向字符串字段(自动生成,无需存到数据库) @Field(name = "establishmentNameEn_reversed", analyzer = @Analyzer(definition = "exact_lowercase")) @Transient public String getEstablishmentNameEnReversed() { if (establishmentNameEn == null) { return null; } return new StringBuilder(establishmentNameEn.toLowerCase()).reverse().toString(); } // getter和setter(注意给establishmentNameEn的setter,确保字段同步) public void setEstablishmentNameEn(String establishmentNameEn) { this.establishmentNameEn = establishmentNameEn; } public String getEstablishmentNameEn() { return establishmentNameEn; } }
2. 实现严格前缀匹配
针对.Ali这种“字段必须以Ali开头”的需求,我们用范围查询代替wildcard,这样性能更好,而且能精准匹配整字段的前缀:
// 输入的前缀term,比如"Ali" String term = "Ali"; String lowerTerm = term.toLowerCase(); // 范围查询:匹配所有以lowerTerm开头的字符串(\uffff是Unicode最大字符,确保覆盖所有前缀变体) query = queryBuilder.range().onField("establishmentNameEn_exact") .from(lowerTerm) .to(lowerTerm + "\uffff") .createQuery();
这个查询只会返回establishmentNameEn转小写后严格以ali开头的文档,像AlAli转小写是alali,不在ali到ali\uffff的范围内,自然不会被匹配,完全符合你的需求。
3. 实现严格后缀匹配
针对Kamran.这种“字段必须以Kamran结尾”的需求,直接用开头带*的wildcard性能很差,我们用反向字符串+前缀匹配的技巧:
// 输入的后缀term,比如"Kamran" String term = "Kamran"; String lowerTerm = term.toLowerCase(); // 把term反转,比如"Kamran"变成"narmak" String reversedTerm = new StringBuilder(lowerTerm).reverse().toString(); // 查询反转后的字段前缀,等价于原字段的后缀匹配 query = queryBuilder.range().onField("establishmentNameEn_reversed") .from(reversedTerm) .to(reversedTerm + "\uffff") .createQuery();
原理很简单:原字段如果是xxxKamran,反转后是narmakxxx;我们查询反转字段以narmak开头的文档,就等价于原字段以Kamran结尾的文档。而Kamranullah反转后是hallunarmak,不在narmak到narmak\uffff的范围内,不会被匹配,完美满足要求。
为什么你原来的代码不行?
你之前用keyword().wildcard().matching(term + "*"),但因为establishmentNameEn用的是StandardTokenizer,它会把字符串拆分成多个词(比如Ali Baba拆成ali和baba),wildcard查询会匹配任何分词后的词以Ali开头的文档,比如AlAli会被拆成alali一个词,这个词的前缀是ali,所以会被匹配,这就是你得到混合结果的原因。
内容的提问来源于stack exchange,提问作者Kamran Ullah

