/* SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. * * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ using System; using System.Collections.Generic; using System.Text; using OpenSearch.OpenSearch.Xunit.XunitPlumbing; using OpenSearch.Net; using OpenSearch.Client; using OpenSearch.Client.Specification.IndicesApi; using Tests.Core.Client; using Tests.Framework; using static Tests.Core.Serialization.SerializationTestHelper; namespace Tests.ClientConcepts.HighLevel.Mapping { /** * [[auto-map]] * === Auto mapping * * When creating a mapping either when creating an index or through the Put Mapping API, * OSC offers a feature called auto mapping that can automagically infer the correct * OpenSearch field datatypes from the CLR POCO property types you are mapping. **/ public class AutoMap { private readonly IOpenSearchClient _client = TestClient.DisabledStreaming; /** * We'll look at the features of auto mapping with a number of examples. For this, * we'll define two POCOs, `Company`, which has a name * and a collection of Employees, and `Employee` which has various properties of * different types, and itself has a collection of `Employee` types. */ public abstract class Document { public JoinField Join { get; set; } } public class Company : Document { public string Name { get; set; } public List Employees { get; set; } } public class Employee : Document { public string LastName { get; set; } public int Salary { get; set; } public DateTime Birthday { get; set; } public bool IsManager { get; set; } public List Employees { get; set; } public TimeSpan Hours { get; set; } } [U] public void UsingAutoMap() { /** * Auto mapping can take the pain out of having to define a manual mapping for all properties * on the POCO. In this case we want to index two subclasses into a single index. We call Map * for the base class and then call AutoMap foreach of the types we want it to implement */ var createIndexResponse = _client.Indices.Create("myindex", c => c .Map(m => m .AutoMap() // <1> Auto map `Company` using the generic method .AutoMap(typeof(Employee)) // <2> Auto map `Employee` using the non-generic method ) ); /** * This produces the following JSON request */ // json var expected = new { mappings = new { properties = new { birthday = new {type = "date"}, employees = new { properties = new { birthday = new {type = "date"}, employees = new { properties = new { }, type = "object" }, hours = new {type = "long"}, isManager = new {type = "boolean"}, join = new { properties = new { }, type = "object" }, lastName = new { fields = new { keyword = new { ignore_above = 256, type = "keyword" } }, type = "text" }, salary = new {type = "integer"} }, type = "object" }, hours = new {type = "long"}, isManager = new {type = "boolean"}, join = new { properties = new { }, type = "object" }, lastName = new { fields = new { keyword = new { ignore_above = 256, type = "keyword" } }, type = "text" }, name = new { fields = new { keyword = new { ignore_above = 256, type = "keyword" } }, type = "text" }, salary = new {type = "integer"} } } }; // hide Expect(expected).FromRequest(createIndexResponse); } /** * Observe that OSC has inferred the OpenSearch types based on the CLR type of our POCO properties. * In this example, * * - Birthday is mapped as a `date`, * - Hours is mapped as a `long` (`TimeSpan` ticks) * - IsManager is mapped as a `boolean`, * - Salary is mapped as an `integer` * - Employees is mapped as an `object` * * and the remaining string properties as multi field `text` datatypes, each with a `keyword` datatype * sub field. * * [float] * [[inferred-dotnet-type-mapping]] * === Inferred .NET type mapping * * OSC has inferred mapping support for the following .NET types * * [horizontal] * `String`:: maps to `"text"` with a `"keyword"` sub field. See <>. * `Int32`:: maps to `"integer"` * `UInt16`:: maps to `"integer"` * `Int16`:: maps to `"short"` * `Byte`:: maps to `"short"` * `Int64`:: maps to `"long"` * `UInt32`:: maps to `"long"` * `TimeSpan`:: maps to `"long"` * `Single`:: maps to `"float"` * `Double`:: maps to `"double"` * `Decimal`:: maps to `"double"` * `UInt64`:: maps to `"double"` * `DateTime`:: maps to `"date"` * `DateTimeOffset`:: maps to `"date"` * `Boolean`:: maps to `"boolean"` * `Char`:: maps to `"keyword"` * `Guid`:: maps to `"keyword"` * * and supports a number of special types defined in OSC * * [horizontal] * `OpenSearch.Client.QueryContainer`:: maps to `"percolator"` * `OpenSearch.Client.GeoLocation`:: maps to `"geo_point"` * `OpenSearch.Client.IGeoShape`:: maps to `"geo_shape"` (if you want to map to a `"shape"` type use explicit mapping or the [Shape] attribute on the property) * `OpenSearch.Client.CompletionField`:: maps to `"completion"` * `OpenSearch.Client.DateRange`:: maps to `"date_range"` * `OpenSearch.Client.DoubleRange`:: maps to `"double_range"` * `OpenSearch.Client.FloatRange`:: maps to `"float_range"` * `OpenSearch.Client.IntegerRange`:: maps to `"integer_range"` * `OpenSearch.Client.LongRange`:: maps to `"long_range"` * `OpenSearch.Client.IpAddressRange`:: maps to `"ip_range"` * * All other types map to `"object"` by default. * *[IMPORTANT] * -- * Some .NET types do not have direct equivalent OpenSearch types. For example, `System.Decimal` is a type * commonly used to express currencies and other financial calculations that require large numbers of significant * integral and fractional digits and no round-off errors. There is no equivalent type in OpenSearch, and the * nearest type is {ref_current}/number.html[double], a double-precision 64-bit IEEE 754 floating point. * * When a POCO has a `System.Decimal` property, it is automapped to the OpenSearch `double` type. With the caveat * of a potential loss of precision, this is generally acceptable for a lot of use cases, but it can however cause * problems in _some_ edge cases. * * As the https://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/csharp%20language%20specification.doc[C# Specification states], * * [quote, C# Specification section 6.2.1] * For a conversion from `decimal` to `float` or `double`, the `decimal` value is rounded to the nearest `double` or `float` value. * While this conversion may lose precision, it never causes an exception to be thrown. * * This conversion does cause an exception to be thrown at deserialization time for `Decimal.MinValue` and `Decimal.MaxValue` because, at * serialization time, the nearest `double` value that is converted to is outside of the bounds of `Decimal.MinValue` or `Decimal.MaxValue`, * respectively. In these cases, it is advisable to use `double` as the POCO property type. * -- */ /**[float] * === Mapping Recursion * If you notice in our previous `Company` and `Employee` example, the `Employee` type is recursive * in that the `Employee` class itself contains a collection of type `Employee`. By default, `.AutoMap()` will only * traverse a single depth when it encounters recursive instances like this; the collection of type `Employee` * on the `Employee` class did not get any of its properties mapped. * * This is done as a safe-guard to prevent stack overflows and all the fun that comes with * __infinite__ recursion. Additionally, in most cases, when it comes to OpenSearch mappings, it is * often an edge case to have deeply nested mappings like this. However, you may still have * the need to do this, so you can control the recursion depth of `.AutoMap()`. * * Let's introduce a very simple class, `A`, which itself has a property * Child of type `A`. */ public class A { public A Child { get; set; } } [U] public void ControllingRecursionDepth() { /** By default, `.AutoMap()` only goes as far as depth 1 */ var createIndexResponse = _client.Indices.Create("myindex", c => c .Map(m => m.AutoMap()) ); /** Thus we do not map properties on the second occurrence of our Child property */ //json var expected = new { mappings = new { properties = new { child = new { properties = new { }, type = "object" } } } }; //hide Expect(expected).FromRequest(createIndexResponse); /** Now let's specify a maxRecursion of `3` */ createIndexResponse = _client.Indices.Create("myindex", c => c .Map(m => m.AutoMap(3)) ); /** `.AutoMap()` has now mapped three levels of our Child property */ //json var expectedWithMaxRecursion = new { mappings = new { properties = new { child = new { type = "object", properties = new { child = new { type = "object", properties = new { child = new { type = "object", properties = new { child = new { type = "object", properties = new { } } } } } } } } } } }; //hide Expect(expectedWithMaxRecursion).FromRequest(createIndexResponse); } //hide [U] public void PutMappingAlsoAdheresToMaxRecursion() { var descriptor = new PutMappingDescriptor().AutoMap(); var expected = new { properties = new { child = new { properties = new { }, type = "object" } } }; Expect(expected).WhenSerializing((IPutMappingRequest) descriptor); var withMaxRecursionDescriptor = new PutMappingDescriptor().AutoMap(3); var expectedWithMaxRecursion = new { properties = new { child = new { type = "object", properties = new { child = new { type = "object", properties = new { child = new { type = "object", properties = new { child = new { type = "object", properties = new { } } } } } } } } } }; Expect(expectedWithMaxRecursion).WhenSerializing((IPutMappingRequest)withMaxRecursionDescriptor); } } }