[Hibernate Search] (5) 解析和条件索引

解析(Analysis)

当一个实体域被Lucene索引时,往往还会经历一个语法分析(Parsing)和转换(Conversion)的步骤,这些步骤被称为解析。在前文中,我们提到过Hibernate Search会默认对字符串类型的实体域进行分词,而这个分词过程就需要用到解析器(Analyzer)。在需要对实体域进行排序的场合,需要禁用这个默认的分词行为。

在解析过程中,还可以借助Apache Solr提供的组件来完成更多的操作。为了弄清楚Solr组件是如何参与到这个过程中并完成更多的操作,需要首先明白Lucene在进行解析时经理的三个步骤:

  • 字符过滤(Character Filtering)
  • 分词(Tokenization)
  • 词条过滤(Token Filtering)

在第一个阶段,会使用零个或者多个字符过滤器(Character Filter)来帮助完成这个过程。它们会在字符这个水平上对数据源进行操作,比如将特定的字符进行替换,删除等等。

在第二个阶段,分词器会根据其定义的规则对数据源进行分词,得到一系列的token。这样做能够让基于关键字的搜索更具效率。

在第三个阶段,会使用零个或者多个词条过滤器来将不需要的token从数据中移除。

经历了以上三个阶段后,数据才会真正地被保存到索引中。 下面对这三个阶段进行详细的介绍。

字符过滤(Character
Filtering)

当需要创建自定义的解析器时,字符过滤的定义是可选的。目前有三个可选的字符过滤器:

  • MappingCharFilterFactory 这个过滤器会将特定的字符或者字符序列根据定义进行替换,比如将1替换成one,将2替换成two等。被替换的字符和替换字符通过java.util.Properties资源文件进行声明,这个资源文件需要置于classpath上。比如1=one就表示将1替换成one。
  • PatternReplaceCharFilter 会基于正则表达式进行操作。正则表达式通过参数pattern传入,而替换的字符通过replacement参数传入。
  • HTMLStripCharFilterFactory 这个过滤器在处理HTML文本时非常有用,它会移除HTML的标签,同时也会将转义字符替换成其原始的形式,比如将&\gt;替换成为>

分词(Tokenization)

和第一阶段及第三阶段使用的过滤器不一样,分词阶段使用的分词器必须有且只有一个。

常用的分词器举例如下:

  • WhitespaceTokenizerFactory 通过简单地对空白字符进行分割来得到结果,比如“Hello World”的分词结果就是Hello和World。
  • LetterTokenizerFactory 这个分词器在WhitespaceTokenizerFactory上更进一步,对于非字母类型的字符也会进行分割,所以“Please don‘t go”这句话会被分割成:Please, don, t, go。
  • StandardTokenizerFactory 这是默认使用的分词器。它会通过空白字符进行分割,同时会忽略掉多余的字符。多余的字符可以是各种标点符号等。比如“it‘s 25.5 degrees outside!!!”的分词结果是:it‘s, 25.5, degrees, and outside。对分词没有特殊要求时,使用默认分词即能够达到较好的效果。

词条过滤(Token
Filtering)

词条过滤可以说是整个解析功能中最丰富多彩的一个阶段了,Solr提供了很多的可用组件。下面列举一些:

  • StopFilterFactory 这个过滤器会将停用词(Stop Word)全部丢弃,同时也会将一些太常见的词语过滤拆掉,因为一般而言这些词语是不会被当做关键字进行查询的,比如a,the,if,for,and,or等词都在此列。更详细的停用词和常见词汇过滤表,可以搜索此Filter的文档。
  • PhoneticFilterFactory 当你使用搜索引擎时,也许已经发现了它能够很智能地自动对一些输入错误进行更正。更正的方法之一就是查询发音相似的词语。它会将发音相似的词语也保存到索引中,因此当输入了错误的单词时,或许仍然能够返回期望的结果。
  • SnowballPorterFilterFactory 这个过滤器名中的Snowball和Porter指代的是词干提取(Stemming)算法,所谓的词干提取,就是将词干从词汇中抽取出来,比如developer和development这两个词汇在进行词干提取后,得到的结果都是develop。因此当你输入develop作为搜索关键字时,包含developer和development的内容也会被返回。这个过滤器需要接受一个language作为参数,比如English。

定义和选择解析器(Analyzer)

定义解析器实际上就是根据解析的三个步骤,定义或者选择每个步骤需要使用的组件的过程。解析器只是一个统称,用来包含它所使用的各种字符过滤器,分词器和词条过滤器。解析器可以通过静态或者动态的方式进行定义:

静态定义解析器

无论使用哪种方式定义解析器,都会使用@AnalyzerDef。比如,我们这里会为App实体类中的description字段定义一个解析器,用来将HTML标签全部移除,同时使用各种词条过滤器来减少被索引内容的噪声:

@AnalyzerDef(
    name="appAnalyzer",
    charFilters={
        @CharFilterDef(factory=HTMLStripCharFilterFactory.class)
    },
    tokenizer=@TokenizerDef(factory=StandardTokenizerFactory.class),
    filters={
        @TokenFilterDef(factory=StandardFilterFactory.class),
        @TokenFilterDef(factory=StopFilterFactory.class),
        @TokenFilterDef(factory=PhoneticFilterFactory.class,
            params = {
                @Parameter(name="encoder", value="DoubleMetaphone")
            }),
        @TokenFilterDef(factory=SnowballPorterFilterFactory.class,
            params = {
                @Parameter(name="language", value="English")
            })
        }
)

可以清晰的发现,在定义中,charFilters用来定义字符过滤器;tokenizer有且只有一个用来定义分词器;filters用来定义词条过滤器。另外,charFilters和filters的执行顺序是通过定义它们的顺序决定的。这一点需要特别注意,以防出现意料之外的结果。

实际上,一个类型可以定义多个解析器。此时对每个域就可以使用不同的解析器来实现具体需求了。

@AnalyzerDefs({
    @AnalyzerDef(name="stripHTMLAnalyzer", ...),
    @AnalyzerDef(name="applyRegexAnalyzer", ...)
})

定义了解析器后,使用@Analyzer注解来使用以上定义的解析器:

@Column(length = 1000)
@Field
@Analyzer(definition="appAnalyzer")
private String description;

@Analyzer注解不仅可以用在单独的域,还可以直接用在类上。此时类中所有被索引的域(即被@Field标注的域)都会使用该解析器。

动态定义解析器

在支持多语言的应用中,语言的切换势必造成需要索引的数据的改变。而根据每种语言的特点,往往需要定义不同的解析器。所以这个定义的过程应该是一个动态的过程。为了实现这一过程,可以使用@AnalyzerDiscriminator注解,它和@Analyzer一样,可以使用在域或者类上。在以下代码中,我们将该注解使用在类之上:

@AnalyzerDefs({
@AnalyzerDef(name="englishAnalyzer", ...),
@AnalyzerDef(name="frenchAnalyzer", ...)
})
@AnalyzerDiscriminator(impl=CustomerReviewDiscriminator.class)
public class CustomerReview {
    // ...

    @Field
    private String language;

    // ...
}

具体而言,CustomerReviewDiscriminator需要实现Discriminator接口来完成解析器的动态选择工作,它通过评论的语言来分别使用英语解析器或者法语解析器:

public class CustomerReviewDiscriminator implements Discriminator {
    public String getAnalyzerDefinitionName(Object value, Object entity, String field) {
        if( entity == null || !(entity instanceof CustomerReview) ) {
            return null;
        }
        CustomerReview review = (CustomerReview) entity;
        if(review.getLanguage() == null) {
            return null;
        } else if(review.getLanguage().equals("en")) {
            return "englishAnalyzer";
        } else if(review.getLanguage().equals("fr")) {
            return "frenchAnalyzer";
        } else {
            return null;
        }
    }
}

当@AnalyzerDiscriminator直接使用在某个域上时,传入到getAnalyzerDefinitionName方法的第一个参数就是当前域,第二个参数entity为null。而像上述代码那样当@AnalyzerDiscriminator用在类型上时,传入的第一个参数是null,第二个参数是当前的实例对象。

当getAnalyzerDefinitionName方法返回的是null时,会告诉Hibernate Search使用默认的解析器。

提升搜索结果的相关度

搜索结果的默认排序是根据结果和搜索关键字之间的相关度进行的。这就意味着如果一个结果中有两个域对搜索关键字匹配成功,而另外一个结果只有一个域匹配成功,那么会认为第一个结果的相关度更高。

Hibernate Search允许我们通过提升某些实体或者某些域的重要程度来对相关度的计算造成影响,从而最终得到更加有意义的搜索结果。这个提升的过程可以是静态的或者动态的,即我们可以根据运行时的环境来动态地调节某些实体和域的重要程度。

索引时的静态提升

可以通过使用@Boost注解来设定实体或者域的重要性权重,@Boost的默认值是1.0F。当设置的值大于1.0F时,表示重要性被提升了,当设置的值小于1.0F时,表示重要性被降低了。

比如当进行如下设置时:

@Boost(2.0f)
public class App implements Serializable {
    // ...

    @Boost(1.5f)
    private String name;

    @Boost(1.2f)
    private String description;

    // ...
}

App的重要性相比那些没有被提升的实体如Device,提升了一倍。除此之外,App实体中的name及description域也被提升了。这些提升会进行合并和叠加,意味着name和description的重要性会发生如下变化:

name:1.0F -> 1.0F * 2.0F * 1.5F = 3.0F description:1.0F -> 1.0F * 2.0F * 1.2F = 2.4F

索引时的动态提升

对于App的评价,我们希望5星评价会有更高的权重。这就是动态提升的一个典型用例,我们需要通过检查评价对象的星级来决定该评价的权重。此时需要使用@DynamicBoost注解结合实现BoostStrategy接口来完成:

@DynamicBoost(impl=FiveStarBoostStrategy.class)
public class CustomerReview
public class FiveStarBoostStrategy implements BoostStrategy {
    public float defineBoost(Object value) {
        if(value == null || !(value instanceof CustomerReview)) {
            return 1;
        }
        CustomerReview customerReview = (CustomerReview) value;
        if(customerReview.getStars() == 5) {
            return 1.5f;
        } else {
            return 1;
        }
    }
}

当@DynamicBoost被应用在类型上时,传入到defineBoost方法中的参数是该实体的当前实例。 当@DynamicBoost被应用在实体域上时,传入到defineBoost方法中的参数是该域的值。

这种模式在Hibernate Search的诸多接口中都有体现,传入的参数值会根据注解是应用到类或者域而发生不同,这一点需要注意。

以上代码的功能很简单,只有当评价是5星评价时才会将该评论的重要性提升一些。

条件索引(Conditional
Indexing)

对于某些实例,我们也许并不想将它们设置为可搜索的。比如典型的当实例的active属性被设置为false时,往往意味着该实例不应该被搜索到。这个时候就需要应用条件索引了,即只有当实体符合某种要求时,它才会被索引。

在@Indexed注解中有一个名为interceptor的属性,它能够帮助我们完成条件索引。为该属性进行配置后,正常的索引过程会被拦截,从而让索引行为更具可控性。

比如,我们可以在App实体中添加一个active属性来控制App的实例是否需要被索引:

@Column
private boolean active;

public App(String name, String image, String description) {
    this.name = name;
    this.image = image;
    this.description = description;
    this.active = true;
}

public boolean isActive() {
    return active;
}

public void setActive(boolean active) {
    this.active = active;
}

在正常情况下,active被设置成true,意味着它是可以被搜索到的。而当它变成false时,即意味着对应的App实例信息应该从索引中移除,通过@Indexed注解的interceptor属性:

@Entity
@Indexed(interceptor = IndexWhenActiveInterceptor.class)
public class App {
    // ...
}

该拦截器IndexWhenActiveInterceptor需要实现EntityIndexingInterceptor接口:

public class IndexWhenActiveInterceptor implements EntityIndexingInterceptor<App> {
    /** Only index newly-created App‘s when they are active */
    public IndexingOverride onAdd(App entity) {
        if(entity.isActive()) {
            return IndexingOverride.APPLY_DEFAULT;
        }
        return IndexingOverride.SKIP;
    }

    public IndexingOverride onDelete(App entity) {
        return IndexingOverride.APPLY_DEFAULT;
    }

    /** Index active App‘s, and remove inactive ones */
    public IndexingOverride onUpdate(App entity) {
        if(entity.isActive()) {
            return IndexingOverride.UPDATE;
        } else {
            return IndexingOverride.REMOVE;
        }
    }   

    public IndexingOverride onCollectionUpdate(App entity) {
        return onUpdate(entity);
    }
}

在EntityIndexingInterceptor接口中定义了四个会被Hibernate Search调用的方法,调用的时机则是根据实体对象的生命周期决定:

  • onAdd: 当实体对象被创建时发生调用。
  • onDelete: 当实体对象从数据库中移除时发生调用。
  • onUpdate: 当已经存在的实体对象被更新时发生调用。
  • onCollectionUpdate: 当实体对象是通过批量的方式完成更新时发生调用。一般而言,直接调用onUpdate方法就可以了。

以上的每个方法都需要返回IndexingOverride枚举类型,它拥有四个枚举值:

  • IndexingOverride.SKIP: 用来告诉Hibernate Search不要为该实例更新Lucene索引。
  • IndexingOverride.REMOVE: 用来告诉Hibernate Search删除该实例的Lucene索引,如果该实例本身就没有被索引,那么不会执行任何操作。
  • IndexingOverride.UPDATE: 用来告诉Hibernate Search为该实例更新Lucene索引,如果该实例没有被索引过,就为它添加索引。
  • IndexingOverride.APPLY_DEFAULT: 代表了默认行为。在onAdd中使用它意味着会为该实例添加索引;在onDelete中使用意味着会将该实例的索引信息移除;在onUpdate或者onCollectionUpdate中使用则是用来更新实例的索引。

了解了EntityIndexingInterceptor接口和IndexingOverride的用法,上述代码的作用就一目了然了。

时间: 2024-10-06 20:05:28

[Hibernate Search] (5) 解析和条件索引的相关文章

[Hibernate Search] (5) 高级查询 - 过滤,投影和分面

高级查询 在介绍了更多的高级映射功能之后,是时候回顾一下之前介绍过的查询功能了,看看如何借助这些高级的映射功能来使用一些高级的查询功能.本文会通过以下几个方面进行介绍: 如何在不和数据库进行任何交互的前提下,借助Lucene的力量来动态的筛选结果 如何通过使用基于投影(Projection)的查询来获取需要的属性,从而避免与数据库的交互 如何使用分面搜索(Faceted Search)对搜索结果进行划分 如何使用查询时提升(Boosting) 如何给查询设置时间限制 过滤(Filtering)

[Hibernate Search] (3) 基础查询

基础查询 眼下我们仅仅用到了基于keyword的查询,实际上Hibenrate Search DSL还提供了其他的查询方式,以下我们就来一探到底. 映射API和查询API 对于映射API.我们能够通过使用Hibernate提供的注解来完毕映射工作.同一时候我们也能够使用JPA提供的注解来完毕.类似的,对于查询API,我们也能够从Hibernate和JPA提供的查询API中进行选择. 每种方式都有它的长处和缺点,比方当我们使用Hibernate提供的查询API时,意味着能够使用很多其它的特性,毕竟

Hibernate Search JPA + Lucene 简单应用实例

假设现在有这么一个应用场景.数据库里存放了大量文章,我们想要通过输入关键字,从数据库中检索出相关的文章. 1.建立Article Entity: public class Article { private String id;            //ID private String title;         //标题 private String content;       //内容         private String isPublication; //发布状态 publ

S2SH+Hibernate search出现的问题

一  java.lang.NoSuchMethodError: org.hibernate.engine.transaction.spi.TransactionEnvironment.getJtaPlatform()Lorg/hibernate/service/jta/platform/spi/JtaPlatform 相信大家遇到这个问题一定是累觉不爱了吧,呵呵.本人qq1413557667 出现这个问题的原因是不同版本的(例如4.2跟4.5)的hibernate-core jar包里面In H

[Hibernate Search] 实体类型的映射

实体类型映射 在上一篇文章中,简要介绍了如何使用Hibernate Search来对一个实体进行全文搜索. 然而,在真实的应用中,实体与实体之间的关系也许更为复杂.为了对复杂实体进行搜索,就需要让底层的Lucene查询也能够理解这些关系. 下图反映了Database,Hibernate,Hibernate Search和Lucene之间的关系: 域映射选项(Field Mapping Options) 我们已经知道@Field注解用来让某个域可以被全文搜索到. 实际上,在添加该注解后,Hiber

[Hibernate Search] 初识Hibernate Search

初识Hibernate Search 要让你的应用具备Hibernate Search赋予的全文搜索的能力,需要做以下三件事: 给项目添加必要的依赖和配置信息 给你的实体类添加必要的信息,从而让Lucene知道如何对它们进行索引(Indexing) 在需要的地方使用符合Hibernate Search规范的查询来完成业务逻辑 对于需要添加的依赖信息,会在以后进行介绍.我们首先来看看代码该如何写. 我们会使用一个类似于经典的"Java Pet Store"那样的Web应用来展示Hiber

Hibernate 3 深度解析--苏春波

Hibernate 3 深度解析 Hibernate 作为 Java ORM 模式的优秀开源实现, 当下已经成为一种标准,为饱受 JDBC 折磨的 Java 开发者带来了“福音.快速的版本更新,想开发者所想,Hibernate 的开发团队俨然就是我们开发者的知音呀.随着 Hibernate 的用户群的越来越壮大,Hibernate 的功能也变得更加强大和健硕起来,下面就对于我们比较常用和关心的一些内容做一些比较深入的探讨,我们的宗旨就是不但会灵活应用,还能知其所以然,不要求大家一定要把 Hibe

[Hibernate Search] (4) 实体类型的高级映射功能

高级映射 前面介绍的可搜索的域基本上都是字符串类型,实际上可搜索的类型是非常丰富的. 本文会介绍以下几个方面的内容: Lucene对实体进行索引的过程 借助Solr组件对这个过程的改进 修改域的重要程度,从而让基于相关度的排序更加有意义 动态决定是否对一个实体类型进行索引 桥接器(Bridges) 实体类型中可以使用的类型是无穷无尽的,但是对于Lucene索引而言,任何类型归根到底都会以字符串来表示.所以,在对实体的域进行索引时,这些域最终需要被转换为字符串类型的对象. 在Hibernate S

将Hibernate Search集成进已有项目中,实现全文检索功能

本来是准备使用Lucene的但是新版本的API过于繁琐,最后还是决定使用Hibernate Search来实现全文检索.这篇博文以我以前做的博客为例来实现全文检索. 1.修改Hibernate配置文件,因为我的系统采用的是SSH2来开发的所以我修改的是spring配置文件 <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">