【问题标题】:Hibernate Search: How to use wildcards correctly?Hibernate Search:如何正确使用通配符?
【发布时间】:2018-04-04 15:02:18
【问题描述】:

我有以下查询来按全名搜索特定医疗中心的患者:

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());

它运行良好,但前提是用户输入患者的完整名字和/或姓氏。

但是,即使用户键入名字或姓氏的一部分,我也想让 if 工作。

例如,如果有一个名为“Bilbo Baggins”的患者,我希望搜索能够找到他,当用户键入“Bilbo Baggins”、“Bilbo”、“Baggins”、或者即使他只键入“Bil " 或 "包"

为了实现这一点,我将上面的查询修改如下:

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() 函数的

但是,这会中断搜索并且不返回任何结果。我做错了什么?

【问题讨论】:

  • 不分析通配符查询。简单的解决方案是小写term

标签: hibernate lucene wildcard hibernate-search


【解决方案1】:

Hibernate Search 6 的更新答案

简答:不要使用通配符查询,使用带有EdgeNGramFilterFactory 的自定义分析器。另外,不要尝试自己分析查询(这就是您通过将查询拆分为术语所做的):Lucene 会做得更好(特别是 WhitespaceTokenizerFactoryASCIIFoldingFilterFactoryLowercaseFilterFactory)。

长答案:

通配符查询作为一次性问题的快速简便的解决方案非常有用,但它们不是很灵活,而且很快就会达到极限。特别是,正如@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”转换为以下标记列表:

  • 毛尔
  • 毛里
  • 莫瑞克
  • 毛里奇
  • 毛里西奥
  • 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 会做得更好(特别是 WhitespaceTokenizerFactoryASCIIFoldingFilterFactoryLowercaseFilterFactory)。

长答案:

通配符查询作为一次性问题的快速简便的解决方案非常有用,但它们不是很灵活,而且很快就会达到极限。特别是,正如@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”转换为以下标记列表:

  • 毛尔
  • 毛里
  • 莫瑞克
  • 毛里奇
  • 毛里西奥
  • ub
  • ...
  • 乌比拉
  • c
  • 加州
  • ...
  • 卡瓦哈尔

查询分析器会将查询“mau UB”转换为[“mau”,“ub”],这将匹配索引名称(两个标记都存在于索引中)。

请注意,您显然必须将分析器分配给该字段。对于索引部分,使用@Analyzer annotation 完成。 对于查询部分,您必须在查询生成器上使用overridesForField,如here 所示:

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

【讨论】:

  • 我将查询拆分为术语的原因是因为我希望所有术语都是强制性的。例如,如果我搜索“Mauricio Ubilla”,我希望结果匹配两个词,(所有名字为“Mauricio”和姓氏“Ubilla”的患者,而不是所有名字为“Mauricio”或姓氏“Ubilla”的患者)。如果有其他方法可以做到这一点,如果你能告诉我,我将不胜感激。
  • 要使用您提到的过滤器,我必须导入 Solr jar 对吗?我已经看到很多使用这种解决方案的示例,我不知道为什么它们在 Hibernate Search 包中被包含为“可选”,而它们似乎是解决这个问题的首选方法。我正在修复一些依赖问题以尝试您的解决方案,非常感谢。
  • 非常感谢!它工作得很好。我只有最后一个问题:我想在我的数据源类中定义“edgeNGram_query”分析器,但由于某种原因,当我在其中放置@AnalyzerDef 注释时,查询代码找不到它的定义(在运行时)。但是,如果我将定义放在我的 Patient 类中,它就可以正常工作。您知道为什么 Hibernate 搜索会在我的 Patient 类中找到 Analyzer,而不是在我的 Datasource 类中吗?
  • "我拆分查询的原因..." => 您可能还想看看 5.8 中引入的simple query strings,尤其是withAndAsDefaultOperator 方法。它不适合您的用例(因为它允许用户使用自己的运算符,例如 *+),但它可能适合您。
  • "我想在我的 Datasource 类中定义 edgeNGram_query Analyzer" => 我不知道你的 Datasource 类是什么,但请注意 AnalyzerDef 注释仅在索引类和索引包上检查类。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-23
  • 2019-05-31
  • 1970-01-01
  • 1970-01-01
  • 2018-06-04
  • 2022-01-22
相关资源
最近更新 更多