索引定义

搜索的索引是通过 Java 接口定义的,接口中描述哪些实体以及哪些属性需要建立索引。建议为所有需要使用全文搜索的实体创建此类接口。

索引定义接口

索引定义的 Java 接口需要满足下列条件:

  • 命名无限制。

  • 必须使用 @JmixEntitySearchIndex 注解。

@JmixEntitySearchIndex(entity = Order.class)
public interface OrderIndexDefinition {

}

@JmixEntitySearchIndex

属性:

  • entity - 必需属性。指定该索引定义关联的实体类。

  • indexName - 可选属性。指定实体索引的全名。如果没设置,则索引名依据 前缀和实体名 生成。

实体属性索引通过接口的方法定义。方法需要满足下列条件:

  • 必须返回 void

  • 命名无限制。

  • 无参数。

  • 无具体实现。

  • 需要使用 @AutoMappedField 注解。

@JmixEntitySearchIndex(entity = Order.class)
public interface OrderIndexDefinition {

    @AutoMappedField(includeProperties =
            {"number", "product", "customer.status", "customer.lastName"})
    void orderMapping();
}

AutoMappedField 注解

@AutoMappedField 注解支持将实体属性根据其类型映射为 ES 索引(参见 下文)。该注解有下列参数:

  • includeProperties - 应该建立索引的实体属性列表。支持使用点标记指定关联实体的属性。默认不包含任何属性,即不建立任何属性的索引。

  • excludeProperties - 不需要建立索引的实体属性列表。支持使用点标记指定关联实体的属性。默认不包含任何属性。

  • analyzer - ES/OpenSearch 中定义的分析器(Analyzer)名称,用在索引字段映射中。如果未指定,则使用 standard 分析器。

  • indexFileContent - 定义是否对文件属性的文件内容建立索引。默认 true

includePropertiesexcludeProperties 都支持 * 通配符。该通配符将转换为相应级别的本地属性:

  • * - 索引实体的本地属性。

  • refField.* - refField 属性指定的关联实体的本地属性。

通配符不覆盖反向引用属性和实体 特性 属性:versioncreatedBy 等。

仅当 includeProperties 包含通配符时,使用 excludeProperties 比较方便,用于限制属性范围。示例:

@AutoMappedField(
        includeProperties = {"*", "customer.*"},
        excludeProperties = {"number", "customer.firstName"})
void orderCustomerMapping();

分析器以不同的方式转换输入文本值,包括对某些语言词法的解析。指定的分析器用于索引和搜索步骤。

@JmixEntitySearchIndex(entity = Customer.class)
public interface CustomerIndexDefinition {
    @AutoMappedField(
            includeProperties = {"firstName", "lastName"},
            analyzer = "german")
    void customerMapping();
}

可以在单个方法上使用多个映射注解,也可以在多个方法之间拆分以定义不同的分组。以下示例都表示同一个定义:

@JmixEntitySearchIndex(entity = Order.class)
public interface OrderIndexDefinition {

    @AutoMappedField(includeProperties =
            {"number", "product", "customer.status", "customer.lastName"})
    void orderMapping();
}
@JmixEntitySearchIndex(entity = Order.class)
public interface OrderIndexDefinition {

    @AutoMappedField(includeProperties = {"number", "product"})
    @AutoMappedField(includeProperties = {"customer.status", "customer.lastName"})
    void orderMapping();
}
@JmixEntitySearchIndex(entity = Order.class)
public interface OrderIndexDefinition {

    @AutoMappedField(includeProperties = {"number", "product"})
    void orderMapping();

    @AutoMappedField(includeProperties = {"customer.status", "customer.lastName"})
    void customerMapping();
}

自动映射

@AutoMappedField 注解的自动映射支持下列类型的实体属性:

通配符能覆盖所有这些属性。

文本属性

String 类型的属性是最常见的情况,属性值即用作索引值。

索引字段类似这样:

"textualFieldName": "value"

如果有多个值:

"textualFieldName": ["value1", "value2"]

引用属性

这类属性是关联实体的引用。只有关联实体的实例名用作索引值,不包含关联实体的任何内部属性。如需对关联实体的内部属性建立索引,需要在映射中显式添加 refProperty.nestedPropertyrefProperty.*

索引字段类似这样:

"refFieldName": {
  "_instance_name": "instanceNameValue"
}

如果有多个值:

"refFieldName": {
  "_instance_name": ["instanceNameValue1", "instanceNameValue2"]
}

文件属性

这是 FileRef 类型的属性,指向 文件存储 中的文件。文件名和文件内容默认都会用作索引值。如果仅需对文件名建立索引,需要设置 @AutoMappedFieldindexFileContent 参数为 false

@AutoMappedField(
        includeProperties = {"*"},
        indexFileContent = false)
void fileMapping();

索引字段类似这样:

"fileRefField": {
	"_file_name" : "File name",
	"_content" : "File content if enabled"
}

如果有多个值:

"fileRefField": [
	{
		"_file_name" : "File name 1",
		"_content" : "File content 1"
	},
	{
		"_file_name" : "File name 2",
		"_content" : "File content 2"
	}
]

枚举属性

对于 枚举类型 属性,会使用其全部语言环境的 本地化值 作为索引值。

索引字段类似这样:

"enumFieldName": ["enValue", "ruValue"]

如果属性包含有多个枚举值,则会展示所有值的所有语言本地化值:

"enumFieldName": ["enValue1", "ruValue1", "enValue2", "ruValue2"]

嵌入实体属性

这是内部 JPA 实体的引用。添加一个嵌入实体属性也就是包含其所有内部属性("someEmbeddedProperty" = "someEmbeddedProperty.*")。索引值根据内部属性的不同类型构建,会忽略不支持的类型。

假设一个根实体的 customer 属性关联至内部的 Customer 实体(具有 firstNamelastName 属性)。如果在映射中包含了 customer 属性,则会自动包含 customer.firstNamecustomer.lastName 属性。

内部属性和集合

内部嵌套属性可用点标记指定:refProperty.nestedRefProperty.targetDataProperty

此外,系统也支持集合属性,可以包含不同级别的内部集合属性。此时索引会存储最低一级属性的全部值。例如,collectionOfReferences.nestedCollectionOfAnotherReferences.name 保存如下:

"collectionOfEntityA": {
	"nestedCollectionOfEntityB": {
		"name": ["value1", ..., "valueN"]
	}
}

数组包含根实体中全部 EntityA 实例的全部 EntityB 实例的 name 属性值。

编程式映射

除了使用注解之外,还可以通过编程的方式构建映射定义。

需要在索引定义接口中创建满足下列条件的方法:

  • 是 default 方法。

  • 命名无限制。

  • 为了自定义配置,可以使用 Spring bean 作为参数。

  • 必须返回 MappingDefinition 类型。

  • 必须使用 @ManualMappingDefinition 注解。

方法体中,可以使用 MappingDefinition.builder() 创建映射定义。

@JmixEntitySearchIndex(entity = Customer.class)
public interface CustomerIndexDefinition {
    @ManualMappingDefinition (1)
    default MappingDefinition mapping(FilePropertyValueExtractor filePropertyValueExtractor) { (2)
        return MappingDefinition.builder()
                .addElement(
                        MappingDefinitionElement.builder()
                                .includeProperties("*") (3)
                                .excludeProperties("hobby", "maritalStatus") (4)
                                .withFieldMappingStrategyClass(AutoMappingStrategy.class) (5)
                                .withPropertyValueExtractor(filePropertyValueExtractor) (6)
                                .build()
                )
                .build();
    }
}
1 @ManualMappingDefinition 注解标记的方法使用了手动创建映射定义。
2 为了自定义映射配置,可以传入 Spring bean 作为参数。
3 需要建立索引的属性列表。这里使用 * 通配符包含 Customer 实体的所有本地属性。
4 不需要建立索引的属性列表。
5 需要用 FieldMappingStrategy 的实现类做属性映射。映射策略也可以定义为类的实例,需要用 withFieldMappingStrategy() 方法设置,参数为映射策略的实例。
6 显式指定属性值解析器。例如,FilePropertyValueExtractor 可用于处理 FileRef 类型的属性。

还有一个方式是用文本配置属性映射,示例:

@ManualMappingDefinition
default MappingDefinition mappingMethod(AutoMappingStrategy autoMappingStrategy,
                                  SimplePropertyValueExtractor simplePropertyValueExtractor) {
    return MappingDefinition.builder()
            .addElement(
                    MappingDefinitionElement.builder()
                            .includeProperties("*")
                            .excludeProperties("firstName", "lastName") (1)
                            .withFieldMappingStrategyClass(AutoMappingStrategy.class)
                            .build()
            )
            .addElement(
                    MappingDefinitionElement.builder()
                            .includeProperties("firstName") (2)
                            .withFieldMappingStrategy(autoMappingStrategy) (3)
                            .withFieldConfiguration( (4)
                                    "{\n" +
                                            "    \"type\": \"text\",\n" +
                                            "    \"analyzer\": \"standard\",\n" +
                                            "    \"boost\": 2\n" +
                                            "}"
                            )
                            .build()
            )
            .addElement(
                    MappingDefinitionElement.builder()
                            .includeProperties("lastName")
                            .withFieldConfiguration( (5)
                                    "{\n" +
                                            "    \"type\": \"text\",\n" +
                                            "    \"analyzer\": \"english\"\n" +
                                            "}"
                            )
                            .withPropertyValueExtractor(simplePropertyValueExtractor) (6)
                            .withOrder(1) (7)
                            .build()
            )
            .build();
}
1 排除两个特定属性:firstNamelastName。将由后续元素单独处理以进行微调。
2 仅用于 firstName 字段。
3 首先使用自动映射策略(获得最基本的映射)。
4 用包含原生配置的字符串 json 对象定义字段配置。将覆盖和扩展策略生成的配置:
  • "type": "text" — 显式指定文本类型。

  • "analyzer": "standard" — 使用标准 Elasticsearch 分析器进行分词。

  • "boost": 2 — 为 firstName 字段赋予双倍权重。名称匹配的文档将在结果中排名更高。

5 完全定义映射,绕过所有策略。该字段将使用 "english" 分析器(专用于英语)作为文本进行索引。
6 直接指定配置(withFieldConfiguration)时的必填参数。指定使用哪个组件从 Customer 实体中提取该属性的值。没有提取器,引擎将不知道如何获得数据。
7 为此映射元素设置顺序。如果元素相互冲突,就必须设置顺序。
只能有一个编程式映射的方法。如果存在此方法,则会忽略所有的映射注解。

FieldMappingStrategy

io.jmix.search.index.mapping.strategy.FieldMappingStrategy - 映射策略的基础接口。

定义实体属性如何转换为搜索索引字段。每个策略决定:

  • 支持哪些属性类型(isSupported() 方法)。

  • 如何为搜索引擎生成字段配置。

  • 使用哪种值提取方法。

  • 当多个策略适用时的执行顺序。

PropertyValueExtractor

io.jmix.search.index.mapping.propertyvalue.PropertyValueExtractor - 一个函数式接口,负责在索引期间从实体实例中提取属性值。将实体属性数据转换为可由搜索引擎索引的 JSON 格式。当需要超出标准属性映射的特殊值提取逻辑时,请使用自定义实现。

@ExtendedSearch 注解

可选的 @ExtendedSearch 注解可以用于索引定义接口,以启用基于前缀的搜索功能。

默认情况下,搜索引擎中的 Starts with 查询在使用通配符或正则表达式时可能会消耗大量资源。@ExtendedSearch 注解通过预索引单词前缀提供了一种高性能的替代方案。

当存在此注解时,系统会为索引中定义的每个“真实”字段创建额外的“虚拟”子字段。这些子字段存储专用分析器生成的前缀词。

工作原理:

底层机制涉及用于虚拟子字段的特定分析链(过滤器和分析器):

  • 前缀生成:对于索引文本中的每个单词,分析器会生成一组从最小长度到最大长度的前缀。

  • 存储:这些前缀存储在子字段中。

  • 搜索:当用户执行搜索时,系统会根据这些预索引的前缀进行匹配,而不是执行实时的通配符计算。

前缀生成的粒度通过 jmix.search.min-prefix-lengthjmix.search.max-prefix-length 应用程序属性进行控制。

属性:

  • enabled - 全局启用/禁用扩展搜索功能。默认设置为 true

  • tokenizer - 前缀分析器中使用的分词器。默认值为 whitespace

  • additionalFilters - 除了标准的 lowercaseedge N-gram 之外的额外分析过滤器。默认值:none

示例:

@ExtendedSearch(tokenizer = "letter",
        additionalFilters = {"asciifolding"})
@JmixEntitySearchIndex(entity = Order.class)
public interface OrderIndexDefinition {

}

可索引谓词

建立索引的过程中,可以使用额外的实体级别的条件。 条件是通过配置可索引谓词(Indexable Predicate)进行添加。在建立索引的过程中,这个谓词会在每个实体实例上做检查,判断实例是否能加入索引。

谓词不会在删除时使用。

如需配置可索引谓词,请添加满足下列条件的方法:

  • 是 default 方法

  • 命名无限制。

  • 返回类型为 Predicate<TargetEntity>,这里 'TargetEntity' 是当前注解中 entity() 参数的值。

  • 谓词逻辑需要的 Spring bean 可以作为参数。

  • @IndexablePredicate 注解。

在方法体中创建实际的谓词并作为结果返回。

传递给谓词的实体实例仅包含声明用于索引的属性,其他属性均未定义。如需访问其他属性,需要在谓词中用合适的 fetch plan 重新加载。
@JmixEntitySearchIndex(entity = Customer.class)
public interface CustomerIndexDefinition {
    @AutoMappedField(
            includeProperties = {"firstName", "lastName"},
            analyzer = "german")
    void customerMapping();

    @IndexablePredicate
    default Predicate<Customer> indexCustomerWithGoldStatusOnlyPredicate(DataManager dataManager) {
        return (instance) -> {
            Id<Customer> id = Id.of(instance);
            Customer reloadedInstance = dataManager.load(id)
                    .fetchPlanProperties("status")
                    .one();
            Status status = reloadedInstance.getStatus();
            return Status.GOLD.equals(status);
        };
    }
}