Fluent mapping
editFluent mapping
editFluent mapping POCO properties to fields within an Elasticsearch type mapping offers the most control over the process. With fluent mapping, each property of the POCO is explicitly mapped to an Elasticsearch type field mapping.
To demonstrate, we’ll define two POCOs
-
Company
, which has a name and a collection of Employees -
Employee
which has various properties of different types and has itself a collection ofEmployee
types.
public class Company { public string Name { get; set; } public List<Employee> Employees { get; set; } } public class Employee { public string FirstName { get; set; } public string LastName { get; set; } public int Salary { get; set; } public DateTime Birthday { get; set; } public bool IsManager { get; set; } public List<Employee> Employees { get; set; } public TimeSpan Hours { get; set; } }
Manual mapping
editTo create a mapping for our Company type, we can use the fluent API and map each property explicitly
var createIndexResponse = _client.Indices.Create("myindex", c => c .Map<Company>(m => m .Properties(ps => ps .Text(s => s .Name(n => n.Name) ) .Object<Employee>(o => o .Name(n => n.Employees) .Properties(eps => eps .Text(s => s .Name(e => e.FirstName) ) .Text(s => s .Name(e => e.LastName) ) .Number(n => n .Name(e => e.Salary) .Type(NumberType.Integer) ) ) ) ) ) );
Here, the Name property of the Company
type has been mapped as a text datatype and
the Employees
property mapped as an object datatype. Within this object mapping,
only the FirstName
, LastName
and Salary
properties of the Employee
type have been mapped.
The json mapping for this example looks like
{ "mappings": { "properties": { "name": { "type": "text" }, "employees": { "type": "object", "properties": { "firstName": { "type": "text" }, "lastName": { "type": "text" }, "salary": { "type": "integer" } } } } } }
Manual mapping in this way is powerful but can become verbose and unwieldy for large POCOs. The majority of the time you simply want to map all the properties of a POCO in a single go without having to specify the mapping for each property, particularly when there is inferred mapping from CLR types to Elasticsearch types.
This is where the fluent mapping in conjunction with auto mapping comes in.
Auto mapping with fluent overrides
editIn most cases, you’ll want to map more than just the vanilla datatypes and also provide
various options for your properties, such as the analyzer to use, whether to enable doc_values
, etc.
In this case, it’s possible to use .AutoMap()
in conjunction with explicitly mapped properties.
Here we are using .AutoMap()
to automatically infer the mapping of our Company
type from the
CLR property types, but then we’re overriding the Employees
property to make it a
nested datatype, since by default .AutoMap()
will infer the
List<Employee>
property as an object
datatype
var createIndexResponse = _client.Indices.Create("myindex", c => c .Map<Company>(m => m .AutoMap() .Properties(ps => ps .Nested<Employee>(n => n .Name(nn => nn.Employees) ) ) ) );
{ "mappings": { "properties": { "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "employees": { "type": "nested" } } } }
.AutoMap()
is idempotent therefore calling it before or after
manually mapped properties will still yield the same result. The next example
generates the same mapping as the previous
createIndexResponse = _client.Indices.Create("myindex", c => c .Map<Company>(m => m .Properties(ps => ps .Nested<Employee>(n => n .Name(nn => nn.Employees) ) ) .AutoMap() ) );
Auto mapping overrides down the object graph
editJust as we were able to override the inferred properties from auto mapping in the previous example, fluent mapping also takes precedence over Attribute Mapping. In this way, fluent, attribute and auto mapping can be combined. We’ll demonstrate with an example.
Consider the following two POCOS
[ElasticsearchType(RelationName = "company")] public class CompanyWithAttributes { [Keyword(NullValue = "null", Similarity = "BM25")] public string Name { get; set; } [Text(Name = "office_hours")] public TimeSpan? HeadOfficeHours { get; set; } [Object(Store = false)] public List<EmployeeWithAttributes> Employees { get; set; } } [ElasticsearchType(RelationName = "employee")] public class EmployeeWithAttributes { [Text(Name = "first_name")] public string FirstName { get; set; } [Text(Name = "last_name")] public string LastName { get; set; } [Number(DocValues = false, IgnoreMalformed = true, Coerce = true)] public int Salary { get; set; } [Date(Format = "MMddyyyy")] public DateTime Birthday { get; set; } [Boolean(NullValue = false, Store = true)] public bool IsManager { get; set; } [Nested] [PropertyName("empl")] public List<Employee> Employees { get; set; } }
Now when mapping, AutoMap()
is called to infer the mapping from the POCO property types and
attributes, and inferred mappings are overridden with fluent mapping
var createIndexResponse = _client.Indices.Create("myindex", c => c .Map<CompanyWithAttributes>(m => m .AutoMap() .Properties(ps => ps .Nested<EmployeeWithAttributes>(n => n .Name(nn => nn.Employees) .AutoMap() .Properties(pps => pps .Text(s => s .Name(e => e.FirstName) .Fields(fs => fs .Keyword(ss => ss .Name("firstNameRaw") ) .TokenCount(t => t .Name("length") .Analyzer("standard") ) ) ) .Number(nu => nu .Name(e => e.Salary) .Type(NumberType.Double) .IgnoreMalformed(false) ) .Date(d => d .Name(e => e.Birthday) .Format("MM-dd-yy") ) ) ) ) ) );
Automap company |
|
Override company inferred mappings |
|
Automap nested employee type |
|
Override employee inferred mappings |
{ "mappings": { "properties": { "employees": { "type": "nested", "properties": { "birthday": { "format": "MM-dd-yy", "type": "date" }, "empl": { "properties": { "birthday": { "type": "date" }, "employees": { "properties": {}, "type": "object" }, "firstName": { "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }, "type": "text" }, "hours": { "type": "long" }, "isManager": { "type": "boolean" }, "lastName": { "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }, "type": "text" }, "salary": { "type": "integer" } }, "type": "nested" }, "first_name": { "fields": { "firstNameRaw": { "type": "keyword" }, "length": { "analyzer": "standard", "type": "token_count" } }, "type": "text" }, "isManager": { "null_value": false, "store": true, "type": "boolean" }, "last_name": { "type": "text" }, "salary": { "ignore_malformed": false, "type": "double" } } }, "name": { "null_value": "null", "similarity": "BM25", "type": "keyword" }, "office_hours": { "type": "text" } } } }
As demonstrated, by calling .AutoMap()
inside of the .Nested<Employee>
mapping, it is possible to auto map the
Employee
nested properties and again, override any inferred mapping from the automapping process,
through manual mapping
Mapping runtime fields
editA runtime field is a field that is evaluated at query time. Runtime fields may be defined in the mapping of an index.
In this example, we’ll define a CompanyRuntimeFields
class with a single property which we may then use in
the strongly-typed runtime field mapping.
public class CompanyRuntimeFields { public string BirthDayOfWeek { get; set; } } var createIndexResponse = _client.Indices.Create("myindex", c => c .Map<Company>(m => m .RuntimeFields<CompanyRuntimeFields>(rtf => rtf .RuntimeField(f => f.BirthDayOfWeek, FieldType.Keyword, f => f.Script("emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"))) ) );
Use the |
|
Use the |
{ "mappings": { "runtime": { "birthDayOfWeek": { "type": "keyword", "script": { "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))" } } } } }
It’s not necessary to define a type for the runtime field mapping. Runtime fields can optionally be defined
by providing a string
name.
createIndexResponse = _client.Indices.Create("myindex", c => c .Map<Company>(m => m .RuntimeFields(rtf => rtf .RuntimeField("birthDayOfWeek", FieldType.Keyword, f => f.Script("emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"))) ) );
One may also include and use parameters in the script.
createIndexResponse = _client.Indices.Create("myindex", c => c .Map<Company>(m => m .RuntimeFields(rtf => rtf .RuntimeField("birthDayOfWeek", FieldType.Keyword, f => f .Script(s => s .Source("emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT) + params.suffix)") .Params(p => p.Add("suffix", " with a suffix.")) ))) ) );
{ "mappings": { "runtime": { "birthDayOfWeek": { "type": "keyword", "script": { "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT) + params.suffix)", "params": { "suffix": " with a suffix." } } } } } }