[[elasticsearch.mapping]]
= Elasticsearch Object Mapping

Spring Data Elasticsearch allows to choose between two mapping implementations abstracted via the `EntityMapper` interface:

* <<elasticsearch.mapping.jackson2>>
* <<elasticsearch.mapping.meta-model>>

[[elasticsearch.mapping.jackson2]]
== Jackson Object Mapping

The Jackson2 based approach (used by default) utilizes a customized `ObjectMapper` instance with spring data specific modules.
Extensions to the actual mapping need to be customized via Jackson annotations like `@JsonInclude`.

.Jackson2 Object Mapping Configuration
====
[source,java]
----
@Configuration
public class Config extends AbstractElasticsearchConfiguration { <1>

  @Override
  public RestHighLevelClient elasticsearchClient() {
    return RestClients.create(ClientConfiguration.create("localhost:9200")).rest();
  }
}
----
<1> `AbstractElasticsearchConfiguration` already defines a Jackson2 based `entityMapper` via `ElasticsearchConfigurationSupport`.
====

[WARNING]
`CustomConversions`, `@ReadingConverter` & `@WritingConverter` cannot be applied when using the Jackson based `EntityMapper`.  +
Setting the name of a mapped field with `@Field(name="custom-name")` also cannot be used with this Mapper.

[[elasticsearch.mapping.meta-model]]
== Meta Model Object Mapping

The Metamodel based approach uses domain type information for reading/writing from/to Elasticsearch.
This allows to register `Converter` instances for specific domain type mapping.

.Meta Model Object Mapping Configuration
====
[source,java]
----
@Configuration
public class Config extends AbstractElasticsearchConfiguration {

  @Override
  public RestHighLevelClient elasticsearchClient() {
    return RestClients.create(ClientConfiguration.create("localhost:9200")).rest()
  }

  @Bean
  @Override
  public EntityMapper entityMapper() {                                 <1>

    ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(
      elasticsearchMappingContext(), new DefaultConversionService()    <2>
    );
    entityMapper.setConversions(elasticsearchCustomConversions());     <3>

  	return entityMapper;
  }
}
----
<1> Overwrite the default `EntityMapper` from `ElasticsearchConfigurationSupport` and expose it as bean.
<2> Use the provided `SimpleElasticsearchMappingContext` to avoid inconsistencies and provide a `GenericConversionService`
for `Converter` registration.
<3> Optionally set `CustomConversions` if applicable.
====

[[elasticsearch.mapping.meta-model.annotations]]
=== Mapping Annotation Overview

The `ElasticsearchEntityMapper` can use metadata to drive the mapping of objects to documents. The following annotations are available:

* `@Id`: Applied at the field level to mark the field used for identity purpose.
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database. The most important attributes are:
** `indexName`: the name of the index to store this entity in
** `type`: the mapping type. If not set, the lowercased simple name of the class is used.
** `shards`: the number of shards for the index.
** `replicas`: the number of replicas for the index.
** `refreshIntervall`: Refresh interval for the index. Used for index creation. Default value is _"1s"_.
** `indexStoreType`:  Index storage type for the index. Used for index creation. Default value is _"fs"_.
** `createIndex`: Configuration whether to create an index on repository bootstrapping. Default value is _true_.
** `versionType`: Configuration of version management. Default value is _EXTERNAL_.
* `@Transient`: By default all private fields are mapped to the document, this annotation excludes the field where it is applied from being stored in the database
* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved Document.
* `@Field`: Applied at the field level and defines properties of the field, most of the attributes map to the respective https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html[Elasticsearch Mapping] definitions:
** `name`: The name of the field as it will be represented in the Elasticsearch document, if not set, the Java field name is used.
** `type`: the field type, can be one of _Text, Integer, Long, Date, Float, Double, Boolean, Object, Auto, Nested, Ip, Attachment, Keyword_.
** `format` and `pattern` custom definitions for the _Date_ type.
** `store`: Flag wether the original field value should be store in Elasticsearch, default value is _false_.
** `analyzer`, `searchAnalyzer`, `normalizer` for specifying custom custom analyzers and normalizer.
** `copy_to`: the target field to copy multiple document fields to.
* `@GeoPoint`: marks a field as _geo_point_ datatype. Can be omitted if the field is an instance of the `GeoPoint` class.


The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology agnostic.

[[elasticsearch.mapping.meta-model.rules]]
=== Mapping Rules

==== Type Hints

Mapping uses _type hints_ embedded in the document sent to the server to allow generic type mapping.
Those type hints are represented as `_class` attributes within the document and are written for each aggregate root.

.Type Hints
====
[source,java]
----
public class Person {              <1>

  @Id String id;
  String firstname;
  String lastname;
}
----
[source,json]
----
{
  "_class" : "com.example.Person", <1>
  "id" : "cb7bef",
  "firstname" : "Sarah",
  "lastname" : "Connor"
}
----
<1> By default the domain types class name is used for the type hint.
====

Type hints can be configured to hold custom information. Use the `@TypeAlias` annotation to do so.

NOTE: Make sure to add types with `@TypeAlias` to the initial entity set (`AbstractElasticsearchConfiguration#getInitialEntitySet`)
to already have entity information available when first reading data from the store.

.Type Hints with Alias
====
[source,java]
----
@TypeAlias("human")                <1>
public class Person {

  @Id String id;
  // ...
}
----
[source,json]
----
{
  "_class" : "human",              <1>
  "id" : ...
}
----
<1> The configured alias is used when writing the entity.
====

NOTE: Type hints will not be written for nested Objects unless the properties type is `Object`, an interface or the actual value type does not match the properties declaration.

==== Geospatial Types

Geospatial types like `Point` & `GeoPoint` are converted into _lat/lon_ pairs.

.Geospatial types
====
[source,java]
----
public class Address {

  String city, street;
  Point location;
}
----
[source,json]
----
{
  "city" : "Los Angeles",
  "street" : "2800 East Observatory Road",
  "location" : { "lat" : 34.118347, "lon" : -118.3026284 }
}
----
====

==== Collections

For values inside Collections apply the same mapping rules as for aggregate roots when it comes to _type hints_ and <<elasticsearch.mapping.meta-model.conversions>>.

.Collections
====
[source,java]
----
public class Person {

  // ...

  List<Person> friends;

}
----
[source,json]
----
{
  // ...

  "friends" : [ { "firstname" : "Kyle", "lastname" : "Reese" } ]
}
----
====

==== Maps

For values inside Maps apply the same mapping rules as for aggregate roots when it comes to _type hints_ and <<elasticsearch.mapping.meta-model.conversions>>.
However the Map key needs to a String to be processed by Elasticsearch.

.Collections
====
[source,java]
----
public class Person {

  // ...

  Map<String, Address> knownLocations;

}
----
[source,json]
----
{
  // ...

  "knownLocations" : {
    "arrivedAt" : {
       "city" : "Los Angeles",
       "street" : "2800 East Observatory Road",
       "location" : { "lat" : 34.118347, "lon" : -118.3026284 }
     }
  }
}
----
====

[[elasticsearch.mapping.meta-model.conversions]]
=== Custom Conversions

Looking at the `Configuration` from the <<elasticsearch.mapping.meta-model, previous section>> `ElasticsearchCustomConversions` allows registering specific rules for mapping domain and simple types.

.Meta Model Object Mapping Configuration
====
[source,java]
----
@Configuration
public class Config extends AbstractElasticsearchConfiguration {

  @Override
  public RestHighLevelClient elasticsearchClient() {
    return RestClients.create(ClientConfiguration.create("localhost:9200")).rest();
  }

  @Bean
  @Override
  public EntityMapper entityMapper() {

    ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(
      elasticsearchMappingContext(), new DefaultConversionService());
    entityMapper.setConversions(elasticsearchCustomConversions());  <1>

  	return entityMapper;
  }

  @Bean
  @Override
  public ElasticsearchCustomConversions elasticsearchCustomConversions() {
    return new ElasticsearchCustomConversions(
      Arrays.asList(new AddressToMap(), new MapToAddress()));       <2>
  }

  @WritingConverter                                                 <3>
  static class AddressToMap implements Converter<Address, Map<String, Object>> {

    @Override
    public Map<String, Object> convert(Address source) {

      LinkedHashMap<String, Object> target = new LinkedHashMap<>();
      target.put("ciudad", source.getCity());
      // ...

      return target;
    }
  }

  @ReadingConverter                                                 <4>
  static class MapToAddress implements Converter<Map<String, Object>, Address> {

    @Override
    public Address convert(Map<String, Object> source) {

      // ...
      return address;
    }
  }
}
----
[source,json]
----
{
  "ciudad" : "Los Angeles",
  "calle" : "2800 East Observatory Road",
  "localidad" : { "lat" : 34.118347, "lon" : -118.3026284 }
}
----
<1> Register `ElasticsearchCustomConversions` with the `EntityMapper`.
<2> Add `Converter` implementations.
<3> Set up the `Converter` used for writing `DomainType` to Elasticsearch.
<4> Set up the `Converter` used for reading `DomainType` from search result.
====
