7.4. 将实体映射到索引结构


7.4.1. 映射实体

索引实体所需的所有元数据信息都通过注释进行描述,因此无需 XML 映射文件。您仍然可以将 Hibernate 映射文件用于基本 Hibernate 配置,但是必须通过注释来表示 Hibernate Search 特定配置。

7.4.1.1. 基本映射

让我们从最常用于映射实体的注释开始。

基于 Lucene 的查询 API 使用以下常见注解来映射实体:

  • @Indexed
  • @field
  • @NumericField
  • @Id

7.4.1.2. @Indexed

最重要的是,我们必须将持久类声明为可索引。这可以通过使用 @Indexed 为类添加注解(索引流程将忽略未注释 @Indexed 的 所有实体):

@Entity
@Indexed
public class Essay {
...
}
Copy to Clipboard Toggle word wrap

您可以选择指定 @Indexed 注释的 index 属性,以更改索引的默认名称。

7.4.1.3. @field

对于实体的每个属性(或属性),您可以描述如何对其进行索引。默认(不存在)表示索引过程中会忽略该属性。

注意

在 Hibernate Search 5 之前,只有通过 @NumericField 明确请求时,才会选择数字字段编码。从 Hibernate Search 5 开始,系统将自动为数字类型选择此编码。为避免数字编码,您可以通过 @Field.bridge 或 @Field Bridge 明确指定非数字字段网桥。软件包 org.hibernate.search.bridge.builtin 包含一组网桥,这些网桥编号编码为字符串,如 org.hibernate.search.bridge.builtin.IntegerBridge

@field 确实将属性声明 为索引,并允许通过设置以下一个或多个属性来配置索引过程的多个方面:

  • Name : describe 在哪个名称下,该属性应存储在 Lucene 文档中。默认值为属性名称(遵循 JavaBeans 约定)
  • 存储 :描述属性是否存储在 Lucene 索引中。您可以存储值 Store.YES (在索引中消耗更多空间,但允许 投射,以压缩方式 存储 Store.COMPRESS (这确实消耗更多 CPU),或者避免任何存储 Store.NO (这是默认值)。存储属性时,您可以从 Lucene 文档检索其原始值。这与元素是否索引无关。
  • index :描述属性是否索引。不同的值是 index .NO,这表示它不会被索引,不能被查询和 Index.YES 找到,即元素会被索引并可以搜索。默认值为 Index.YESindex. no 在不需要可搜索属性但应该可用于投射的情形中,将非常有用。

    注意

    index.NO 与 Analyze .YESNorms.YES 结合使用,因为 分析和 强制要求 对该属性进行索引。

  • 分析 :确定属性是否被分析(Analyze.YES)还是不分析(Analyze.NO)。默认值为 Analyze .YES

    注意

    是否要分析属性取决于您是否希望按原样搜索元素,或按其包含的词语搜索。分析文本字段会有意义,但可能不是日期字段。

    注意

    不得 分析用于排序的字段。

  • 强制 :描述索引时间提升信息应存储(Norms.YES)还是不存储(Norms.NO)。不存储存储会节省大量内存,但没有任何索引时间提高可用信息。默认值为 Norms.YES
  • termVector :描述术语频率对的集合。这个属性允许在索引过程中将术语向量存储在文档中。默认值为 TermVector.NO

    此属性的不同值有:

    Expand
    定义

    TermVector.YES

    存储每个文档的术语向量。这会生成两个同步的数组,一个包含文档术语,另一个包含术语的频率。

    TermVector.NO

    不要存储术语向量。

    TermVector.WITH_OFFSETS

    存储术语向量和令牌偏移信息。这与 TermVector.YES 加上它包含术语的起始和结束偏移位置信息相同。

    TermVector.WITH_POSITIONS

    存储术语向量和令牌位置信息。这与 TermVector.YES 相同,还包含每一次在文档中出现某个术语的规范。

    TermVector.WITH_POSITION_OFFSETS

    存储术语向量、令牌位置和偏移信息。这是 YES、WITH_OFFSETS 和 WITH_POSITIONS 的组合。

  • indexNullAs : Per default null 值将被忽略且未索引。不过,您可以使用 indexNullAs 指定一个字符串,该字符串将被插入为 null 值的令牌。默认情况下,此值设置为 Field。DO_NOT_INDEX_NULL 表示不应索引 null 值。您可以将这个值设置为 Field.DEFAULT_NULL_TOKEN,以指示应使用默认的 令牌。可以使用 hibernate.search.default_null_token 在配置中指定此默认 令牌。如果未设置此属性,并且指定了 Field.DEFAULT_NULL_TOKEN,则字符串 "null" 将用作默认值。

    注意

    使用 indexNullAs 参数时,务必要在搜索查询中使用相同的令牌来搜索 null 值。此外,建议仅在未分析的字段(分析.NO)中使用此功能。

    警告

    在实施自定义 FieldBridge 或 TwoWayFieldBridge 时,开发人员可以处理 null 值的索引(请参阅 JavaDocs LuceneOptions.indexNullAs())。

7.4.1.4. @NumericField

@Field 有一个相应的注释,名为 @NumericField,其范围与 @Field 或 @DocumentId 相同。它可用于 Integer、Long、Float 和 Double 属性。在索引时,将使用 Trie 结构对该值进行索引。当属性索引为数字字段时,它实现了有效的范围查询和排序,与对标准 @Field 属性执行相同的查询相比,它实现了高效的范围查询和排序。@NumericField 注释接受以下参数:

Expand
定义

forField

(可选)指定将索引为数字的相关 @Field 的名称。只有 属性包含超过 @Field 声明时,才强制使用它

exactStep

(可选)更改 Trie 结构存储在索引中的方式。较小的精度Steps会导致更多磁盘空间使用量、更快的范围和排序查询。较大的值会导致使用空间减少,范围查询性能更接近普通 @Fields 中的范围查询。默认值为 4。

@NumericField 仅支持 Double、Long、Integer 和 Float。其他数字类型无法利用 Lucene 中的类似功能,因此其余类型应使用默认或自定义 TwoWayFieldBridge 进行字符串编码。

假设您可以在类型转换过程中处理近似值,可以使用自定义 NumericFieldBridge:

示例:定义自定义数字FieldBridge

public class BigDecimalNumericFieldBridge extends NumericFieldBridge {
   private static final BigDecimal storeFactor = BigDecimal.valueOf(100);

   @Override
   public void set(String name, Object value, Document document, LuceneOptions luceneOptions) {
      if ( value != null ) {
         BigDecimal decimalValue = (BigDecimal) value;
         Long indexedValue = Long.valueOf( decimalValue.multiply( storeFactor ).longValue() );
         luceneOptions.addNumericFieldToDocument( name, indexedValue, document );
      }
   }

    @Override
    public Object get(String name, Document document) {
        String fromLucene = document.get( name );
        BigDecimal storedBigDecimal = new BigDecimal( fromLucene );
        return storedBigDecimal.divide( storeFactor );
    }

}
Copy to Clipboard Toggle word wrap

7.4.1.5. @Id

最后,实体的 id (识别符)属性是 Hibernate Search 使用的特殊属性,用于确保给定实体的索引唯一性。按照设计,必须存储 id,且不能进行令牌化。要将属性标记为索引标识符,可使用 @DocumentId 注释。如果您使用的是 Jakarta Persistence 并且指定了 @Id,您可以省略 @DocumentId。选定的实体标识符也可用作文档标识符。

Infinispan Query 使用实体的 id 属性来确保索引唯一标识。按照设计,ID 会存储,且不得转换为令牌。要将属性标记为索引 ID,可使用 @DocumentId 注释。

示例:指定索引的属性

@Entity
@Indexed
public class Essay {
    ...
    @Id
    @DocumentId
    public Long getId() { return id; }

    @Field(name="Abstract", store=Store.YES)
    public String getSummary() { return summary; }

    @Lob
    @Field
    public String getText() { return text; }

    @Field @NumericField( precisionStep = 6)
    public float getGrade() { return grade; }
}
Copy to Clipboard Toggle word wrap

上例定义了四个字段的索引: id、Abs tract文本评级。请注意,默认情况下字段名称没有大写,符合 JavaBean 规范。grade 字段标为数字,其精确步骤略大于默认值。

7.4.1.6. 映射属性多次

有时,您需要为每个索引多次映射一个属性,索引策略略有不同。例如,根据字段排序查询需要取消分析字段。要按此属性上的词语搜索,仍需要对它进行索引 - 旦分析过,一旦未分析一次。@Fields 允许您实现此目标。

示例:使用 @Fields 映射多个属性时间

@Entity
@Indexed(index = "Book" )
public class Book {
    @Fields( {
            @Field,
            @Field(name = "summary_forSort", analyze = Analyze.NO, store = Store.YES)
            } )
    public String getSummary() {
        return summary;
    }
    ...
}
Copy to Clipboard Toggle word wrap

在本例中,字段 概述 被索引两次,一次以令牌化的方式作为 概述,一次是以未令牌的方式作为 summary_forSort 索引。

7.4.1.7. 嵌入式和关联对象

可以将关联的对象和嵌入式对象作为根实体索引的一部分进行索引。如果您希望根据相关对象的属性搜索给定实体,这很有用。目标是返回相关城市为 Atlanta 的位置(在 Lucene 查询解析器语言中,语言将转换为 address.city:Atlanta)。位置字段将在位置索引中 索引Place 索引文档还将包含您可以 查询的 address.id、address .street 和 address.city 字段。

示例:索引关联

@Entity
@Indexed
public class Place {
    @Id
    @GeneratedValue
    @DocumentId
    private Long id;

    @Field
    private String name;

    @OneToOne( cascade = { CascadeType.PERSIST, CascadeType.REMOVE } )
    @IndexedEmbedded
    private Address address;
    ....
}

@Entity
public class Address {
    @Id
    @GeneratedValue
    private Long id;

    @Field
    private String street;

    @Field
    private String city;

    @ContainedIn
    @OneToMany(mappedBy="address")
    private Set<Place> places;
    ...
}
Copy to Clipboard Toggle word wrap

由于在使用 @IndexedEmbedded 技术时,Lucene 索引中数据已被非常规化,因此 Hibernate Search 必须了解 Place 对象中的任何变化,以及 Address 对象中的任何更改,以便索引保持最新。为确保在地址发生更改时 Lucene 文档得到更新,请使用 @ContainedIn 标记双向关系的另一侧。

注意

@ContainedIn 对于指向实体的关联和嵌入式(收集)对象的关联都很有用。

若要对此展开,以下示例演示了嵌套 @IndexedEmbedded

示例:@IndexedEmbedded 和 @ContainedIn 的嵌套使用

@Entity
@Indexed
public class Place {
    @Id
    @GeneratedValue
    @DocumentId
    private Long id;

    @Field
    private String name;

    @OneToOne( cascade = { CascadeType.PERSIST, CascadeType.REMOVE } )
    @IndexedEmbedded
    private Address address;
    ....
}

@Entity
public class Address {
    @Id
    @GeneratedValue
    private Long id;

    @Field
    private String street;

    @Field
    private String city;

    @IndexedEmbedded(depth = 1, prefix = "ownedBy_")
    private Owner ownedBy;

    @ContainedIn
    @OneToMany(mappedBy="address")
    private Set<Place> places;
    ...
}

@Embeddable
public class Owner {
    @Field
    private String name;
   ...
}
Copy to Clipboard Toggle word wrap

任何 @*ToMany@*ToOne@Embedded 属性都可以标上 @IndexedEmbedded。然后,关联的类的属性将添加到主实体索引中。索引将包含以下字段:

  • id
  • name
  • address.street
  • address.city
  • address.ownedBy_name

默认前缀是 propertyName.,遵循传统的对象导航约定。您可以使用 prefix 属性覆盖它,如 ownedBy 属性中所示。

注意

前缀不能设置为空字符串。

当对象图包含类(而非实例)的循环依赖项时,需要 深度 属性。例如,如果所有者指向位置:Hibernate 搜索将在到达预期深度(或到达对象图形边界)后停止包含索引的嵌入式属性。具有自引用的类是 cyclic 依赖项示例。在我们的示例中,由于 深度 设置为 1,Owner 中的任何 @IndexedEmbedded 属性都将被忽略。

使用 @IndexedEmbedded 作为对象关联可让您表达查询(使用 Lucene 的查询语法),例如:

  • 返回名称包含 JBoss 以及地址城市为 Atlanta 的位置。在 Lucene 查询中,这是:

    +name:jboss +address.city:atlanta
    Copy to Clipboard Toggle word wrap
  • 返回名称包含 JBoss 的位置,以及所有者名称包含 Joe 的位置。在 Lucene 查询中,该选项为

    +name:jboss +address.ownedBy_name:joe
    Copy to Clipboard Toggle word wrap

此行为以更有效的方式(降低数据重复的成本)模仿关系连接操作。请记住,开箱即用时 Lucene 索引没有任何关联概念,连接操作也不存在。这有助于保持关系模式规范化,同时从完整的文本索引速度和特性丰富的中受益。

注意

关联的对象本身(但不必)为 @Indexed

@IndexedEmbedded 指向某一实体时,其关联必须是方向的,并且必须给另一方添加注释 @ContainedIn (如上例中所示)。如果没有,Hibernate Search 将无法在相关实体更新时更新根索引(在本示例中,相关地址实例更新时必须更新 Place 索引文档)。

有时,由 @IndexedEmbedded 标注的对象类型并非 Hibernate 和 Hibernate Search 所针对的对象类型。当接口用于代替其实施时,尤其会出现这种情况。因此,您可以使用 targetElement 参数覆盖 Hibernate Search 所针对的对象类型。

示例:使用 @IndexedEmbedded targetElement 属性

@Entity
@Indexed
public class Address {
    @Id
    @GeneratedValue
    @DocumentId
    private Long id;

    @Field
    private String street;

    @IndexedEmbedded(depth = 1, prefix = "ownedBy_", )
    @Target(Owner.class)
    private Person ownedBy;
    ...
}

@Embeddable
public class Owner implements Person { ... }
Copy to Clipboard Toggle word wrap

7.4.1.8. 将对象嵌入式限制为特定路径

@IndexedEmbedded 注释也提供 includePaths 属性,可用于作为深度的替代方案或与其组合。

仅使用深度时,会在同一深度以递归方式添加嵌入式类型的所有索引字段。这样更加难以仅选择特定路径而不添加所有其他字段,而可能不需要这些字段。

为避免不必要的加载和索引实体,您可以精确指定所需的路径。典型的应用程序可能需要不同路径的不同深度,或者换句话说可能需要明确指定路径,如下例所示:

示例:使用 @IndexedEmbedded 的 includePaths 属性

@Entity
@Indexed
public class Person {

   @Id
   public int getId() {
      return id;
   }

   @Field
   public String getName() {
      return name;
   }

   @Field
   public String getSurname() {
      return surname;
   }

   @OneToMany
   @IndexedEmbedded(includePaths = { "name" })
   public Set<Person> getParents() {
      return parents;
   }

   @ContainedIn
   @ManyToOne
   public Human getChild() {
      return child;
   }

    ...//other fields omitted
Copy to Clipboard Toggle word wrap

使用上例中所示的映射,您可以按 名称和 /或 姓氏 搜索 Person,以及/或父 名称。它不会索引父名称 的姓氏,因此无法对父名进行搜索,但会加快索引、节省空间并提高整体性能。

@IndexedEmbeddedincludePaths 除常规地为深度指定有限值 的索引外,还包括指定的路径。使用 includePaths 并保留深度未定义时,行为等同于设置 depth=0:只有包含的路径才会被索引。

示例:使用 @IndexedEmbedded 的 includePaths 属性

@Entity
@Indexed
public class Human {

   @Id
   public int getId() {
      return id;
   }

   @Field
   public String getName() {
      return name;
   }

   @Field
   public String getSurname() {
      return surname;
   }

   @OneToMany
   @IndexedEmbedded(depth = 2, includePaths = { "parents.parents.name" })
   public Set<Human> getParents() {
      return parents;
   }

   @ContainedIn
   @ManyToOne
   public Human getChild() {
      return child;
   }

    ...//other fields omitted
Copy to Clipboard Toggle word wrap

在上面的示例中,每个人都将拥有其名称和姓氏属性索引。父项的名称和姓氏也将进行索引,其中最多可递归为第二行,因为存在depth 属性。可以按姓名或姓氏直接搜索该人员、其父项或父项。除第二个级别外,我们还会再为一个级别编制索引,而仅索引名称,而非姓氏。

这会在索引中生成以下字段:

  • ID :作为主密钥
  • _hibernate_class :存储实体类型
  • 名称 :作为直接字段
  • Sur name:作为直接字段
  • parent.name :作为嵌入式字段(深度 1)
  • parent.surname :作为嵌入式字段,在深度 1 中
  • parent.parents.name :作为嵌入式字段,位于深度 2
  • parent.parents.surname :作为嵌入式字段,显示深度 2
  • parent.parents.parents.name :作为 includePaths 指定的额外路径。第一个 父级. 从字段名称中推断,剩余的路径是 includePaths 的属性。

如果您要首先定义所需的查询(如此时您准确知道需要哪些字段)以及不需要哪些其他字段来实施您的用例,那么明确控制索引路径可能会更加简单。

7.4.2. boosting

Lucene 具有 增强 概念,使您可以提供某些文档或字段的重要性比其他文档或字段多或更低。Lucene 区分索引和搜索时间提升。以下小节介绍了如何使用 Hibernate Search 实现索引时间提升。

7.4.2.1. 静态索引时间嵌套

若要为索引化的类或属性定义静态提升值,您可以使用 @Boost 注释。您可以在 @Field 内使用此注释,或者直接在方法或类级别上指定。

示例:使用 @Boost 的不同方式

@Entity
@Indexed

public class Essay {
    ...

    @Id
    @DocumentId
    public Long getId() { return id; }

    @Field(name="Abstract", store=Store.YES, boost=@Boost(2f))
    @Boost(1.5f)
    public String getSummary() { return summary; }

    @Lob
    @Field(boost=@Boost(1.2f))
    public String getText() { return text; }

    @Field
    public String getISBN() { return isbn; }
}
Copy to Clipboard Toggle word wrap

在上面的示例中,Essay 达到搜索列表顶部的可能性将乘以 1.7。summary 字段将为 3.0(2 * 1.5),因为属性上的 @Field.boost 和 @Boost 是累加的,比 isbn 字段更重要。文本字段将比 isbn 字段更重要 1.2 倍。请注意,该解释最严格无误,但对于所有实际用途而言,它非常简单,非常接近现实。

7.4.2.2. 动态索引时间嵌套

Static Index Time Boosting 中使用的 @Boost 注释定义一个静态提升因子,它独立于运行时索引的实体的状态。然而,在有些用例中,增速因素可能取决于实体的实际状态。在这种情况下,您可以使用 @DynamicBoost 注释和附带的自定义 BoostStrategy。

示例:Dynamic Boost

public enum PersonType {
    NORMAL,
    VIP
}

@Entity
@Indexed
@DynamicBoost(impl = VIPBoostStrategy.class)
public class Person {
    private PersonType type;

    // ....
}

public class VIPBoostStrategy implements BoostStrategy {
    public float defineBoost(Object value) {
        Person person = ( Person ) value;
        if ( person.getType().equals( PersonType.VIP ) ) {
            return 2.0f;
        }
        else {
            return 1.0f;
        }
    }
}
Copy to Clipboard Toggle word wrap

在上例中,在将 VIPBoostStrategy 指定为要在索引时使用的 BoostStrategy 接口的实现的类级别上定义动态提升。您可以将 @DynamicBoost 放置在类或字段级别上。根据注释的放置,整个实体都传递到 defineBoost 方法,或者仅传递注解的字段/质量值。您要将传递的对象投给正确的类型。在示例中,VIP 人员的所有索引值都将与普通人的值加倍。

注意

指定的 BoostStrategy 实现必须定义一个公共的 no-arg 构造器。

当然,您可以在实体中混合和匹配 @Boost@DynamicBoost 注释。所有定义的增压因素都是累计的。

7.4.3. 分析

分析 是将文本转换为单个术语(词)的过程,可以视为全文本搜索引擎的主要功能之一。Lucene 利用分析器的概念来控制此过程。在下一节中,我们将介绍 Hibernate 搜索提供的多种方式来配置分析器。

7.4.3.1. 类的默认 Analyzer 和 Analyzer

用于索引令牌字段的默认分析器类可通过 hibernate.search.analyzer 属性进行配置。此属性的默认值为 org.apache.lucene.analysis.standard.StandardAnalyzer

您还可以为每个实体定义分析器类、属性甚至每个 @Field(在一个属性中索引多个字段时很有用)。

示例:使用 @Analyzer 的不同方式

@Entity
@Indexed
@Analyzer(impl = EntityAnalyzer.class)
public class MyEntity {
    @Id
    @GeneratedValue
    @DocumentId
    private Integer id;

    @Field
    private String name;

    @Field
    @Analyzer(impl = PropertyAnalyzer.class)
    private String summary;

    @Field(analyzer = @Analyzer(impl = FieldAnalyzer.class)
    private String body;
    ...
}
Copy to Clipboard Toggle word wrap

在本例中,EnityAnalyzer 用于索引令牌化属性(名称),但 摘要正文 除外,它们分别使用 propertiesAnalyzer 和 FieldAnalyzer 索引。

警告

在同一个实体中混合不同的分析器大部分时候是不良做法。它使查询构建更复杂,结果更难以预测(对于 novice),尤其是如果您使用 QueryParser(对整个查询使用同一个分析器)。作为经验法,对于任何给定字段,应将同一分析器用于索引和查询。

7.4.3.2. 命名分析器

分析器可能会变得非常复杂。因此,增加了 Hibernate 搜索分析器定义的概念。分析器定义可以被许多 @Analyzer 声明重复使用,其包含:

  • name: 用于引用定义的唯一字符串
  • char 过滤器列表: 每个 char 过滤器负责在令牌化之前预先处理输入字符。Char 过滤器可以添加、更改或删除字符;一个常用的用法是字符规范化
  • 令牌器: 负责将输入流令牌到单个单词
  • 过滤器列表: 每个过滤器负责删除、修改或有时在 tokenizer 提供的流中添加词语

这种任务分离(一个 char 过滤器列表和一个跟一个过滤器列表)可轻松重复利用各个组件,并让您以非常灵活的方式(如 Lego)构建自定义分析器。通常而言,Car 过滤器在字符输入中执行一些预处理,然后令牌程序通过将字符输入转换为令牌来启动令牌化过程,然后由 TokenFilters 进一步处理。Hibernate Search 利用 Solr 分析器框架来支持此基础架构。

让我们回顾一下下面描述的一个具体示例。首先,一个 char 过滤器由工厂定义。在我们的示例中,使用了映射 char 过滤器,并将根据映射文件中指定的规则替换输入中的字符。接下来定义了令牌程序。这个示例使用标准令牌程序。最后但并非最不重要的是,一个过滤器列表由其工厂定义。在我们的示例中,StopFilter 过滤器构建为读取专用的词语属性文件。过滤器还应忽略大小写。

示例:@AnalyzerDef 和 Solr Framework

@AnalyzerDef(name="customanalyzer",
  charFilters = {
    @CharFilterDef(factory = MappingCharFilterFactory.class, params = {
      @Parameter(name = "mapping",
        value = "org/hibernate/search/test/analyzer/solr/mapping-chars.properties")
    })
  },
  tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
  filters = {
    @TokenFilterDef(factory = ISOLatin1AccentFilterFactory.class),
    @TokenFilterDef(factory = LowerCaseFilterFactory.class),
    @TokenFilterDef(factory = StopFilterFactory.class, params = {
      @Parameter(name="words",
        value= "org/hibernate/search/test/analyzer/solr/stoplist.properties" ),
      @Parameter(name="ignoreCase", value="true")
    })
})
public class Team {
    ...
}
Copy to Clipboard Toggle word wrap

注意

过滤器和 char 过滤器按照 @AnalyzerDef 注释中定义的顺序应用。订单很重要!

某些令牌程序、令牌过滤器或 char 过滤器会加载资源,如配置或元数据文件。stop 过滤器和同义词过滤器就是这种情况。如果资源 charset 没有使用虚拟机默认值,您可以通过添加 resource_charset 参数来显式指定它。

示例:使用特定 Charset 加载属性文件

@AnalyzerDef(name="customanalyzer",
  charFilters = {
    @CharFilterDef(factory = MappingCharFilterFactory.class, params = {
      @Parameter(name = "mapping",
        value = "org/hibernate/search/test/analyzer/solr/mapping-chars.properties")
    })
  },
  tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
  filters = {
    @TokenFilterDef(factory = ISOLatin1AccentFilterFactory.class),
    @TokenFilterDef(factory = LowerCaseFilterFactory.class),
    @TokenFilterDef(factory = StopFilterFactory.class, params = {
      @Parameter(name="words",
        value= "org/hibernate/search/test/analyzer/solr/stoplist.properties" ),
      @Parameter(name="resource_charset", value = "UTF-16BE"),
      @Parameter(name="ignoreCase", value="true")
  })
})
public class Team {
    ...
}
Copy to Clipboard Toggle word wrap

定义之后,分析器定义就可通过 @Analyzer 声明重复利用,如下例中所示:

示例:按名称引用分析器

@Entity
@Indexed
@AnalyzerDef(name="customanalyzer", ... )
public class Team {
    @Id
    @DocumentId
    @GeneratedValue
    private Integer id;

    @Field
    private String name;

    @Field
    private String location;

    @Field
    @Analyzer(definition = "customanalyzer")
    private String description;
}
Copy to Clipboard Toggle word wrap

@AnalyzerDef 声明的 AnalyzerDef 实例通过其名称在 SearchFactory 中也可用,该名称在构建查询时非常有用。

Analyzer analyzer = fullTextSession.getSearchFactory().getAnalyzer("customanalyzer");
Copy to Clipboard Toggle word wrap

查询中的字段必须使用用于索引字段的同一分析器进行分析,以便它们使用共同"语言":在查询和索引过程之间重复利用相同的令牌。该规则有一些例外情况,但大部分时候确实如此。尊重它,除非您知道自己正在做什么。

7.4.3.3. 可用分析器

SOLR 和 Lucene 附带许多有用的默认 char 过滤器、令牌程序和过滤器。您可以在 http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters 找到 char 过滤器工厂、令牌方工厂和过滤工厂的完整列表。我们来看一下其中几个。

Expand
表 7.7. 可用的 Char Filters
工厂描述参数

MappingCharFilterFactory

根据资源文件中指定的映射,将一个或多个字符替换为一个或多个字符

映射 :指向包含映射的资源文件,格式为:"á" SAS "a"; "ñ" SAS "n"; "您" SAS "o"

HTMLStripCharFilterFactory

删除 HTML 标准标签,保留文本

none

Expand
表 7.8. 可用令牌程序
工厂描述参数

StandardTokenizerFactory

使用 Lucene StandardTokenizer

none

HTMLStripCharFilterFactory

删除 HTML 标签,保留文本并将其传递到标准Tokenizer。

none

PatternTokenizerFactory

将文本拆分到指定的正则表达式模式.

pattern :用于令牌化的正则表达式

Group :表示要提取到令牌中的哪个模式组

Expand
表 7.9. 可用的过滤器
工厂描述参数

StandardFilterFactory

从单词中删除缩写和 的点数

none

LowerCaseFilterFactory

小写所有单词

none

StopFilterFactory

删除与 stop 词语列表匹配的词语(令牌)

word: 指向包含 stop 词语的资源文件

ignoreCase: true,如果比较 stop 字时应忽略大小写,否则为 false

SnowballPorterFilterFactory

以给定语言将单词减到其 root。(例如:保护、保护、保护共享同一根)。使用这样的过滤器可以搜索匹配相关的词语。

语言 :丹麦、荷兰语、英语、芬兰语、法语、德语、意大利语、葡萄牙语、俄语、西班牙语、瑞典语以及一些其他语言

我们建议检查 IDE 中 org.apache.lucene.analysis.TokenizerFactoryorg.apache.lucene.analysis.TokenFilterFactory 中的所有实施,以查看可用的实施。

7.4.3.4. 动态分析器选择

目前,所有介绍的指定分析器的方法都是静态的。然而,在某些用例中,根据要索引的实体的当前状态(例如在多语言应用程序中)来选择分析器很有用。例如,对于 BlogEntry 类,分析器可能依赖于条目的语言属性。根据此属性,应当选择正确的语言特定语言来索引实际文本。

要启用此动态分析器选择 Hibernate Search,请引入 AnalyzerDiscriminator 注释。以下示例演示了此注解的用法。

示例:@AnalyzerDiscriminator 的使用

@Entity
@Indexed
@AnalyzerDefs({
  @AnalyzerDef(name = "en",
    tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
    filters = {
      @TokenFilterDef(factory = LowerCaseFilterFactory.class),
      @TokenFilterDef(factory = EnglishPorterFilterFactory.class
      )
    }),
  @AnalyzerDef(name = "de",
    tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class),
    filters = {
      @TokenFilterDef(factory = LowerCaseFilterFactory.class),
      @TokenFilterDef(factory = GermanStemFilterFactory.class)
    })
})
public class BlogEntry {

    @Id
    @GeneratedValue
    @DocumentId
    private Integer id;

    @Field
    @AnalyzerDiscriminator(impl = LanguageDiscriminator.class)
    private String language;

    @Field
    private String text;

    private Set<BlogEntry> references;

    // standard getter/setter
    ...
}
Copy to Clipboard Toggle word wrap

public class LanguageDiscriminator implements Discriminator {

    public String getAnalyzerDefinitionName(Object value, Object entity, String field) {
        if ( value == null || !( entity instanceof BlogEntry ) ) {
            return null;
        }
        return (String) value;

    }
}
Copy to Clipboard Toggle word wrap

使用 @AnalyzerDiscriminator 的先决条件是,将要动态使用的所有分析器都通过 @AnalyzerDef 定义预定义。如果出现这种情况,可以将 @AnalyzerDiscriminator 注释放在该类或动态选择分析器的实体的特定属性上。通过 AnalyzerDiscriminatorimpl 参数,您可以指定特定的 Discriminator 接口实施。您要为此接口提供一个实施。您唯一需要实施的方法是 getAnalyzerDefinitionName(),它为添加到 Lucene 文档中的每个字段调用。索引化的实体也传递到 interface 方法。只有在将 AnalyzerDiscriminator 放置到属性级别而不是类级别时,才会设置 value 参数。在本例中,值表示此属性的当前值。

如果不应覆盖默认分析器,则 Discriminator 接口的实施必须返回现有分析器定义的名称或 null。上面的示例假定语言参数为 'de' 或 'en',它与 @AnalyzerDefs 中的 指定名称匹配。

7.4.3.5. 检索分析器

当在域模型中使用了多个分析器时,可以检索分析器,以便从医疗或手机近似中受益。在这种情况下,使用相同的分析器来构建查询。或者,使用 Hibernate Search 查询 DSL,它会自动选择正确的分析器。请查看

无论您是使用 Lucene 编程 API,还是 Lucene 查询解析器,您都可以检索给定实体的范围分析器。有作用域分析器是一个分析器,它根据字段索引来应用正确的分析器。请记住,可以在给定实体上定义多个分析器,每个实体都在单个字段上工作。一个作用域分析器可将所有这些分析器统一到上下文感知分析器中。尽管该理论似乎比较复杂,但在查询中使用正确的分析器非常简单。

注意

当您对子实体使用编程映射时,只能查看子实体定义的字段。从父实体继承的字段或方法(使用 @MappedSuperclass 标注)不可配置。要配置从父实体继承的属性,可覆盖子实体中的 属性,或者为父实体创建编程映射。这模拟了注解的使用,除非子实体中重新定义了父实体的字段或方法。

示例:构建全文本查询时使用 Scoped Analyzer

org.apache.lucene.queryParser.QueryParser parser = new QueryParser(
    "title",
    fullTextSession.getSearchFactory().getAnalyzer( Song.class )
);

org.apache.lucene.search.Query luceneQuery =
    parser.parse( "title:sky Or title_stemmed:diamond" );

org.hibernate.Query fullTextQuery =
    fullTextSession.createFullTextQuery( luceneQuery, Song.class );

List result = fullTextQuery.list(); //return a list of managed objects
Copy to Clipboard Toggle word wrap

在上例中,歌曲标题按照两个字段索引:标准分析器在字段 标题中 使用,greject _stemmed 字段中则使用一个适当的 分析器。通过使用搜索工厂提供的分析器,查询会根据目标字段使用适当的分析器。

注意

您还可以使用 searchFactory.getAnalyzer(String)根据定义名称检索 @Analyzer Def 定义的分析器。

7.4.4. 网桥

在讨论实体的基本映射时,到目前为止一个重要的事实被忽略。在 Lucene 中,所有索引字段都必须以字符串表示。标有 @Field 的所有实体 属性都必须转换为要索引的字符串。我们目前尚未提到过的原因是,对于您的大多数属性,Hibernate Search 为您承担了转换任务,得益于一组内置桥。然而,在某些情况下,您需要更精细地控制转换过程。

7.4.4.1. 内置网桥

Hibernate Search 附带一组 Java 属性类型和完整文本表示法之间的内置桥接。

null
每个默认 null 元素都没有索引。Lucene 不支持 null 元素。但在某些情况下,插入代表 值的自定义令牌会很有用。如需更多信息,请参阅。
java.lang.String
字符串的索引方式是短、短、整数、Integer、长、Long、float、Float、双.
Double, BigInteger, BigDecimal

数字转换为字符串表示法。请注意,数字不能由 Lucene(即在范围查询中使用)从开箱即用:它们必须被 padded。

注意

使用 Range 查询存在缺点,另一种方法是使用 Filter 查询,它将结果查询过滤到适当的范围。Hibernate Search 还支持使用自定义 StringBridge,如 自定义网桥 中所述。

java.util.Date

日期存储为 yyyMMddHHmmsSSS,时间为 yyyMMddHHmmsSSS,时间为 2006 年 11 月 7 日 4:03PM 和 12ms EST。您不应该真正对内部格式造成干扰。重要的是,在使用 TermRangeQuery 时,您应该知道日期必须以 GMT 时间表示。

通常,不需要存储毫秒的最新数据。@DateBridge 定义您希望在索引中存储的适当解析(@DateBridge(resolution=Resolution.DAY)。然后,将相应地截断日期模式。

@Entity
@Indexed
public class Meeting {
    @Field(analyze=Analyze.NO)

    private Date date;
    ...
Copy to Clipboard Toggle word wrap
警告

解析低于 MILLISECOND 的日期不能是 @DocumentId

重要

默认日期网桥使用 Lucene 的 DateTools 从和转换为 String。这意味着所有日期都以 GMT 时间表示。如果您的要求要在固定的时区中存储日期,您必须实施自定义日期桥接。确保您了解应用程序与日期索引和搜索相关的要求。

java.net.URI, java.net.URL
URI 和 URL 将转换为其字符串表示法。
java.lang.Class
class 将转换为其完全限定类名称。重新验证类时,使用线程上下文类加载程序。

7.4.4.2. 自定义网桥

有时,Hibernate Search 的内置网桥不涵盖某些属性类型,或者网桥使用的字符串表示不满足您的要求。以下段落描述了此问题的几种解决方案。

7.4.4.2.1. StringBridge

最简单的自定义解决方案是让 Hibernate 搜索您的预期对象实施到 String 网桥。为此,您需要实施 org.hibernate.search.bridge.StringBridge 接口。所有实施都必须是并发使用线程安全。

示例:自定义字符串实现

/**
 * Padding Integer bridge.
 * All numbers will be padded with 0 to match 5 digits
 *
 * @author Emmanuel Bernard
 */
public class PaddedIntegerBridge implements StringBridge {

    private int PADDING = 5;

    public String objectToString(Object object) {
        String rawInteger = ( (Integer) object ).toString();
        if (rawInteger.length() > PADDING)
            throw new IllegalArgumentException( "Try to pad on a number too big" );
        StringBuilder paddedInteger = new StringBuilder( );
        for ( int padIndex = rawInteger.length() ; padIndex < PADDING ; padIndex++ ) {
            paddedInteger.append('0');
        }
        return paddedInteger.append( rawInteger ).toString();
    }
}
Copy to Clipboard Toggle word wrap

根据上例中定义的字符串网桥,任何属性或字段都可通过 @FieldBridge 注释来使用此网桥:

@FieldBridge(impl = PaddedIntegerBridge.class)
private Integer length;
Copy to Clipboard Toggle word wrap
7.4.4.2.2. 参数网桥

参数也可以传递到网桥实施,使其更灵活。下例实施参数化Bridge 接口和参数通过 @FieldBridge 注释传递:

示例:将参数传递给网桥实施

public class PaddedIntegerBridge implements StringBridge, ParameterizedBridge {

    public static String PADDING_PROPERTY = "padding";
    private int padding = 5; //default

    public void setParameterValues(Map<String,String> parameters) {
        String padding = parameters.get( PADDING_PROPERTY );
        if (padding != null) this.padding = Integer.parseInt( padding );
    }

    public String objectToString(Object object) {
        String rawInteger = ( (Integer) object ).toString();
        if (rawInteger.length() > padding)
            throw new IllegalArgumentException( "Try to pad on a number too big" );
        StringBuilder paddedInteger = new StringBuilder( );
        for ( int padIndex = rawInteger.length() ; padIndex < padding ; padIndex++ ) {
            paddedInteger.append('0');
        }
        return paddedInteger.append( rawInteger ).toString();
    }
}


//property
@FieldBridge(impl = PaddedIntegerBridge.class,
             params = @Parameter(name="padding", value="10")
            )
private Integer length;
Copy to Clipboard Toggle word wrap

ParameterizedBridge 接口可通过TwoWayString Bridge 和FieldBridge 实现 String Bridge 实施来实现。

所有实施都必须是线程安全,但参数在初始化过程中设置,此阶段不需要特别注意。

7.4.4.2.3. 类型 Aware Bridge

有时对于获得应用桥接的类型很有用:

  • 字段/工具级网桥的属性的返回类型。
  • 类级网桥的类类型。

例如,一个网桥,它以自定义的方式处理枚举,但需要访问实际的枚举类型。实现 AppliedOnTypeAwareBridge 的任何网桥都会获得注入时应用桥接的类型。与参数一样,注入的类型无需特别注意线程安全。

7.4.4.2.4. 双Way Bridge

如果您希望在 id 属性(即带有 @DocumentId 注释)上使用网桥实施,您需要使用名为 TwoWayString Bridge 的 String Bridge 版本。Hibernate Search 需要读取标识符的字符串表示,并从中生成对象。使用 @FieldBridge 注释的方式没有区别。

示例:实施双WayStringBridge Usable for id Properties

public class PaddedIntegerBridge implements TwoWayStringBridge, ParameterizedBridge {

    public static String PADDING_PROPERTY = "padding";
    private int padding = 5; //default

    public void setParameterValues(Map parameters) {
        Object padding = parameters.get( PADDING_PROPERTY );
        if (padding != null) this.padding = (Integer) padding;
    }

    public String objectToString(Object object) {
        String rawInteger = ( (Integer) object ).toString();
        if (rawInteger.length() > padding)
            throw new IllegalArgumentException( "Try to pad on a number too big" );
        StringBuilder paddedInteger = new StringBuilder( );
        for ( int padIndex = rawInteger.length() ; padIndex < padding ; padIndex++ ) {
            paddedInteger.append('0');
        }
        return paddedInteger.append( rawInteger ).toString();
    }

    public Object stringToObject(String stringValue) {
        return new Integer(stringValue);
    }
}

//id property
@DocumentId
@FieldBridge(impl = PaddedIntegerBridge.class,
             params = @Parameter(name="padding", value="10")
private Integer id;
Copy to Clipboard Toggle word wrap

重要

双向过程务必要具有幂等性(即,object = stringToObject(objectToString(对象))。

7.4.4.2.5. FieldBridge

当将属性映射到 Lucene 索引时,有些用例需要不仅仅是简单的对象来字符串转换。为为您提供最大灵活性,您还可以将桥实施为 FieldBridge。此界面为您提供一个属性值,并允许您在 Lucene Document 中按照您想要的方式对其进行映射。例如,您可以在两个不同的文档字段中存储属性。接口的概念与 Hibernate 用户类型非常相似。

示例:实施 FieldBridge Interface

/**
 * Store the date in 3 different fields - year, month, day - to ease Range Query per
 * year, month or day (eg get all the elements of December for the last 5 years).
 * @author Emmanuel Bernard
 */
public class DateSplitBridge implements FieldBridge {
    private final static TimeZone GMT = TimeZone.getTimeZone("GMT");

    public void set(String name, Object value, Document document, LuceneOptions luceneOptions) {
        Date date = (Date) value;
        Calendar cal = GregorianCalendar.getInstance(GMT);
        cal.setTime(date);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH) + 1;
        int day = cal.get(Calendar.DAY_OF_MONTH);

        // set year
        luceneOptions.addFieldToDocument(
            name + ".year",
            String.valueOf( year ),
            document );

        // set month and pad it if needed
        luceneOptions.addFieldToDocument(
            name + ".month",
            month < 10 ? "0" : "" + String.valueOf( month ),
            document );

        // set day and pad it if needed
        luceneOptions.addFieldToDocument(
            name + ".day",
            day < 10 ? "0" : "" + String.valueOf( day ),
            document );
    }
}

//property
@FieldBridge(impl = DateSplitBridge.class)
private Date date;
Copy to Clipboard Toggle word wrap

在上面的示例中,这些字段不会直接添加到 Document 中。相反,添加被委派给 LuceneOptions 帮助程序;此帮助程序将应用您在 @Field(StoreTermVector )上选择的选项,或应用所选的 @Boost 值。封装 COMPRESS 实施的复杂性特别有用。尽管建议将任何字段委派给 LuceneOptions 以向文档添加字段,但不会阻止您直接编辑文档,并在您需要时忽略 LuceneOptions。

注意

创建类似 LuceneOptions 的类,以方便您的应用程序免受 Lucene API 的变化,并简化您的代码。如果您愿意,请使用它们,但如果您需要更大的灵活性,则不需要.

7.4.4.2.6. ClassBridge

有时,组合给定实体的多个属性并以特定的方式将此组合索引到 Lucene 索引中很有用。@ClassBridge@ClassBridges 注释可以在类级别上定义,而非属性级别。在本例中,自定义字段网桥实施接收实体实例作为 value 参数,而不是特定属性。虽然如下例中未显示,但 @ClassBridge 支持 Basic Mapping 部分中讨论的 termVector 属性。

示例:实施类桥接

@Entity
@Indexed
(name="branchnetwork",
             store=Store.YES,
             impl = CatFieldsClassBridge.class,
             params = @Parameter( name="sepChar", value=" " ) )
public class Department {
    private int id;
    private String network;
    private String branchHead;
    private String branch;
    private Integer maxEmployees
    ...
}

public class CatFieldsClassBridge implements FieldBridge, ParameterizedBridge {
    private String sepChar;

    public void setParameterValues(Map parameters) {
        this.sepChar = (String) parameters.get( "sepChar" );
    }

    public void set( String name, Object value, Document document, LuceneOptions luceneOptions) {
        // In this particular class the name of the new field was passed
        // from the name field of the ClassBridge Annotation. This is not
        // a requirement. It just works that way in this instance. The
        // actual name could be supplied by hard coding it below.
        Department dep = (Department) value;
        String fieldValue1 = dep.getBranch();
        if ( fieldValue1 == null ) {
            fieldValue1 = "";
        }
        String fieldValue2 = dep.getNetwork();
        if ( fieldValue2 == null ) {
            fieldValue2 = "";
        }
        String fieldValue = fieldValue1 + sepChar + fieldValue2;
        Field field = new Field( name, fieldValue, luceneOptions.getStore(),
            luceneOptions.getIndex(), luceneOptions.getTermVector() );
        field.setBoost( luceneOptions.getBoost() );
        document.add( field );
   }
}
Copy to Clipboard Toggle word wrap

在本例中,特定的 CatFieldsClassBridge 应用到 部门 实例,字段桥接随后串联分支和网络,并索引串联。

Red Hat logoGithubredditYoutubeTwitter

学习

尝试、购买和销售

社区

关于红帽文档

通过我们的产品和服务,以及可以信赖的内容,帮助红帽用户创新并实现他们的目标。 了解我们当前的更新.

让开源更具包容性

红帽致力于替换我们的代码、文档和 Web 属性中存在问题的语言。欲了解更多详情,请参阅红帽博客.

關於紅帽

我们提供强化的解决方案,使企业能够更轻松地跨平台和环境(从核心数据中心到网络边缘)工作。

Theme

© 2026 Red Hat
返回顶部