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 {
...
}
您可以选择指定 @Indexed 注释的 index 属性,以更改索引的默认名称。
7.4.1.3. @Field 复制链接链接已复制到粘贴板!
对于实体的每个属性(或属性),您可以描述如何对其进行索引。默认(不存在)表示索引过程中会忽略该属性。
在 Hibernate Search 5 之前,只有通过 @NumericField 明确请求时,才会选择数字字段编码。从 Hibernate Search 5 开始,系统将自动为数字类型选择此编码。为避免数字编码,您可以通过 @Field.bridge 或 明确指定非数字字段网桥。软件包 @Field Bridgeorg.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.YES。index. no在不需要可搜索属性但应该可用于投射的情形中,将非常有用。注意index.NO与 Analyze.YES或Norms.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 注释接受以下参数:
| 值 | 定义 |
|---|---|
| forField | (可选)指定将索引为数字的相关 @Field 的名称。只有 属性包含超过 @Field 声明时,才强制使用它 |
| precisionStep | (可选)更改 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 );
}
}
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; }
}
上例定义了四个字段的索引: 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;
}
...
}
在本例中,字段 概述 被索引两次,一次以令牌化的方式作为 概述,一次是以未令牌的方式作为 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;
...
}
由于在使用 @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;
...
}
任何 @*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返回名称包含 JBoss 的位置,以及所有者名称包含 Joe 的位置。在 Lucene 查询中,该选项为
+name:jboss +address.ownedBy_name:joe
此行为以更有效的方式(降低数据重复的成本)模仿关系连接操作。请记住,开箱即用时 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 { ... }
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
使用上例中所示的映射,您可以按 名称和 /或 姓氏 搜索 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
在上面的示例中,每个人都将拥有其名称和姓氏属性索引。父项的名称和姓氏也将进行索引,其中最多可递归为第二行,因为存在depth 属性。可以按姓名或姓氏直接搜索该人员、其父项或父项。除第二个级别外,我们还会再为一个级别编制索引,而仅索引名称,而非姓氏。
这会在索引中生成以下字段:
-
ID:作为主密钥 -
_hibernate_class:存储实体类型 -
名称:作为直接字段 -
Surname:作为直接字段 -
parent.name:作为嵌入式字段(深度 1) -
parent.surname:作为嵌入式字段,在深度 1 中 -
parent.parents.name:作为嵌入式字段,位于深度 2 -
parent.parents.surname:作为嵌入式字段,显示深度 2 -
parent.parents.parents.name:作为 includePaths 指定的额外路径。第一个父级.从字段名称中推断,剩余的路径是 includePaths 的属性。
如果您要首先定义所需的查询(如此时您准确知道需要哪些字段)以及不需要哪些其他字段来实施您的用例,那么明确控制索引路径可能会更加简单。