Hibernate Search:如何正确使用通配符?
Hibernate Search: How to use wildcards correctly?
我有以下查询来按全名搜索特定医疗中心的患者:
MustJunction mj = qb.bool().must(qb.keyword()
.onField("medicalCenter.id")
.matching(medicalCenter.getId())
.createQuery());
for(String term: terms)
if(!term.equals(""))
mj.must(qb.keyword()
.onField("fullName")
.matching(term+"*")
.createQuery());
它运行良好,但前提是用户键入患者的完整名字 and/or 姓氏。
但是,即使用户键入名字或姓氏的一部分,我也希望能够正常工作。
例如,如果有一位名叫 "Bilbo Baggins" 的患者,当用户键入 "Bilbo Baggins, "Bilbo、"Baggins"、 或时,我希望搜索能找到他即使他只输入 "Bil" 或 "Bag"
为此,我修改了上面的查询,如下所示:
MustJunction mj = qb.bool().must(qb.keyword()
.onField("medicalCenter.id")
.matching(medicalCenter.getId())
.createQuery());
for(String term: terms)
if(!term.equals(""))
mj.must(qb.keyword()
.wildcard()
.onField("fullName")
.matching(term+"*")
.createQuery());
请注意我是如何在调用 onField() 之前添加 wildcard() 函数的
但是,这会中断搜索并且 returns 没有结果。我做错了什么?
Hibernate Search 6 的更新答案
简短回答:不要使用通配符查询,使用带有 EdgeNGramFilterFactory
的自定义分析器。另外,不要尝试自己分析查询(这是您通过将查询拆分为术语所做的):Lucene 会做得更好(使用 WhitespaceTokenizerFactory
、ASCIIFoldingFilterFactory
和 LowercaseFilterFactory
特别是)。
长答案:
通配符查询作为一次性问题的快速简便的解决方案很有用,但它们不是很灵活并且很快就会达到极限。特别是,正如@femtoRgon 提到的,这些查询未被分析(至少 not completely, and not with every backend),因此大写查询不会匹配小写名称,例如。
Lucene/Elasticsearch 世界中大多数问题的经典解决方案是在索引时间和查询时间(不一定相同)使用特制的分析器。在你的情况下,你会想要使用这种分析器(一种用于索引,一种用于搜索):
Lucene:
public class MyAnalysisConfigurer implements LuceneAnalysisConfigurer {
@Override
public void configure(LuceneAnalysisConfigurationContext context) {
context.analyzer( "autocomplete_indexing" ).custom()
.tokenizer( WhitespaceTokenizerFactory.class )
// Lowercase all characters
.tokenFilter( LowerCaseFilterFactory.class )
// Replace accented characters by their simpler counterpart (è => e, etc.)
.tokenFilter( ASCIIFoldingFilterFactory.class )
// Generate prefix tokens
.tokenFilter( EdgeNGramFilterFactory.class )
.param( "minGramSize", "1" )
.param( "maxGramSize", "10" );
// Same as "autocomplete-indexing", but without the edge-ngram filter
context.analyzer( "autocomplete_search" ).custom()
.tokenizer( WhitespaceTokenizerFactory.class )
// Lowercase all characters
.tokenFilter( LowerCaseFilterFactory.class )
// Replace accented characters by their simpler counterpart (è => e, etc.)
.tokenFilter( ASCIIFoldingFilterFactory.class );
}
}
弹性搜索:
public class MyAnalysisConfigurer implements ElasticsearchAnalysisConfigurer {
@Override
public void configure(ElasticsearchAnalysisConfigurationContext context) {
context.analyzer( "autocomplete_indexing" ).custom()
.tokenizer( "whitespace" )
.tokenFilters( "lowercase", "asciifolding", "autocomplete_edge_ngram" );
context.tokenFilter( "autocomplete_edge_ngram" )
.type( "edge_ngram" )
.param( "min_gram", 1 )
.param( "max_gram", 10 );
// Same as "autocomplete_indexing", but without the edge-ngram filter
context.analyzer( "autocomplete_search" ).custom()
.tokenizer( "whitespace" )
.tokenFilters( "lowercase", "asciifolding" );
}
}
索引分析器会将“Mauricio Ubilla Carvajal”转换为这个标记列表:
- 米
- ma
- 毛
- 毛
- 毛里
- 莫里克
- 毛里奇
- 毛里西奥
- 你
- ub
- ...
- 乌比拉
- c
- 约
- ...
- 卡瓦哈尔
并且查询分析器会将查询“mau UB”转换为 [“mau”、“ub”],这将匹配索引名称(两个标记都存在于索引中)。
请注意,您显然必须将分析器分配给该字段。
在 Hibernate Search 6 中,这很容易,您可以 assign a searchAnalyzer
to a field,与索引分析器分开:
@FullTextField(analyzer = "autocomplete_indexing", searchAnalyzer = "autocomplete_search")
然后你可以很容易地搜索,比如 simpleQueryString
predicate:
List<Patient> hits = searchSession.search( Patient.class )
.where( f -> f.simpleQueryString().field( "fullName" )
.matching( "mau + UB" ) )
.fetchHits( 20 );
或者,如果您不需要额外的语法和运算符,match
predicate 应该可以:
List<Patient> hits = searchSession.search( Patient.class )
.where( f -> f.match().field( "fullName" )
.matching( "mau UB" ) )
.fetchHits( 20 );
Hibernate Search 5 的原始答案
简短回答:不要使用通配符查询,使用带有 EdgeNGramFilterFactory
的自定义分析器。另外,不要尝试自己分析查询(这是您通过将查询拆分为术语所做的):Lucene 会做得更好(使用 WhitespaceTokenizerFactory
、ASCIIFoldingFilterFactory
和 LowercaseFilterFactory
特别是)。
长答案:
通配符查询作为一次性问题的快速简便的解决方案很有用,但它们不是很灵活并且很快就会达到极限。特别是,正如@femtoRgon 提到的,这些查询没有被分析,因此大写查询不会匹配小写名称,例如。
Lucene 世界中大多数问题的经典解决方案是在索引时间和查询时间(不一定相同)使用特制的分析器。在你的情况下,你会想在索引时使用这种分析器:
@AnalyzerDef(name = "edgeNgram",
tokenizer = @TokenizerDef(factory = WhitespaceTokenizerFactory.class),
filters = {
@TokenFilterDef(factory = ASCIIFoldingFilterFactory.class), // Replace accented characeters by their simpler counterpart (è => e, etc.)
@TokenFilterDef(factory = LowerCaseFilterFactory.class), // Lowercase all characters
@TokenFilterDef(
factory = EdgeNGramFilterFactory.class, // Generate prefix tokens
params = {
@Parameter(name = "minGramSize", value = "1"),
@Parameter(name = "maxGramSize", value = "10")
}
)
})
而这种查询时:
@AnalyzerDef(name = "edgeNGram_query",
tokenizer = @TokenizerDef(factory = WhitespaceTokenizerFactory.class),
filters = {
@TokenFilterDef(factory = ASCIIFoldingFilterFactory.class), // Replace accented characeters by their simpler counterpart (è => e, etc.)
@TokenFilterDef(factory = LowerCaseFilterFactory.class) // Lowercase all characters
})
索引分析器会将“Mauricio Ubilla Carvajal”转换为这个标记列表:
- 米
- ma
- 毛
- 毛
- 毛里
- 莫里克
- 毛里奇
- 毛里西奥
- 你
- ub
- ...
- 乌比拉
- c
- 约
- ...
- 卡瓦哈尔
并且查询分析器会将查询“mau UB”转换为 [“mau”、“ub”],这将匹配索引名称(两个标记都存在于索引中)。
请注意,您显然必须将分析器分配给该字段。对于索引部分,它是使用 @Analyzer
annotation 完成的。
对于查询部分,您必须在查询生成器上使用 overridesForField
,如图所示 :
QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(Hospital.class)
.overridesForField( "name", "edgeNGram_query" )
.get();
// Then it's business as usual
另请注意,在 Hibernate Search 5 中,Elasticsearch 分析器定义仅在实际分配给索引时由 Hibernate Search 生成。因此,默认情况下不会生成查询分析器定义,Elasticsearch 会抱怨它不知道分析器。这是一个解决方法:https://discourse.hibernate.org/t/cannot-find-the-overridden-analyzer-when-using-overridesforfield/1043/4?u=yrodiere
我有以下查询来按全名搜索特定医疗中心的患者:
MustJunction mj = qb.bool().must(qb.keyword()
.onField("medicalCenter.id")
.matching(medicalCenter.getId())
.createQuery());
for(String term: terms)
if(!term.equals(""))
mj.must(qb.keyword()
.onField("fullName")
.matching(term+"*")
.createQuery());
它运行良好,但前提是用户键入患者的完整名字 and/or 姓氏。
但是,即使用户键入名字或姓氏的一部分,我也希望能够正常工作。
例如,如果有一位名叫 "Bilbo Baggins" 的患者,当用户键入 "Bilbo Baggins, "Bilbo、"Baggins"、 或时,我希望搜索能找到他即使他只输入 "Bil" 或 "Bag"
为此,我修改了上面的查询,如下所示:
MustJunction mj = qb.bool().must(qb.keyword()
.onField("medicalCenter.id")
.matching(medicalCenter.getId())
.createQuery());
for(String term: terms)
if(!term.equals(""))
mj.must(qb.keyword()
.wildcard()
.onField("fullName")
.matching(term+"*")
.createQuery());
请注意我是如何在调用 onField() 之前添加 wildcard() 函数的
但是,这会中断搜索并且 returns 没有结果。我做错了什么?
Hibernate Search 6 的更新答案
简短回答:不要使用通配符查询,使用带有 EdgeNGramFilterFactory
的自定义分析器。另外,不要尝试自己分析查询(这是您通过将查询拆分为术语所做的):Lucene 会做得更好(使用 WhitespaceTokenizerFactory
、ASCIIFoldingFilterFactory
和 LowercaseFilterFactory
特别是)。
长答案:
通配符查询作为一次性问题的快速简便的解决方案很有用,但它们不是很灵活并且很快就会达到极限。特别是,正如@femtoRgon 提到的,这些查询未被分析(至少 not completely, and not with every backend),因此大写查询不会匹配小写名称,例如。
Lucene/Elasticsearch 世界中大多数问题的经典解决方案是在索引时间和查询时间(不一定相同)使用特制的分析器。在你的情况下,你会想要使用这种分析器(一种用于索引,一种用于搜索):
Lucene:
public class MyAnalysisConfigurer implements LuceneAnalysisConfigurer {
@Override
public void configure(LuceneAnalysisConfigurationContext context) {
context.analyzer( "autocomplete_indexing" ).custom()
.tokenizer( WhitespaceTokenizerFactory.class )
// Lowercase all characters
.tokenFilter( LowerCaseFilterFactory.class )
// Replace accented characters by their simpler counterpart (è => e, etc.)
.tokenFilter( ASCIIFoldingFilterFactory.class )
// Generate prefix tokens
.tokenFilter( EdgeNGramFilterFactory.class )
.param( "minGramSize", "1" )
.param( "maxGramSize", "10" );
// Same as "autocomplete-indexing", but without the edge-ngram filter
context.analyzer( "autocomplete_search" ).custom()
.tokenizer( WhitespaceTokenizerFactory.class )
// Lowercase all characters
.tokenFilter( LowerCaseFilterFactory.class )
// Replace accented characters by their simpler counterpart (è => e, etc.)
.tokenFilter( ASCIIFoldingFilterFactory.class );
}
}
弹性搜索:
public class MyAnalysisConfigurer implements ElasticsearchAnalysisConfigurer {
@Override
public void configure(ElasticsearchAnalysisConfigurationContext context) {
context.analyzer( "autocomplete_indexing" ).custom()
.tokenizer( "whitespace" )
.tokenFilters( "lowercase", "asciifolding", "autocomplete_edge_ngram" );
context.tokenFilter( "autocomplete_edge_ngram" )
.type( "edge_ngram" )
.param( "min_gram", 1 )
.param( "max_gram", 10 );
// Same as "autocomplete_indexing", but without the edge-ngram filter
context.analyzer( "autocomplete_search" ).custom()
.tokenizer( "whitespace" )
.tokenFilters( "lowercase", "asciifolding" );
}
}
索引分析器会将“Mauricio Ubilla Carvajal”转换为这个标记列表:
- 米
- ma
- 毛
- 毛
- 毛里
- 莫里克
- 毛里奇
- 毛里西奥
- 你
- ub
- ...
- 乌比拉
- c
- 约
- ...
- 卡瓦哈尔
并且查询分析器会将查询“mau UB”转换为 [“mau”、“ub”],这将匹配索引名称(两个标记都存在于索引中)。
请注意,您显然必须将分析器分配给该字段。
在 Hibernate Search 6 中,这很容易,您可以 assign a searchAnalyzer
to a field,与索引分析器分开:
@FullTextField(analyzer = "autocomplete_indexing", searchAnalyzer = "autocomplete_search")
然后你可以很容易地搜索,比如 simpleQueryString
predicate:
List<Patient> hits = searchSession.search( Patient.class )
.where( f -> f.simpleQueryString().field( "fullName" )
.matching( "mau + UB" ) )
.fetchHits( 20 );
或者,如果您不需要额外的语法和运算符,match
predicate 应该可以:
List<Patient> hits = searchSession.search( Patient.class )
.where( f -> f.match().field( "fullName" )
.matching( "mau UB" ) )
.fetchHits( 20 );
Hibernate Search 5 的原始答案
简短回答:不要使用通配符查询,使用带有 EdgeNGramFilterFactory
的自定义分析器。另外,不要尝试自己分析查询(这是您通过将查询拆分为术语所做的):Lucene 会做得更好(使用 WhitespaceTokenizerFactory
、ASCIIFoldingFilterFactory
和 LowercaseFilterFactory
特别是)。
长答案:
通配符查询作为一次性问题的快速简便的解决方案很有用,但它们不是很灵活并且很快就会达到极限。特别是,正如@femtoRgon 提到的,这些查询没有被分析,因此大写查询不会匹配小写名称,例如。
Lucene 世界中大多数问题的经典解决方案是在索引时间和查询时间(不一定相同)使用特制的分析器。在你的情况下,你会想在索引时使用这种分析器:
@AnalyzerDef(name = "edgeNgram",
tokenizer = @TokenizerDef(factory = WhitespaceTokenizerFactory.class),
filters = {
@TokenFilterDef(factory = ASCIIFoldingFilterFactory.class), // Replace accented characeters by their simpler counterpart (è => e, etc.)
@TokenFilterDef(factory = LowerCaseFilterFactory.class), // Lowercase all characters
@TokenFilterDef(
factory = EdgeNGramFilterFactory.class, // Generate prefix tokens
params = {
@Parameter(name = "minGramSize", value = "1"),
@Parameter(name = "maxGramSize", value = "10")
}
)
})
而这种查询时:
@AnalyzerDef(name = "edgeNGram_query",
tokenizer = @TokenizerDef(factory = WhitespaceTokenizerFactory.class),
filters = {
@TokenFilterDef(factory = ASCIIFoldingFilterFactory.class), // Replace accented characeters by their simpler counterpart (è => e, etc.)
@TokenFilterDef(factory = LowerCaseFilterFactory.class) // Lowercase all characters
})
索引分析器会将“Mauricio Ubilla Carvajal”转换为这个标记列表:
- 米
- ma
- 毛
- 毛
- 毛里
- 莫里克
- 毛里奇
- 毛里西奥
- 你
- ub
- ...
- 乌比拉
- c
- 约
- ...
- 卡瓦哈尔
并且查询分析器会将查询“mau UB”转换为 [“mau”、“ub”],这将匹配索引名称(两个标记都存在于索引中)。
请注意,您显然必须将分析器分配给该字段。对于索引部分,它是使用 @Analyzer
annotation 完成的。
对于查询部分,您必须在查询生成器上使用 overridesForField
,如图所示
QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(Hospital.class)
.overridesForField( "name", "edgeNGram_query" )
.get();
// Then it's business as usual
另请注意,在 Hibernate Search 5 中,Elasticsearch 分析器定义仅在实际分配给索引时由 Hibernate Search 生成。因此,默认情况下不会生成查询分析器定义,Elasticsearch 会抱怨它不知道分析器。这是一个解决方法:https://discourse.hibernate.org/t/cannot-find-the-overridden-analyzer-when-using-overridesforfield/1043/4?u=yrodiere