Index Definitions

Search indexes are specified through Java interfaces that outline which entities and attributes should be indexed. It is recommended to create these interfaces for every entity that requires full-text search functionality.

Index Definition Interface

A Java interface defining an index must adhere to these requirements:

  • It can be named arbitrarily.

  • It must be annotated with the @JmixEntitySearchIndex annotation.

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

}

@JmixEntitySearchIndex

Attributes

  • entity - specifies the entity class associated with this Index Definition. Mandatory.

  • indexName - specifies the full name of index that will be used for that entity. Optional. If not set index name is based on prefix and entity name.

Indexed entity properties are specified using methods within the interface. These methods must adhere to the following guidelines:

  • They should return void.

  • They can be named arbitrarily.

  • They should not accept parameters.

  • They should not contain any implementation logic.

  • Each method should be annotated with the @AutoMappedField annotation.

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

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

AutoMappedField Annotation

The @AutoMappedField annotation enables you to map specific entity attributes based on their data type (see below). It can include the following parameters:

  • includeProperties - a list of entity attributes to be indexed. It accepts dot notation to specify attributes of related entities. By default, none are indexed for search.

  • excludeProperties - a list of entity attributes to be excluded from indexing. It accepts dot notation to specify attributes of related entities. By default, none are excluded.

  • analyzer - the name of an analyzer defined in Elasticsearch/OpenSearch to be used in index field mapping. If not specified, the standard analyzer is used.

  • indexFileContent - a boolean flag that determines if the content of a file found in matched file properties should be indexed. By default, it is set to true.

Both includeProperties and excludeProperties support * wildcard. It is expanded to local properties at the corresponding level:

  • * - local properties of the indexed entity.

  • refField.* - local properties of the entity referenced by the refField property.

The wildcard does not apply to back-reference attributes and attributes of entity traits such as version, createdBy, etc.

excludeProperties is beneficial only when includeProperties contains a wildcard to restrict its expansion. For instance:

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

An analyzer is employed to alter incoming text values in various manners, including language morphologies. A specified analyzer is utilized during both the indexing and searching phases.

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

Multiple mapping annotations can be applied to a single method or distributed across multiple methods for some kind of grouping. The examples below illustrate the identical definition:

@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();
}

Automatic Mapping

Automatic mapping by the @AutoMappedField annotation is supported for entity attributes of the following types:

The wildcard encompasses all these attribute types.

Textual Attributes

These attributes are of the String type. It is the most common case, a value of an attribute is used as the indexed value.

The field in the index appears as follows:

"textualFieldName": "value"

In case of multiple values:

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

Reference Attributes

These are references to related entities. The indexed value comprises solely the instance name of the associated entity, omitting any nested properties. To index nested properties of a related entity, make sure to explicitly incorporate refProperty.nestedProperty or refProperty.* into your mapping.

The field in the index appears as follows:

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

In case of multiple values:

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

Files

These are attributes of the FileRef type referring files in File Storage. By default, both the file name and content are utilized as indexed values. If you wish to index only the file name, you must adjust the indexFileContent parameter of @AutoMappedField to false:

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

The field in the index appears as follows:

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

In case of multiple values:

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

Enumeration Attributes

In the case of enumeration attributes, the indexed values include localized values for all available locales.

An entry in the index appears as:

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

If multiple enumeration values are present, each value in all available locales is included, leading to a multiplication of values:

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

Embedded Attributes

These are references to embedded JPA entities. Adding an embedded attribute implies including all its nested attributes ("someEmbeddedProperty" = "someEmbeddedProperty.*"). The index value is determined by the type of the nested attribute, and unsupported types will be ignored.

For instance, imagine a root entity with a customer attribute linked to the embedded Customer entity, which holds the firstName and lastName attributes. If you choose to include the customer attribute, it will automatically trigger the inclusion of the customer.firstName and customer.lastName attributes.

Nested Attributes and Collections

You can specify nested properties using the dot notation: refProperty.nestedRefProperty.targetDataProperty.

Furthermore, the system supports collection attributes, which includes nested collections at various levels. In such cases, the index consolidates all attribute values at the lowest level. For instance, a property like collectionOfReferences.nestedCollectionOfAnotherReferences.name is stored in the following format:

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

Within the array, you’ll find values of the name attribute from all instances of EntityB within all instances of EntityA in the root entity.

Programmatic Mapping

Instead of using annotations, you can build mapping definition programmatically.

To do this, you will need to define a method in your index definition interface. This method should satisfy the following conditions:

  • It must be a default method.

  • It can be named arbitrarily.

  • It can accept Spring beans as parameters for custom configuration.

  • It must return the MappingDefinition type.

  • It must be annotated with @ManualMappingDefinition.

Within the method body, you can create a mapping definition using 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 The @ManualMappingDefinition annotation marks methods with manual creation of mapping definition.
2 You can pass Spring beans as parameters for custom mapping configuration.
3 The list of properties that should be indexed. Here the * wildcard is used to include all local properties of the indexed Customer entity.
4 The list of properties that should not be indexed.
5 The FieldMappingStrategy implementation class that should be used to map properties. The mapping strategy can also be defined as instance using the withFieldMappingStrategy() method with the strategy instance as a parameter.
6 An explicit property value extractor. For example, a FilePropertyValueExtractor instance can be used for processing attributes of the FileRef type.
There can be only one method with programmatic mapping. If a method with programmatic mapping exists, all mapping annotations are ignored.

Indexable Predicate

Indexing process can have additional instance-level condition. It can be added by configuring Indexable Predicate. This predicate applies to each entity instance during indexing and defines if it should be indexed or not.

It doesn’t apply during deletion.

To configure Indexable Predicate add method that fulfils the following requirements:

  • With default modifier.

  • With any name.

  • With return type - Predicate<TargetEntity>, where 'TargetEntity' is the value of entity() parameter of current annotation.

  • With Spring beans required for predicate logic as parameters.

  • Annotated with @IndexablePredicate.

Actual predicate should be created within method body and returned as a result.

Instance passed to predicate includes only declared indexable properties, others are unfetched. To get access to them you need to reload instance with proper fetch plan within predicate.
@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);
        };
    }
}