Hibernate Search 中文操作指南

10. Mapping entities to indexes

10.1. Configuring the mapping

10.1.1. Annotation-based mapping

Entity definition Entity/index mapping 和以下部分中所述,将实体映射到索引的主要方法是使用注释。

默认情况下,Hibernate Search 会自动处理实体类型的映射注释、以及这些实体类型中的嵌套类型(例如嵌入类型)。

可以通过为 Hibernate ORM integration 设置 hibernate.search.mapping.process_annotationsfalse,或通过 AnnotationMappingConfigurationContext 为任何映射器来禁用基于注释的映射:请参见 Mapping configurer 以访问该上下文,并参见 AnnotationMappingConfigurationContext 的 javadoc 以了解可用选项。

如果您禁用基于注释的映射,则可能需要通过编程方式配置映射:请参见 Programmatic mapping

Hibernate Search 还会尝试通过 classpath scanning 找到一些带注释的类型。

10.1.2. Classpath scanning

Basics

Hibernate Search 会在启动时自动扫描实体类型的 JAR,查找以“根映射注释”进行注释的类型,以便将这些类型自动添加到应处理其注释的类型列表。

根映射注释是作为映射入口点的映射注释,例如 @ProjectionConstructorcustom root mapping annotations 。如果没有这种扫描,Hibernate Search 将了解诸如投影构造函数之类的内容太晚了(当投影实际被执行时),并且会因缺少元数据而失败。

该扫描由 Jandex 支持,后者是一个索引 JAR 的内容的库。

Scanning dependencies of the application

默认情况下,Hibernate Search 仅扫描包含您的 Hibernate ORM 实体的 JAR。

如果您希望 Hibernate Search 在其他 JAR 中检测到用 root mapping annotations 注释的类型,则首先需要 access an AnnotationMappingConfigurationContext

从该上下文,可能执行以下操作之一:

  1. 调用 _annotationMappingContext.add( MyType.class )_以明确告知 Hibernate Search 处理 _MyType_上的注释,并在包含 _MyType_的 JAR 中发现使用 root mapping annotations注释的其他类型。

  2. 或者(高级用法,孵化)调用 _annotationMappingContext.addJandexIndex( <an IndexView instance> )_以明确告知 Hibernate Search 在给定的 Jandex 索引中查找使用 root mapping annotations注释的类型。

Configuring scanning

Hibernate Search 的扫描可能会在应用程序启动时通过 Jandex 触发 JAR 索引。在一些更多复杂的环境中,此索引可能无法获得对要索引类的访问权限,或可能会不必要地减慢启动速度。

在 Quarkus 或 Wildfly 中运行 Hibernate Search 具有以下好处:

  1. 使用 Quarkus框架,Hibernate Search 启动部分扫描在构建时执行,索引会自动提供给它。

  2. 使用 WildFly应用服务器,Hibernate Search 启动的这一部分会以优化方式执行,索引也会自动提供给它。

在其他情况下,根据应用程序需要,可在应用程序的构建阶段使用 Jandex Maven 插件,以便在应用程序启动时已经构建好索引并准备就绪。

或者,如果你的应用程序不使用 @ProjectionConstructorcustom root mapping annotations,则可以完全或部分禁用此功能。

通常不建议这样做,因为它可能导致引导失败或映射注释被忽略,因为 Hibernate Search 将不再能够自动发现 JAR(没有嵌入 Jandex 索引)中用 root annotations 注释的类型。

为此,有两个选项可用:

  1. 通过将 hibernate.search.mapping.discover_annotated_types_from_root_mapping_annotations 设置为 false ,即使有可用的、部分或完整的 Jandex 索引,也能禁用任何尝试的自动发现,如果根本没有用根映射注释注释过的类型,或者它们通过 mapping configurerAnnotatedTypeSource 显式列出,则这可能会有所帮助。

  2. hibernate.search.mapping.build_missing_discovered_jandex_indexes 设置为 false 将在启动时禁用 Jandex 索引构建,但仍将使用所有预构建的 Jandex 索引。如果需要部分自动发现,即现有的索引将用于发现,但如果没有索引,源将被忽略,除非通过 mapping configurerAnnotatedTypeSource 显式列出其 @ProjectionConstructor 注释的类型,则这可能有所帮助。

10.1.3. Programmatic mapping

此文档中的大多数示例使用基于注释的映射,这通常足以满足大多数应用程序的需求。然而,一些应用程序的需求超出了注释所能提供的范围:

  1. 单个实体类型必须为不同的部署——例如,为不同的客户——映射成不同的类型。

  2. 许多实体类型必须以类似的方式映射,不需要复制代码。

要解决这些需求,您可以使用 programmatic 映射:通过将在启动时执行的代码定义映射。

通过 ProgrammaticMappingConfigurationContext 配置编程映射:请参见 Mapping configurer 以访问该上下文。

默认情况下,程序映射将与注释映射(如果有)合并。

要禁用注释映射,请参阅 Annotation-based mapping

程序映射是声明式的,它显示与基于注释的映射完全相同的功能。

为了实现更复杂、更“命令式”的映射(例如将两个实体属性组合成一个索引字段),请使用 custom bridges

或者,如果您只需要为多种类型或属性重复相同的映射,则可以在那些类型或属性上应用自定义注释,在 Hibernate Search 遇到该注释时执行一些程序映射代码。此解决方案不需要特殊于映射器的配置。

有关更多信息,请参阅 Custom mapping annotations

10.1.4. Mapping configurer

Hibernate ORM integration

通过 Hibernate ORM 集成,一个自定义 HibernateOrmSearchMappingConfigurer 可以插入到 Hibernate Search 中,以便配置注释映射 (AnnotationMappingConfigurationContext)、编程映射 (ProgrammaticMappingConfigurationContext) 等。

插入一个自定义配置器需要两个步骤:

  • 定义一个实现了 _org.hibernate.search.mapper.orm.mapping.HibernateOrmSearchMappingConfigurer_界面的类。

  • 通过将配置属性 hibernate.search.mapping.configurer_设置为一个指向实现的 bean reference,例如 _class:com.mycompany.MyMappingConfigurer,来配置 Hibernate Search 以使用该实现。

Hibernate Search 在启动时会调用此实现的 configure 方法,并且配置器将能够利用一个 DSL 来配置注释映射或定义编程映射,例如:

示例 16. 通过 Hibernate ORM 集成实现映射配置器

public class MySearchMappingConfigurer implements HibernateOrmSearchMappingConfigurer {
    @Override
    public void configure(HibernateOrmMappingConfigurationContext context) {
        ProgrammaticMappingConfigurationContext mapping = context.programmaticMapping(); (1)
        TypeMappingStep bookMapping = mapping.type( Book.class ); (2)
        bookMapping.indexed(); (3)
        bookMapping.property( "title" ) (4)
                .fullTextField().analyzer( "english" ); (5)
    }
}
Standalone POJO Mapper

Standalone POJO Mapper 目前不提供“映射配置器”( HSEARCH-4615)。但是,可在构建 SearchMapping 时访问 AnnotationMappingConfigurationContextProgrammaticMappingConfigurationContext

通过 Hibernate ORM 集成,一个自定义 StandalonePojoMappingConfigurer 可以插入到 Hibernate Search 中,以便配置注释映射 (AnnotationMappingConfigurationContext)、编程映射 (ProgrammaticMappingConfigurationContext) 等。

插入一个自定义配置器需要两个步骤:

  • 定义一个实现了 _org.hibernate.search.mapper.pojo.standalone.mapping.StandalonePojoMappingConfigurer_界面的类。

  • 通过将配置属性 hibernate.search.mapping.configurer_设置为一个指向实现的 bean reference,例如 _class:com.mycompany.MyMappingConfigurer,来配置 Hibernate Search 以使用该实现。

Hibernate Search 在启动时会调用此实现的 configure 方法,并且配置器将能够利用一个 DSL 来配置注释映射或定义编程映射,例如:

示例 17. 通过独立 POJO 映射器实现映射配置器

public class MySearchMappingConfigurer implements StandalonePojoMappingConfigurer {
    @Override
    public void configure(StandalonePojoMappingConfigurationContext context) {
        context.annotationMapping() (1)
                .discoverAnnotationsFromReferencedTypes( false )
                .discoverAnnotatedTypesFromRootMappingAnnotations( false );

        ProgrammaticMappingConfigurationContext mappingContext = context.programmaticMapping(); (2)
        TypeMappingStep bookMapping = mappingContext.type( Book.class ); (3)
        bookMapping.searchEntity(); (4)
        bookMapping.indexed(); (5)
        bookMapping.property( "id" ) (6)
                .documentId(); (7)
        bookMapping.property( "title" ) (8)
                .fullTextField().analyzer( "english" ); (9)
    }
}

10.2. Entity definition

10.2.1. Basics

在类型可以成为 mapped to indexes 之前,Hibernate Search 需要知晓应用程序领域模型中的哪些类型是 entity types

indexing Hibernate ORM entities 时,实体类型由 Hibernate ORM 完全定义(通常通过 Jakarta 的 @Entity 注释),并且不需要明确的定义:您可以安全地跳过这一整节。

在使用 Standalone POJO Mapper 时,实体类型需要是 defined explicitly

10.2.2. Explicit entity definition

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

@SearchEntity 及其相应的程序映射 .searchEntity() 对于 Hibernate ORM 实体是没必要的,并且在使用 Hibernate ORM integration 时事实上也不受支持。

请参阅 HSEARCH-5076 以追踪允许在 Hibernate ORM 集成中使用 @SearchEntity 映射非 ORM 实体的进度。

Standalone POJO Mapper 中, entity types 必须用 @SearchEntity 注解明确标记。

示例 18. 使用 @SearchEntity 将类标记为实体
@SearchEntity (1)
@Indexed (2)
public class Book {

即使类型拥有复合结构,但并非所有类型都是实体类型。

错误地将类型标记为实体类型可能迫使您在领域模型中添加不必要的复杂度,例如不会被使用 defining identifiersan inverse side for "associations" to such types

请务必阅读 this section 以了解更多关于实体类型是什么以及它们为什么是必要的信息。

子类不会继承 @SearchEntity 注解。

每个子类都必须使用 @SearchEntity 进行注释,否则它将不会被 Hibernate Search 视为实体。

但是,对于同时使用 @SearchEntity 进行注释的子类型,可继承某些实体相关配置;请参阅相关章节了解详情。

在使用 Standalone POJO Mapper 时,默认情况下:

  1. entity name将等于类名(java.lang.Class#getSimpleName)。

  2. 该实体不会被配置为加载,无论是用于 return entities as hits in search queries还是 mass indexing

请参阅以下部分来覆盖这些默认值。

10.2.3. Entity name

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

entity 名称(不同于相应类的名称)涉及多个地方,包括但不限于:

实体名称默认为类的简单名称 (java.lang.Class#getSimpleName)。

要更改 indexed 实体的实体名称,可能需要 full reindexing ,尤其是在使用 Elasticsearch/OpenSearch backend 时。

请参阅 this section 以了解更多信息。

使用 Hibernate ORM integration 时,此名称可以通过不同方式覆盖,但通常是通过 Jakarta Persistence 的 @Entity 注解,例如,使用 @Entity(name = …​)

对于 Standalone POJO Mapper ,实体类型是 defined with @SearchEntity ,且实体名称可以使用 @SearchEntity(name = …​) 覆盖。

@SearchEntity 及其相应的程序映射 .searchEntity() 对于 Hibernate ORM 实体是没必要的,并且在使用 Hibernate ORM integration 时事实上也不受支持。

请参阅 HSEARCH-5076 以追踪允许在 Hibernate ORM 集成中使用 @SearchEntity 映射非 ORM 实体的进度。

示例 19. 设置自定义实体名称与 @SearchEntity(name = …​)
@SearchEntity(name = "MyAuthorName")
@Indexed
public class Author {

10.2.4. Mass loading strategy

“大容量加载策略”使得 Hibernate Search 能够针对 mass indexing 加载给定类型的实体。

使用 Hibernate ORM integration 时,针对每一个 Hibernate ORM 实体都会自动配置大容量加载策略,无需进一步配置。

对于 Standalone POJO Mapper ,实体类型是 defined with @SearchEntity ,且为了利用大批量索引的优势,必须使用 @SearchEntity(loadingBinder = …​) 显式地应用大批量加载策略。

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

@SearchEntity 及其相应的程序映射 .searchEntity() 对于 Hibernate ORM 实体是没必要的,并且在使用 Hibernate ORM integration 时事实上也不受支持。

请参阅 HSEARCH-5076 以追踪允许在 Hibernate ORM 集成中使用 @SearchEntity 映射非 ORM 实体的进度。

示例 20. 使用 Standalone POJO Mapper 分配大批量加载策略
@SearchEntity(loadingBinder = @EntityLoadingBinderRef(type = MyLoadingBinder.class)) (1)
@Indexed
public class Book {
@Singleton
public class MyLoadingBinder implements EntityLoadingBinder { (1)
    private final MyDatastore datastore;

    @Inject (2)
    public MyLoadingBinder(MyDatastore datastore) {
        this.datastore = datastore;
    }

    @Override
    public void bind(EntityLoadingBindingContext context) { (3)
        context.massLoadingStrategy( (4)
                Book.class, (5)
                new MyMassLoadingStrategy<>( datastore, Book.class ) (6)
        );
    }
}

下面是针对虚拟数据存储的 MassLoadingStrategy 实现示例。

示例 21. 实现 MassLoadingStrategy
public class MyMassLoadingStrategy<E>
        implements MassLoadingStrategy<E, String> {

    private final MyDatastore datastore; (1)
    private final Class<E> rootEntityType;

    public MyMassLoadingStrategy(MyDatastore datastore, Class<E> rootEntityType) {
        this.datastore = datastore;
        this.rootEntityType = rootEntityType;
    }

    @Override
    public MassIdentifierLoader createIdentifierLoader(
            LoadingTypeGroup<E> includedTypes, (2)
            MassIdentifierSink<String> sink, MassLoadingOptions options) {
        int batchSize = options.batchSize(); (3)
        Collection<Class<? extends E>> typeFilter =
                includedTypes.includedTypesMap().values(); (4)
        return new MassIdentifierLoader() {
            private final MyDatastoreConnection connection =
                    datastore.connect(); (5)
            private final MyDatastoreCursor<String> identifierCursor =
                    connection.scrollIdentifiers( typeFilter );

            @Override
            public void close() {
                connection.close(); (5)
            }

            @Override
            public long totalCount() { (6)
                return connection.countEntities( typeFilter );
            }

            @Override
            public void loadNext() throws InterruptedException {
                List<String> batch = identifierCursor.next( batchSize );
                if ( batch != null ) {
                    sink.accept( batch ); (7)
                }
                else {
                    sink.complete(); (8)
                }
            }
        };
    }

    @Override
    public MassEntityLoader<String> createEntityLoader(
            LoadingTypeGroup<E> includedTypes, (9)
            MassEntitySink<E> sink, MassLoadingOptions options) {
        return new MassEntityLoader<String>() {
            private final MyDatastoreConnection connection =
                    datastore.connect(); (10)

            @Override
            public void close() { (8)
                connection.close();
            }

            @Override
            public void load(List<String> identifiers)
                    throws InterruptedException {
                sink.accept( (11)
                        connection.loadEntitiesById( rootEntityType, identifiers )
                );
            }
        };
    }
}

Hibernate Search 会通过将具有相同 MassLoadingStrategy 的类型或不同策略(根据 equals() / hashCode() 相等)分组在一起,来优化加载。

在将类型分组在一起时,只会调用其中一种策略,且它将传递一个“类型组”,其中包括应该加载的所有类型。

在从“父级”实体类型配置了加载绑定器时,当子类型继承了它并为子类型设置了相同的策略时,尤其会发生这种情况。

当传递给 createIdentifierLoader 方法的“类型组”包含父类型(例如,Animal)且没有一个子类型(既没有 Lion 也没有 Zebra)时,请小心继承树中的非抽象(可实例化)父类,那么加载器实际上应该只加载父类型实例的标识符,而不是子类型的标识符(应该加载类型完全是 Animal 的实体的标识符,而不是 LionZebra)。

一旦所有要重新索引的类型实现了其大容量加载策略并分配给它们,就可以使用 mass indexer 重新索引它们:

示例 22. 使用单机 POJO 映射器进行批量索引

SearchMapping searchMapping = /* ... */ (1)
searchMapping.scope( Object.class ).massIndexer() (2)
        .startAndWait(); (3)

10.2.5. Selection loading strategy

“选择性加载策略”使得 Hibernate Search 能够加载给定类型的实体来 return entities loaded from an external source as hits in search queries

使用 Hibernate ORM integration 时,针对每一个 Hibernate ORM 实体都会自动配置选择性加载策略,无需进一步配置。

使用 Standalone POJO Mapper ,实体类型为 defined with @SearchEntity ,并且为了在搜索查询中返回从外部源加载的实体,必须显式地使用 @SearchEntity(loadingBinder = …​) 应用选择加载策略。

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

@SearchEntity 及其相应的程序映射 .searchEntity() 对于 Hibernate ORM 实体是没必要的,并且在使用 Hibernate ORM integration 时事实上也不受支持。

请参阅 HSEARCH-5076 以追踪允许在 Hibernate ORM 集成中使用 @SearchEntity 映射非 ORM 实体的进度。

示例 23. 使用单机 POJO 映射器分配选择加载策略

@SearchEntity(loadingBinder = @EntityLoadingBinderRef(type = MyLoadingBinder.class)) (1)
@Indexed
public class Book {
@Singleton
public class MyLoadingBinder implements EntityLoadingBinder { (1)
    @Override
    public void bind(EntityLoadingBindingContext context) { (2)
        context.selectionLoadingStrategy( (3)
                Book.class, (4)
                new MySelectionLoadingStrategy<>( Book.class ) (5)
        );
    }
}

以下是 SelectionLoadingStrategy 为虚拟数据存储实现的示例。

示例 24. 实现 SelectionLoadingStrategy

public class MySelectionLoadingStrategy<E>
        implements SelectionLoadingStrategy<E> {
    private final Class<E> rootEntityType;

    public MySelectionLoadingStrategy(Class<E> rootEntityType) {
        this.rootEntityType = rootEntityType;
    }

    @Override
    public SelectionEntityLoader<E> createEntityLoader(
            LoadingTypeGroup<E> includedTypes, (1)
            SelectionLoadingOptions options) {
        MyDatastoreConnection connection =
                options.context( MyDatastoreConnection.class ); (2)
        return new SelectionEntityLoader<E>() {
            @Override
            public List<E> load(List<?> identifiers, Deadline deadline) {
                return connection.loadEntitiesByIdInSameOrder( (3)
                        rootEntityType, identifiers );
            }
        };
    }
}

Hibernate Search 将通过将具有相同 SelectionLoadingStrategy 的类型或根据 equals() / hashCode() 相等的具有不同策略的类型分组在一起来优化加载。

在将类型分组在一起时,只会调用其中一种策略,且它将传递一个“类型组”,其中包括应该加载的所有类型。

在从“父级”实体类型配置了加载绑定器时,当子类型继承了它并为子类型设置了相同的策略时,尤其会发生这种情况。

一旦所有要搜索的所有类型实现了其选择性加载策略并分配给它们,它们就可以在 querying 时作为命中加载:

示例 25. 使用单机 POJO 映射器将实体作为搜索查询命中加载

MyDatastore datastore = /* ... */ (1)
SearchMapping searchMapping = /* ... */ (2)
try ( MyDatastoreConnection connection = datastore.connect(); (3)
        SearchSession searchSession = searchMapping.createSessionWithOptions() (4)
                .loading( o -> o.context( MyDatastoreConnection.class, connection ) ) (5)
                .build() ) { (6)
    List<Book> hits = searchSession.search( Book.class ) (7)
            .where( f -> f.matchAll() )
            .fetchHits( 20 ); (8)
}

10.2.6. Programmatic mapping

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

@SearchEntity 及其相应的程序映射 .searchEntity() 对于 Hibernate ORM 实体是没必要的,并且在使用 Hibernate ORM integration 时事实上也不受支持。

请参阅 HSEARCH-5076 以追踪允许在 Hibernate ORM 集成中使用 @SearchEntity 映射非 ORM 实体的进度。

你也可以通过 programmatic mapping 将类型标记为实体类型。行为和选项与基于注解的映射相同。

示例 26. 使用 .searchEntity() 将类型标记为实体类型

TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.searchEntity();
TypeMappingStep authorMapping = mapping.type( Author.class );
authorMapping.searchEntity().name( "MyAuthorName" );

10.3. Entity/index mapping

10.3.1. Basics

要索引一个实体,必须使用 @Indexed 为其添加注释。

示例 27. 使用 @Indexed 将类标记为已编入索引

@Entity
@Indexed
public class Book {

子类继承 @Indexed 注释,并且默认情况下还将被编入索引。每个已编入索引的子类将具有其自己的索引,尽管在搜索时这将是透明的 ( all targeted indexes will be queried simultaneously )。

如果 @Indexed 被继承的事实对您的应用程序造成问题,您可以使用 @Indexed(enabled = false) 对子类进行注释。

默认情况下:

  1. 索引名称将等于实体名称,在 Hibernate ORM 中使用 _@Entity_批注设置,其默认值为简单类名。

  2. 使用 Hibernate ORM integration ,编入索引的文档的标识符将为 generated from the entity identifier 。开箱即用支持用于实体标识符的最常用类型,但对于更特殊的类型,您可能需要特定的配置。

使用 Standalone POJO Mapper 时,已索引文档的标识符需要是 mapped explicitly

有关详细信息,参见 Mapping the document identifier

  1. 索引将没有任何字段。必须将字段显式映射到属性。有关详细信息,请参见 Mapping a property to an index field with @GenericField, @FullTextField, …​

10.3.2. Explicit index/backend

可以通过设置 @Indexed(index = …​) 更改索引的名称。请注意,索引名称在给定的应用程序中必须唯一。

示例 28. 使用 @Indexed.index 显式指定索引名称

@Entity
@Indexed(index = "AuthorIndex")
public class Author {

如果你 defined named backends,可以将实体映射到除默认后端以外的其他后端。通过设置 @Indexed(backend = "backend2"),会通知 Hibernate Search,你的实体的索引必须在名为“backend2”的后端中创建。如果模型有明确定义且索引要求有很大差异的子部分,这可能有用。

示例 29. 使用 @Indexed.backend 显式指定后端

@Entity
@Table(name = "\"user\"")
@Indexed(backend = "backend2")
public class User {

由不同后端索引的实体不能成为同一查询的目标。例如,根据以上定义的映射,以下代码将引发异常,因为 AuthorUser 在不同的后端中被索引:

// This will fail because Author and User are indexed in different backends searchSession.search( Arrays.asList( Author.class, User.class ) ) .where( f → f.matchAll() ) .fetchHits( 20 ); // This will fail because Author and User are indexed in different backends searchSession.search( Arrays.asList( Author.class, User.class ) ) .where( f → f.matchAll() ) .fetchHits( 20 );

10.3.3. Conditional indexing and routing

将实体映射到索引并不总是像 “此实体类型转到此索引” 这么简单。由于多种原因,但主要出于性能原因,您可能希望自定义给定实体在何时和何处编制索引:

  1. 你可能不想索引给定类型的所有实体:例如,当其 _status_属性设置为 _DRAFT_或 _ARCHIVED_时,防止实体索引,因为用户不应该搜索这些实体。

  2. 你可能希望 route entities to a specific shard of the index:例如,根据其 _language_属性对实体进行路由,因为每个用户都有一个特定语言,只搜索其语言中的实体。

这些行为可以通过使用 @Indexed(routingBinder = …​) 为已编制索引的实体类型分配路由桥梁在 Hibernate Search 中实现。

有关路由桥的详细信息,参见 Routing bridge

10.3.4. Programmatic mapping

你也可以通过 programmatic mapping 将实体标记为已索引。行为和选项与基于注解的映射相同。

示例 30. 使用 .indexed() 将类标记为已编入索引

TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
TypeMappingStep authorMapping = mapping.type( Author.class );
authorMapping.indexed().index( "AuthorIndex" );
TypeMappingStep userMapping = mapping.type( User.class );
userMapping.indexed().backend( "backend2" );

10.4. Mapping the document identifier

10.4.1. Basics

索引文档(与实体类似)需要分配一个标识符,以便 Hibernate Search 可以处理更新和删除。

indexing Hibernate ORM entities 中,实体标识符默认用作文档标识符。如果实体标识符有 supported type,标识符映射将立即生效,无需显式映射。

在使用 Standalone POJO Mapper 时,文档标识符需要是 mapped explicitly

10.4.2. Explicit identifier mapping

在以下情况下需要显式标识符映射:

  1. Hibernate Search 不了解实体标识符(例如,当使用 Standalone POJO Mapper时)。

  2. 或者文档标识符不是实体标识符。

  3. 或者实体标识符的类型在默认情况下不受支持。尤其是复合标识符(Hibernate ORM 的 @EmbeddedId@IdClass)的情况。

要选择一个映射到文档标识符的属性,只需对该属性应用 @DocumentId 注释:

示例 31. 使用 @DocumentId 显式地将属性映射到文档标识符

@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    @NaturalId
    @DocumentId
    private String isbn;

    public Book() {
    }

    // Getters and setters
    // ...

}

当属性类型不受支持时,还需要 implement a custom identifier bridge,然后在 @DocumentId 注解中引用它:

示例 32. 使用 @DocumentId 将具有不受支持类型的属性映射到文档标识符

@Entity
@Indexed
public class Book {

    @Id
    @Convert(converter = ISBNAttributeConverter.class)
    @DocumentId(identifierBridge = @IdentifierBridgeRef(type = ISBNIdentifierBridge.class))
    private ISBN isbn;

    public Book() {
    }

    // Getters and setters
    // ...

}

10.4.3. Supported identifier property types

以下是列出所有具有内置标识符桥接器的类型的表格,即在将属性映射到文档标识符时开箱即用支持的属性类型。

该表格还解释了分配给文档标识符的值,即传递给底层后端的价值。

表 3. 具有内置标识符桥接器的属性类型

Property type

Value of document identifiers

Limitations

All enum types

name() as a java.lang.String

-

java.lang.String

Unchanged

-

java.lang.Character, char

A single-character java.lang.String

-

java.lang.Byte, byte

toString()

-

java.lang.Short, short

toString()

-

java.lang.Integer, int

toString()

-

java.lang.Long, long

toString()

-

java.lang.Double, double

toString()

-

java.lang.Float, float

toString()

-

java.lang.Boolean, boolean

toString()

-

java.math.BigDecimal

toString()

-

java.math.BigInteger

toString()

-

java.net.URI

toString()

-

java.net.URL

toExternalForm()

-

java.time.Instant

Formatted according to DateTimeFormatter.ISO_INSTANT.

-

java.time.LocalDate

Formatted according to DateTimeFormatter.ISO_LOCAL_DATE.

-

java.time.LocalTime

Formatted according to DateTimeFormatter.ISO_LOCAL_TIME.

-

java.time.LocalDateTime

Formatted according to DateTimeFormatter.ISO_LOCAL_DATE_TIME.

-

java.time.OffsetDateTime

Formatted according to DateTimeFormatter.ISO_OFFSET_DATE_TIME.

-

java.time.OffsetTime

Formatted according to DateTimeFormatter.ISO_OFFSET_TIME.

-

java.time.ZonedDateTime

Formatted according to DateTimeFormatter.ISO_ZONED_DATE_TIME.

-

java.time.ZoneId

getId()

-

java.time.ZoneOffset

getId()

-

java.time.Period

根据 ISO 8601 format for a duration格式化(例如 P1900Y12M21D)。

-

java.time.Duration

根据 ISO 8601 format for a duration格式化,仅使用秒和纳秒(例如 PT1.000000123S)。

-

java.time.Year

根据 ISO 8601 format for a Year格式化(例如 _2017_表示公元 2017 年, _0000_表示公元前 1 年, _-10000_表示公元前 10001 年等)。

-

java.time.YearMonth

根据 ISO 8601 format for a Year-Month格式化(例如 _2017-11_表示 2017 年 11 月)。

-

java.time.MonthDay

根据 ISO 8601 format for a Month-Day格式化(例如 _&#8212;&#8203;11-06_表示 11 月 6 日)。

-

java.util.UUID

toString() as a java.lang.String

-

java.util.Calendar

一个 java.time.ZonedDateTime 表示相同的日期/时间和时区,根据 DateTimeFormatter.ISO_ZONED_DATE_TIME 进行格式化。

参见 Support for legacy java.util date/time APIs.

java.util.Date

Instant.ofEpochMilli(long) 作为 java.time.Instant 根据 DateTimeFormatter.ISO_INSTANT 进行格式化。

参见 Support for legacy java.util date/time APIs.

java.sql.Timestamp

Instant.ofEpochMilli(long) 作为 java.time.Instant 根据 DateTimeFormatter.ISO_INSTANT 进行格式化。

参见 Support for legacy java.util date/time APIs.

java.sql.Date

Instant.ofEpochMilli(long) 作为 java.time.Instant 根据 DateTimeFormatter.ISO_INSTANT 进行格式化。

参见 Support for legacy java.util date/time APIs.

java.sql.Time

Instant.ofEpochMilli(long) 作为 java.time.Instant,根据 DateTimeFormatter.ISO_INSTANT 进行格式化。

参见 Support for legacy java.util date/time APIs.

GeoPoint_ and subtypes_

纬度(double 类型)和经度(double 类型),并用逗号分隔(例如 41.8919, 12.51133)。

-

10.4.4. Programmatic mapping

你也可以通过 programmatic mapping 映射文档标识符。行为和选项与基于注解的映射相同。

示例 33. 使用 .documentId() 显式地将属性映射到文档标识符

TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "isbn" ).documentId();

10.5. Mapping a property to an index field with @GenericField, @FullTextField, …​

10.5.1. Basics

实体的属性可以直接映射到索引字段:您只需添加注释,通过注释属性配置字段,而 Hibernate Search 将负责提取属性值并在必要时填充索引字段。

将属性映射到索引字段如下所示:

示例 34. 直接将属性映射到字段
@FullTextField(analyzer = "english", projectable = Projectable.YES) (1)
@KeywordField(name = "title_sort", normalizer = "english", sortable = Sortable.YES) (2)
private String title;

@GenericField(projectable = Projectable.YES, sortable = Sortable.YES) (3)
private Integer pageCount;

在映射一个属性之前,您必须考虑两件事:

The @*Field annotation

最简单的形式中,属性/字段映射是通过将 @GenericField 标注应用于属性来实现的。此标注适用于每种受支持的属性类型,但受到限制:它尤其不允许全文搜索。要更深入,你需要依赖于不同的更具体的标注,这些标注提供特定的属性。可在 Available field annotations 中详细了解可用标注。

The type of the property

为了使 @*Field 标注正确工作,Hibernate Search 必须支持映射属性的类型。请参阅 Supported property types 以获取开箱即用支持的所有类型的列表,并参阅 Mapping custom property types 以获取有关如何处理更复杂类型的指示,无论是简单的容器 ( List<String>Map<String, Integer> 、…),还是自定义类型。

10.5.2. Available field annotations

存在各种字段注释,每个注释提供自己的一组属性。

本节列出了不同的注解及其用法。有关可用属性的详细信息,参见 Field annotation attributes

@GenericField

在内置支持下适用于每种属性类型的一个可靠的默认选择。

使用此注释映射的字段不提供任何高级功能,例如全文搜索:对通用字段的匹配是精确匹配。

@FullTextField

值被视为多个单词的文本字段。仅适用于 String 字段。

全文文本字段上的匹配可以是:匹配包含给定词的字段、匹配不区分大小写的字段、忽略重音符号的匹配字段…。

全文文本字段还允许进行 highlighting

应该给全文文本字段分配一个 analyzer,通过其名称引用。默认情况下,名为_default_的分析器将被使用。有关分析器和全文文本分析的更多详细信息,请参阅 Analysis。有关如何更改默认分析器的说明,请参阅后端文档中的专用部分: LuceneElasticsearch

注意,你还可以定义 a search analyzer以对搜索词进行不同的分析。

全文字段不能被排序或聚合。如果你需要对属性值排序或聚合,建议使用 @KeywordField ,必要时结合使用一个规范化器(请参阅下文)。请注意,可以将多个字段添加到同一个属性,因此,如果同时需要全文搜索和排序,你可以同时使用 @FullTextField@KeywordField :你只需要为这两个字段中的每一个使用一个不同的 name

@KeywordField

值被视为单个关键词的文本字段。仅适用于 String 字段。

关键词字段允许 more subtle matches,类似于全文字段,区别在于关键词字段仅包含一个令牌。另一方面,这一限制允许关键词字段成为 sorted onaggregated

可以给关键字字段分配一个 normalizer,通过其名称引用。有关规范化器和全文文本分析的更多详细信息,请参阅 Analysis

@ScaledNumberField

用于整数或浮点值的数字字段,这些值需要比双精度更高的精度,但始终具有大致相同的数量级。仅适用于 java.math.BigDecimaljava.math.BigInteger 字段。

缩放后的数字被索引为整数,通常是长整数(64 位),并且对于所有文档中字段的所有值都具有固定比例。由于缩放后的数字使用固定精度进行索引,因此它们无法表示所有 BigDecimalBigInteger 值。太大而无法索引的值将触发运行时异常。具有尾随小数位的值将四舍五入到最接近的整数。

此注释允许设置 the decimalScale attribute

@NonStandardField

用于高级用例的标注,其中使用了 value binder ,而绑定器预计定义一个索引字段类型,该类型不支持任何标准选项: searchablesortable 、…​

当需要后端内生字段类型时,此标注非常有用:对于 Elasticsearch 为 defining the mapping directly as JSON ,对于 Lucene 为 manipulating IndexableField directly

使用此注解映射的字段只能从注解中配置极少数选项(无 searchable/sortable/等),但值绑定器将能够选取非标准字段类型,这通常提供更大的灵活性。

@VectorField

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

+ 用于 vector search中的特定字段类型。

+ 向量场接受 float[]byte[] 类型的值,并且要求预先指定存储向量的 dimension ,并且索引向量大小与该维度匹配。

+ 此外,向量场允许选择性配置在搜索期间使用的 similarity function 、在索引期间使用的 efConstructionm

+ 警告:与其他字段类型相反,向量字段默认情况下禁用容器提取。手动将 extraction 设置为 _DEFAULT_会导致异常。仅明确 configured extractors 允许用于向量字段。

+ 警告:不允许在同一字段内索引多个向量,即向量字段不能为 multivalued

10.5.3. Field annotation attributes

存在各种字段映射注解,每个注解都提供自己的属性集。

本部分列出了不同的注释属性及其用途。有关可用注释的更多详细信息,请参阅 Available field annotations

name

索引字段的名称。默认情况下,它与属性名称相同。当将单个属性映射到多个字段时,你可能需要更改它。

值:String。名称不得包含点字符 (.)。默认为属性名称。

sortable

该字段是否可 sorted on ,即是否向索引添加特定的数据结构,以便在查询时允许高效排序。

值:Sortable.YESSortable.NOSortable.DEFAULT

此选项不适用于 @FullTextField。请参阅 here以获取说明和一些解决方案。

projectable

该字段是否可 projected on ,即字段值是否存储在索引中,以便稍后在查询时进行检索。

值:Projectable.YESProjectable.NOProjectable.DEFAULT

LuceneElasticsearch后端的默认值不同:对于 Lucene,默认值为 Projectable.NO,而对于 Elasticsearch,默认值为 Projectable.YES

对于 Elasticsearch,如果 projectable_或 _sortable_中的任何属性在 _GeoPoint_字段上解析为 _YES,那么此字段将自动成为 projectable_和 _sortable,即使其中一个显式设置为 _NO_也是如此。

aggregable

该字段是否可 aggregated ,即字段值是否存储在索引中的特定数据结构中,以便在稍后查询时允许聚合。

值:Aggregable.YESAggregable.NOAggregable.DEFAULT

此选项不适用于 @FullTextField。请参阅 here以获取说明和一些解决方案。

searchable

该字段是否可进行搜索。即该字段是否被索引,以便稍后在查询时允许应用谓词。

值:Searchable.YESSearchable.NOSearchable.DEFAULT

indexNullAs

在属性值为 null 时用作替换的值。

默认已禁用。

替换被定义为 String。因此,必须对其值进行分析。在 Supported property types查找列 _Parsing method for 'indexNullAs'_以了解解析时使用的格式。

extraction

在容器类型 ( ListOptionalMap 、…) 的情况下,如何从属性中提取要索引的元素。

默认情况下,对于具有容器类型的属性,最内层元素将被索引。例如,对于类型为 List<String> 的属性,类型为 String 的元素将被索引。

向量字段默认禁用提取。

此默认行为和如何覆盖它的方法在部分 Mapping container types with container extractors中进行了说明。

analyzer

索引和查询时对字段值应用的分析器。只适用于 @FullTextField

默认情况下,将使用名为 default 的分析器。

有关分析器和全文文本分析的更多详细信息,请参阅 Analysis

searchAnalyzer

一个可选项的不同的分析器,推翻 analyzer 属性中定义的一个,仅在分析搜索项时使用。

如果未定义,则会使用分配给 analyzer 的分析器。

有关分析器和全文文本分析的更多详细信息,请参阅 Analysis

normalizer

在索引和查询时对字段值应用的归一化器。只适用于 @KeywordField

有关规范化器和全文文本分析的更多详细信息,请参阅 Analysis

norms

是否应存储字段的索引时评分信息。只适用于 @KeywordField@FullTextField

启用规范将提高评分的质量。禁用规范将减少索引使用的磁盘空间。

值:Norms.YESNorms.NONorms.DEFAULT

termVector

存储词向量的策略。只适用于 @FullTextField

此属性的不同值是:

ValueDefinition_TermVector.YES_

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

TermVector.NO

不存储术语向量。

TermVector.WITH_POSITIONS

存储术语向量和令牌位置信息。这与 TermVector.YES 相同,此外还包含文档中每个术语出现的顺序位置。

TermVector.WITH_OFFSETS

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

TermVector.WITH_POSITION_OFFSETS

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

TermVector.WITH_POSITIONS_PAYLOADS

存储术语向量、令牌位置和令牌有效负载。这与 TermVector.WITH_POSITIONS 相同,此外还包含文档中每个术语出现的有效负载。

TermVector.WITH_POSITIONS_OFFSETS_PAYLOADS

存储术语向量、令牌位置、偏移量信息和令牌有效负载。这与 TermVector.WITH_POSITION_OFFSETS 相同,此外还包含文档中每个术语出现的有效负载。

请注意,全文文本字段中 highlighter types requested可能会影响最终确定的词向量存储策略。因为 rapid 矢量高亮显示器类型在词向量存储策略方面具有 specific requirements,如果明确或隐式请求它,通过使用_Highlightable.ANY_,它将把策略设置为_TermVector.WITH_POSITIONS_OFFSETS_,除非已经指定策略。如果使用了与 rapid 矢量高亮显示器不兼容的非默认策略,将抛出异常。

decimalScale

在大数字 ( BigIntegerBigDecimal ) 作为定点整数进行索引之前,如何调整其刻度。只适用于 @ScaledNumberField

要索引小数点后有有效数字的数字,请将 decimalScale 设置为您需要索引的数字位数。小数点在索引之前将向右移动相应次数,从而保留小数部分中的相应位数。要索引无法放入长整数的非常大数字,请将小数点设置为负数。小数点在索引之前将向左移动相应次数,从而丢弃小数部分中的所有数字。

BigDecimal 允许具有严格正值 decimalScale,因为 BigInteger 值没有小数位。

请注意,小数点的移动是完全透明的,并且不会影响您使用搜索 DSL 的方式:您需要提供“正常”的 BigDecimalBigInteger 值,而 Hibernate Search 将透明地应用 decimalScale 和舍入。

由于舍入,搜索谓词和排序仅将与 decimalScale 允许的值一样精确。

请注意,舍入不会影响投影,投影会返回原始值,而不会损失任何精度。

典型的用例是货币金额,其小数刻度为 2,因为通常小数点后只使用两个数字。使用 Hibernate ORM integration 时,会自动从相关 SQL @Column 的基础 scale 值采用默认 decimalScale ,并使用 Hibernate ORM 元数据。使用 decimalScale 属性可以明确重写该值。

highlightable

该字段是否可以为 highlighted ,如果可以,可以对其应用哪些高亮类型。即,字段值是否以特定格式进行索引/存储,以允许在以后查询时高亮。只适用于 @FullTextField

虽然在大多数情况下选择一种高亮显示类型就足够了,但此属性可以接受多个不矛盾的值。请参阅 highlighter types section以查看选择哪个高亮显示。可用的值包括:

ValueDefinition_Highlightable.NO_

不允许对该字段进行高亮显示。

Highlightable.ANY

允许对该字段应用任何高亮显示器类型进行高亮显示。

Highlightable.PLAIN

允许将普通高亮类型应用于高亮字段。

Highlightable.UNIFIED

允许将统一高亮类型应用于高亮字段。

Highlightable.FAST_VECTOR

允许将快速向量高亮类型应用于对字段的高亮显示。此高亮显示类型要求一个 term vector storage strategy被设置为 WITH_POSITIONS_OFFSETS_或 _WITH_POSITIONS_OFFSETS_PAYLOADS

Highlightable.DEFAULT

使用取决于整体字段配置的后端特定默认值。 Elasticsearch’s默认值是 [Highlightable.PLAIN, Highlightable.UNIFIED]Lucene’s默认值取决于为该字段配置的 projectable value。如果该字段是可投影的,则支持 [PLAIN, UNIFIED]_高亮显示。否则,不支持高亮显示 (_Highlightable.NO)。此外,如果 term vector storing strategy被设置为 WITH_POSITIONS_OFFSETS_或 _WITH_POSITIONS_OFFSETS_PAYLOADS,如果后端已支持其他两个 ([PLAIN, UNIFIED]),则两个后端都将支持 _FAST_VECTOR_高亮显示。

dimension

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

+ 存储向量的尺寸。这是一个必填字段。该尺寸应与用于将数据转换为向量表示的模型生成的向量的向量尺寸相符。它应当是一个正整数。后端对允许的最大值有具体规定。对于 Lucene backend,维度必须在 _[1, 4096]_范围内。对于 Elasticsearch backend,范围取决于分布情况。请参阅 Elasticsearch/ OpenSearch 的特定文档来了解这些分布的向量类型。

+ 仅在 @VectorField 上可用。

vectorSimilarity

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

+ 定义在 vector search期间如何计算向量相似度。

+ 仅在 @VectorField 上可用。

+

Value

Definition

VectorSimilarity.L2

L2(欧几里得)范数,这是大多数场景的合理默认值。矢量 xy 之间的距离计算为 \(d(x,y) = \sqrt{\sum_{i=1}^{n} (x_i - y_i)^2 } \) 评分函数为 \(s = \frac{1}{1+d^2}\)

VectorSimilarity.DOT_PRODUCT

内积(特别是点积)。向量之间的距离 x_和 _y_被计算为 \(d(x,y) = \sum{i=1}^{n} x_i \cdot y_i \) 而得分函数是 \(s = \frac{1}{1+d}\)。为了有效利用这个相似性,索引和搜索向量 *must*应该被归一化;否则,搜索可能会产生较差的结果。浮点向量必须 normalized to be of unit length,而字节向量应简单地都具有相同的规范。

为了有效地使用这种相似度,索引和搜索向量 *must*必须经过规范化;否则搜索可能会产生较差的结果。浮点向量必须 normalized to be of unit length,而字节向量应简单地具有相同的范数。

VectorSimilarity.COSINE

余弦相似性。矢量 xy 之间的距离计算为 \(d(x,y) = \frac{1 - \sum_{i=1} ^{n} x_i \cdot y_i }{ \sqrt{ \sum_{i=1} ^{n} x_i^2 } \sqrt{ \sum_{i=1} ^{n} y_i^2 }} \) 评分函数为 \(s = \frac{1}{1+d}\)

VectorSimilarity.MAX_INNER_PRODUCT

类似于点积相似性,但不需要矢量标准化。矢量 xy 之间的距离计算为 \(d(x,y) = \sum_{i=1}^{n} x_i \cdot y_i \) 评分函数为 \(s = \begin{cases} \frac{1}{1-d} & \text{if d < 0}\\ d+1 & \text{otherwise} \end{cases} \)

VectorSimilarity.DEFAULT

使用后端特定的默认值。对于 Lucene backend,使用 _L2_相似性。

+ 矢量相似性如何匹配后端特定的值

Hibernate Search Value

Lucene Backend

Elasticsearch Backend

Elasticsearch Backend (OpenSearch distribution)

DEFAULT

EUCLIDEAN

Elasticsearch default

OpenSearch default.

L2

EUCLIDEAN

l2_norm

l2

DOT_PRODUCT

DOT_PRODUCT

dot_product

当前由 OpenSearch not supported,并将导致异常。

COSINE

COSINE

cosine

cosinesimil

MAX_INNER_PRODUCT

MAXIMUM_INNER_PRODUCT

max_inner_product

当前由 OpenSearch not supported,并将导致异常。

efConstruction

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

+ efConstruction 是在 k-NN 图表创建期间使用的动态列表的尺寸。它会影响矢量的存储方式。更高的值会导致图表更加准确,但索引速度较慢。

+ 默认值特定于后端。

+ 仅在 @VectorField 上可用。

m

以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。

通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。

我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。

+ HNSW (Hierarchical Navigable Small World graphs) graph 中每个节点将连接到的邻居数。修改此值会对内存消耗产生影响。建议将此值保持在 2 到 100 之间。

+ 默认值特定于后端。

+ 仅在 @VectorField 上可用。

10.5.4. Supported property types

下表列出了内置值桥接器的所有类型,即在将属性映射到索引字段时开箱即用支持的属性类型。

该表还解释了分配给索引字段的值,即传递给底层后端以进行索引的值。

有关后端使用的基础索引和存储的信息,请参阅 Lucene field typesElasticsearch field types(取决于您的后端)。

表 4. 具有内置值桥接器的属性类型

Property type

索引字段值(如果不同)

Limitations

查询字符串谓词中“indexNullAs”/术语的解析方法

All enum types

name() as a java.lang.String

-

Enum.valueOf(String)

java.lang.String

-

-

-

java.lang.Character, char

A single-character java.lang.String

-

Accepts any single-character java.lang.String

java.lang.Byte, byte

-

-

Byte.parseByte(String)

java.lang.Short, short

-

-

Short.parseShort(String)

java.lang.Integer, int

-

-

Integer.parseInt(String)

java.lang.Long, long

-

-

Long.parseLong(String)

java.lang.Double, double

-

-

Double.parseDouble(String)

java.lang.Float, float

-

-

Float.parseFloat(String)

java.lang.Boolean, boolean

-

-

接受字符串 truefalse,不区分大小写

java.math.BigDecimal

-

-

new BigDecimal(String)

java.math.BigInteger

-

-

new BigInteger(String)

java.net.URI

toString() as a java.lang.String

-

new URI(String)

java.net.URL

toExternalForm() as a java.lang.String

-

new URL(String)

java.time.Instant

-

Possibly lower range/resolution

Instant.parse(String)

java.time.LocalDate

-

Possibly lower range/resolution

LocalDate.parse(String).

java.time.LocalTime

-

Possibly lower range/resolution

LocalTime.parse(String)

java.time.LocalDateTime

-

Possibly lower range/resolution

LocalDateTime.parse(String)

java.time.OffsetDateTime

-

Possibly lower range/resolution

OffsetDateTime.parse(String)

java.time.OffsetTime

-

Possibly lower range/resolution

OffsetTime.parse(String)

java.time.ZonedDateTime

-

Possibly lower range/resolution

ZonedDateTime.parse(String)

java.time.ZoneId

getId() as a java.lang.String

-

ZoneId.of(String)

java.time.ZoneOffset

getTotalSeconds() as a java.lang.Integer

-

ZoneOffset.of(String)

java.time.Period

格式化的 java.lang.String: &lt;years on 11 characters&gt;&lt;months on 11 characters&gt;&lt;days on 11 characters&gt;

-

Period.parse(String)

java.time.Duration

toNanos() as a java.lang.Long

Possibly lower range/resolution

Duration.parse(String)

java.time.Year

-

Possibly lower range/resolution

Year.parse(String)

java.time.YearMonth

-

Possibly lower range/resolution

YearMonth.parse(String)

java.time.MonthDay

-

-

MonthDay.parse(String)

java.util.UUID

toString() as a java.lang.String

-

UUID.fromString(String)

java.util.Calendar

表示相同的日期/时间和时区的 java.time.ZonedDateTime

参见 Support for legacy java.util date/time APIs.

ZonedDateTime.parse(String)

java.util.Date

Instant.ofEpochMilli(long) as a java.time.Instant.

参见 Support for legacy java.util date/time APIs.

Instant.parse(String)

java.sql.Timestamp

Instant.ofEpochMilli(long) as a java.time.Instant.

参见 Support for legacy java.util date/time APIs.

Instant.parse(String)

java.sql.Date

Instant.ofEpochMilli(long) as a java.time.Instant.

参见 Support for legacy java.util date/time APIs.

Instant.parse(String)

java.sql.Time

Instant.ofEpochMilli(long) as a java.time.Instant.

参见 Support for legacy java.util date/time APIs.

Instant.parse(String)

GeoPoint_ and subtypes_

-

-

作为双精度的纬度和作为双精度的经度,用逗号分隔。例如 41.8919, 12.51133

日期/时间字段的范围和分辨率除了少数例外,大多数日期和时间值都原样传递到后端;例如, LocalDateTime 属性将作为 LocalDateTime 传递到后端。

然而,在内部,Lucene 和 Elasticsearch 后端会使用日期/时间类型的不同表示方法。因此,存储在索引中的日期和时间字段的范围和分辨率可能小于相应的 Java 类型。

每个后端的文档都提供了更多信息:请参阅 here for Lucenehere for Elasticsearch

10.5.5. Support for legacy java.util date/time APIs

不建议使用 java.util.Calendarjava.util.Datejava.sql.Timestampjava.sql.Datejava.sql.Time 等旧版日期/时间类型,因为它们有很多怪异之处且存在不足之处。一般来说,应该优先考虑在 Java 8 中引入的 java.time 包。

话虽如此,集成约束可能会迫使您依赖于旧版日期/时间 API,这就是为什么 Hibernate Search 仍会尽力支持它们。

由于 Hibernate Search 使用 java.time API 在内部表示日期/时间,因此在可以索引过时的日期/时间类型之前需要对其进行转换。Hibernate Search 保持简单:java.util.Datejava.util.Calendar 等将使用其时间值(自纪元以来的毫秒数)进行转换,假定它在 Java 8 API 中表示相同的日期/时间。在 java.util.Calendar 的情况下,时区信息将保留用于投影。

对于 1900 年后的所有日期,这将按预期的那样工作。

在 1900 年之前,通过 Hibernate Search API 索引和搜索也会像预期的那样工作,但是如果您需要以本机方式访问索引,例如通过对 Elasticsearch 服务器进行直接 HTTP 调用,您会注意到索引值略有“偏差”。这是由于 java.time 和旧版日期/时间 API 的实现不同,导致在解释时间值(自历元以来的毫秒数)时出现细微差异。

“偏移量”是一致的:在构建谓词时它们也将发生,并且在投影时将向相反方向发生。结果,仅依赖于 Hibernate Search API 的应用程序将看不到差异。然而,原生访问索引时会看到差异。

绝大多数用例中,这不会带来问题。如果此行为对于您的应用程序不可接受,然后应该考虑实现自定义 value bridges 并指示 Hibernate Search 默认为 java.util.Date, java.util.Calendar 等使用它们:请参阅 Assigning default bridges with the bridge resolver

从技术上讲,转换很难,因为 java.time API 和旧版日期/时间 API 的内部日历不同。

具体来说:

_java.time_假设 1900 年之前的“地方平太阳时间”,而旧式日期/时间 API 不支持它( JDK-6281408)。结果,对于 1900 年之前的日期,两个 API 报告的时间值(从历元起的毫秒数)将不同。

_java.time_在 1582 年 10 月 15 日之前使用前格里历,这意味着它表现得好像格里历及其闰年系统始终存在一样。另一方面,旧式日期/时间 API 在该日期之前使用儒略历(默认情况下),这意味着闰年并不完全相同。结果,一个 API 认为有效的某些日期将被另一个 API 视为无效,例如 1500 年 2 月 29 日。

这是两个主要问题,但可能还有其他问题。

10.5.6. Mapping custom property types

甚至非 supported out of the box类型也能映射。解决方案多种多样,有的简单,有的功能更强大,但它们都归结为从不支持的类型中提取数据,并将其转换为后端支持的类型。

有两种情况需要区分:

  • 如果不受支持的类型只是一个容器(List&lt;String&gt;)或多个元素类型受支持的嵌套容器(Map&lt;Integer, List&lt;String&gt;&gt;),那么你需要的便是容器提取器。有关更多信息,请参阅 Mapping container types with container extractors

  • 否则,你将不得不依赖一个称为桥接器的自定义组件来从类型提取数据。有关自定义桥接器的更多信息,请参阅 Binding and bridges

10.5.7. Programmatic mapping

你也可以使用 programmatic mapping直接将实体的属性映射到索引字段。行为和选项与基于注释的映射相同。

示例 35. 使用 .genericField().fullTextField() …​ 直接将属性映射到字段中
TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "title" )
        .fullTextField()
                .analyzer( "english" ).projectable( Projectable.YES )
        .keywordField( "title_sort" )
                .normalizer( "english" ).sortable( Sortable.YES );
bookMapping.property( "pageCount" )
        .genericField().projectable( Projectable.YES ).sortable( Sortable.YES );

10.6. Mapping associated elements with @IndexedEmbedded

10.6.1. Basics

仅使用 @Indexed@*Field 注解相结合即可对实体及其直接属性进行索引,这种方式简单易用。实际模型将包含多个对象类型,相互之间持有引用,如以下示例中的 authors 关联。

示例 36.带关联的多实体模型
@Entity
@Indexed (1)
public class Book {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english") (2)
    private String title;

    @ManyToMany
    private List<Author> authors = new ArrayList<>(); (3)

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class Author {

    @Id
    private Integer id;

    private String name;

    @ManyToMany(mappedBy = "authors")
    private List<Book> books = new ArrayList<>();

    public Author() {
    }

    // Getters and setters
    // ...

}

在搜索书籍时,用户可能需要按作者姓名进行搜索。在高性能索引领域,跨索引联接代价高昂,通常不是一种选择。解决此类用例的最佳方式通常是复制数据:在索引 Book 时,只需将所有作者的姓名复制到 Book 文档中。

这就是 @IndexedEmbedded 的作用:它指示 Hibernate Search 将关联对象的字段 embed 到主对象中。在以下示例中,它将指示 Hibernate Search 将 Author 中定义的 name 字段嵌入到 Book 中,创建 authors.name 字段。

@IndexedEmbedded 可以用在 Hibernate ORM 的 @Embedded 属性以及关联(@OneToOne@OneToMany@ManyToMany、…​…​)上。

示例 37. 使用 @IndexedEmbedded 索引关联元素
@Entity
@Indexed
public class Book {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english")
    private String title;

    @ManyToMany
    @IndexedEmbedded (1)
    private List<Author> authors = new ArrayList<>();

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class Author {

    @Id
    private Integer id;

    @FullTextField(analyzer = "name") (2)
    private String name;

    @ManyToMany(mappedBy = "authors")
    private List<Book> books = new ArrayList<>();

    public Author() {
    }

    // Getters and setters
    // ...

}

Document identifiers 不是索引字段。因此, @IndexedEmbedded 将忽略它们。

若要使用 @IndexedEmbedded 嵌入另一个实体的标识符,请使用 @GenericField 或其他 @*Field 注解将该标识符显式映射到字段中。

@IndexedEmbedded 应用于关联(即引用实体的属性(如上例))时,关联必须是双向的。否则,Hibernate Search 会在启动时抛出异常。

请参阅 Reindexing when embedded elements change 以了解此限制背后的原因以及规避它的方法。

可以在多个级别上嵌套索引嵌入;例如,你可以决定对作者的出生地的索引嵌入,以便可以专门搜索由俄罗斯作者编写的书籍:

示例 38. 嵌套多个 @IndexedEmbedded
@Entity
@Indexed
public class Book {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english")
    private String title;

    @ManyToMany
    @IndexedEmbedded (1)
    private List<Author> authors = new ArrayList<>();

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class Author {

    @Id
    private Integer id;

    @FullTextField(analyzer = "name") (2)
    private String name;

    @Embedded
    @IndexedEmbedded (3)
    private Address placeOfBirth;

    @ManyToMany(mappedBy = "authors")
    private List<Book> books = new ArrayList<>();

    public Author() {
    }

    // Getters and setters
    // ...

}
@Embeddable
public class Address {

    @FullTextField(analyzer = "name") (4)
    private String country;

    private String city;

    private String street;

    public Address() {
    }

    // Getters and setters
    // ...

}

默认情况下, @IndexedEmbedded 将嵌套在索引嵌入类型中遇到的其他 @IndexedEmbedded 中,递归且无任何限制,这可能会导致无限递归。

10.6.2. @IndexedEmbedded and null values

@IndexedEmbedded 针对的属性包含 null 元素时,这些元素根本不会被索引。

Mapping a property to an index field with @GenericField, @FullTextField, …​ 相反,没有 indexNullAs 来为 null 对象建立特定值的索引,但是,你可以利用搜索查询中的 exists 谓词,在某个 @IndexedEmbedded 有无值的文档中查找:只需将对象字段的名称传递给 exists 谓词,例如上面的 authors

10.6.3. @IndexedEmbedded on container types

@IndexedEmbedded 针对的属性具有容器类型(ListOptionalMap、…​)时,将嵌入最内层元素。例如,对于类型为 List<MyEntity> 的属性,将嵌入类型为 MyEntity 的元素。

此默认行为和如何覆盖它的方法在部分 Mapping container types with container extractors中进行了说明。

10.6.4. Setting the object field name with name

默认情况下,@IndexedEmbedded 将创建与带注解的属性同名的对象字段,并将嵌入字段添加到该对象字段。因此,如果将 @IndexedEmbedded 应用于 Book 实体中的名为 authors 的属性,则在索引 Book 时,作者的 name 索引字段将被复制到 authors.name 索引字段。

可以通过设置 name 属性来更改对象字段的名称;例如在上述示例中使用 @IndexedEmbedded(name = "allAuthors") 将导致作者的名称被复制到 allAuthors.name 索引字段,而不是 authors.name

名称不得包含点字符(.)。

10.6.5. Setting the field name prefix with prefix

@IndexedEmbedded 中的 prefix 属性已被废弃,并最终将被删除。请改用 name

默认情况下,@IndexedEmbedded 会用它所应用属性的名称和一个点来缀注嵌入字段的名称。因此,如果在 Book 实体中将 @IndexedEmbedded 应用于名为 authors 的属性,则当对 Book 进行索引时,@{7} 字段的作者将被复制到 authors.name 字段中。

可以通过设置 prefix 属性来更改此前缀,例如 @IndexedEmbedded(prefix = "author.")(不要忘记结尾的点!)。

前缀通常应为以单点结尾的非点序列,例如 my_Property.

将前缀更改为结尾不包含任何点的字符串( my_Property ),或包含点但不在末尾的字符串( my.Property. ),将导致复杂、未记录的旧版行为。自行承担风险。

特别是,前缀不以点结尾将导致 some APIs exposed to custom bridges 出现不正确行为:接受字段名称的 addValue / addObject 方法。

10.6.6. Casting the target of @IndexedEmbedded with targetType

默认情况下,自动使用反射检测已索引嵌入值类型,如有必要,还会考虑 container extraction;例如,将检测 @IndexedEmbedded List<MyEntity> 的值为类型 MyEntity。将从值的类型及其超类型的映射中推断要嵌入的字段;在示例中,将会考虑在 MyEntity 及其超类上显示的 @GenericField 注释,但这将忽略在其子类中定义的注释。

如果由于某种原因,模式未公开属性的正确类型(例如,原始 ListList<MyEntityInterface> 而不是 List<MyEntityImpl>),可以通过在 @IndexedEmbedded 中设置 targetType 属性来定义预期的值类型。在引导期间,Hibernate Search 随后将基于给定的目标类型解析要嵌入的字段,并在运行时将值强制转换为给定的目标类型。

将已编入索引的嵌入式值强制转换为指定类型的失败将传播出去并导致索引失败。

10.6.7. Reindexing when embedded elements change

当“嵌入”实体发生更改时,Hibernate Search 将处理“嵌入”实体的重新索引。

只要关联 @IndexedEmbedded 应用于双向(使用 Hibernate ORM 的 mappedBy),这种方式在大多数情况下都将透明地进行。

当 Hibernate Search 无法处理关联时,它将在引导时引发异常。如果发生这种情况,请参阅 Basics了解更多信息。

10.6.8. Embedding the entity identifier

在已索引嵌入类型中将属性映射为 identifier不会在该类型上使用_@IndexedEmbedded_时自动嵌入,因为文档标识符不是字段。

若要内嵌此类属性的数据,可使用 @IndexedEmbedded(includeEmbeddedObjectId = true),而它将使 Hibernate 搜索在生成的内嵌对象中自动插入一个字段,用于索引内嵌类型 的 identifier property

索引字段将被定义为好像将以下 field annotation 放置在嵌入式类型的标识符的属性上: @GenericField(searchable = Searchable.YES, projectable = Projectable.YES) 。索引字段的名称将是标识符属性的名称。默认情况下,其桥将是嵌入式类型的 @DocumentId annotation 引用的标识符桥(如果存在),或者标识符属性类型的默认值桥。

如果你需要更多高级映射(自定义名称、自定义桥、可排序的等),请勿使用 includeEmbeddedObjectId

相反,通过使用 @GenericField or a similar field annotation 注释标识符属性,并在索引嵌入式类型中明确定义该字段,并确保该字段由 configuring filters as necessary 中的 @IndexedEmbedded 包含。

下面是使用 includeEmbeddedObjectId 的示例:

示例 39. 使用 includeEmbeddedObjectId 包含索引嵌入式 ID

@Entity
public class Department {

    @Id
    private Integer id; (1)

    @FullTextField
    private String name;

    @OneToMany(mappedBy = "department")
    private List<Employee> employees = new ArrayList<>();

    // Getters and setters
    // ...

}
@Entity
@Indexed
public class Employee {

    @Id
    private Integer id;

    @FullTextField
    private String name;

    @ManyToOne
    @IndexedEmbedded(includeEmbeddedObjectId = true) (1)
    private Department department;

    // Getters and setters
    // ...

}

10.6.9. Filtering embedded fields and breaking @IndexedEmbedded cycles

默认情况下,@IndexedEmbedded 将“嵌入”一切:索引嵌入元素中遇到的每个字段,以及递归地遇到索引嵌入元素中的每个 @IndexedEmbedded

对于简单的用例,这将非常适用,但对于更复杂的模型可能会导致问题:

  1. 如果索引嵌入式元素声明了许多索引字段(Hibernate Search 字段),其中只有部分字段实际上对“索引嵌入”类型有用,那么额外的字段将不必要地降低索引性能。

  2. 如果存在 @IndexedEmbedded_循环(例如,_A_索引嵌入类型 _B_的 _b,它索引嵌入类型 A_的 _a),索引嵌入式类型将最终拥有无限数量的字段(a.b.someFielda.b.a.b.someFielda.b.a.b.a.b.someField,…​),而 Hibernate Search 会检测到异常并拒绝。

要解决这些问题,可以过滤待嵌入的字段,仅包括实际有用的字段。@IndexedEmbedded 中可用的过滤属性为:

includePaths

应嵌入的索引嵌入式元素的索引字段的路径。

所提供的路径必须相对于索引内嵌元素,即,它们不应包括其 nameprefix

这优先于 includeDepth(见下文)。

不能与同一 @IndexedEmbedded 中的 excludePaths 结合使用。

excludePaths

不得嵌入的索引嵌入式元素的索引字段的路径。

所提供的路径必须相对于索引内嵌元素,即,它们不应包括其 nameprefix

这优先于 includeDepth(见下文)。

不能与同一 @IndexedEmbedded 中的 includePaths 结合使用。

includeDepth

默认情况下,将包含所有字段的所有级别的索引嵌入式。

includeDepth 是将遍历的 @IndexedEmbedded 的数量,即使未通过 includePaths 显式包含这些字段(除非通过 excludePaths 显式排除这些字段),索引嵌入元素的所有字段也将被包含:

includeDepth=0 表示不会包含此索引嵌入式元素的字段,也不会包含任何嵌套索引嵌入式元素的字段,除非通过 includePaths 明确包含这些字段。

includeDepth=1 表示会包含此索引嵌入式元素的字段,除非通过 excludePaths 明确排除这些字段,但嵌套索引嵌入式元素的字段除外( @IndexedEmbedded 中的 @IndexedEmbedded ),除非通过 includePaths 明确包含这些字段。

includeDepth=2 表示会包含此索引嵌入式元素的字段,以及立即嵌套的 @IndexedEmbedded 中的 @IndexedEmbedded 的索引嵌入式元素的字段,除非通过 excludePaths 明确排除这些字段,但不会包含嵌套级别更低 @IndexedEmbedded 中的 @IndexedEmbedded 的索引嵌入式元素的字段,除非通过 includePaths 明确包含这些字段。

依此类推。

默认值取决于 includePaths 属性的值:

如果 includePaths_为空,则默认为 _Integer.MAX_VALUE(包含每个级别的所有字段)

如果 includePaths 不为空,则默认值为 0 (仅包括明确包含的字段)。

动态字段和过滤 Dynamic fields 不会直接受过滤规则影响:动态字段仅在它的父级包含时才会包含。

这意味着实际上 includeDepthincludePaths 约束只需要匹配动态字段的最近静态父级,该字段才会被包括。

在不同的嵌套层级混合使用 includePathsexcludePaths 通常可以在嵌套的 @IndexedEmbedded 的不同层级中使用 includePathsexcludePaths 。在这样做时,请记住每个层级的过滤器只能引用可达路径,即过滤器无法引用被嵌套 @IndexedEmbedded (隐式或显式)排除的路径。

下面有三个示例:一个仅利用 includePaths,一个利用 excludePaths,一个利用 includePathsincludeDepth

示例 40. 使用 includePaths 过滤索引嵌入字段
@Entity
@Indexed
public class Human {

    @Id
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @FullTextField(analyzer = "name")
    private String nickname;

    @ManyToMany
    @IndexedEmbedded(includePaths = { "name", "nickname", "parents.name" })
    private List<Human> parents = new ArrayList<>();

    @ManyToMany(mappedBy = "parents")
    private List<Human> children = new ArrayList<>();

    public Human() {
    }

    // Getters and setters
    // ...

}
示例 41. 使用 excludePaths 过滤索引嵌入字段
@Entity
@Indexed
public class Human {

    @Id
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @FullTextField(analyzer = "name")
    private String nickname;

    @ManyToMany
    @IndexedEmbedded(excludePaths = { "parents.nickname", "parents.parents" })
    private List<Human> parents = new ArrayList<>();

    @ManyToMany(mappedBy = "parents")
    private List<Human> children = new ArrayList<>();

    public Human() {
    }

    // Getters and setters
    // ...

}
示例 42. 使用 includePathsincludeDepth 过滤索引嵌入字段
@Entity
@Indexed
public class Human {

    @Id
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @FullTextField(analyzer = "name")
    private String nickname;

    @ManyToMany
    @IndexedEmbedded(includeDepth = 2, includePaths = { "parents.parents.name" })
    private List<Human> parents = new ArrayList<>();

    @ManyToMany(mappedBy = "parents")
    private List<Human> children = new ArrayList<>();

    public Human() {
    }

    // Getters and setters
    // ...

}

10.6.10. Structuring embedded elements as nested documents using structure

索引嵌入字段可以用 @IndexedEmbedded 注释的 structure 属性配置的两种方式之一进行组织。为了说明结构选项,让我们假设类 Book@Indexed 注释,且其 authors 属性带 @IndexedEmbedded 注释:

  1. Book instance

title = 列维坦觉醒

authors =

Author 实例

firstName = Daniel

lastName = Abraham

Author 实例

firstName = Ty

lastName = Frank

  1. title = Leviathan Wakes

  2. authors =

Author 实例

firstName = Daniel

lastName = Abraham

Author 实例

firstName = Ty

lastName = Frank

  1. Author instance

firstName = Daniel

lastName = Abraham

  1. firstName = Daniel

  2. lastName = Abraham

  3. Author instance

firstName = Ty

lastName = Frank

  1. firstName = Ty

  2. lastName = Frank

DEFAULT or FLATTENED structure

默认情况下,或在使用 @IndexedEmbedded(structure = FLATTENED)(如下所示)时,索引嵌入字段“扁平化”,这意味着树结构不会被保留。

示例 43. 具有扁平结构的 @IndexedEmbedded
@Entity
@Indexed
public class Book {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english")
    private String title;

    @ManyToMany
    @IndexedEmbedded(structure = ObjectStructure.FLATTENED) (1)
    private List<Author> authors = new ArrayList<>();

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class Author {

    @Id
    private Integer id;

    @FullTextField(analyzer = "name")
    private String firstName;

    @FullTextField(analyzer = "name")
    private String lastName;

    @ManyToMany(mappedBy = "authors")
    private List<Book> books = new ArrayList<>();

    public Author() {
    }

    // Getters and setters
    // ...

}

前面提到的图书实例的索引将包含与下述结构大致类似的结构:

  1. Book document

title = 列维坦觉醒

authors.firstName = [Daniel, Ty]

authors.lastName = [Abraham, Frank]

  1. title = Leviathan Wakes

  2. authors.firstName = [Daniel, Ty]

  3. authors.lastName = [Abraham, Frank]

authors.firstNameauthors.lastName 字段已“扁平化”,现在每个字段都有两个值;哪个姓氏对应于哪个名字的知识已丢失。

对于索引编制和查询,这是更高效的,但在根据作者姓氏和作者名字同时查询索引时会产生意外的行为。

例如,即使“Ty Abraham”并不是这本书的作者之一,上文描述的书籍实例仍会显示为与 authors.firstname:Ty AND authors.lastname:Abraham 等查询的匹配项:

示例 44. 使用扁平结构搜索
List<Book> hits = searchSession.search( Book.class )
        .where( f -> f.and(
                f.match().field( "authors.firstName" ).matching( "Ty" ), (1)
                f.match().field( "authors.lastName" ).matching( "Abraham" ) (1)
        ) )
        .fetchHits( 20 );

assertThat( hits ).isNotEmpty(); (2)
NESTED structure

当索引嵌入元素“嵌套”时,即在如下所示情况下使用 @IndexedEmbedded(structure = NESTED) 时,树形结构会通过透明地为每个索引嵌入元素创建一个独立的“嵌套”文档而得以保留。

示例 45. 具有嵌套结构的 @IndexedEmbedded
@Entity
@Indexed
public class Book {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english")
    private String title;

    @ManyToMany
    @IndexedEmbedded(structure = ObjectStructure.NESTED) (1)
    private List<Author> authors = new ArrayList<>();

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class Author {

    @Id
    private Integer id;

    @FullTextField(analyzer = "name")
    private String firstName;

    @FullTextField(analyzer = "name")
    private String lastName;

    @ManyToMany(mappedBy = "authors")
    private List<Book> books = new ArrayList<>();

    public Author() {
    }

    // Getters and setters
    // ...

}

前面提到的图书实例的索引将包含与下述结构大致类似的结构:

  1. Book document

title = 列维坦觉醒

嵌套文档

“authors” 的嵌套文档 1

authors.firstName = Daniel

authors.lastName = Abraham

“authors” 的嵌套文档 2

authors.firstName = Ty

authors.lastName = Frank

  1. title = Leviathan Wakes

  2. Nested documents

“authors” 的嵌套文档 1

authors.firstName = Daniel

authors.lastName = Abraham

“authors” 的嵌套文档 2

authors.firstName = Ty

authors.lastName = Frank

  1. “authors” 的嵌套文档 1

authors.firstName = Daniel

authors.lastName = Abraham

  1. authors.firstName = Daniel

  2. authors.lastName = Abraham

  3. “authors” 的嵌套文档 2

authors.firstName = Ty

authors.lastName = Frank

  1. authors.firstName = Ty

  2. authors.lastName = Frank

从本质上说,该图书编入三个文档索引:图书的根文档以及两个内部“嵌套”文档以供作者使用,保留了哪个姓氏对应于哪个名字的知识,代价是索引编制和查询的性能下降。

如果在嵌套文档中的字段上构建谓词时特别小心,则使用包含作者姓氏和名的谓词的查询会按照直觉预期的那样表现。

例如,得益于谓词 nested (仅能在使用 NESTED 结构进行索引时使用),上面描述的图书实例不会显示为匹配查询 authors.firstname:Ty AND authors.lastname:Abraham

示例 46. 使用嵌套结构搜索

List<Book> hits = searchSession.search( Book.class )
        .where( f -> f.nested( "authors" ) (1)
                .add( f.match().field( "authors.firstName" ).matching( "Ty" ) ) (2)
                .add( f.match().field( "authors.lastName" ).matching( "Abraham" ) ) ) (2)
        .fetchHits( 20 );

assertThat( hits ).isEmpty(); (3)

10.6.11. Filtering association elements

有时,关联的某些元素才能包含在 @IndexedEmbedded 中。

例如,一个 Book 实体可能会索引嵌入 BookEdition 实例,但一些版本可能会被停用,需要在索引编制之前过滤掉。

通过将 @IndexedEmbedded 应用到表示已过滤关联的瞬态 getter,并通过 @AssociationInverseSide@IndexingDependency.derivedFrom 配置重新索引,可以实现这种过滤。

示例 47. 通过瞬态 getter、 @AssociationInverseSide@IndexingDependency.derivedFrom 过滤 @IndexedEmbedded 关联

@Entity
@Indexed
public class Book {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english")
    private String title;

    @OneToMany(mappedBy = "book")
    @OrderBy("id asc")
    private List<BookEdition> editions = new ArrayList<>(); (1)

    public Book() {
    }

    // Getters and setters
    // ...


    @Transient (2)
    @IndexedEmbedded (3)
    @AssociationInverseSide(inversePath = @ObjectPath({ (4)
            @PropertyValue(propertyName = "book")
    }))
    @IndexingDependency(derivedFrom = @ObjectPath({ (5)
            @PropertyValue(propertyName = "editions"),
            @PropertyValue(propertyName = "status")
    }))
    public List<BookEdition> getEditionsNotRetired() {
        return editions.stream()
                .filter( e -> e.getStatus() != BookEdition.Status.RETIRED )
                .collect( Collectors.toList() );
    }
}
@Entity
public class BookEdition {

    public enum Status {
        PUBLISHING,
        RETIRED
    }

    @Id
    private Integer id;

    @ManyToOne
    private Book book;

    @FullTextField(analyzer = "english")
    private String label;

    private Status status; (6)

    public BookEdition() {
    }

    // Getters and setters
    // ...

}

10.6.12. Programmatic mapping

同样,也可以通过 programmatic mapping 将关联对象字段内嵌到主对象中。该行为和选项与基于注释的映射相同。

示例 48. 使用 .indexedEmbedded() 索引关联元素

TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "title" )
        .fullTextField().analyzer( "english" );
bookMapping.property( "authors" )
        .indexedEmbedded();
TypeMappingStep authorMapping = mapping.type( Author.class );
authorMapping.property( "name" )
        .fullTextField().analyzer( "name" );

10.7. Mapping container types with container extractors

10.7.1. Basics

应用于属性的大多数内置注解在应用于容器类型时都能透明地工作:

  1. _@GenericField_应用于 _String_类型的属性会直接编制索引属性值。

  2. _@GenericField_应用于 _OptionalInt_类型的属性会编制索引选项值(一个整数)。

  3. _@GenericField_应用于 _List&lt;String&gt;_类型的属性会编制索引列表元素(字符串)。

  4. _@GenericField_应用于 _Map&lt;Integer, String&gt;_类型的属性会编制索引映射值(字符串)。

  5. _@GenericField_应用于 _Map&lt;Integer, List&lt;String&gt;&gt;_类型的属性会编制索引映射值中的列表元素(字符串)。

  6. Etc.

同样适用于其他字段注释,如 @FullTextField,特别是 @IndexedEmbedded。而 @VectorField 是此行为的例外,要求 explicit instructions 从容器中提取值。

幕后发生的情况是,Hibernate Search 会检查属性类型并尝试应用“容器提取器”,选择第一个能正常使用的提取器。

10.7.2. Explicit container extraction

在某些情况下,您会希望明确选择要使用的容器提取器。当必须为地图的键而不是值编制索引时就会出现这种情况。相关注解提供一个 extraction 属性来对此进行配置,如下面的示例中所示。

示例 49. 使用显式容器提取器定义将 Map 关键字映射到索引字段

@ElementCollection (1)
@JoinTable(name = "book_pricebyformat")
@MapKeyColumn(name = "format")
@Column(name = "price")
@OrderBy("format asc")
@GenericField( (2)
        name = "availableFormats",
        extraction = @ContainerExtraction(BuiltinContainerExtractors.MAP_KEY) (3)
)
private Map<BookFormat, BigDecimal> priceByFormat = new LinkedHashMap<>();

可以实现和使用自定义容器提取器,但在当下,Hibernate Search 不会检测到此类容器中数据更改必须触发其包含元素的重新索引。因此,相应属性必须 disable reindexing on change

有关更多信息,请参阅 HSEARCH-3688

10.7.3. Disabling container extraction

在某些罕见的情况下,不需要容器提取,而 @GenericField/@IndexedEmbedded 的目的是直接应用于 List/Optional/等。如需忽略默认的容器提取器,大多数注解都提供了一个 extraction 属性。如下所示进行设置以完全禁用提取:

示例 50. 禁用容器提取

@ManyToMany
@GenericField( (1)
        name = "authorCount",
        valueBridge = @ValueBridgeRef(type = MyCollectionSizeBridge.class), (2)
        extraction = @ContainerExtraction(extract = ContainerExtract.NO) (3)
)
private List<Author> authors = new ArrayList<>();

10.7.4. Programmatic mapping

也可以通过 programmatic mapping 在定义 fieldsindexed-embeddeds 时明确选择要使用的容器抽取器。该行为和选项与基于注释的映射相同。

示例 51. 使用 .extractor(…​) / .extactors(…​) 为显式容器提取器定义将 Map 关键字映射到索引字段

bookMapping.property( "priceByFormat" )
        .genericField( "availableFormats" )
                .extractor( BuiltinContainerExtractors.MAP_KEY );

同样,您可以禁用容器提取。

示例 52. 通过 .noExtractors() 禁用容器提取

bookMapping.property( "authors" )
        .genericField( "authorCount" )
                .valueBridge( new MyCollectionSizeBridge() )
                .noExtractors();

10.8. Mapping geo-point types

10.8.1. Basics

Hibernate Search 提供多种空间要素,例如 a distance predicatea distance sort 。这些要素需要索引空间坐标。更准确地说,它需要地理坐标系中的地理点,即纬度和经度进行索引。

地理点有些例外,因为标准 Java 库中没有要表示它们的类型。因此,Hibernate 搜索定义了自己的界面 org.hibernate.search.engine.spatial.GeoPoint。由于您的模型可能会使用不同的类型来表示地理点,因此映射地理点需要一些额外的步骤。

提供了两个选项:

  1. 如果你的地理点由一个专用的不可变类型表示,请直接使用 _@GenericField_和 _GeoPoint_接口,如 here中所解释的。

  2. 对于任何其他情况,按照 here 中的说明,使用更复杂(但功能更强大)的 @GeoPointBinding

10.8.2. Using @GenericField and the GeoPoint interface

当实体模型中的地理点通过专用且不可变的类型表示时,您可以简单地让该类型实现 GeoPoint 接口,并使用简单 property/field mapping@GenericField :

示例 53. 通过实现 GeoPoint 并使用 @GenericField 映射空间坐标
@Embeddable
public class MyCoordinates implements GeoPoint { (1)

    @Basic
    private Double latitude;

    @Basic
    private Double longitude;

    protected MyCoordinates() {
        // For Hibernate ORM
    }

    public MyCoordinates(double latitude, double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    @Override
    public double latitude() { (2)
        return latitude;
    }

    @Override
    public double longitude() {
        return longitude;
    }
}
@Entity
@Indexed
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    private String name;

    @Embedded
    @GenericField (3)
    private MyCoordinates placeOfBirth;

    public Author() {
    }

    // Getters and setters
    // ...

}

地理点类型必须是不可变的,即该实例的纬度和经度永远不会改变。

这是 @GenericField 的核心假设,一般来说,所有 @*Field 注释也都是如此:将忽略坐标变化,并且不会像预期的那样触发重新索引。

如果容纳您坐标的类型是可变的,请不要使用 @GenericField ,而改用 Using @GeoPointBinding, @Latitude and @Longitude

如果您的地理点类型不可变,但扩展 GeoPoint 接口不是一个选项,您还可以使用一个自定义 value bridge,将自定义地理点类型和 GeoPoint 相互转换。GeoPoint 提供了快速构建 GeoPoint 实例的静态方法。

10.8.3. Using @GeoPointBinding, @Latitude and @Longitude

在坐标存储在可变对象中的情况下,解决方案是 @GeoPointBinding 注解。结合 @Latitude@Longitude 注解,它可以映射声明纬度和经度为 double 类型的任何类型的坐标:

示例 54. 使用 @GeoPointBinding 映射空间坐标
@Entity
@Indexed
@GeoPointBinding(fieldName = "placeOfBirth") (1)
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    private String name;

    @Latitude (2)
    private Double placeOfBirthLatitude;

    @Longitude (3)
    private Double placeOfBirthLongitude;

    public Author() {
    }

    // Getters and setters
    // ...

}

@GeoPointBinding 注解也可能应用于属性,在这种情况下,@Latitude@Longitude 必须应用于属性类型的属性:

示例 55. 使用 @GeoPointBinding 映射一个属性中的空间坐标
@Embeddable
public class MyCoordinates { (1)

    @Basic
    @Latitude (2)
    private Double latitude;

    @Basic
    @Longitude (3)
    private Double longitude;

    protected MyCoordinates() {
        // For Hibernate ORM
    }

    public MyCoordinates(double latitude, double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(Double latitude) { (4)
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(Double longitude) {
        this.longitude = longitude;
    }
}
@Entity
@Indexed
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @Embedded
    @GeoPointBinding (5)
    private MyCoordinates placeOfBirth;

    public Author() {
    }

    // Getters and setters
    // ...

}

可以通过多次应用注解并将 markerSet 属性设置为唯一值来处理多组坐标:

示例 56. 使用 @GeoPointBinding 映射多组空间坐标
@Entity
@Indexed
@GeoPointBinding(fieldName = "placeOfBirth", markerSet = "birth") (1)
@GeoPointBinding(fieldName = "placeOfDeath", markerSet = "death") (2)
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @Latitude(markerSet = "birth") (3)
    private Double placeOfBirthLatitude;

    @Longitude(markerSet = "birth") (4)
    private Double placeOfBirthLongitude;

    @Latitude(markerSet = "death") (5)
    private Double placeOfDeathLatitude;

    @Longitude(markerSet = "death") (6)
    private Double placeOfDeathLongitude;

    public Author() {
    }

    // Getters and setters
    // ...

}

10.8.4. Programmatic mapping

同样,也可以通过 programmatic mapping映射地理点字段文档标识符。该行为和选项与基于注释的映射相同。

示例 57. 通过实现 GeoPoint 并使用 .genericField() 映射空间坐标
TypeMappingStep authorMapping = mapping.type( Author.class );
authorMapping.indexed();
authorMapping.property( "placeOfBirth" )
        .genericField();
示例 58. 使用 GeoPointBinder 映射空间坐标
TypeMappingStep authorMapping = mapping.type( Author.class );
authorMapping.indexed();
authorMapping.binder( GeoPointBinder.create().fieldName( "placeOfBirth" ) );
authorMapping.property( "placeOfBirthLatitude" )
        .marker( GeoPointBinder.latitude() );
authorMapping.property( "placeOfBirthLongitude" )
        .marker( GeoPointBinder.longitude() );

10.9. Mapping multiple alternatives

10.9.1. Basics

在某些情况下,根据另一个属性的值,必须以不同的方式对特定属性进行索引。

例如,可能有一个实体,其文本属性的内容根据另一个属性的值(例如 language)以不同的语言显示。在这种情况下,您可能希望根据语言对文本进行不同的分析。

虽然肯定可以用自定义 type bridge 解决此问题,但便捷的解决方案是使用 AlternativeBinder。此粘合剂以这种方式解决此问题:

  1. 在引导程序中,为每个语言声明一个索引字段,将不同的分析器分配给每个字段;

  2. 在运行时,根据语言将文本属性的内容放在不同的字段中。

为了使用此粘合剂,您需要:

  1. 使用 @AlternativeDiscriminator 标注一个属性(例如 language 属性);

  2. 实现一个 AlternativeBinderDelegate,声明索引字段(例如,按语言分隔的字段)并创建一个 AlternativeValueBridge。此桥梁负责在运行时将属性值传递给相关的字段。

  3. AlternativeBinder 应用于托管属性的类型(例如,声明 language 属性和多语言文本属性的类型)。通常你需要为其创建自己的注解。

下面是使用粘合剂的示例。

示例 59. 根据 language 属性使用 AlternativeBinder 将一个属性映射到不同的索引字段
public enum Language { (1)

    ENGLISH( "en" ),
    FRENCH( "fr" ),
    GERMAN( "de" );

    public final String code;

    Language(String code) {
        this.code = code;
    }
}
@Entity
@Indexed
public class BlogEntry {

    @Id
    private Integer id;

    @AlternativeDiscriminator (1)
    @Enumerated(EnumType.STRING)
    private Language language;

    @MultiLanguageField (2)
    private String text;

    // Getters and setters
    // ...

}
@Retention(RetentionPolicy.RUNTIME) (1)
@Target({ ElementType.METHOD, ElementType.FIELD }) (2)
@PropertyMapping(processor = @PropertyMappingAnnotationProcessorRef( (3)
        type = MultiLanguageField.Processor.class
))
@Documented (4)
public @interface MultiLanguageField {

    String name() default ""; (5)

    class Processor (6)
            implements PropertyMappingAnnotationProcessor<MultiLanguageField> { (7)
        @Override
        public void process(PropertyMappingStep mapping, MultiLanguageField annotation,
                PropertyMappingAnnotationProcessorContext context) {
            LanguageAlternativeBinderDelegate delegate = new LanguageAlternativeBinderDelegate( (8)
                    annotation.name().isEmpty() ? null : annotation.name()
            );
            mapping.hostingType() (9)
                    .binder( AlternativeBinder.create( (10)
                            Language.class, (11)
                            context.annotatedElement().name(), (12)
                            String.class, (13)
                            BeanReference.ofInstance( delegate ) (14)
                    ) );
        }
    }
}
public class LanguageAlternativeBinderDelegate implements AlternativeBinderDelegate<Language, String> { (1)

    private final String name;

    public LanguageAlternativeBinderDelegate(String name) { (2)
        this.name = name;
    }

    @Override
    public AlternativeValueBridge<Language, String> bind(IndexSchemaElement indexSchemaElement, (3)
            PojoModelProperty fieldValueSource) {
        EnumMap<Language, IndexFieldReference<String>> fields = new EnumMap<>( Language.class );
        String fieldNamePrefix = ( name != null ? name : fieldValueSource.name() ) + "_";

        for ( Language language : Language.values() ) { (4)
            String languageCode = language.code;
            IndexFieldReference<String> field = indexSchemaElement.field(
                    fieldNamePrefix + languageCode, (5)
                    f -> f.asString().analyzer( "text_" + languageCode ) (6)
            )
                    .toReference();
            fields.put( language, field );
        }

        return new Bridge( fields ); (7)
    }

    private static class Bridge (8)
            implements AlternativeValueBridge<Language, String> { (9)
        private final EnumMap<Language, IndexFieldReference<String>> fields;

        private Bridge(EnumMap<Language, IndexFieldReference<String>> fields) {
            this.fields = fields;
        }

        @Override
        public void write(DocumentElement target, Language discriminator, String bridgedElement) {
            target.addValue( fields.get( discriminator ), bridgedElement ); (10)
        }
    }
}

10.9.2. Programmatic mapping

同样,也可以通过 programmatic mapping 应用 AlternativeBinder。该行为和选项与基于注释的映射相同。

示例 60. 使用 .binder(…​) 应用 AlternativeBinder
TypeMappingStep blogEntryMapping = mapping.type( BlogEntry.class );
blogEntryMapping.indexed();
blogEntryMapping.property( "language" )
        .marker( AlternativeBinder.alternativeDiscriminator() );
LanguageAlternativeBinderDelegate delegate = new LanguageAlternativeBinderDelegate( null );
blogEntryMapping.binder( AlternativeBinder.create( Language.class,
        "text", String.class, BeanReference.ofInstance( delegate ) ) );

10.10. Tuning when to trigger reindexing

10.10.1. Basics

当实体属性映射到索引(通过 @GenericField@IndexedEmbeddedcustom bridge)时,此映射会引入依赖关系:在属性更改时需要更新文档。

对于更简单的单实体映射,这只意味着 Hibernate 搜索需要检测实体何时更改并重新索引实体。这将以透明的方式处理。

如果映射包含一个“派生”属性,即一个未直接持久化的属性,而是动态地在使用其他属性作为输入的 getter 中计算的,那么 Hibernate Search 将无法猜测这些属性所基于的持久状态的哪一部分。在这种情况下,将需要一些显式配置;有关详细信息,请参阅 Reindexing when a derived value changes with @IndexingDependency

当映射跨越实体边界时,事情会变得更加复杂。让我们考虑一个 Book 实体映射到一个文档的映射,该文档必须包含 Author 实体的 name 属性(例如,使用 @IndexedEmbedded )。每当作者姓名更改时,Hibernate Search 都需要 retrieve all the books of that author ,以重新对它们编制索引。

实际上,这意味着每当实体映射依赖于与另一个实体的关联时,此关联必须是双向的:如果 Book.authors@IndexedEmbedded,则 Hibernate 搜索必须意识到反向关联 Author.books。如果无法解析反向关联,则将在启动时抛出异常。

多数时候,当使用 Hibernate ORM integration 时,Hibernate 搜索能够利用 Hibernate ORM 元数据(@OneToOne@OneToManymappedBy 属性)解析关联的逆向端,因此这一切都可以透明处理。

在某些罕见的情况下,对于更复杂的映射,即使 Hibernate ORM 也可能不知道关联是双向的,这是因为 _mappedBy_无法使用,或因为正在使用 Standalone POJO Mapper。有一些解决方案:

  1. 可以简单地忽略关联。这意味着,每当关联实体更改时,索引就会过时,但如果定期重建索引,这可能是一个可接受的解决方案。有关详细信息,请参阅 Limiting reindexing of containing entities with @IndexingDependency

  2. 如果关联实际上是双向的,可以使用 @AssociationInverseSide 向 Hibernate Search 显式指定其反向方面。有关详细信息,请参阅 Enriching the entity model with @AssociationInverseSide

10.10.2. Enriching the entity model with @AssociationInverseSide

针对来自实体类型 A 到实体类型 B 的关联,@AssociationInverseSide 定义关联的逆向,即从 BA 的路径。

当使用 Standalone POJO MapperHibernate ORM integration 且 Hibernate ORM 中没有将双向关联映射为这样的关联(没有 mappedBy)时,此功能尤其实用。

示例 61. 使用 @AssociationInverseSide 映射关联的反向方面
@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    private String title;

    @ElementCollection (1)
    @JoinTable(
            name = "book_editionbyprice",
            joinColumns = @JoinColumn(name = "book_id")
    )
    @MapKeyJoinColumn(name = "edition_id")
    @Column(name = "price")
    @OrderBy("edition_id asc")
    @IndexedEmbedded( (2)
            name = "editionsForSale",
            extraction = @ContainerExtraction(BuiltinContainerExtractors.MAP_KEY)
    )
    @AssociationInverseSide( (3)
            extraction = @ContainerExtraction(BuiltinContainerExtractors.MAP_KEY),
            inversePath = @ObjectPath(@PropertyValue(propertyName = "book"))
    )
    private Map<BookEdition, BigDecimal> priceByEdition = new LinkedHashMap<>();

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class BookEdition {

    @Id
    @GeneratedValue
    private Integer id;

    @ManyToOne (4)
    private Book book;

    @FullTextField(analyzer = "english")
    private String label;

    public BookEdition() {
    }

    // Getters and setters
    // ...

}

10.10.3. Reindexing when a derived value changes with @IndexingDependency

当属性不是直接保留,而是在使用其他属性作为输入的 getter 中动态计算时,Hibernate 搜索将无法猜测这些属性基于持续状态的哪一部分,从而无法在相关持续状态更改时 trigger reindexing。默认情况下,Hibernate 搜索将在引导时检测此类情况并引发异常。

使用 @IndexingDependency(derivedFrom = …​) 为属性添加注释将为 Hibernate 搜索提供所需的信息,并允许 triggering reindexing

示例 62. 使用 @IndexingDependency.derivedFrom 映射 派生值
@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    private String title;

    @ElementCollection
    private List<String> authors = new ArrayList<>(); (1)

    public Book() {
    }

    // Getters and setters
    // ...

    @Transient (2)
    @FullTextField(analyzer = "name") (3)
    @IndexingDependency(derivedFrom = @ObjectPath({ (4)
            @PropertyValue(propertyName = "authors")
    }))
    public String getMainAuthor() {
        return authors.isEmpty() ? null : authors.get( 0 );
    }
}

10.10.4. Limiting reindexing of containing entities with @IndexingDependency

在有的情况下,每次给定属性更改时, triggering reindexing实体是不切实际的:

  1. 当某个关联庞大时,例如一个实体实例被 indexed-embedded thousands 在其他实体中。

  2. 当映射到索引的属性非常频繁地更新,导致非常频繁地重新索引以及不可接受地使用磁盘或数据库。

  3. Etc.

当这种情况发生时,可以告诉 Hibernate Search 忽略对特定属性(且在 @IndexedEmbedded 情况下,忽略该属性以外的任何内容)的更新。

有若干选项可用于确切控制对给定属性的更新如何影响重新索引。请参阅以下章节,了解每个选项的说明。

ReindexOnUpdate.SHALLOW: limiting reindexing to same-entity updates only

ReindexOnUpdate.SHALLOW 在关联高度不对称,因此单向时最有用。考虑诸如类别、类型、城市、国家/地区等“参考”数据的关联…​

它实质上告知 Hibernate Search,更改关联会触发更改所发生对象的重新索引(添加或删除关联元素,即“浅层”更新),但更改关联实体的属性(“深层”更新)不会触发重新索引。

例如,考虑以下(不正确的)映射:

示例 63. 一个高度不对称的单向关联
@Entity
@Indexed
public class Book {

    @Id
    private Integer id;

    private String title;

    @ManyToOne (1)
    @IndexedEmbedded (2)
    private BookCategory category;

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class BookCategory {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english")
    private String name;

    (3)

    // Getters and setters
    // ...

}

有了此映射,当类别名称发生更改时,Hibernate Search 将无法重新索引所有书籍:该类别将列出所有书籍的 getter 根本不存在。由于 Hibernate Search 尝试默认保持安全,因此它将拒绝此映射,并会在引导时抛出异常,指出其需要 BookBookCategory 关联的逆向。

然而,在此情况下,我们不期望 _BookCategory_名称发生更改。这确实是 “引用” 数据,更改频率很低,因此我们可以计划这种情况并在那时 reindex all books。因此,我们确实不介意 Hibernate 搜索忽略对 _BookCategory_的更改……

这就是 @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW) 的用处:它告诉 Hibernate Search 忽略对关联实体进行更新的影响。请参阅以下修改后的映射:

示例 64. 使用 ReindexOnUpdate.SHALLOW 将再索引限制到同一实体更新
@Entity
@Indexed
public class Book {

    @Id
    private Integer id;

    private String title;

    @ManyToOne
    @IndexedEmbedded
    @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW) (1)
    private BookCategory category;

    public Book() {
    }

    // Getters and setters
    // ...

}

Hibernate Search 将接受上述映射并成功启动,因为从 BookBookCategory 的关联的逆向不再是被认为是必要的。

只有 shallow 对书籍类别的更改会触发对该书籍的重新索引:

  1. 当为一本书分配一个新类别 (book.setCategory( newCategory )) 时,Hibernate Search 将认为这是一个“浅层”更改,因为它仅影响 Book 实体。因此,Hibernate Search 将重新索引该书。

  2. 当某个类别本身发生更改 ( category.setName( newName ) ) 时,Hibernate Search 会将其视为“深度”更改,因为它发生在 Book 实体的边界之外。因此,Hibernate Search 本身不会重新索引该类别的书籍。索引将变得略微不同步,但这可以通过 reindexing Book 实体来解决,例如每晚解决一次。

ReindexOnUpdate.NO: disabling reindexing caused by updates of a particular property

ReindexOnUpdate.NO 最适用于非常频繁更改且不需要在索引中保持最新的属性。

这实质上告诉 Hibernate 搜索不应该 trigger reindexing对该属性进行的更改。

例如,考虑以下映射:

示例 65. 经常更改的属性
@Entity
@Indexed
public class Sensor {

    @Id
    private Integer id;

    @FullTextField
    private String name; (1)

    @KeywordField
    private SensorStatus status; (1)

    @Column(name = "\"value\"")
    private double value; (2)

    @GenericField
    private double rollingAverage; (3)

    public Sensor() {
    }

    // Getters and setters
    // ...

}

名称和状态的更新(更新频率很低)完全能够触发重新索引。但是考虑到有成千上万个传感器,传感器值的更新不可能对重新索引产生合理的影响:每隔几毫秒重新索引数千个传感器可能不会有很好的效果。

然而,在此场景中,对传感器值进行搜索并不被认为是关键的,索引无需最新。当涉及传感器值时,我们能够接受索引落后几分钟。我们可以考虑每隔几秒钟设置一个批处理进程,以通过 mass indexer(使用 Jakarta Batch mass indexing job)或 explicitly重新索引所有传感器。因此,我们确实不介意 Hibernate 搜索忽略对传感器值的更改……

@IndexingDependency(reindexOnUpdate = ReindexOnUpdate.NO) 正是为此而设计的:它让 Hibernate Search 忽略更新对 rollingAverage 属性的影响。下面是修改过的映射:

示例 66. 使用 ReindexOnUpdate.NO 为特定属性禁用由侦听器触发的再索引
@Entity
@Indexed
public class Sensor {

    @Id
    private Integer id;

    @FullTextField
    private String name;

    @KeywordField
    private SensorStatus status;

    @Column(name = "\"value\"")
    private double value;

    @GenericField
    @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.NO) (1)
    private double rollingAverage;

    public Sensor() {
    }

    // Getters and setters
    // ...

}

使用这种映射:

  1. 当为传感器分配一个新名称 (sensor.setName( newName )) 或状态 (sensor.setStatus( newStatus )) 时,Hibernate Search 将 trigger reindexing 该传感器。

  2. 当将新的滚动平均值分配给传感器时 ( sensor.setRollingAverage( newName ) ),Hibernate Search 不会 trigger reindexing 传感器。

10.10.5. Programmatic mapping

同样,也可以通过 programmatic mapping 控制重新索引。该行为和选项与基于注释的映射相同。

示例 67. 使用 .associationInverseSide(…​) 映射关联的反向端
TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "priceByEdition" )
        .indexedEmbedded( "editionsForSale" )
                .extractor( BuiltinContainerExtractors.MAP_KEY )
        .associationInverseSide( PojoModelPath.parse( "book" ) )
                .extractor( BuiltinContainerExtractors.MAP_KEY );
TypeMappingStep bookEditionMapping = mapping.type( BookEdition.class );
bookEditionMapping.property( "label" )
        .fullTextField().analyzer( "english" );
示例 68. 使用 .indexingDependency().derivedFrom(…​) 映射导出值
TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "mainAuthor" )
        .fullTextField().analyzer( "name" )
        .indexingDependency().derivedFrom( PojoModelPath.parse( "authors" ) );
示例 69. 使用 .indexingDependency().reindexOnUpdate(…​) 限制 triggering reindexing
TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "category" )
        .indexedEmbedded()
        .indexingDependency().reindexOnUpdate( ReindexOnUpdate.SHALLOW );
TypeMappingStep bookCategoryMapping = mapping.type( BookCategory.class );
bookCategoryMapping.property( "name" )
        .fullTextField().analyzer( "english" );

10.11. Changing the mapping of an existing application

在应用程序的生命周期中,会发生这种情况:特定索引实体类型的映射必须更改。当这种情况发生时,映射更改很可能需要更改索引的结构,即其 schema 。Hibernate Search 不会自动处理此结构更改,因此需要手动干预。

当需要更改索引结构时,最简单的解决方案是:

  • 删除并重新创建索引及其架构,可以使用以下方式之一手动完成:删除用于 Lucene 的文件系统目录,使用 REST API 删除 Elasticsearch 的索引,或使用 Hibernate Search 的 schema management features

  • 重新填充索引,例如使用 mass indexer

从技术上讲,如果映射更改包括 only ,则不必 strictly 删除索引并重新索引:

添加不会有任何持久化实例的新索引实体,例如在数据库中没有任何行的实体上添加 @Indexed 注释。

添加当前所有持久化实体都为空的新字段,例如,在实体类型上添加新属性,并将其映射到字段,但保证此属性最初对于此实体的每个实例都将为 null;

和/或从现有索引/字段中删除数据,例如删除索引字段,或删除对字段存储的需求。

但是,你仍然需要:

创建缺失的索引:这通常可以通过使用 createcreate-or-validatecreate-or-update 架构管理策略启动应用程序来自动完成。

(仅限 Elasticsearch:)更新现有索引的架构以声明新字段。这将更加复杂:要么使用 Elasticsearch 的 REST API 手动执行,要么使用 create-or-update strategy 启动应用程序,但请注意它 may fail

10.12. Custom mapping annotations

10.12.1. Basics

默认情况下,Hibernate Search 仅识别内置映射注释,例如 @Indexed@GenericField@IndexedEmbedded

要在 Hibernate Search 映射中使用自定义注释,则需要两个步骤:

  • 实现该注解的处理器:TypeMappingAnnotationProcessor,用于类型注解;PropertyMappingAnnotationProcessor,用于方法/字段注解;ConstructorMappingAnnotationProcessor,用于构造函数注解;MethodParameterMappingAnnotationProcessor,用于构造函数参数注解。

  • 使用 @TypeMapping@PropertyMapping@ConstructorMapping@MethodParameterMapping 标注文本注解,传递注解处理器的引用作为参数。

完成后,Hibernate Search 将能够检测索引类中的自定义注释(尽管不一定出现在自定义投影类型中,请参见 Custom root mapping annotations )。每当遇到自定义注释时,Hibernate Search 会实例化注释处理器并调用其 process 方法,同时将以下内容作为参数传递:

  1. 一个 mapping 参数,允许定义使用 programmatic mapping API 的类型、属性、构造函数或构造函数参数的映射。

  2. 一个 annotation 参数,表示注解实例。

  3. 一个 context 对象,带有多个帮助器。

自定义注释最常被用于应用自定义的参数化绑定器或转换器。你可以在以下部分中找到示例:

完全有可能使用自定义注释用于无参数粘合器或桥接,甚至用于更复杂的功能,例如索引嵌入: programmatic mapping API 中的每个可用功能都可以由自定义注释触发。

10.12.2. Custom root mapping annotations

若要让 Hibernate 搜索将自定义注释视为 root mapping annotation,请将 @RootMapping 元注释添加到自定义注释中。

这将确保 Hibernate 搜索处理包含已应用自定义注释的类型上的注释,即使这些类型没有在索引映射中被引用,这对于与 projection mapping 相关的自定义注释十分实用。

10.13. Inspecting the mapping

在 Hibernate Search 成功启动后,可以使用 SearchMapping 获取已索引实体列表并更直接地访问相应的索引,如下面的示例所示。

示例 70. 访问索引实体
SearchMapping mapping = /* ... */ (1)
SearchIndexedEntity<Book> bookEntity = mapping.indexedEntity( Book.class ); (2)
String jpaName = bookEntity.jpaName(); (3)
IndexManager indexManager = bookEntity.indexManager(); (4)
Backend backend = indexManager.backend(); (5)

SearchIndexedEntity<?> bookEntity2 = mapping.indexedEntity( "Book" ); (6)
Class<?> javaClass = bookEntity2.javaClass();

for ( SearchIndexedEntity<?> entity : mapping.allIndexedEntities() ) { (7)
    // ...
}

然后,您可以从一个 _IndexManager_访问索引元模型,以检查可用字段及其主要特性,如下所示。

示例 71. 访问索引元模型
SearchIndexedEntity<Book> bookEntity = mapping.indexedEntity( Book.class ); (1)
IndexManager indexManager = bookEntity.indexManager(); (2)
IndexDescriptor indexDescriptor = indexManager.descriptor(); (3)

indexDescriptor.field( "releaseDate" ).ifPresent( field -> { (4)
    String path = field.absolutePath(); (5)
    String relativeName = field.relativeName();
    // Etc.

    if ( field.isValueField() ) { (6)
        IndexValueFieldDescriptor valueField = field.toValueField(); (7)

        IndexValueFieldTypeDescriptor type = valueField.type(); (8)
        boolean projectable = type.projectable();
        Class<?> dslArgumentClass = type.dslArgumentClass();
        Class<?> projectedValueClass = type.projectedValueClass();
        Optional<String> analyzerName = type.analyzerName();
        Optional<String> searchAnalyzerName = type.searchAnalyzerName();
        Optional<String> normalizerName = type.normalizerName();
        // Etc.
        Set<String> traits = type.traits(); (9)
        if ( traits.contains( IndexFieldTraits.Aggregations.RANGE ) ) {
            // ...
        }
    }
    else if ( field.isObjectField() ) { (10)
        IndexObjectFieldDescriptor objectField = field.toObjectField();

        IndexObjectFieldTypeDescriptor type = objectField.type();
        boolean nested = type.nested();
        // Etc.
    }
} );

Collection<? extends AnalyzerDescriptor> analyzerDescriptors = indexDescriptor.analyzers(); (11)
for ( AnalyzerDescriptor analyzerDescriptor : analyzerDescriptors ) {
    String analyzerName = analyzerDescriptor.name();
    // ...
}

Optional<? extends AnalyzerDescriptor> analyzerDescriptor = indexDescriptor.analyzer( "some-analyzer-name" ); (12)
// ...

Collection<? extends NormalizerDescriptor> normalizerDescriptors = indexDescriptor.normalizers(); (13)
for ( NormalizerDescriptor normalizerDescriptor : normalizerDescriptors ) {
    String normalizerName = normalizerDescriptor.name();
    // ...
}

Optional<? extends NormalizerDescriptor> normalizerDescriptor = indexDescriptor.normalizer( "some-normalizer-name" ); (14)
// ...

BackendIndexManager 也可以用于 retrieve the Elasticsearch REST clientretrieve Lucene analyzers

@{15} 还公开了一些方法,用于按照名称检索 IndexManager,甚至按照名称检索整个 Backend