Hibernate Search 中文操作指南

12. Binding and bridges

12.1. Basics

在 Hibernate Search 中, binding是将自定义组件分配给域模型的过程。

可以绑定的最直观的组件是桥接器,负责将实体模型中的数据块转换为文档模型。

例如,当 @GenericField 应用到自定义枚举类型的属性时,在编制索引时,将使用内置桥接器将此枚举转换为字符串,并在投影时将该字符串转换回枚举。

类似地,当类型为 Long 的实体标识符映射到文档标识符时,在编制索引时,将使用内置桥接器将 Long 转换为 String(因为所有文档标识符都是字符串),并在从搜索结果加载时将 String 转换为 Long

桥接不限于一对一映射:例如, @GeoPointBinding 注解(将用 @Latitude@Longitude 标注的两个属性映射到一个字段)由另一个内置桥接支持。

虽然为各种标准类型提供了内置桥接器,但它们可能不足以满足复杂模型的需求。这就是为什么桥接器非常有用的原因:可以实现自定义桥接器并将其引用到 Hibernate Search 映射中。使用自定义桥接器,可以映射自定义类型,甚至是需要在编制索引时执行用户代码的复杂类型。

有多种类型的桥接器,详情将在下一节中详细介绍。如果您需要实现自定义桥接器,但又不太清楚需要哪种类型的桥接器,下表可能会有所帮助:

表 5. 现有桥接器类型的比较

Bridge type

ValueBridge

PropertyBridge

TypeBridge

IdentifierBridge

RoutingBridge

Applied to…​

Class field or getter

Class field or getter

Class

类字段或 getter(通常为实体 ID)

Class

Maps to…​

一个索引字段。仅值字段:整数、文本、地理位置等。无 object field(复合)。

一个或多个索引字段。既是值字段也是 object fields(复合)。

一个或多个索引字段。既是值字段也是 object fields(复合)。

Document identifier

路由(条件索引, routing key

Built-in annotation(s)

@GenericField_, @FullTextField_, …​

@PropertyBinding

@TypeBinding

@DocumentId

@Indexed( routingBinder = …​ )

Supports container extractors

Yes

No

No

No

No

Supports mutable types

No

Yes

Yes

No

Yes

但并非所有粘合剂都与索引相关。 projection constructors中涉及的构造函数参数也可以绑定;可以在 this section中的更多信息。

12.2. Value bridge

12.2.1. Basics

值桥接是一个可插入组件,实现从属性到索引字段的映射。它应用于具有 @*Field annotation ( @GenericField@FullTextField ,…​)或具有 custom annotation 的属性。

值桥的实现相对直接:在最简单的形式中,它可以归结为将值从属性类型转换为索引字段类型。由于集成到 @*Field 注释中,一些特性免费提供:

  1. 索引字段的类型可以在 @*Field 注解中直接自定义:可以将其定义为 sortable, projectable, 可以为其分配 analyzer, …​

  2. 该桥梁可以透明地应用到容器的元素上。例如,可以实现 ValueBridge<ISBN, String> 并在类型为 _List<ISBN>_的属性上透明地使用它:该桥梁将简单地应用到每个列表元素一次,并用尽可能多的值填充索引字段。

然而,由于这些特性,对值桥施加了几个限制,例如 property bridge中不存在这些限制:

  1. 值桥只允许一对一映射:一个属性到一个索引字段。单个值桥不能填充多个索引字段。

  2. 当应用于可变类型时,值桥接将无法正常工作。值桥接应应用于“原子”数据,如 LocalDate ;如果它应用于实体(例如,从其属性中提取数据),Hibernate Search 将不会意识到哪些属性被使用,并且在这些属性更改时将无法 detect that reindexing is required

下面是一个自定义值桥示例,它将自定义 ISBN 类型转换为其字符串表示以对其进行索引:

示例 78. 实现和使用 ValueBridge
public class ISBNValueBridge implements ValueBridge<ISBN, String> { (1)

    @Override
    public String toIndexedValue(ISBN value, ValueBridgeToIndexedValueContext context) { (2)
        return value == null ? null : value.getStringValue();
    }

}
@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    @Convert(converter = ISBNAttributeConverter.class) (1)
    @KeywordField( (2)
            valueBridge = @ValueBridgeRef(type = ISBNValueBridge.class), (3)
            normalizer = "isbn" (4)
    )
    private ISBN isbn;

    // Getters and setters
    // ...

}
{
  "isbn": "978-0-58-600835-5"
}

以上示例只是一个最小的实现。自定义值桥还能执行更多操作:

有关详细信息,请参见下一部分。

12.2.2. Type resolution

默认情况下,值桥的属性类型和索引字段类型使用反射自动确定,以提取 ValueBridge 接口的泛型类型参数:第一个参数是属性类型,而第二个参数是索引字段类型。

例如,在 public class MyBridge implements ValueBridge<ISBN, String> 中,属性类型被解析为 ISBN,索引字段类型被解析为 String:该桥将应用于 ISBN 类型的属性,并填充 String 类型的索引字段。

使用反射自动解析类型这一事实带来了一些限制。特别是,这意味着泛型类型参数不可能是任何类型;通常,您应该坚持使用明确类型 (MyBridge implements ValueBridge<ISBN, String>) 并避免使用泛型类型参数和通配符 (MyBridge<T> implements ValueBridge<List<T>, T>)。

如果你需要更复杂类型,你可以绕过自动解析并使用 ValueBinder 明确指定类型。

12.2.3. Using value bridges in other @*Field annotations

为了将自定义值桥与专业注释(例如 @FullTextField)一起使用,该桥必须声明一个兼容的索引字段类型。

例如:

  1. @FullTextField@KeywordField 需要一个类型为 String (ValueBridge&lt;Whatever, String&gt;) 的索引字段类型;

  2. @ScaledNumberField 需要一个类型为 BigDecimal (ValueBridge&lt;Whatever, BigDecimal&gt;) 或 BigInteger (ValueBridge&lt;Whatever, BigInteger&gt;) 的索引字段类型。

请参阅 Available field annotations以了解每个注释的特定约束。

尝试使用声明不兼容类型的桥会在引导时引发异常。

12.2.4. Supporting projections with fromIndexedValue()

默认情况下,尝试使用自定义桥在字段上投影将导致异常,因为 Hibernate Search 不知道如何将从索引获得的投影值转换回属性类型。

可以 disable conversion explicitly以从索引中获取原始值,但解决问题的另一种方法是在自定义桥接器中简单地实现 fromIndexedValue。将在需要转换投影值时调用此方法。

示例 79. 实现 fromIndexedValue 来转换投影值
public class ISBNValueBridge implements ValueBridge<ISBN, String> {

    @Override
    public String toIndexedValue(ISBN value, ValueBridgeToIndexedValueContext context) {
        return value == null ? null : value.getStringValue();
    }

    @Override
    public ISBN fromIndexedValue(String value, ValueBridgeFromIndexedValueContext context) {
        return value == null ? null : ISBN.parse( value ); (1)
    }
}
@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    @Convert(converter = ISBNAttributeConverter.class) (1)
    @KeywordField( (2)
            valueBridge = @ValueBridgeRef(type = ISBNValueBridge.class), (3)
            normalizer = "isbn",
            projectable = Projectable.YES (4)
    )
    private ISBN isbn;

    // Getters and setters
    // ...

}

12.2.5. Parsing the string representation to an index field type with parse()

默认情况下,在使用自定义桥接时,某些 Hibernate Search 功能(如指定 @*Field 注解的 indexNullAs 属性,或在查询字符串谓词( simpleQueryString() / queryString() )中使用具有此类自定义桥接的字段)将无法使用本地后端(例如 Lucene)。

为了使其正常工作,该桥需要实现 parse 方法,以便 Hibernate Search 可以将字符串表示转换为索引字段的正确类型的值。

示例 80 实现 parse
public class ISBNValueBridge implements ValueBridge<ISBN, String> {

    @Override
    public String toIndexedValue(ISBN value, ValueBridgeToIndexedValueContext context) {
        return value == null ? null : value.getStringValue();
    }

    @Override
    public String parse(String value) {
        // Just check the string format and return the string
        return ISBN.parse( value ).getStringValue(); (1)
    }
}
@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    @Convert(converter = ISBNAttributeConverter.class) (1)
    @KeywordField( (2)
            valueBridge = @ValueBridgeRef(type = ISBNValueBridge.class), (3)
            normalizer = "isbn",
            indexNullAs = "000-0-00-000000-0" (4)
    )
    private ISBN isbn;

    // Getters and setters
    // ...

}
List<Book> result = searchSession.search( Book.class )
        .where( f -> f.queryString().field( "isbn" )
                .matching( "978-0-13-468599-1" ) ) (1)
        .fetchHits( 20 );

12.2.6. Compatibility across indexes with isCompatibleWith()

值桥参与索引以及各种搜索 DSL,以将传递给 DSL 的值转换为后端将理解的索引字段值。

在创建跨多个索引定位单个字段的谓词时,Hibernate Search 将有多个桥可供选择:每个索引一个。由于只能创建具有单个值的单个谓词,因此 Hibernate Search 需要选择一个桥。默认情况下,当将自定义桥分配给字段时,Hibernate Search 将抛出异常,因为它无法决定选择哪个桥。

如果分配给所有索引中字段的桥接器生成相同的结果,则可以通过实现 isCompatibleWith 向 Hibernate Search 指示任何桥接器都可以执行操作。

此方法接受参数中的另一个桥接器,并且如果预期该桥接器始终与 this 的行为相同,则返回 true

示例 81 实现 isCompatibleWith 以支持多索引搜索
public class ISBNValueBridge implements ValueBridge<ISBN, String> {

    @Override
    public String toIndexedValue(ISBN value, ValueBridgeToIndexedValueContext context) {
        return value == null ? null : value.getStringValue();
    }

    @Override
    public boolean isCompatibleWith(ValueBridge<?, ?> other) { (1)
        return getClass().equals( other.getClass() );
    }
}

12.2.7. Configuring the bridge more finely with ValueBinder

若要更精细地配置桥接器,可以实现将在引导时执行的值绑定器。此绑定器尤其能够定义自定义索引字段类型。

示例 82 实现 ValueBinder
public class ISBNValueBinder implements ValueBinder { (1)
    @Override
    public void bind(ValueBindingContext<?> context) { (2)
        context.bridge( (3)
                ISBN.class, (4)
                new ISBNValueBridge(), (5)
                context.typeFactory() (6)
                        .asString() (7)
                        .normalizer( "isbn" ) (8)
        );
    }

    private static class ISBNValueBridge implements ValueBridge<ISBN, String> {
        @Override
        public String toIndexedValue(ISBN value, ValueBridgeToIndexedValueContext context) {
            return value == null ? null : value.getStringValue(); (9)
        }
    }
}
@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    @Convert(converter = ISBNAttributeConverter.class) (1)
    @KeywordField( (2)
            valueBinder = @ValueBinderRef(type = ISBNValueBinder.class), (3)
            sortable = Sortable.YES (4)
    )
    private ISBN isbn;

    // Getters and setters
    // ...

}

使用带专用 @*Field 注解的值绑定器时,索引字段类型必须与注解兼容。

例如, @FullTextField 仅在使用 asString() 创建索引字段类型时才起作用。

这些限制与直接分配值桥的限制类似;请参阅 Using value bridges in other @*Field annotations

12.2.8. Passing parameters

值桥通常使用内置 @*Field annotation 应用,后者已接受参数以分别配置字段名、字段是否可排序等。

但是,这些参数不会传递给值桥接器或值绑定器。有两种方法可以将参数传递给值桥接器:

  1. 一个(大部分)限于字符串参数,但实现起来很容易。

  2. 另一个可以允许任何类型的参数,但需要声明自己的注解。

Simple, string parameters

可以为 @ValueBinderRef 注释定义字符串参数,然后在绑定器中使用这些参数:

示例 83 使用 @ValueBinderRef 注解将参数传递给 ValueBridge
public class BooleanAsStringBridge implements ValueBridge<Boolean, String> { (1)

    private final String trueAsString;
    private final String falseAsString;

    public BooleanAsStringBridge(String trueAsString, String falseAsString) { (2)
        this.trueAsString = trueAsString;
        this.falseAsString = falseAsString;
    }

    @Override
    public String toIndexedValue(Boolean value, ValueBridgeToIndexedValueContext context) {
        if ( value == null ) {
            return null;
        }
        return value ? trueAsString : falseAsString;
    }
}
public class BooleanAsStringBinder implements ValueBinder {

    @Override
    public void bind(ValueBindingContext<?> context) {
        String trueAsString = context.params().get( "trueAsString", String.class ); (1)
        String falseAsString = context.params().get( "falseAsString", String.class );

        context.bridge( Boolean.class, (2)
                new BooleanAsStringBridge( trueAsString, falseAsString ) );
    }
}
@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    private String title;

    @GenericField(valueBinder = @ValueBinderRef(type = BooleanAsStringBinder.class, (1)
            params = {
                    @Param(name = "trueAsString", value = "yes"),
                    @Param(name = "falseAsString", value = "no")
            }))
    private boolean published;

    @ElementCollection
    @GenericField(valueBinder = @ValueBinderRef(type = BooleanAsStringBinder.class, (2)
            params = {
                    @Param(name = "trueAsString", value = "passed"),
                    @Param(name = "falseAsString", value = "failed")
            }), name = "censorshipAssessments_allYears")
    private Map<Year, Boolean> censorshipAssessments = new HashMap<>();

    // Getters and setters
    // ...

}
Parameters with custom annotations

您可以通过定义具有属性的 custom annotation将任何类型的参数传递给桥接器:

示例 84 使用自定义注解将参数传递给 ValueBridge
public class BooleanAsStringBridge implements ValueBridge<Boolean, String> { (1)

    private final String trueAsString;
    private final String falseAsString;

    public BooleanAsStringBridge(String trueAsString, String falseAsString) { (2)
        this.trueAsString = trueAsString;
        this.falseAsString = falseAsString;
    }

    @Override
    public String toIndexedValue(Boolean value, ValueBridgeToIndexedValueContext context) {
        if ( value == null ) {
            return null;
        }
        return value ? trueAsString : falseAsString;
    }
}
@Retention(RetentionPolicy.RUNTIME) (1)
@Target({ ElementType.METHOD, ElementType.FIELD }) (2)
@PropertyMapping(processor = @PropertyMappingAnnotationProcessorRef( (3)
        type = BooleanAsStringField.Processor.class
))
@Documented (4)
@Repeatable(BooleanAsStringField.List.class) (5)
public @interface BooleanAsStringField {

    String trueAsString() default "true"; (6)

    String falseAsString() default "false";

    String name() default ""; (7)

    ContainerExtraction extraction() default @ContainerExtraction(); (7)

    @Documented
    @Target({ ElementType.METHOD, ElementType.FIELD })
    @Retention(RetentionPolicy.RUNTIME)
    @interface List {
        BooleanAsStringField[] value();
    }

    class Processor (8)
            implements PropertyMappingAnnotationProcessor<BooleanAsStringField> { (9)
        @Override
        public void process(PropertyMappingStep mapping, BooleanAsStringField annotation,
                PropertyMappingAnnotationProcessorContext context) {
            BooleanAsStringBridge bridge = new BooleanAsStringBridge( (10)
                    annotation.trueAsString(), annotation.falseAsString()
            );
            mapping.genericField( (11)
                        annotation.name().isEmpty() ? null : annotation.name()
                    )
                    .valueBridge( bridge ) (12)
                    .extractors( (13)
                            context.toContainerExtractorPath( annotation.extraction() )
                    );
        }
    }
}
@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    private String title;

    @BooleanAsStringField(trueAsString = "yes", falseAsString = "no") (1)
    private boolean published;

    @ElementCollection
    @BooleanAsStringField( (2)
            name = "censorshipAssessments_allYears",
            trueAsString = "passed", falseAsString = "failed"
    )
    private Map<Year, Boolean> censorshipAssessments = new HashMap<>();

    // Getters and setters
    // ...

}

12.2.9. Accessing the ORM session or session factory from the bridge

此功能仅可通过 Hibernate ORM integration 使用。

尤其不能与 Standalone POJO Mapper 一起使用。

传递到桥接方法的上下文可用于检索 Hibernate ORM 会话或会话工厂。

示例 85 从 ValueBridge 获取 ORM 会话或会话工厂
public class MyDataValueBridge implements ValueBridge<MyData, String> {

    @Override
    public String toIndexedValue(MyData value, ValueBridgeToIndexedValueContext context) {
        SessionFactory sessionFactory = context.extension( HibernateOrmExtension.get() ) (1)
                .sessionFactory(); (2)
        // ... do something with the factory ...
    }

    @Override
    public MyData fromIndexedValue(String value, ValueBridgeFromIndexedValueContext context) {
        Session session = context.extension( HibernateOrmExtension.get() ) (3)
                .session(); (4)
        // ... do something with the session ...
    }
}

12.2.10. Injecting beans into the value bridge or value binder

使用 compatible frameworks,Hibernate Search 支持将 bean 注入到 _ValueBridge_和 _ValueBinder_中。

传递到值粘合器的 bind 方法的上下文还公开一个 beanResolver() 方法来访问 bean 解析器并显式实例化 bean。

有关更多详细信息,请参见 Bean injection

12.2.11. Programmatic mapping

您也可以通过 programmatic mapping应用值桥。只需传递桥接器的实例。

示例 86 使用 .valueBridge(…​) 应用 ValueBridge
TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "isbn" )
        .keywordField().valueBridge( new ISBNValueBridge() );

同样,您可以传递绑定器实例。可以通过该绑定器的构造函数或通过设置器传递参数。

示例 87 使用 .valueBinder(…​) 应用 ValueBinder
TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "isbn" )
        .genericField()
                .valueBinder( new ISBNValueBinder() )
                .sortable( Sortable.YES );

12.2.12. Incubating features

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

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

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

传递给值绑定器 bind 方法的上下文公开了一个 bridgedElement() 方法,该方法提供对正在绑定的值(特别是其类型)的元数据访问。

有关更多信息,请参见 javadoc。

12.3. Property bridge

12.3.1. Basics

属性桥类似于 value bridge,它是一个可插入组件,用于实现将属性映射到一个或多个索引字段。它应用于具有 _@PropertyBinding_注释或 custom annotation的属性。

与值桥接器相比,属性桥接器的实现更复杂,但涵盖更广泛的用例:

  1. 属性桥可以将单个属性映射到多个索引字段。

  2. 如果正确实现,属性桥在应用于可变类型时可以正常工作。

然而,由于其相当灵活的性质,属性桥接器并不能透明地提供值桥接器免费提供的全部功能。它们可以得到支持,但必须手动实现。这尤其包括容器提取器,其不能与属性桥接器结合:属性桥接器必须显式地提取容器值。

实现属性桥接器需要两个组件:

  • PropertyBinder 的自定义实现,用于在引导时将桥梁绑定到属性。这包括声明将使用属性的哪些部分,声明将填充的索引字段及其类型,以及实例化属性桥梁。

  • 自定义实现 PropertyBridge,在运行期间执行转换。这涉及从中提取数据根据需要进行转换,并将其推进到索引字段。

以下是一个自定义属性桥接器的示例,该桥接器将发票行项目列表映射到多个字段,这些字段汇总了该发票。

示例 88 实现并使用 PropertyBridge
public class InvoiceLineItemsSummaryBinder implements PropertyBinder { (1)

    @Override
    public void bind(PropertyBindingContext context) { (2)
        context.dependencies() (3)
                .use( "category" )
                .use( "amount" );

        IndexSchemaObjectField summaryField = context.indexSchemaElement() (4)
                .objectField( "summary" );

        IndexFieldType<BigDecimal> amountFieldType = context.typeFactory() (5)
                .asBigDecimal().decimalScale( 2 ).toIndexFieldType();

        context.bridge( (6)
                List.class, (7)
                new Bridge( (8)
                        summaryField.toReference(), (9)
                        summaryField.field( "total", amountFieldType ).toReference(), (10)
                        summaryField.field( "books", amountFieldType ).toReference(), (10)
                        summaryField.field( "shipping", amountFieldType ).toReference() (10)
                )
        );
    }

    // ... class continues below
    // ... class InvoiceLineItemsSummaryBinder (continued)

    private static class Bridge (1)
            implements PropertyBridge<List> { (2)

        private final IndexObjectFieldReference summaryField;
        private final IndexFieldReference<BigDecimal> totalField;
        private final IndexFieldReference<BigDecimal> booksField;
        private final IndexFieldReference<BigDecimal> shippingField;

        private Bridge(IndexObjectFieldReference summaryField, (3)
                IndexFieldReference<BigDecimal> totalField,
                IndexFieldReference<BigDecimal> booksField,
                IndexFieldReference<BigDecimal> shippingField) {
            this.summaryField = summaryField;
            this.totalField = totalField;
            this.booksField = booksField;
            this.shippingField = shippingField;
        }

        @Override
        public void write(DocumentElement target, List bridgedElement, PropertyBridgeWriteContext context) { (4)
            List<InvoiceLineItem> lineItems = (List<InvoiceLineItem>) bridgedElement;

            BigDecimal total = BigDecimal.ZERO;
            BigDecimal books = BigDecimal.ZERO;
            BigDecimal shipping = BigDecimal.ZERO;
            for ( InvoiceLineItem lineItem : lineItems ) { (5)
                BigDecimal amount = lineItem.getAmount();
                total = total.add( amount );
                switch ( lineItem.getCategory() ) {
                    case BOOK:
                        books = books.add( amount );
                        break;
                    case SHIPPING:
                        shipping = shipping.add( amount );
                        break;
                }
            }

            DocumentElement summary = target.addObject( this.summaryField ); (6)
            summary.addValue( this.totalField, total ); (7)
            summary.addValue( this.booksField, books ); (7)
            summary.addValue( this.shippingField, shipping ); (7)
        }
    }
}
@Entity
@Indexed
public class Invoice {

    @Id
    @GeneratedValue
    private Integer id;

    @ElementCollection
    @OrderColumn
    @PropertyBinding(binder = @PropertyBinderRef(type = InvoiceLineItemsSummaryBinder.class)) (1)
    private List<InvoiceLineItem> lineItems = new ArrayList<>();

    // Getters and setters
    // ...

}
{
  "summary": {
    "total": 38.96,
    "books": 30.97,
    "shipping": 7.99
  }
}

12.3.2. Passing parameters

可以通过两种方式将参数传递至属性桥:

  1. 一个(大部分)限于字符串参数,但实现起来很容易。

  2. 另一个可以允许任何类型的参数,但需要声明自己的注解。

Simple, string parameters

可以将字符串参数传递至 @PropertyBinderRef 注解,然后稍后在粘合器中使用它们:

示例 89 使用 @PropertyBinderRef 注解将参数传递给 PropertyBinder
public class InvoiceLineItemsSummaryBinder implements PropertyBinder {

    @Override
    public void bind(PropertyBindingContext context) {
        context.dependencies()
                .use( "category" )
                .use( "amount" );

        String fieldName = context.params().get( "fieldName", String.class ); (1)
        IndexSchemaObjectField summaryField = context.indexSchemaElement()
                .objectField( fieldName ); (2)

        IndexFieldType<BigDecimal> amountFieldType = context.typeFactory()
                .asBigDecimal().decimalScale( 2 ).toIndexFieldType();

        context.bridge( List.class, new Bridge(
                summaryField.toReference(),
                summaryField.field( "total", amountFieldType ).toReference(),
                summaryField.field( "books", amountFieldType ).toReference(),
                summaryField.field( "shipping", amountFieldType ).toReference()
        ) );
    }

    private static class Bridge implements PropertyBridge<List> {

        /* ... same implementation as before ... */

    }
}
@Entity
@Indexed
public class Invoice {

    @Id
    @GeneratedValue
    private Integer id;

    @ElementCollection
    @OrderColumn
    @PropertyBinding(binder = @PropertyBinderRef( (1)
            type = InvoiceLineItemsSummaryBinder.class,
            params = @Param(name = "fieldName", value = "itemSummary")))
    private List<InvoiceLineItem> lineItems = new ArrayList<>();

    // Getters and setters
    // ...

}
Parameters with custom annotations

您可以通过定义具有属性的 custom annotation将任何类型的参数传递给桥接器:

示例 90 使用自定义注解将参数传递给 PropertyBinder
@Retention(RetentionPolicy.RUNTIME) (1)
@Target({ ElementType.METHOD, ElementType.FIELD }) (2)
@PropertyMapping(processor = @PropertyMappingAnnotationProcessorRef( (3)
        type = InvoiceLineItemsSummaryBinding.Processor.class
))
@Documented (4)
public @interface InvoiceLineItemsSummaryBinding {

    String fieldName() default ""; (5)

    class Processor (6)
            implements PropertyMappingAnnotationProcessor<InvoiceLineItemsSummaryBinding> { (7)
        @Override
        public void process(PropertyMappingStep mapping,
                InvoiceLineItemsSummaryBinding annotation,
                PropertyMappingAnnotationProcessorContext context) {
            InvoiceLineItemsSummaryBinder binder = new InvoiceLineItemsSummaryBinder(); (8)
            if ( !annotation.fieldName().isEmpty() ) { (9)
                binder.fieldName( annotation.fieldName() );
            }
            mapping.binder( binder ); (10)
        }
    }
}
public class InvoiceLineItemsSummaryBinder implements PropertyBinder {

    private String fieldName = "summary";

    public InvoiceLineItemsSummaryBinder fieldName(String fieldName) { (1)
        this.fieldName = fieldName;
        return this;
    }

    @Override
    public void bind(PropertyBindingContext context) {
        context.dependencies()
                .use( "category" )
                .use( "amount" );

        IndexSchemaObjectField summaryField = context.indexSchemaElement()
                .objectField( this.fieldName ); (2)

        IndexFieldType<BigDecimal> amountFieldType = context.typeFactory()
                .asBigDecimal().decimalScale( 2 ).toIndexFieldType();

        context.bridge( List.class, new Bridge(
                summaryField.toReference(),
                summaryField.field( "total", amountFieldType ).toReference(),
                summaryField.field( "books", amountFieldType ).toReference(),
                summaryField.field( "shipping", amountFieldType ).toReference()
        ) );
    }

    private static class Bridge implements PropertyBridge<List> {

        /* ... same implementation as before ... */

    }
}
@Entity
@Indexed
public class Invoice {

    @Id
    @GeneratedValue
    private Integer id;

    @ElementCollection
    @OrderColumn
    @InvoiceLineItemsSummaryBinding( (1)
            fieldName = "itemSummary"
    )
    private List<InvoiceLineItem> lineItems = new ArrayList<>();

    // Getters and setters
    // ...

}

12.3.3. Accessing the ORM session from the bridge

此功能仅可通过 Hibernate ORM integration 使用。

尤其不能与 Standalone POJO Mapper 一起使用。

传递给桥接方法的上下文可用于检索 Hibernate ORM 会话。

示例 91 从 PropertyBridge 获取 ORM 会话
private static class Bridge implements PropertyBridge<Object> {

    private final IndexFieldReference<String> field;

    private Bridge(IndexFieldReference<String> field) {
        this.field = field;
    }

    @Override
    public void write(DocumentElement target, Object bridgedElement, PropertyBridgeWriteContext context) {
        Session session = context.extension( HibernateOrmExtension.get() ) (1)
                .session(); (2)
        // ... do something with the session ...
    }
}

12.3.4. Injecting beans into the binder

使用 compatible frameworks,Hibernate Search 支持将 bean 注入到:

  1. 如果你使用 custom annotations,则为 PropertyMappingAnnotationProcessor

  2. 如果你使用 @PropertyBinding annotation 则为 PropertyBinder

传递至属性粘合器 bind 方法的上下文还公开了一个 beanResolver() 方法,用于访问 Bean 解析器并明确实例化 Bean。

有关更多详细信息,请参见 Bean injection

12.3.5. Programmatic mapping

您也可以通过 programmatic mapping应用属性桥。只需传递粘合剂实例。您可以通过粘合剂的构造函数或通过 setter 传递参数。

示例 92.应用带有 PropertyBinder.binder(…​)
TypeMappingStep invoiceMapping = mapping.type( Invoice.class );
invoiceMapping.indexed();
invoiceMapping.property( "lineItems" )
        .binder( new InvoiceLineItemsSummaryBinder().fieldName( "itemSummary" ) );

12.3.6. Incubating features

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

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

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

传递给属性绑定程序的 bind 方法的上下文公开了一个 bridgedElement() 方法,该方法可访问正在绑定的属性的元数据。

元数据可用于详细检查属性:

  1. 获取属性名称。

  2. 检查属性类型。

  3. Getting accessors to properties.

  4. 检测带有标记的属性。标记通过携带 _@MarkerBinding_元注释的特定注释应用。

有关更多信息,请参见 javadoc。

下面是一个最简单的使用此元数据示例:获取属性名称并将其用作字段名称。

示例 93.在 PropertyBinder 中将字段命名为要绑定的属性之后
public class InvoiceLineItemsSummaryBinder implements PropertyBinder {

    @Override
    public void bind(PropertyBindingContext context) {
        context.dependencies()
                .use( "category" )
                .use( "amount" );

        PojoModelProperty bridgedElement = context.bridgedElement(); (1)
        IndexSchemaObjectField summaryField = context.indexSchemaElement()
                .objectField( bridgedElement.name() ); (2)

        IndexFieldType<BigDecimal> amountFieldType = context.typeFactory()
                .asBigDecimal().decimalScale( 2 ).toIndexFieldType();

        context.bridge( List.class, new Bridge(
                summaryField.toReference(),
                summaryField.field( "total", amountFieldType ).toReference(),
                summaryField.field( "books", amountFieldType ).toReference(),
                summaryField.field( "shipping", amountFieldType ).toReference()
        ) );
    }

    private static class Bridge implements PropertyBridge<List> {

        /* ... same implementation as before ... */

    }
}
@Entity
@Indexed
public class Invoice {

    @Id
    @GeneratedValue
    private Integer id;

    @ElementCollection
    @OrderColumn
    @PropertyBinding(binder = @PropertyBinderRef( (1)
            type = InvoiceLineItemsSummaryBinder.class
    ))
    private List<InvoiceLineItem> lineItems = new ArrayList<>();

    // Getters and setters
    // ...

}
{
  "lineItems": {
    "total": 38.96,
    "books": 30.97,
    "shipping": 7.99
  }
}

12.4. Type bridge

12.4.1. Basics

类型桥是一个可插入组件,用于实现将整个类型映射到一个或多个索引字段。它应用于具有 _@TypeBinding_注释或 custom annotation的类型。

类型桥在其核心原则和实现方式上与属性桥非常相似。唯一(显而易见)的区别在于属性桥应用于属性(字段或 getter),而类型桥应用于类型(类或接口)。这就带来了一些类型桥公开的 API 的细微差异。

实现类型桥需要两个组件:

  • 自定义实现 TypeBinder,在引导阶段将网桥绑定到类型。这涉及申明将要使用的该类型的属性,申明将要填充的索引字段及其类型,并实例化类型网桥。

  • 自定义实现 TypeBridge,在运行期间执行转换。这涉及从类型实例中提取数据,根据需要转换数据,并将其推进到索引字段。

下面是一个自定义类型桥的示例,它将 Author 类的两个属性 firstNamelastName 映射到单个 fullName 字段。

示例 94.实现并使用 TypeBridge
public class FullNameBinder implements TypeBinder { (1)

    @Override
    public void bind(TypeBindingContext context) { (2)
        context.dependencies() (3)
                .use( "firstName" )
                .use( "lastName" );

        IndexFieldReference<String> fullNameField = context.indexSchemaElement() (4)
                .field( "fullName", f -> f.asString().analyzer( "name" ) ) (5)
                .toReference();

        context.bridge( (6)
                Author.class, (7)
                new Bridge( (8)
                        fullNameField (9)
                )
        );
    }

    // ... class continues below
    // ... class FullNameBinder (continued)

    private static class Bridge (1)
            implements TypeBridge<Author> { (2)

        private final IndexFieldReference<String> fullNameField;

        private Bridge(IndexFieldReference<String> fullNameField) { (3)
            this.fullNameField = fullNameField;
        }

        @Override
        public void write(
                DocumentElement target,
                Author author,
                TypeBridgeWriteContext context) { (4)
            String fullName = author.getLastName() + " " + author.getFirstName(); (5)
            target.addValue( this.fullNameField, fullName ); (6)
        }
    }
}
@Entity
@Indexed
@TypeBinding(binder = @TypeBinderRef(type = FullNameBinder.class)) (1)
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    private String firstName;

    private String lastName;

    @GenericField (2)
    private LocalDate birthDate;

    // Getters and setters
    // ...

}
{
  "fullName": "Asimov Isaac"
}

12.4.2. Passing parameters

可以通过两种方式将参数传递至类型桥:

  1. 一个(大部分)限于字符串参数,但实现起来很容易。

  2. 另一个可以允许任何类型的参数,但需要声明自己的注解。

Simple, string parameters

可以将字符串参数传递至 @TypeBinderRef 注解,然后稍后在粘合器中使用它们:

示例 95.使用 @TypeBinderRef 标注将参数传递给 TypeBinder
public class FullNameBinder implements TypeBinder {

    @Override
    public void bind(TypeBindingContext context) {
        context.dependencies()
                .use( "firstName" )
                .use( "lastName" );

        IndexFieldReference<String> fullNameField = context.indexSchemaElement()
                .field( "fullName", f -> f.asString().analyzer( "name" ) )
                .toReference();

        IndexFieldReference<String> fullNameSortField = null;
        String sortField = context.params().get( "sortField", String.class ); (1)
        if ( "true".equalsIgnoreCase( sortField ) ) { (2)
            fullNameSortField = context.indexSchemaElement()
                    .field(
                            "fullName_sort",
                            f -> f.asString().normalizer( "name" ).sortable( Sortable.YES )
                    )
                    .toReference();
        }

        context.bridge( Author.class, new Bridge(
                fullNameField,
                fullNameSortField
        ) );
    }

    private static class Bridge implements TypeBridge<Author> {

        private final IndexFieldReference<String> fullNameField;
        private final IndexFieldReference<String> fullNameSortField;

        private Bridge(IndexFieldReference<String> fullNameField,
                IndexFieldReference<String> fullNameSortField) { (2)
            this.fullNameField = fullNameField;
            this.fullNameSortField = fullNameSortField;
        }

        @Override
        public void write(
                DocumentElement target,
                Author author,
                TypeBridgeWriteContext context) {
            String fullName = author.getLastName() + " " + author.getFirstName();

            target.addValue( this.fullNameField, fullName );
            if ( this.fullNameSortField != null ) {
                target.addValue( this.fullNameSortField, fullName );
            }
        }
    }
}
@Entity
@Indexed
@TypeBinding(binder = @TypeBinderRef(type = FullNameBinder.class, (1)
        params = @Param(name = "sortField", value = "true")))
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    private String firstName;

    private String lastName;

    // Getters and setters
    // ...

}
Parameters with custom annotations

您可以通过定义具有属性的 custom annotation将任何类型的参数传递给桥接器:

示例 96.使用自定义标注将参数传递给 TypeBinder
@Retention(RetentionPolicy.RUNTIME) (1)
@Target({ ElementType.TYPE }) (2)
@TypeMapping(processor = @TypeMappingAnnotationProcessorRef(type = FullNameBinding.Processor.class)) (3)
@Documented (4)
public @interface FullNameBinding {

    boolean sortField() default false; (5)

    class Processor (6)
            implements TypeMappingAnnotationProcessor<FullNameBinding> { (7)
        @Override
        public void process(TypeMappingStep mapping, FullNameBinding annotation,
                TypeMappingAnnotationProcessorContext context) {
            FullNameBinder binder = new FullNameBinder() (8)
                    .sortField( annotation.sortField() ); (9)
            mapping.binder( binder ); (10)
        }
    }
}
public class FullNameBinder implements TypeBinder {

    private boolean sortField;

    public FullNameBinder sortField(boolean sortField) { (1)
        this.sortField = sortField;
        return this;
    }

    @Override
    public void bind(TypeBindingContext context) {
        context.dependencies()
                .use( "firstName" )
                .use( "lastName" );

        IndexFieldReference<String> fullNameField = context.indexSchemaElement()
                .field( "fullName", f -> f.asString().analyzer( "name" ) )
                .toReference();

        IndexFieldReference<String> fullNameSortField = null;
        if ( this.sortField ) { (2)
            fullNameSortField = context.indexSchemaElement()
                    .field(
                            "fullName_sort",
                            f -> f.asString().normalizer( "name" ).sortable( Sortable.YES )
                    )
                    .toReference();
        }

        context.bridge( Author.class, new Bridge(
                fullNameField,
                fullNameSortField
        ) );
    }

    private static class Bridge implements TypeBridge<Author> {

        private final IndexFieldReference<String> fullNameField;
        private final IndexFieldReference<String> fullNameSortField;

        private Bridge(IndexFieldReference<String> fullNameField,
                IndexFieldReference<String> fullNameSortField) { (2)
            this.fullNameField = fullNameField;
            this.fullNameSortField = fullNameSortField;
        }

        @Override
        public void write(
                DocumentElement target,
                Author author,
                TypeBridgeWriteContext context) {
            String fullName = author.getLastName() + " " + author.getFirstName();

            target.addValue( this.fullNameField, fullName );
            if ( this.fullNameSortField != null ) {
                target.addValue( this.fullNameSortField, fullName );
            }
        }
    }
}
@Entity
@Indexed
@FullNameBinding(sortField = true) (1)
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    private String firstName;

    private String lastName;

    // Getters and setters
    // ...

}

12.4.3. Accessing the ORM session from the bridge

此功能仅可通过 Hibernate ORM integration 使用。

尤其不能与 Standalone POJO Mapper 一起使用。

传递给桥接方法的上下文可用于检索 Hibernate ORM 会话。

示例 97.从 TypeBridge 中检索 ORM 会话
private static class Bridge implements TypeBridge<Object> {

    private final IndexFieldReference<String> field;

    private Bridge(IndexFieldReference<String> field) {
        this.field = field;
    }

    @Override
    public void write(DocumentElement target, Object bridgedElement, TypeBridgeWriteContext context) {
        Session session = context.extension( HibernateOrmExtension.get() ) (1)
                .session(); (2)
        // ... do something with the session ...
    }
}

12.4.4. Injecting beans into the binder

使用 compatible frameworks,Hibernate Search 支持将 bean 注入到:

  1. 如果你使用 custom annotations,则为 TypeMappingAnnotationProcessor

  2. 如果您使用 @TypeBinding annotation ,则为 TypeBinder

传递至路由键粘合器 bind 方法的上下文还公开了一个 beanResolver() 方法,用于访问 Bean 解析器并明确实例化 Bean。

有关更多详细信息,请参见 Bean injection

12.4.5. Programmatic mapping

您也可以通过 programmatic mapping 应用一个类型桥接。只需传递粘合剂的一个实例。您可以通过粘合剂的构造函数或通过设置器传递参数。

示例 98.应用带有 .binder(…​)TypeBinder
TypeMappingStep authorMapping = mapping.type( Author.class );
authorMapping.indexed();
authorMapping.binder( new FullNameBinder().sortField( true ) );

12.4.6. Incubating features

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

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

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

传递给类型绑定程序的 bind 方法的上下文公开了一个 bridgedElement() 方法,该方法可访问正在绑定的类型的元数据。

尤其是元数据可用于详细检查类型:

  1. Getting accessors to properties.

  2. 检测带有标记的属性。标记通过携带 _@MarkerBinding_元注释的特定注释应用。

有关更多信息,请参见 javadoc。

12.5. Identifier bridge

12.5.1. Basics

标识符桥接是一个可插入组件,该组件实现实体属性到文档标识符的映射。它使用 @DocumentId 注释或使用 custom annotation 应用于属性。

实现标识符桥归结为实现两个方法:

  1. 一种将属性值(任何类型)转换为文档标识符(一个字符串)的方法;

  2. 一种将文档标识符转换回原始属性值的方法。

以下是自定义标识符桥接器的示例,它将自定义 BookId 类型转换为其字符串表示形式,并返回:

示例 99.实现并使用 IdentifierBridge
public class BookIdBridge implements IdentifierBridge<BookId> { (1)

    @Override
    public String toDocumentIdentifier(BookId value,
            IdentifierBridgeToDocumentIdentifierContext context) { (2)
        return value.getPublisherId() + "/" + value.getPublisherSpecificBookId();
    }

    @Override
    public BookId fromDocumentIdentifier(String documentIdentifier,
            IdentifierBridgeFromDocumentIdentifierContext context) { (3)
        String[] split = documentIdentifier.split( "/" );
        return new BookId( Long.parseLong( split[0] ), Long.parseLong( split[1] ) );
    }

}
@Entity
@Indexed
public class Book {

    @EmbeddedId
    @DocumentId( (1)
            identifierBridge = @IdentifierBridgeRef(type = BookIdBridge.class) (2)
    )
    private BookId id = new BookId();

    private String title;

    // Getters and setters
    // ...

}

12.5.2. Type resolution

默认情况下,标识符桥接器的属性类型自动确定,使用反射提取 IdentifierBridge 接口的泛型类型参数。

例如,在 public class MyBridge implements IdentifierBridge<BookId> 中,属性类型解析为 BookId:桥接器将应用于类型为 BookId 的属性。

使用反射自动解析类型的事实带来了一些限制。特别是,这意味着泛型类型参数不能仅用于任何内容;作为一般规则,您应坚持使用文本类型 (MyBridge implements IdentifierBridge<BookId>),避免使用泛型类型参数和通配符 (MyBridge<T extends Number> implements IdentifierBridge<T>,`MyBridge implements IdentifierBridge<List<? extends Number>>)。

如果您需要更复杂的类型,您可以绕过自动解析并使用 IdentifierBinder 显式指定类型。

12.5.3. Compatibility across indexes with isCompatibleWith()

标识符网桥涉及索引,也涉及搜索 DSL,以将传递给 id predicate 的值转换为后端将了解的文档标识符。

创建针对多个实体类型(及其索引)的 id 谓词时,Hibernate Search 将有多个桥接器可供选择:每个实体类型一个。由于只能使用单个值创建一个谓词,因此 Hibernate Search 需要挑选一个桥接器。

默认情况下,当将自定义桥接器分配给字段时,由于无法决定挑选哪个桥接器,因此 Hibernate Search 将抛出异常。

如果分配给所有索引中字段的桥接器生成相同的结果,则可以通过实现 isCompatibleWith 向 Hibernate Search 指示任何桥接器都可以执行操作。

此方法接受参数中的另一个桥接器,并且如果预期该桥接器始终与 this 的行为相同,则返回 true

示例 100.实现 isCompatibleWith 以支持多索引搜索
public class BookOrMagazineIdBridge implements IdentifierBridge<BookOrMagazineId> {

    @Override
    public String toDocumentIdentifier(BookOrMagazineId value,
            IdentifierBridgeToDocumentIdentifierContext context) {
        return value.getPublisherId() + "/" + value.getPublisherSpecificBookId();
    }

    @Override
    public BookOrMagazineId fromDocumentIdentifier(String documentIdentifier,
            IdentifierBridgeFromDocumentIdentifierContext context) {
        String[] split = documentIdentifier.split( "/" );
        return new BookOrMagazineId( Long.parseLong( split[0] ), Long.parseLong( split[1] ) );
    }

    @Override
    public boolean isCompatibleWith(IdentifierBridge<?> other) {
        return getClass().equals( other.getClass() ); (1)
    }
}

12.5.4. Parsing identifier’s string representation with parseIdentifierLiteral(..)

在某些情况下,Hibernate 搜索可能需要分析标识符的字符串表示形式,例如在在 identifier match predicate 的匹配子句中使用了 ValueConvert.PARSE 时。

通过自定义标识符桥接器,默认情况下,Hibernate Search 无法自动解析此类标识符文本。为了解决此问题,可以实现 parseIdentifierLiteral(..)

示例 101.实现 parseIdentifierLiteral(..)
public class BookIdBridge implements IdentifierBridge<BookId> { (1)

    // Implement mandatory toDocumentIdentifier/fromDocumentIdentifier ...
    // ...

    @Override
    public BookId parseIdentifierLiteral(String value) { (2)
        if ( value == null ) {
            return null;
        }
        String[] parts = value.split( "/" );
        if ( parts.length != 2 ) {
            throw new IllegalArgumentException( "BookId string literal must be in a `pubId/bookId` format." );
        }
        return new BookId( Long.parseLong( parts[0] ), Long.parseLong( parts[1] ) );
    }
}
List<Book> result = searchSession.search( Book.class )
        .where( f -> f.id().matching( "1/42", ValueConvert.PARSE ) ) (1)
        .fetchHits( 20 );

12.5.5. Configuring the bridge more finely with IdentifierBinder

要更精细地配置桥接器,可以实现将在启动时执行的值绑定器。此绑定器特别是能够检查属性的类型。

示例 102.实现 IdentifierBinder
public class BookIdBinder implements IdentifierBinder { (1)

    @Override
    public void bind(IdentifierBindingContext<?> context) { (2)
        context.bridge( (3)
                BookId.class, (4)
                new Bridge() (5)
        );
    }

    private static class Bridge implements IdentifierBridge<BookId> { (6)
        @Override
        public String toDocumentIdentifier(BookId value,
                IdentifierBridgeToDocumentIdentifierContext context) {
            return value.getPublisherId() + "/" + value.getPublisherSpecificBookId();
        }

        @Override
        public BookId fromDocumentIdentifier(String documentIdentifier,
                IdentifierBridgeFromDocumentIdentifierContext context) {
            String[] split = documentIdentifier.split( "/" );
            return new BookId( Long.parseLong( split[0] ), Long.parseLong( split[1] ) );
        }
    }
}
@Entity
@Indexed
public class Book {

    @EmbeddedId
    @DocumentId( (1)
            identifierBinder = @IdentifierBinderRef(type = BookIdBinder.class) (2)
    )
    private BookId id = new BookId();

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

    // Getters and setters
    // ...

}

12.5.6. Passing parameters

有两种方法可以将参数传递到标识符桥接器:

  1. 一个(大部分)限于字符串参数,但实现起来很容易。

  2. 另一个可以允许任何类型的参数,但需要声明自己的注解。

Simple, string parameters

您可以将字符串参数传递给 @IdentifierBinderRef 注释,然后在绑定器中使用它们:

示例 103.使用 @IdentifierBinderRef 标注将参数传递给 IdentifierBridge
public class OffsetIdentifierBridge implements IdentifierBridge<Integer> { (1)

    private final int offset;

    public OffsetIdentifierBridge(int offset) { (2)
        this.offset = offset;
    }

    @Override
    public String toDocumentIdentifier(Integer propertyValue, IdentifierBridgeToDocumentIdentifierContext context) {
        return String.valueOf( propertyValue + offset );
    }

    @Override
    public Integer fromDocumentIdentifier(String documentIdentifier,
            IdentifierBridgeFromDocumentIdentifierContext context) {
        return Integer.parseInt( documentIdentifier ) - offset;
    }
}
public class OffsetIdentifierBinder implements IdentifierBinder {

    @Override
    public void bind(IdentifierBindingContext<?> context) {
        String offset = context.params().get( "offset", String.class ); (1)
        context.bridge(
                Integer.class,
                new OffsetIdentifierBridge( Integer.parseInt( offset ) ) (2)
        );
    }
}
@Entity
@Indexed
public class Book {

    @Id
    // DB identifiers start at 0, but index identifiers start at 1
    @DocumentId(identifierBinder = @IdentifierBinderRef( (1)
            type = OffsetIdentifierBinder.class,
            params = @Param(name = "offset", value = "1")))
    private Integer id;

    private String title;

    // Getters and setters
    // ...

}
Parameters with custom annotations

您可以通过定义具有属性的 custom annotation将任何类型的参数传递给桥接器:

示例 104.使用自定义标注将参数传递给 IdentifierBridge
public class OffsetIdentifierBridge implements IdentifierBridge<Integer> { (1)

    private final int offset;

    public OffsetIdentifierBridge(int offset) { (2)
        this.offset = offset;
    }

    @Override
    public String toDocumentIdentifier(Integer propertyValue, IdentifierBridgeToDocumentIdentifierContext context) {
        return String.valueOf( propertyValue + offset );
    }

    @Override
    public Integer fromDocumentIdentifier(String documentIdentifier,
            IdentifierBridgeFromDocumentIdentifierContext context) {
        return Integer.parseInt( documentIdentifier ) - offset;
    }
}
@Retention(RetentionPolicy.RUNTIME) (1)
@Target({ ElementType.METHOD, ElementType.FIELD }) (2)
@PropertyMapping(processor = @PropertyMappingAnnotationProcessorRef( (3)
        type = OffsetDocumentId.Processor.class
))
@Documented (4)
public @interface OffsetDocumentId {

    int offset(); (5)

    class Processor (6)
            implements PropertyMappingAnnotationProcessor<OffsetDocumentId> { (7)
        @Override
        public void process(PropertyMappingStep mapping, OffsetDocumentId annotation,
                PropertyMappingAnnotationProcessorContext context) {
            OffsetIdentifierBridge bridge = new OffsetIdentifierBridge( (8)
                    annotation.offset()
            );
            mapping.documentId() (9)
                    .identifierBridge( bridge ); (10)
        }
    }
}
@Entity
@Indexed
public class Book {

    @Id
    // DB identifiers start at 0, but index identifiers start at 1
    @OffsetDocumentId(offset = 1) (1)
    private Integer id;

    private String title;

    // Getters and setters
    // ...

}

12.5.7. Accessing the ORM session or session factory from the bridge

此功能仅可通过 Hibernate ORM integration 使用。

尤其不能与 Standalone POJO Mapper 一起使用。

传递到桥接方法的上下文可用于检索 Hibernate ORM 会话或会话工厂。

示例 105.从 IdentifierBridge 中检索 ORM 会话或会话工厂
public class MyDataIdentifierBridge implements IdentifierBridge<MyData> {

    @Override
    public String toDocumentIdentifier(MyData propertyValue, IdentifierBridgeToDocumentIdentifierContext context) {
        SessionFactory sessionFactory = context.extension( HibernateOrmExtension.get() ) (1)
                .sessionFactory(); (2)
        // ... do something with the factory ...
    }

    @Override
    public MyData fromDocumentIdentifier(String documentIdentifier,
            IdentifierBridgeFromDocumentIdentifierContext context) {
        Session session = context.extension( HibernateOrmExtension.get() ) (3)
                .session(); (4)
        // ... do something with the session ...
    }
}

12.5.8. Injecting beans into the bridge or binder

使用 compatible frameworks,Hibernate 搜索支持将 bean 注入到 IdentifierBridgeIdentifierBinder 中。

传递到标识符粘合器的 bind 方法的上下文还公开一个 beanResolver() 方法来访问 bean 解析器并显式实例化 bean。

有关更多详细信息,请参见 Bean injection

12.5.9. Programmatic mapping

您也可以通过 programmatic mapping 应用一个标识符桥接。只需传递桥接的一个实例。

示例 106.应用带有 .identifierBridge(…​)IdentifierBridge
TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "id" )
        .documentId().identifierBridge( new BookIdBridge() );

同样,您可以传递绑定器实例:

示例 107.应用带有 .identifierBinder(…​)IdentifierBinder
TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed();
bookMapping.property( "id" )
        .documentId().identifierBinder( new BookIdBinder() );

12.5.10. Incubating features

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

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

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

传递给标识符绑定程序的 bind 方法的上下文公开了一个 bridgedElement() 方法,该方法可访问正在绑定的值的元数据,尤其是其类型。

有关更多信息,请参见 javadoc。

12.6. Routing bridge

12.6.1. Basics

路由桥接是一个可插入组件,它在运行时定义实体是否应被索引和 to which shard the corresponding indexed document should be routed 。通过其 routingBinder 属性 (@Indexed(routingBinder = …​)),使用 @Indexed 注释将其应用于已索引实体类型。

实现路由桥接器需要两个组件:

  • 自定义实现 RoutingBinder,在引导阶段将网桥绑定到索引实体类型。这涉及声明索引实体类型将要使用路由网桥的属性,并实例化路由网桥。

  • 自定义实现 RoutingBridge,在运行期间向索引路由实体。这涉及从类型实例中提取数据,根据需要转换数据,并定义当前路由(或将实体标记为“未索引”)。如果路由可以在实体实例生命周期内更改,你还需要定义潜在的先前路由,以便 Hibernate Search 能够查找并删除此实体实例索引的先前文档。

在下面的章节中,您将找到主要使用案例示例:

12.6.2. Using a routing bridge for conditional indexing

下面是禁用 Book 类的实例的索引的自定义路由桥的第一个示例,如果其状态为 ARCHIVED

示例 108. 实现并使用 RoutingBridge 进行条件索引
public class BookStatusRoutingBinder implements RoutingBinder { (1)

    @Override
    public void bind(RoutingBindingContext context) { (2)
        context.dependencies() (3)
                .use( "status" );

        context.bridge( (4)
                Book.class, (5)
                new Bridge() (6)
        );
    }

    // ... class continues below
    // ... class BookStatusRoutingBinder (continued)

    public static class Bridge (1)
            implements RoutingBridge<Book> { (2)
        @Override
        public void route(DocumentRoutes routes, Object entityIdentifier, (3)
                Book indexedEntity, RoutingBridgeRouteContext context) {
            switch ( indexedEntity.getStatus() ) { (4)
                case PUBLISHED:
                    routes.addRoute(); (5)
                    break;
                case ARCHIVED:
                    routes.notIndexed(); (6)
                    break;
            }
        }

        @Override
        public void previousRoutes(DocumentRoutes routes, Object entityIdentifier, (7)
                Book indexedEntity, RoutingBridgeRouteContext context) {
            routes.addRoute(); (8)
        }
    }
}
@Entity
@Indexed(routingBinder = @RoutingBinderRef(type = BookStatusRoutingBinder.class)) (1)
public class Book {

    @Id
    private Integer id;

    private String title;

    @Basic(optional = false)
    @KeywordField (2)
    private Status status;

    // Getters and setters
    // ...

}

12.6.3. Using a routing bridge to control routing to index shards

有关分片的初步介绍,包括它在 Hibernate Search 中的工作方式以及它的局限性是什么,请参阅 Sharding and routing

路由桥接还可以用于控制 routing to index shards

下面是一个自定义路由桥接的示例,它使用 Book 类的 genre 属性作为路由键。参阅 Routing,了解如何在搜索查询中使用路由的示例,其映射与以下示例相同。

示例 109. 实现并使用 RoutingBridge 来控制路由到索引分片
public class BookGenreRoutingBinder implements RoutingBinder { (1)

    @Override
    public void bind(RoutingBindingContext context) { (2)
        context.dependencies() (3)
                .use( "genre" );

        context.bridge( (4)
                Book.class, (5)
                new Bridge() (6)
        );
    }

    // ... class continues below
    // ... class BookGenreRoutingBinder (continued)

    public static class Bridge implements RoutingBridge<Book> { (1)
        @Override
        public void route(DocumentRoutes routes, Object entityIdentifier, (2)
                Book indexedEntity, RoutingBridgeRouteContext context) {
            String routingKey = indexedEntity.getGenre().name(); (3)
            routes.addRoute().routingKey( routingKey ); (4)
        }

        @Override
        public void previousRoutes(DocumentRoutes routes, Object entityIdentifier, (5)
                Book indexedEntity, RoutingBridgeRouteContext context) {
            for ( Genre possiblePreviousGenre : Genre.values() ) {
                String routingKey = possiblePreviousGenre.name();
                routes.addRoute().routingKey( routingKey ); (6)
            }
        }
    }
}
@Entity
@Indexed(routingBinder = @RoutingBinderRef(type = BookGenreRoutingBinder.class)) (1)
public class Book {

    @Id
    private Integer id;

    private String title;

    @Basic(optional = false)
    @KeywordField (2)
    private Genre genre;

    // Getters and setters
    // ...

}

优化 previousRoutes(…​) 在某些情况下,您可能比上面的示例中关于以前路由有更多信息,您可以利用该信息触发索引中更少的删除:

如果路由键来自不可变属性,那么你可以确保路由永远不会更改。在这种情况下,只需使用传递给 previousRoutes(…​)_的参数调用 _route(…​),告诉 Hibernate Search 先前路由与当前路由相同,那么 Hibernate Search 会跳过删除。

如果路由键是从一个以可预测的方式变化的属性派生的,例如一个总是从 DRAFTPUBLISHED 再到 ARCHIVED 并且永远不会后退的状态,那么您可以确信以前的路由是那些对应于可能的先前值。在这种情况下,只需为每个可能的先前状态添加一条路由,例如,如果当前状态是 PUBLISHED ,则只需要为 DRAFTPUBLISHED 添加一条路由,而不需要为 ARCHIVED 添加路由。

12.6.4. Passing parameters

有两种方法可以将参数传递给路由桥:

  1. 一个(大部分)限于字符串参数,但实现起来很容易。

  2. 另一个可以允许任何类型的参数,但需要声明自己的注解。

参考 this example for TypeBinder ,它与 RoutingBinder 中需要的相当类似。

12.6.5. Accessing the ORM session from the bridge

此功能仅可通过 Hibernate ORM integration 使用。

尤其不能与 Standalone POJO Mapper 一起使用。

传递给桥接方法的上下文可用于检索 Hibernate ORM 会话。

示例 110. 从 RoutingBridge 中检索 ORM 会话
private static class Bridge implements RoutingBridge<MyEntity> {

    @Override
    public void route(DocumentRoutes routes, Object entityIdentifier, MyEntity indexedEntity,
            RoutingBridgeRouteContext context) {
        Session session = context.extension( HibernateOrmExtension.get() ) (1)
                .session(); (2)
        // ... do something with the session ...
    }

    @Override
    public void previousRoutes(DocumentRoutes routes, Object entityIdentifier, MyEntity indexedEntity,
            RoutingBridgeRouteContext context) {
        // ...
    }
}

12.6.6. Injecting beans into the binder

使用 compatible frameworks,Hibernate Search 支持将 bean 注入到:

  1. 如果你使用 custom annotations,则为 TypeMappingAnnotationProcessor

  2. RoutingBinder ,如果您使用 @Indexed(routingBinder = …​)

传递给路由粘合剂 bind 方法的上下文还公开了一个 beanResolver() 方法来访问 bean 解析器并显式实例化 bean。

有关更多详细信息,请参见 Bean injection

12.6.7. Programmatic mapping

您也可以通过 programmatic mapping 应用路由键桥接。只需传递粘合剂的一个实例。

示例 111. 使用 .binder(…​) 应用 RoutingBinder
TypeMappingStep bookMapping = mapping.type( Book.class );
bookMapping.indexed()
        .routingBinder( new BookStatusRoutingBinder() );
bookMapping.property( "status" ).keywordField();

12.6.8. Incubating features

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

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

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

传递给路由绑定程序的 bind 方法的上下文公开了一个 bridgedElement() 方法,该方法可访问正在绑定的类型的元数据。

尤其是元数据可用于详细检查类型:

  1. Getting accessors to properties.

  2. 检测带有标记的属性。标记通过携带 _@MarkerBinding_元注释的特定注释应用。

有关更多信息,请参见 javadoc。

12.7. Declaring dependencies to bridged elements

12.7.1. Basics

为了使索引保持同步,Hibernate Search 需要了解用于生成已编入索引文档的所有实体属性,以便在它们更改时触发重新索引。

在使用 type bridgeproperty bridge 时,桥接本身决定在索引时访问哪些实体属性。因此,它需要让 Hibernate 搜索知道其“依赖关系”(它可能访问的实体属性)。

这是通过一个专用的 DSL 完成的,可以通过 TypeBinderPropertyBinderbind(…​) 方法访问该 DSL。

以下是类型粘合剂的一个示例,该粘合剂希望应用于 ScientificPaper 类型,并声明对论文作者的姓氏和名字的依赖性。

示例 112. 在桥中声明依赖关系
public class AuthorFullNameBinder implements TypeBinder {

    @Override
    public void bind(TypeBindingContext context) {
        context.dependencies() (1)
                .use( "author.firstName" ) (2)
                .use( "author.lastName" ); (3)

        IndexFieldReference<String> authorFullNameField = context.indexSchemaElement()
                .field( "authorFullName", f -> f.asString().analyzer( "name" ) )
                .toReference();

        context.bridge( Book.class, new Bridge( authorFullNameField ) );
    }

    private static class Bridge implements TypeBridge<Book> {

        // ...
    }
}

以上内容足以开始,但如果您想了解更多信息,这里有一些关于声明依赖项的事实。

Paths are relative to the bridged element

例如:

对于类型 _ScientificPaper_上的类型网桥,路径 _author_将引用 _ScientificPaper_实例上属性 _author_的值。

对于 _ScientificPaper_的属性 _author_上的属性网桥,路径 _name_将引用 _Author_实例上属性 _name_的值。

Every component of given paths will be considered as a dependency

您不需要声明任何父路径。

例如,如果路径 myProperty.someOtherProperty 被声明为已使用,Hibernate Search 将自动假设 myProperty 也被使用。

Only mutable properties need to be declared

如果一个属性在实体首次持久化后永远不会改变,那么它永远不会触发重新索引,并且 Hibernate Search 无需了解该依赖关系。

如果您的桥只依赖于不可变属性,请参见 useRootOnly(): declaring no dependency at all

Associations included in dependency paths need to have an inverse side

如果您通过关联声明跨实体边界交叉存在的依赖关系,并且该关联在其他实体中没有反向侧,那么将会抛出一个异常。

例如,当您声明对路径 author.lastName 的依赖项时,Hibernate Search 推断出每当作者的姓氏更改时,都需要重新对他的书进行索引。因此,当它检测到作者的姓氏更改时,Hibernate Search 需要检索这些书以对其重新编制索引。这就是为什么实体 ScientificPaper 中的 author 关联在实体 Author 中需要有一个反向端,例如 books 关联。

有关这些约束以及如何处理非平凡模型的详细信息,请参阅 Tuning when to trigger reindexing

12.7.2. Traversing non-default containers (map keys, …​)

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

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

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

当路径元素引用容器类型的属性(ListMapOptional、…​)时,路径将隐式解析为该容器的元素。例如,someMap.otherObject 将解析为 someMapvaluesotherObject 属性(而非键)。

如果默认解析不符合您的需要,您可以显式控制如何通过容器,只需传递 PojoModelPath 对象,而不是仅仅字符串:

示例 113. 在具有显式容器提取器的桥中声明依赖关系
@Entity
@Indexed
@TypeBinding(binder = @TypeBinderRef(type = BookEditionsForSaleTypeBinder.class)) (1)
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

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

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

    public Book() {
    }

    // Getters and setters
    // ...

}
public class BookEditionsForSaleTypeBinder implements TypeBinder {

    @Override
    public void bind(TypeBindingContext context) {
        context.dependencies()
                .use( PojoModelPath.builder() (1)
                        .property( "priceByEdition" ) (2)
                        .value( BuiltinContainerExtractors.MAP_KEY ) (3)
                        .property( "label" ) (4)
                        .toValuePath() ); (5)

        IndexFieldReference<String> editionsForSaleField = context.indexSchemaElement()
                .field( "editionsForSale", f -> f.asString().analyzer( "english" ) )
                .multiValued()
                .toReference();

        context.bridge( Book.class, new Bridge( editionsForSaleField ) );
    }

    private static class Bridge implements TypeBridge<Book> {

        private final IndexFieldReference<String> editionsForSaleField;

        private Bridge(IndexFieldReference<String> editionsForSaleField) {
            this.editionsForSaleField = editionsForSaleField;
        }

        @Override
        public void write(DocumentElement target, Book book, TypeBridgeWriteContext context) {
            for ( BookEdition edition : book.getPriceByEdition().keySet() ) { (6)
                target.addValue( editionsForSaleField, edition.getLabel() );
            }
        }
    }
}

对于应用到容器属性的属性粘合剂,您可以通过将容器提取器路径作为第一个参数传递给 use(…​) 来控制如何遍历该属性本身:

示例 114. 在具有桥接属性的显式容器提取器的桥中声明依赖关系
@Entity
@Indexed
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

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

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

    public Book() {
    }

    // Getters and setters
    // ...

}
public class BookEditionsForSalePropertyBinder implements PropertyBinder {

    @Override
    public void bind(PropertyBindingContext context) {
        context.dependencies()
                .use( ContainerExtractorPath.explicitExtractor( BuiltinContainerExtractors.MAP_KEY ), (1)
                        "label" ); (2)

        IndexFieldReference<String> editionsForSaleField = context.indexSchemaElement()
                .field( "editionsForSale", f -> f.asString().analyzer( "english" ) )
                .multiValued()
                .toReference();

        context.bridge( Map.class, new Bridge( editionsForSaleField ) );
    }

    private static class Bridge implements PropertyBridge<Map> {

        private final IndexFieldReference<String> editionsForSaleField;

        private Bridge(IndexFieldReference<String> editionsForSaleField) {
            this.editionsForSaleField = editionsForSaleField;
        }

        @Override
        public void write(DocumentElement target, Map bridgedElement, PropertyBridgeWriteContext context) {
            Map<BookEdition, ?> priceByEdition = (Map<BookEdition, ?>) bridgedElement;

            for ( BookEdition edition : priceByEdition.keySet() ) { (3)
                target.addValue( editionsForSaleField, edition.getLabel() );
            }
        }
    }
}

12.7.3. useRootOnly(): declaring no dependency at all

如果您的桥只访问不可变属性,那么可以安全地声明其唯一的依赖项是对根对象。

要做到这一点,请调用 .dependencies().useRootOnly()

如果没有此调用,Hibernate Search 会怀疑存在缺陷,并在启动时抛出一个异常。

12.7.4. fromOtherEntity(…​): declaring dependencies using the inverse path

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

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

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

并不总是可以将依赖关系表示为从桥接元素到桥接访问的值的路径。

尤其当桥梁依赖其他组件(查询、服务)来检索另一实体时,可能甚至不存在从桥梁元素到该实体的路径。在这种情况下,如果存在从其他实体到桥接元素的 inverse 路径,并且桥接元素是一个实体,则您可以简单地声明依赖关系,如下所示。

示例 115. 在使用反向路径的桥中声明依赖关系
@Entity
@Indexed
@TypeBinding(binder = @TypeBinderRef(type = ScientificPapersReferencedByBinder.class)) (1)
public class ScientificPaper {

    @Id
    private Integer id;

    private String title;

    @ManyToMany
    private List<ScientificPaper> references = new ArrayList<>();

    public ScientificPaper() {
    }

    // Getters and setters
    // ...

}
public class ScientificPapersReferencedByBinder implements TypeBinder {

    @Override
    public void bind(TypeBindingContext context) {
        context.dependencies()
                .fromOtherEntity( ScientificPaper.class, "references" ) (1)
                .use( "title" ); (2)

        IndexFieldReference<String> papersReferencingThisOneField = context.indexSchemaElement()
                .field( "referencedBy", f -> f.asString().analyzer( "english" ) )
                .multiValued()
                .toReference();

        context.bridge( ScientificPaper.class, new Bridge( papersReferencingThisOneField ) );
    }

    private static class Bridge implements TypeBridge<ScientificPaper> {

        private final IndexFieldReference<String> referencedByField;

        private Bridge(IndexFieldReference<String> referencedByField) { (2)
            this.referencedByField = referencedByField;
        }

        @Override
        public void write(DocumentElement target, ScientificPaper paper, TypeBridgeWriteContext context) {
            for ( String referencingPaperTitle : findReferencingPaperTitles( context, paper ) ) { (3)
                target.addValue( referencedByField, referencingPaperTitle );
            }
        }

        private List<String> findReferencingPaperTitles(TypeBridgeWriteContext context, ScientificPaper paper) {
            Session session = context.extension( HibernateOrmExtension.get() ).session();
            Query<String> query = session.createQuery(
                    "select p.title from ScientificPaper p where :this member of p.references",
                    String.class );
            query.setParameter( "this", paper );
            return query.list();
        }
    }
}

当前,以这种方式声明的依赖关系将在“其他实体”被删除时被忽略。

请参见 HSEARCH-3567 以跟踪解决这个问题的进度。

12.8. Declaring and writing to index fields

12.8.1. Basics

实现 PropertyBinderTypeBinder 时,必须声明桥将编写的索引字段。此声明使用专门的 DSL 执行。

此 DSL 的入口点是 IndexNode,它表示装订器将数据推送到其中的文档结构部分。从 IndexNode 开始,可以声明字段。

每个字段的声明都会产生一个字段 reference。此引用应存储在桥梁中,桥梁将在运行时使用它来设置给定文档(由 DocumentElement 表示)中此字段的值。

下面是一个简单的示例,通过使用 DSL 在属性装订器中声明单个字段,然后将此字段写入属性桥梁中。

示例 116. 声明一个简单的索引字段和写入该字段
public class ISBNBinder implements PropertyBinder {

    @Override
    public void bind(PropertyBindingContext context) {
        context.dependencies()
                /* ... (declaration of dependencies, not relevant) ... */

        IndexSchemaElement schemaElement = context.indexSchemaElement(); (1)

        IndexFieldReference<String> field =
                schemaElement.field( (2)
                        "isbn", (3)
                        f -> f.asString() (4)
                                .normalizer( "isbn" )
                )
                        .toReference(); (5)

        context.bridge( (6)
                ISBN.class, (7)
                new ISBNBridge( field ) (8)
        );
    }
}
private static class ISBNBridge implements PropertyBridge<ISBN> {

    private final IndexFieldReference<String> fieldReference;

    private ISBNBridge(IndexFieldReference<String> fieldReference) {
        this.fieldReference = fieldReference;
    }

    @Override
    public void write(DocumentElement target, ISBN bridgedElement, PropertyBridgeWriteContext context) {
        String indexedValue = /* ... (extraction of data, not relevant) ... */
        target.addValue( this.fieldReference, indexedValue ); (1)
    }
}

12.8.2. Type objects

声明每个字段类型的 lambda 语法很方便,但有时会产生一些问题,尤其是在必须使用完全相同的类型声明多个字段时。

因此,传递给装订器的上下文对象会公开 typeFactory() 方法。使用此工厂,可以构建可以在多个字段声明中重复使用的 IndexFieldType 对象。

示例 117. 在多个字段声明中重复使用一个索引字段类型
@Override
public void bind(TypeBindingContext context) {
    context.dependencies()
            /* ... (declaration of dependencies, not relevant) ... */

    IndexSchemaElement schemaElement = context.indexSchemaElement();

    IndexFieldType<String> nameType = context.typeFactory() (1)
            .asString() (2)
            .analyzer( "name" )
            .toIndexFieldType(); (3)

    context.bridge( Author.class, new Bridge(
            schemaElement.field( "firstName", nameType ) (4)
                    .toReference(),
            schemaElement.field( "lastName", nameType ) (4)
                    .toReference(),
            schemaElement.field( "fullName", nameType ) (4)
                    .toReference()
    ) );
}

12.8.3. Multivalued fields

默认情况下,字段被视为单值的:如果尝试在索引编制期间向单值字段添加多个值,则会引发异常。

若要向字段添加多个值,则必须在字段声明期间将其标记为多值的:

示例 118. 将一个字段声明为多值
@Override
public void bind(TypeBindingContext context) {
    context.dependencies()
            /* ... (declaration of dependencies, not relevant) ... */

    IndexSchemaElement schemaElement = context.indexSchemaElement();

    context.bridge( Author.class, new Bridge(
            schemaElement.field( "names", f -> f.asString().analyzer( "name" ) )
                    .multiValued() (1)
                    .toReference()
    ) );
}

12.8.4. Object fields

前几部分只介绍了带有值字段的扁平模式,但索引模式实际上可以组织成树结构,其中有两个类别的索引字段:

  1. 值字段,通常称为“字段”,保存特定类型的原子值:字符串、整数、日期,……

  2. 对象字段,包含一个复合值。

对象字段的声明方式类似于值字段,但需要一个附加步骤来声明每个子字段,如下所示。

示例 119. 声明一个对象字段
@Override
public void bind(PropertyBindingContext context) {
    context.dependencies()
            /* ... (declaration of dependencies, not relevant) ... */

    IndexSchemaElement schemaElement = context.indexSchemaElement();

    IndexSchemaObjectField summaryField =
            schemaElement.objectField( "summary" ); (1)

    IndexFieldType<BigDecimal> amountFieldType = context.typeFactory()
            .asBigDecimal().decimalScale( 2 )
            .toIndexFieldType();

    context.bridge( List.class, new Bridge(
            summaryField.toReference(), (2)
            summaryField.field( "total", amountFieldType ) (3)
                    .toReference(),
            summaryField.field( "books", amountFieldType ) (3)
                    .toReference(),
            summaryField.field( "shipping", amountFieldType ) (3)
                    .toReference()
    ) );
}

对象字段的子字段可以包括对象字段。

如同值字段一样,对象字段默认情况下是单值的。如果您希望使其成为多值的,请务必在对象字段定义期间调用 .multiValued()

对象字段及其子字段各分配一个引用,该引用将供桥接用于写入文档,如下例所示。

示例 120. 写入对象字段
@Override
public void write(DocumentElement target, List bridgedElement, PropertyBridgeWriteContext context) {
    List<InvoiceLineItem> lineItems = (List<InvoiceLineItem>) bridgedElement;

    BigDecimal total = BigDecimal.ZERO;
    BigDecimal books = BigDecimal.ZERO;
    BigDecimal shipping = BigDecimal.ZERO;
    /* ... (computation of amounts, not relevant) ... */

    DocumentElement summary = target.addObject( this.summaryField ); (1)
    summary.addValue( this.totalField, total ); (2)
    summary.addValue( this.booksField, books ); (2)
    summary.addValue( this.shippingField, shipping ); (2)
}

12.8.5. Object structure

默认情况下,对象字段将变平,这意味着不会保留树结构。有关详细信息,请参阅 DEFAULT or FLATTENED structure

可以通过向 objectField 方法传递参数来切换到 a nested structure,如下所示。然后,对象字段的每个值都会作为一个单独的嵌套文档进行透明索引,而无需对桥接的 write 方法进行任何更改。

示例 121. 将对象字段声明为嵌套
@Override
public void bind(PropertyBindingContext context) {
    context.dependencies()
            /* ... (declaration of dependencies, not relevant) ... */

    IndexSchemaElement schemaElement = context.indexSchemaElement();

    IndexSchemaObjectField lineItemsField =
            schemaElement.objectField( (1)
                    "lineItems", (2)
                    ObjectStructure.NESTED (3)
            )
                    .multiValued(); (4)

    context.bridge( List.class, new Bridge(
            lineItemsField.toReference(), (5)
            lineItemsField.field( "category", f -> f.asString() ) (6)
                    .toReference(),
            lineItemsField.field( "amount", f -> f.asBigDecimal().decimalScale( 2 ) ) (7)
                    .toReference()
    ) );
}

12.8.6. Dynamic fields with field templates

在上述几部分中声明的字段都是 static:它们的路径和类型在引导时是已知的。

在一些非常特殊的情况下,字段的路径在您实际索引它之前是未知的。例如,您可能希望通过使用映射键作为字段名称来索引 Map<String, Integer>,或者索引事先不知道其模式的 JSON 对象的属性。那么,字段将被视为 dynamic

动态字段不会在引导时声明,但需要匹配在引导时声明的字段 template。模板包括字段类型和结构信息(多值或不是,…​),但省略字段名称。

字段模板始终在粘合剂中声明:在 type binderproperty binder 中。对于静态字段,声明模板的入口点是传递给粘合剂的 bind(…​) 方法的 IndexNode。对模式元素执行 fieldTemplate 方法的调用将声明一个字段模板。

假设在绑定期间声明了字段模板,那么桥梁可以通过调用 addValue 并传递字段名称(作为字符串)和字段值,在索引编制时将动态字段添加到 DocumentElement

以下是使用 DSL 声明属性绑定器中的字段模板,然后写入该字段中的一个简单示例。

示例 122. 声明一个字段模板并写入动态字段
public class UserMetadataBinder implements PropertyBinder {

    @Override
    public void bind(PropertyBindingContext context) {
        context.dependencies()
                /* ... (declaration of dependencies, not relevant) ... */

        IndexSchemaElement schemaElement = context.indexSchemaElement();

        IndexSchemaObjectField userMetadataField =
                schemaElement.objectField( "userMetadata" ); (1)

        userMetadataField.fieldTemplate( (2)
                "userMetadataValueTemplate", (3)
                f -> f.asString().analyzer( "english" ) (4)
        ); (5)

        context.bridge( Map.class, new UserMetadataBridge(
                userMetadataField.toReference() (6)
        ) );
    }
}
private static class UserMetadataBridge implements PropertyBridge<Map> {

    private final IndexObjectFieldReference userMetadataFieldReference;

    private UserMetadataBridge(IndexObjectFieldReference userMetadataFieldReference) {
        this.userMetadataFieldReference = userMetadataFieldReference;
    }

    @Override
    public void write(DocumentElement target, Map bridgedElement, PropertyBridgeWriteContext context) {
        Map<String, String> userMetadata = (Map<String, String>) bridgedElement;

        DocumentElement indexedUserMetadata = target.addObject( userMetadataFieldReference ); (1)

        for ( Map.Entry<String, String> entry : userMetadata.entrySet() ) {
            String fieldName = entry.getKey();
            String fieldValue = entry.getValue();
            indexedUserMetadata.addValue( fieldName, fieldValue ); (2)
        }
    }
}

尽管很少需要,但您还可以使用 objectFieldTemplate 方法为对象字段声明模板。

还可以将具有不同类型的多个字段添加到同一对象。为此,请确保可以从字段名称推断出字段的格式。然后,您可以声明多个模板,并为每个模板分配一个路径模式,如下例所示。

示例 123. 声明具有不同类型多个字段模板
public class MultiTypeUserMetadataBinder implements PropertyBinder {

    @Override
    public void bind(PropertyBindingContext context) {
        context.dependencies()
                /* ... (declaration of dependencies, not relevant) ... */

        IndexSchemaElement schemaElement = context.indexSchemaElement();

        IndexSchemaObjectField userMetadataField =
                schemaElement.objectField( "multiTypeUserMetadata" ); (1)

        userMetadataField.fieldTemplate( (2)
                "userMetadataValueTemplate_int", (3)
                f -> f.asInteger().sortable( Sortable.YES ) (4)
        )
                .matchingPathGlob( "*_int" ); (5)

        userMetadataField.fieldTemplate( (6)
                "userMetadataValueTemplate_default",
                f -> f.asString().analyzer( "english" )
        );

        context.bridge( Map.class, new Bridge( userMetadataField.toReference() ) );
    }
}
private static class Bridge implements PropertyBridge<Map> {

    private final IndexObjectFieldReference userMetadataFieldReference;

    private Bridge(IndexObjectFieldReference userMetadataFieldReference) {
        this.userMetadataFieldReference = userMetadataFieldReference;
    }

    @Override
    public void write(DocumentElement target, Map bridgedElement, PropertyBridgeWriteContext context) {
        Map<String, Object> userMetadata = (Map<String, Object>) bridgedElement;

        DocumentElement indexedUserMetadata = target.addObject( userMetadataFieldReference ); (1)

        for ( Map.Entry<String, Object> entry : userMetadata.entrySet() ) {
            String fieldName = entry.getKey();
            Object fieldValue = entry.getValue();
            indexedUserMetadata.addValue( fieldName, fieldValue ); (2)
        }
    }
}

字段模板优先级Hibernate Search 尝试按照声明模板的顺序匹配模板,因此你应始终先声明具有最具体路径模式的模板。

在给定模式元素声明的模板可以在该元素的子元素中匹配。例如,如果在实体根处声明模板(通过 type bridge ),这些模板将在该实体的每个属性桥接中隐式可用。在这种情况下,在属性桥接声明的模板优先级高于在类型桥接声明的模板。

12.9. Defining index field types

12.9.1. Basics

基于 Lucene 的搜索引擎(包括 Elasticsearch)的一个特殊性是,字段类型比仅数据类型(“string”、“integer”等)复杂得多。

声明字段时,必须不仅声明数据类型,还要声明各种特性,这些特性将定义数据如何确切存储:该字段是否可以排序,是否可以投影,是否进行分析,如果是,则使用哪个分析器等。

正因为有了这样的复杂性,所以当字段类型必须在各种绑定器中定义时(ValueBinderPropertyBinderTypeBinder),则使用专用 DSL 定义它们。

该 DSL 的入口点是 IndexFieldTypeFactory。该类型工厂通常可以通过传递给绑定器的上下文对象访问(context.typeFactory())。对于 PropertyBinderTypeBinder,该类型工厂还可以传递给传递给 field 方法的 lambda 表达式,以在线定义字段类型。

该类型工厂公开了各种 as*() 方法,例如 asStringasLocalDate。这些是类型定义 DSL 的第一步,其中定义数据类型。它们返回其他步骤,从中可以设置选项,如分析器。有关示例,请参见以下内容。

示例 124. 定义一个字段类型
IndexFieldType<String> type = context.typeFactory() (1)
        .asString() (2)
        .normalizer( "isbn" ) (3)
        .sortable( Sortable.YES ) (3)
        .toIndexFieldType(); (4)

ValueBinder 中,省略了对 toIndexFieldType() 的调用: context.bridge(…​) 期待接收传入的最后一个 DSL 步骤,而不是一个完全构建的类型。

field declaration DSLfield 方法传递的 lambda 表达式中也省略了 toIndexFieldType()

12.9.2. Available data types

所有可用的数据类型在 IndexFieldTypeFactory 中都有一个专门的 as*() 方法。有关详细信息,请参阅 IndexFieldTypeFactory 的 javadoc 或特定于后端的文档:

12.9.3. Available type options

索引字段类型 DSL 中的大多数可用选项与 @*Field 注释公开的选项相同。有关它们的详细信息,请参阅 Field annotation attributes

其他选项在以下部分进行了解释。

12.9.4. DSL converter

本节与 ValueBinder 无关:Hibernate Search 自动为值桥设置 DSL 转换器,创建一个仅仅是委托给值桥的 DSL 转换器。

各种搜索 DSL 公开了某些预期字段值的方法:matching()between()atMost()missingValue().use()、…​ 按默认,预期类型将与数据类型相同,即,如果您调用了 asString(),则为 String;如果您调用了 asLocalDate(),则为 LocalDate,等等。

当桥在索引时将值从不同类型转换时,这可能会很烦人。例如,如果桥在索引时将枚举转换为字符串,则您可能不想将字符串传递给搜索谓词,而是枚举。

通过在字段类型上设置 DSL 转换器,可以更改传递给各种 DSL 的值类型。有关示例,请参见以下内容。

示例 125. 向字段类型分配一个 DSL 转换器
IndexFieldType<String> type = context.typeFactory()
        .asString() (1)
        .normalizer( "isbn" )
        .sortable( Sortable.YES )
        .dslConverter( (2)
                ISBN.class, (3)
                (value, convertContext) -> value.getStringValue() (4)
        )
        .toIndexFieldType();
ISBN expectedISBN = /* ... */
List<Book> result = searchSession.search( Book.class )
        .where( f -> f.match().field( "isbn" )
                .matching( expectedISBN ) ) (1)
        .fetchHits( 20 );

12.9.5. Projection converter

本节与 ValueBinder 无关:Hibernate Search 自动为值桥设置投影转换器,创建一个仅仅是委托给值桥的投影转换器。

默认情况下,由 field projectionsaggregations 返回的值的类型将与相应字段的数据类型相同,即如果您调用了 asString(),则为 String,如果您调用了 asLocalDate(),则为 LocalDate,等等。

当桥在索引时将值从不同类型转换时,这可能会很烦人。例如,如果桥在索引时将枚举转换为字符串,则您可能不想让投影返回字符串,而是枚举。

通过在字段类型上设置投影转换器,可以更改字段投影或聚合返回的值的类型。有关示例,请参见以下内容。

示例 126. 向字段类型分配一个投影转换器
IndexFieldType<String> type = context.typeFactory()
        .asString() (1)
        .projectable( Projectable.YES )
        .projectionConverter( (2)
                ISBN.class, (3)
                (value, convertContext) -> ISBN.parse( value ) (4)
        )
        .toIndexFieldType();
List<ISBN> result = searchSession.search( Book.class )
        .select( f -> f.field( "isbn", ISBN.class ) ) (1)
        .where( f -> f.matchAll() )
        .fetchHits( 20 );

12.9.6. Backend-specific types

后端定义对此 DSL 的扩展,以定义特定于后端的类型。

请参阅:

12.10. Defining named predicates

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

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

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

实现 PropertyBinderTypeBinder 时,可以将“命名谓词”分配到索引模式元素(索引根或 object field )。

然后,可以使用这些命名谓词 through the Search DSL,按名称引用它们并可选择传递参数。主要区别在于实施对调用者是隐藏的:他们不需要了解如何索引数据才能使用命名谓词。

以下是使用 DSL 声明对象字段并在属性绑定器中为该字段分配命名谓词的一个简单示例。

示例 127. 声明一个命名谓词
/**
 * A binder for Stock Keeping Unit (SKU) identifiers, i.e. Strings with a specific format.
 */
public class SkuIdentifierBinder implements PropertyBinder {

    @Override
    public void bind(PropertyBindingContext context) {
        context.dependencies().useRootOnly();

        IndexSchemaObjectField skuIdObjectField = context.indexSchemaElement()
                .objectField( context.bridgedElement().name() );

        IndexFieldType<String> skuIdPartType = context.typeFactory()
                .asString().normalizer( "lowercase" ).toIndexFieldType();

        context.bridge( String.class, new Bridge(
                skuIdObjectField.toReference(),
                skuIdObjectField.field( "departmentCode", skuIdPartType ).toReference(),
                skuIdObjectField.field( "collectionCode", skuIdPartType ).toReference(),
                skuIdObjectField.field( "itemCode", skuIdPartType ).toReference()
        ) );

        skuIdObjectField.namedPredicate( (1)
                "skuIdMatch", (2)
                new SkuIdentifierMatchPredicateDefinition() (3)
        );
    }

    // ... class continues below
// ... class SkuIdentifierBinder (continued)

private static class Bridge implements PropertyBridge<String> { (1)

    private final IndexObjectFieldReference skuIdObjectField;
    private final IndexFieldReference<String> departmentCodeField;
    private final IndexFieldReference<String> collectionCodeField;
    private final IndexFieldReference<String> itemCodeField;

    private Bridge(IndexObjectFieldReference skuIdObjectField,
            IndexFieldReference<String> departmentCodeField,
            IndexFieldReference<String> collectionCodeField,
            IndexFieldReference<String> itemCodeField) {
        this.skuIdObjectField = skuIdObjectField;
        this.departmentCodeField = departmentCodeField;
        this.collectionCodeField = collectionCodeField;
        this.itemCodeField = itemCodeField;
    }

    @Override
    public void write(DocumentElement target, String skuId, PropertyBridgeWriteContext context) {
        DocumentElement skuIdObject = target.addObject( this.skuIdObjectField );(2)

        // An SKU identifier is formatted this way: "<department code>.<collection code>.<item code>".
        String[] skuIdParts = skuId.split( "\\." );
        skuIdObject.addValue( this.departmentCodeField, skuIdParts[0] ); (3)
        skuIdObject.addValue( this.collectionCodeField, skuIdParts[1] ); (3)
        skuIdObject.addValue( this.itemCodeField, skuIdParts[2] ); (3)
    }
}

// ... class continues below
    // ... class SkuIdentifierBinder (continued)

    private static class SkuIdentifierMatchPredicateDefinition implements PredicateDefinition { (1)
        @Override
        public SearchPredicate create(PredicateDefinitionContext context) {
            SearchPredicateFactory f = context.predicate(); (2)

            String pattern = context.params().get( "pattern", String.class ); (3)

            return f.and().with( and -> { (4)
                // An SKU identifier pattern is formatted this way: "<department code>.<collection code>.<item code>".
                // Each part supports * and ? wildcards.
                String[] patternParts = pattern.split( "\\." );
                if ( patternParts.length > 0 ) {
                    and.add( f.wildcard()
                            .field( "departmentCode" ) (5)
                            .matching( patternParts[0] ) );
                }
                if ( patternParts.length > 1 ) {
                    and.add( f.wildcard()
                            .field( "collectionCode" )
                            .matching( patternParts[1] ) );
                }
                if ( patternParts.length > 2 ) {
                    and.add( f.wildcard()
                            .field( "itemCode" )
                            .matching( patternParts[2] ) );
                }
            } ).toPredicate(); (6)
        }
    }
}
@Entity
@Indexed
public class ItemStock {

    @Id
    @PropertyBinding(binder = @PropertyBinderRef(type = SkuIdentifierBinder.class)) (1)
    private String skuId;

    private int amountInStock;

    // Getters and setters
    // ...


}

12.11. Assigning default bridges with the bridge resolver

12.11.1. Basics

默认情况下, @*Field annotations@DocumentId annotation 均支持范围广泛的标准类型,无需告知 Hibernate Search 如何将值转换为可索引形式。

在底层,对默认类型支持的处理由桥接解析器负责。例如,当某个属性与 @GenericField 映射,而 @GenericField.valueBridge@GenericField.valueBinder 均未设置时,Hibernate Search 会解析此属性的类型,然后将其传递给桥接解析器,解析器将返回一个合适的桥接,如果没有任何桥接,则会失败。

完全可以自定义桥接解析器,以覆盖现有的默认桥接(例如,对 java.util.Date 进行不同索引),也可以为其他类型(例如,外部库中的地理空间类型)定义默认桥接。

为此,如 Programmatic mapping 中所述定义一个映射配置器,然后按如下所示定义桥接:

示例 128. 使用映射配置程序定义默认桥接器

public class MyDefaultBridgesConfigurer implements HibernateOrmSearchMappingConfigurer {
    @Override
    public void configure(HibernateOrmMappingConfigurationContext context) {
        context.bridges().exactType( MyCoordinates.class )
                .valueBridge( new MyCoordinatesBridge() ); (1)

        context.bridges().exactType( MyProductId.class )
                .identifierBridge( new MyProductIdBridge() ); (2)

        context.bridges().exactType( ISBN.class )
                .valueBinder( new ValueBinder() { (3)
                    @Override
                    public void bind(ValueBindingContext<?> context) {
                        context.bridge( ISBN.class, new ISBNValueBridge(),
                                context.typeFactory().asString().normalizer( "isbn" ) );
                    }
                } );
    }
}

12.11.2. Assigning a single binder to multiple types

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

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

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

对于更高级别的使用情形,还可以将单个绑定程序分配给给定类型的子类型。这在应类似地索引多种类型时很有用。

下面是一个示例,其中枚举不是作为它们的 .name()(默认值)进行索引的,而是作为从外部服务中检索到的标签进行索引的。

示例 129. 使用映射配置程序将单个默认绑定器分配给多个类型

context.bridges().subTypesOf( Enum.class ) (1)
        .valueBinder( new ValueBinder() {
            @Override
            public void bind(ValueBindingContext<?> context) {
                Class<?> enumType = context.bridgedElement().rawType(); (2)
                doBind( context, enumType );
            }

            private <T> void doBind(ValueBindingContext<?> context, Class<T> enumType) {
                BeanHolder<EnumLabelService> serviceHolder = context.beanResolver()
                        .resolve( EnumLabelService.class, BeanRetrieval.ANY ); (3)
                context.bridge(
                        enumType,
                        new EnumLabelBridge<>( enumType, serviceHolder )
                ); (4)
            }
        } );

12.12. Projection binder

12.12.1. Basics

投影粘合器是一个可插入组件,它实现了将构造函数参数绑定到 projection 的操作。它应用于带 @ProjectionBinding 注释或带 custom annotationprojection constructor 的参数。

投影粘合剂可以检查构造函数参数,并且应将投影定义分配给该构造函数参数,以便每当调用 projection constructor 时,Hibernate 搜索将通过该构造函数参数传递该投影的结果。

实现投影粘合剂需要两个组件:

  • ProjectionBinder 的自定义实现,以在引导时将投影定义绑定到参数。如果需要,这包括检查构造函数参数,并将投影定义实例化。

  • ProjectionDefinition_的自定义实现,在运行时实例化投影。这涉及使用 projection DSL 并返回得到的 _SearchProjection

下面是一个自定义投影粘合剂的示例,它将类型为 String 的参数绑定到索引中 title 字段的投影。

示例 130. 实现和使用 ProjectionBinder

public class MyFieldProjectionBinder implements ProjectionBinder { (1)
    @Override
    public void bind(ProjectionBindingContext context) { (2)
        context.definition( (3)
                String.class, (4)
                new MyProjectionDefinition() (5)
        );
    }

    // ... class continues below
    // ... class MyFieldProjectionBinder (continued)

    private static class MyProjectionDefinition (1)
            implements ProjectionDefinition<String> { (2)
        @Override
        public SearchProjection<String> create(SearchProjectionFactory<?, ?> factory,
                ProjectionDefinitionContext context) {
            return factory.field( "title", String.class ) (3)
                    .toProjection(); (4)
        }
    }
}
@ProjectionConstructor
public record MyBookProjection(
        @ProjectionBinding(binder = @ProjectionBinderRef( (1)
                type = MyFieldProjectionBinder.class
        ))
        String title) {
}
List<MyBookProjection> hits = searchSession.search( Book.class )
        .select( MyBookProjection.class )
        .where( f -> f.matchAll() )
        .fetchHits( 20 );

12.12.2. Multi-valued projections

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

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

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

您可以在传递给投影粘合剂的上下文中调用 .multi() 以发现要绑定的构造函数参数是否是多值的(遵循与 implicit inner projection inference 相同的规则),并绑定多值投影。

示例 131. 实现和使用支持多值投影的 ProjectionBinder

public class MyFieldProjectionBinder implements ProjectionBinder {
    @Override
    public void bind(ProjectionBindingContext context) {
        Optional<? extends ProjectionBindingMultiContext> multi = context.multi(); (1)
        if ( multi.isPresent() ) {
            multi.get().definition( String.class, new MyProjectionDefinition() ); (2)
        }
        else {
            throw new RuntimeException( "This binder only supports multi-valued constructor parameters" ); (3)
        }
    }

    private static class MyProjectionDefinition
            implements ProjectionDefinition<List<String>> { (4)
        @Override
        public SearchProjection<List<String>> create(SearchProjectionFactory<?, ?> factory,
                ProjectionDefinitionContext context) {
            return factory.field( "tags", String.class )
                    .multi() (4)
                    .toProjection();
        }
    }
}
@ProjectionConstructor
public record MyBookProjection(
        @ProjectionBinding(binder = @ProjectionBinderRef( (1)
                type = MyFieldProjectionBinder.class
        ))
        List<String> tags) {
}
List<MyBookProjection> hits = searchSession.search( Book.class )
        .select( MyBookProjection.class )
        .where( f -> f.matchAll() )
        .fetchHits( 20 );

12.12.3. Composing projection constructors

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

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

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

您可以根据_SomeType_中的 projection constructor mapping,在投影绑定器中传递的上下文中调用_.createObjectDefinition( "someFieldPath", SomeType.class )_以检索 object projection的定义。

这样实际上允许在投影粘合剂内使用投影构造函数,只需将得到的定义传递给 .definition(…​) 或在自定义投影定义中将其委派给它。

绑定上下文中公开的其他方法工作方式类似:

  1. _.createObjectDefinitionMulti(…​)_返回一个 multi-valued对象投影定义。

  2. _.createCompositeDefinition(…​)_返回一个 (单值) composite projection定义(这与 object projection相反,后者不绑定到索引中的对象字段)。

下面是一个使用 .createObjectDefinition(…​) 委派到另一个投影构造函数的示例。

示例 132. 实现和使用委托给投影构造函数的 ProjectionBinder

public class MyObjectFieldProjectionBinder implements ProjectionBinder {
    @Override
    public void bind(ProjectionBindingContext context) {
        var authorProjection = context.createObjectDefinition( (1)
                "author", (2)
                MyBookProjection.MyAuthorProjection.class, (3)
                TreeFilterDefinition.includeAll() (4)
        );
        context.definition( (5)
                MyBookProjection.MyAuthorProjection.class,
                authorProjection
        );
    }
}
@ProjectionConstructor
public record MyBookProjection(
        @ProjectionBinding(binder = @ProjectionBinderRef( (1)
                type = MyObjectFieldProjectionBinder.class
        ))
        MyAuthorProjection author) {

    @ProjectionConstructor (2)
    public record MyAuthorProjection(String name) {
    }
}
List<MyBookProjection> hits = searchSession.search( Book.class )
        .select( MyBookProjection.class )
        .where( f -> f.matchAll() )
        .fetchHits( 20 );

12.12.4. Passing parameters

可以通过两种方式将参数传递至属性桥:

  1. 一个(大部分)限于字符串参数,但实现起来很容易。

  2. 另一个可以允许任何类型的参数,但需要声明自己的注解。

Simple, string parameters

您可以将字符串参数传递给 @ProjectionBinderRef 注释,然后在粘合剂中使用它们:

示例 133. 使用 @ProjectionBinderRef 注释向 ProjectionBinder 传递参数

public class MyFieldProjectionBinder implements ProjectionBinder {
    @Override
    public void bind(ProjectionBindingContext context) {
        String fieldName = context.param( "fieldName", String.class ); (1)
        context.definition(
                String.class,
                new MyProjectionDefinition( fieldName ) (2)
        );
    }

    private static class MyProjectionDefinition
            implements ProjectionDefinition<String> {

        private final String fieldName;

        public MyProjectionDefinition(String fieldName) { (2)
            this.fieldName = fieldName;
        }

        @Override
        public SearchProjection<String> create(SearchProjectionFactory<?, ?> factory,
                ProjectionDefinitionContext context) {
            return factory.field( fieldName, String.class ) (3)
                    .toProjection();
        }
    }
}
@ProjectionConstructor
public record MyBookProjection(
        @ProjectionBinding(binder = @ProjectionBinderRef(
                type = MyFieldProjectionBinder.class,
                params = @Param( name = "fieldName", value = "title" )
        )) String title) { (1)
}
Parameters with custom annotations

您可以通过定义具有属性的 custom annotation将任何类型的参数传递给桥接器:

示例 134. 使用自定义注释向 PropertyBinder 传递参数

@Retention(RetentionPolicy.RUNTIME) (1)
@Target({ ElementType.PARAMETER }) (2)
@MethodParameterMapping(processor = @MethodParameterMappingAnnotationProcessorRef( (3)
        type = MyFieldProjectionBinding.Processor.class
))
@Documented (4)
public @interface MyFieldProjectionBinding {

    String fieldName() default ""; (5)

    class Processor (6)
            implements MethodParameterMappingAnnotationProcessor<MyFieldProjectionBinding> { (7)
        @Override
        public void process(MethodParameterMappingStep mapping, MyFieldProjectionBinding annotation,
                MethodParameterMappingAnnotationProcessorContext context) {
            MyFieldProjectionBinder binder = new MyFieldProjectionBinder(); (8)
            if ( !annotation.fieldName().isEmpty() ) { (9)
                binder.fieldName( annotation.fieldName() );
            }
            mapping.projection( binder ); (10)
        }
    }
}
public class MyFieldProjectionBinder implements ProjectionBinder {

    private String fieldName = "name";

    public MyFieldProjectionBinder fieldName(String fieldName) { (1)
        this.fieldName = fieldName;
        return this;
    }

    @Override
    public void bind(ProjectionBindingContext context) {
        context.definition(
                String.class,
                new MyProjectionDefinition( fieldName ) (2)
        );
    }

    private static class MyProjectionDefinition
            implements ProjectionDefinition<String> {

        private final String fieldName;

        public MyProjectionDefinition(String fieldName) { (2)
            this.fieldName = fieldName;
        }

        @Override
        public SearchProjection<String> create(SearchProjectionFactory<?, ?> factory,
                ProjectionDefinitionContext context) {
            return factory.field( fieldName, String.class ) (3)
                    .toProjection();
        }
    }
}
@ProjectionConstructor
public record MyBookProjection(
        @MyFieldProjectionBinding(fieldName = "title") (1)
        String title) {
}

12.12.5. Injecting beans into the binder

使用 compatible frameworks,Hibernate Search 支持将 bean 注入到:

  1. 如果你使用 custom annotations,则使用 MethodParameterMappingAnnotationProcessor

  2. 使用 @ProjectionBinding annotation 的情况下,最好使用 ProjectionBinder

传递至属性粘合器 bind 方法的上下文还公开了一个 beanResolver() 方法,用于访问 Bean 解析器并明确实例化 Bean。

有关更多详细信息,请参见 Bean injection

12.12.6. Programmatic mapping

您也可以通过 programmatic mapping应用投影绑定器。只需将绑定器的实例传递给_.projection(…​)_即可。您可以通过绑定器的构造函数或setter传递参数。

示例 135. 使用 .projection(…​) 应用 ProjectionBinder

TypeMappingStep myBookProjectionMapping = mapping.type( MyBookProjection.class );
myBookProjectionMapping.mainConstructor().projectionConstructor();
myBookProjectionMapping.mainConstructor().parameter( 0 )
        .projection( new MyFieldProjectionBinder().fieldName( "title" ) );

12.12.7. Other incubating features

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

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

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

传递给投影绑定程序的 bind 方法的上下文公开了一个 constructorParameter() 方法,该方法可访问正在绑定的构造函数参数的元数据。

元数据可用于详细检查构造函数参数:

  1. 获取构造函数参数的名称。

  2. 检查构造函数参数的类型。

同样, context used for multi-valued projection binding也公开了一个_containerElement()_方法,该方法允许访问元素类型的类型(多值)构造函数参数类型。

有关更多信息,请参见 javadoc。

构造函数参数的名称仅可用:

对于记录类型的规范构造方法,无论编译标志如何。

对于非记录类型或仅当类型使用 -parameters 编译标志编译时,记录类型的非规范构造方法。

以下是此元数据最简单用法的一个示例,即获取构造函数参数名称并将其用作字段名称。

示例 136. 投影到名称与 ProjectionBinder 中的构造函数参数名称相同的字段上

public class MyFieldProjectionBinder implements ProjectionBinder {
    @Override
    public void bind(ProjectionBindingContext context) {
        var constructorParam = context.constructorParameter(); (1)
        context.definition(
                String.class,
                new MyProjectionDefinition( constructorParam.name().orElseThrow() ) (2)
        );
    }

    private static class MyProjectionDefinition
            implements ProjectionDefinition<String> {
        private final String fieldName;

        private MyProjectionDefinition(String fieldName) {
            this.fieldName = fieldName;
        }

        @Override
        public SearchProjection<String> create(SearchProjectionFactory<?, ?> factory,
                ProjectionDefinitionContext context) {
            return factory.field( fieldName, String.class ) (3)
                    .toProjection();
        }
    }
}
@ProjectionConstructor
public record MyBookProjection(
        @ProjectionBinding(binder = @ProjectionBinderRef( (1)
                type = MyFieldProjectionBinder.class
        ))
        String title) {
}