From fc84d117abb7075420af2730d31db6d4e957b5f5 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Mon, 23 Apr 2018 16:48:15 +1000 Subject: [PATCH 1/2] Support serialization of geo shapes on documents Fixes #3100 Fixes #3096 --- build/scripts/Commandline.fsx | 2 +- src/Nest/QueryDsl/Geo/GeoLocation.cs | 64 ++++----- .../Geo/Shape/Envelope/EnvelopeGeoShape.cs | 5 +- src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs | 134 ++++++++++++++++-- .../Geo/Shape/GeoShapeQueryJsonConverter.cs | 134 ++++-------------- .../GeometryCollection/GeometryCollection.cs | 13 ++ .../HandleNestTypesOnSourceJsonConverter.cs | 12 +- src/Tests/Framework/MockData/Developer.cs | 3 +- src/Tests/Framework/MockData/Person.cs | 3 +- src/Tests/Framework/MockData/Shape.cs | 122 ++++++++++++++++ .../GeoShapeSerializationTests.cs | 14 ++ src/Tests/Framework/TestClient.cs | 4 + .../QueryDsl/Geo/GeoShapeQueryUsageTests.cs | 130 +++++++++++++++++ 13 files changed, 475 insertions(+), 165 deletions(-) create mode 100644 src/Tests/Framework/MockData/Shape.cs create mode 100644 src/Tests/Framework/SerializationTests/GeoShapeSerializationTests.cs create mode 100644 src/Tests/QueryDsl/Geo/GeoShapeQueryUsageTests.cs diff --git a/build/scripts/Commandline.fsx b/build/scripts/Commandline.fsx index 2f1e13d0ae5..d50f260c673 100644 --- a/build/scripts/Commandline.fsx +++ b/build/scripts/Commandline.fsx @@ -36,7 +36,7 @@ Execution hints can be provided anywhere on the command line - skiptests : skip running tests as part of the target chain - skipdocs : skip generating documentation - seed: : provide a seed to run the tests with. -- random:<:B> : sets random K to bool B if if B is ommitted will default to true +- random:<:B> : sets random K to bool B if if B is omitted will default to true K can be: sourceserializer, typedkeys or oldconnection (only valid on windows) """ diff --git a/src/Nest/QueryDsl/Geo/GeoLocation.cs b/src/Nest/QueryDsl/Geo/GeoLocation.cs index 65af3d1ca72..4047d8b89bd 100644 --- a/src/Nest/QueryDsl/Geo/GeoLocation.cs +++ b/src/Nest/QueryDsl/Geo/GeoLocation.cs @@ -8,7 +8,6 @@ namespace Nest { - /// /// Represents a Latitude/Longitude as a 2 dimensional point that gets serialized as { lat, lon } /// @@ -18,18 +17,16 @@ public class GeoLocation : IEquatable, IFormattable /// Latitude /// [JsonProperty("lat")] - public double Latitude => _latitude; - private readonly double _latitude; + public double Latitude { get; } /// /// Longitude /// [JsonProperty("lon")] - public double Longitude => _longitude; - private readonly double _longitude; + public double Longitude { get; } /// - /// Represents a Latitude/Longitude as a 2 dimensional point. + /// Represents a Latitude/Longitude as a 2 dimensional point. /// /// Value between -90 and 90 /// Value between -180 and 180 @@ -42,8 +39,8 @@ public GeoLocation(double latitude, double longitude) if (!IsValidLongitude(longitude)) throw new ArgumentOutOfRangeException(string.Format(CultureInfo.InvariantCulture, "Invalid longitude '{0}'. Valid values are between -180 and 180", longitude)); - _latitude = latitude; - _longitude = longitude; + Latitude = latitude; + Longitude = longitude; } /// @@ -51,23 +48,17 @@ public GeoLocation(double latitude, double longitude) /// /// /// - public static bool IsValidLatitude(double latitude) - { - return latitude >= -90 && latitude <= 90; - } + public static bool IsValidLatitude(double latitude) => latitude >= -90 && latitude <= 90; /// /// True if is a valid longitude. Otherwise false. /// /// /// - public static bool IsValidLongitude(double longitude) - { - return longitude >= -180 && longitude <= 180; - } + public static bool IsValidLongitude(double longitude) => longitude >= -180 && longitude <= 180; /// - /// Try to create a . + /// Try to create a . /// Return null if either or are invalid. /// /// Value between -90 and 90 @@ -80,10 +71,9 @@ public static GeoLocation TryCreate(double latitude, double longitude) return null; } - public override string ToString() - { - return _latitude.ToString("#0.0#######", CultureInfo.InvariantCulture) + "," + _longitude.ToString("#0.0#######", CultureInfo.InvariantCulture); - } + public override string ToString() => + Latitude.ToString("#0.0#######", CultureInfo.InvariantCulture) + "," + + Longitude.ToString("#0.0#######", CultureInfo.InvariantCulture); public bool Equals(GeoLocation other) { @@ -91,7 +81,7 @@ public bool Equals(GeoLocation other) return false; if (ReferenceEquals(this, other)) return true; - return _latitude.Equals(other._latitude) && _longitude.Equals(other._longitude); + return Latitude.Equals(other.Latitude) && Longitude.Equals(other.Longitude); } public override bool Equals(object obj) @@ -106,7 +96,7 @@ public override bool Equals(object obj) } public override int GetHashCode() => - unchecked((_latitude.GetHashCode() * 397) ^ _longitude.GetHashCode()); + unchecked((Latitude.GetHashCode() * 397) ^ Longitude.GetHashCode()); public string ToString(string format, IFormatProvider formatProvider) => ToString(); @@ -117,21 +107,18 @@ public static implicit operator GeoLocation(string latLon) var parts = latLon.Split(','); if (parts.Length != 2) throw new ArgumentException("Invalid format: string must be in the form of lat,lon"); - double lat; - if (!double.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out lat)) + if (!double.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var lat)) throw new ArgumentException("Invalid latitude value"); - double lon; - if (!double.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out lon)) + if (!double.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var lon)) throw new ArgumentException("Invalid longitude value"); return new GeoLocation(lat, lon); } public static implicit operator GeoLocation(double[] lonLat) { - if (lonLat.Length != 2) - return null; - - return new GeoLocation(lonLat[1], lonLat[0]); + return lonLat.Length != 2 + ? null + : new GeoLocation(lonLat[1], lonLat[0]); } } @@ -141,14 +128,21 @@ public static implicit operator GeoLocation(double[] lonLat) [JsonConverter(typeof(GeoCoordinateJsonConverter))] public class GeoCoordinate : GeoLocation { - public GeoCoordinate(double latitude, double longitude) : base(latitude, longitude) - { - } + /// + /// Creates a new instance of + /// + public GeoCoordinate(double latitude, double longitude) : base(latitude, longitude) { } + /// + /// Creates a new instance of from a pair of coordinates + /// in the order Latitude then Longitude. + /// public static implicit operator GeoCoordinate(double[] coordinates) { if (coordinates == null || coordinates.Length != 2) - throw new ArgumentOutOfRangeException(nameof(coordinates), "Can not create a GeoCoordinate from an array that does not have two doubles"); + throw new ArgumentOutOfRangeException( + nameof(coordinates), + $"Can not create a {nameof(GeoCoordinate)} from an array that does not have two doubles"); return new GeoCoordinate(coordinates[0], coordinates[1]); } diff --git a/src/Nest/QueryDsl/Geo/Shape/Envelope/EnvelopeGeoShape.cs b/src/Nest/QueryDsl/Geo/Shape/Envelope/EnvelopeGeoShape.cs index a326224afac..96f56ee1c6c 100644 --- a/src/Nest/QueryDsl/Geo/Shape/Envelope/EnvelopeGeoShape.cs +++ b/src/Nest/QueryDsl/Geo/Shape/Envelope/EnvelopeGeoShape.cs @@ -13,11 +13,8 @@ public class EnvelopeGeoShape : GeoShapeBase, IEnvelopeGeoShape { public EnvelopeGeoShape() : this(null) { } - public EnvelopeGeoShape(IEnumerable coordinates) - : base("envelope") - { + public EnvelopeGeoShape(IEnumerable coordinates) : base("envelope") => this.Coordinates = coordinates; - } public IEnumerable Coordinates { get; set; } } diff --git a/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs b/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs index 83d6cceb6a1..7e291f28d66 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeoShapeBase.cs @@ -1,30 +1,144 @@ -using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Nest { + [ContractJsonConverterAttribute(typeof(GeoShapeConverter))] public interface IGeoShape { + /// + /// The type of geo shape + /// [JsonProperty("type")] string Type { get; } + /// + /// Will ignore an unmapped field and will not match any documents for this query. + /// This can be useful when querying multiple indexes which might have different mappings. + /// [JsonProperty("ignore_unmapped")] bool? IgnoreUnmapped { get; set; } } public abstract class GeoShapeBase : IGeoShape { - protected GeoShapeBase(string type) - { - this.Type = type; - } + protected GeoShapeBase(string type) => this.Type = type; + /// public string Type { get; protected set; } - /// - /// Will ignore an unmapped field and will not match any documents for this query. - /// This can be useful when querying multiple indexes which might have different mappings. - /// - [JsonProperty("ignore_unmapped")] + /// public bool? IgnoreUnmapped { get; set; } } + + internal class GeoShapeConverter : JsonConverter + { + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => + throw new NotSupportedException(); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + var shape = JObject.Load(reader); + return ReadJToken(shape, serializer); + } + + internal static object ReadJToken(JToken shape, JsonSerializer serializer) + { + var type = shape["type"]; + var typeName = type?.Value(); + switch (typeName) + { + case "circle": + var radius = shape["radius"]; + return ParseCircleGeoShape(shape, serializer, radius); + case "envelope": + return ParseEnvelopeGeoShape(shape, serializer); + case "linestring": + return ParseLineStringGeoShape(shape, serializer); + case "multilinestring": + return ParseMultiLineStringGeoShape(shape, serializer); + case "point": + return ParsePointGeoShape(shape, serializer); + case "multipoint": + return ParseMultiPointGeoShape(shape, serializer); + case "polygon": + return ParsePolygonGeoShape(shape, serializer); + case "multipolygon": + return ParseMultiPolygonGeoShape(shape, serializer); + case "geometrycollection": + return ParseGeometryCollection(shape, serializer); + default: + return null; + } + } + + public override bool CanConvert(Type objectType) => typeof(IGeoShape).IsAssignableFrom(objectType) || + typeof(IGeometryCollection).IsAssignableFrom(objectType); + + private static GeometryCollection ParseGeometryCollection(JToken shape, JsonSerializer serializer) + { + if (!(shape["geometries"] is JArray geometries)) + return new GeometryCollection { Geometries = Enumerable.Empty() }; + + var geoShapes = new List(geometries.Count); + for (var index = 0; index < geometries.Count; index++) + { + var geometry = geometries[index]; + if (ReadJToken(geometry, serializer) is IGeoShape innerShape) + geoShapes.Add(innerShape); + } + + return new GeometryCollection { Geometries = geoShapes }; + } + + private static MultiPolygonGeoShape ParseMultiPolygonGeoShape(JToken shape, JsonSerializer serializer) => + new MultiPolygonGeoShape + { + Coordinates = GetCoordinates>>>(shape, serializer) + }; + + private static PolygonGeoShape ParsePolygonGeoShape(JToken shape, JsonSerializer serializer) => + new PolygonGeoShape {Coordinates = GetCoordinates>>(shape, serializer)}; + + private static MultiPointGeoShape ParseMultiPointGeoShape(JToken shape, JsonSerializer serializer) => + new MultiPointGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; + + private static PointGeoShape ParsePointGeoShape(JToken shape, JsonSerializer serializer) => + new PointGeoShape {Coordinates = GetCoordinates(shape, serializer)}; + + private static MultiLineStringGeoShape ParseMultiLineStringGeoShape(JToken shape, JsonSerializer serializer) => + new MultiLineStringGeoShape + { + Coordinates = GetCoordinates>>(shape, serializer) + }; + + private static LineStringGeoShape ParseLineStringGeoShape(JToken shape, JsonSerializer serializer) => + new LineStringGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; + + private static EnvelopeGeoShape ParseEnvelopeGeoShape(JToken shape, JsonSerializer serializer) => + new EnvelopeGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; + + private static CircleGeoShape ParseCircleGeoShape(JToken shape, JsonSerializer serializer, JToken radius) => + new CircleGeoShape + { + Coordinates = GetCoordinates(shape, serializer), + Radius = radius?.Value() + }; + + private static T GetCoordinates(JToken shape, JsonSerializer serializer) + { + var coordinates = shape["coordinates"]; + return coordinates != null + ? coordinates.ToObject(serializer) + : default(T); + } + } } diff --git a/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs b/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs index 0cb40b796ca..2ae6714d889 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeoShapeQueryJsonConverter.cs @@ -11,7 +11,7 @@ namespace Nest /// internal class GeoShapeQueryFieldNameConverter : FieldNameQueryJsonConverter { - private static string[] SkipProperties = {"boost", "_name"}; + private static readonly string[] SkipProperties = {"boost", "_name"}; protected override bool SkipWriteProperty(string propertyName) => SkipProperties.Contains(propertyName); protected override void SerializeJson(JsonWriter writer, object value, IFieldNameQuery castValue, JsonSerializer serializer) @@ -33,20 +33,13 @@ protected override void SerializeJson(JsonWriter writer, object value, IFieldNam writer.WriteEndObject(); } } + internal class GeoShapeQueryJsonConverter : JsonConverter { public override bool CanConvert(Type objectType) => true; public override bool CanRead => true; public override bool CanWrite => false; - public virtual T GetCoordinates(JToken shape, JsonSerializer serializer) - { - var coordinates = shape["coordinates"]; - return coordinates != null - ? coordinates.ToObject(serializer) - : default(T); - } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var j = JObject.Load(reader); @@ -87,159 +80,80 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist return query; } - private IGeoShapeQuery ParseIndexedShapeQuery(JToken indexedShape) => + private static IGeoShapeQuery ParseIndexedShapeQuery(JToken indexedShape) => new GeoIndexedShapeQuery {IndexedShape = (indexedShape as JObject)?.ToObject()}; - private IGeoShapeQuery ParseShapeQuery(JToken shape, JsonSerializer serializer) + private static IGeoShapeQuery ParseShapeQuery(JToken shape, JsonSerializer serializer) { var type = shape["type"]; var typeName = type?.Value(); var ignoreUnmapped = shape["ignore_unmapped"]?.Value(); + + var geometry = GeoShapeConverter.ReadJToken(shape, serializer); + switch (typeName) { case "circle": - var radius = shape["radius"]; return new GeoShapeCircleQuery { - Shape = SetIgnoreUnmapped(ParseCircleGeoShape(shape, serializer, radius), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as ICircleGeoShape, ignoreUnmapped) }; case "envelope": return new GeoShapeEnvelopeQuery { - Shape = SetIgnoreUnmapped(ParseEnvelopeGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IEnvelopeGeoShape, ignoreUnmapped) }; case "linestring": return new GeoShapeLineStringQuery { - Shape = SetIgnoreUnmapped(ParseLineStringGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as ILineStringGeoShape, ignoreUnmapped) }; case "multilinestring": return new GeoShapeMultiLineStringQuery { - Shape = SetIgnoreUnmapped(ParseMultiLineStringGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IMultiLineStringGeoShape, ignoreUnmapped) }; case "point": return new GeoShapePointQuery { - Shape = SetIgnoreUnmapped(ParsePointGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IPointGeoShape, ignoreUnmapped) }; case "multipoint": return new GeoShapeMultiPointQuery { - Shape = SetIgnoreUnmapped(ParseMultiPointGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IMultiPointGeoShape, ignoreUnmapped) }; case "polygon": return new GeoShapePolygonQuery { - Shape = SetIgnoreUnmapped(ParsePolygonGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IPolygonGeoShape, ignoreUnmapped) }; case "multipolygon": return new GeoShapeMultiPolygonQuery { - Shape = SetIgnoreUnmapped(ParseMultiPolygonGeoShape(shape, serializer), ignoreUnmapped) + Shape = SetIgnoreUnmapped(geometry as IMultiPolygonGeoShape, ignoreUnmapped) }; case "geometrycollection": - return new GeoShapeGeometryCollectionQuery + var geometryCollection = geometry as IGeometryCollection; + if (geometryCollection != null) { - Shape = ParseGeometryCollection(shape, serializer) - }; + foreach (var innerGeometry in geometryCollection.Geometries) + SetIgnoreUnmapped(innerGeometry, ignoreUnmapped); + } + + return new GeoShapeGeometryCollectionQuery { Shape = geometryCollection }; default: return null; } } - private GeometryCollection ParseGeometryCollection(JToken shape, JsonSerializer serializer) - { - if (!(shape["geometries"] is JArray geometries)) - return new GeometryCollection { Geometries = Enumerable.Empty() }; - - var geoShapes = new List(geometries.Count); - - void AddGeoShape(TShape s, bool? ignoreUnmapped) where TShape : IGeoShape - { - s = SetIgnoreUnmapped(s, ignoreUnmapped); - geoShapes.Add(s); - } - - foreach (var geometry in geometries) - { - var ignoreUnmapped = geometry["ignore_unmapped"]?.Value(); - var type = geometry["type"]; - var typeName = type?.Value(); - switch (typeName) - { - case "circle": - var radius = geometry["radius"]; - AddGeoShape(ParseCircleGeoShape(geometry, serializer, radius), ignoreUnmapped); - break; - case "envelope": - AddGeoShape(ParseEnvelopeGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "linestring": - AddGeoShape(ParseLineStringGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "multilinestring": - AddGeoShape(ParseMultiLineStringGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "point": - AddGeoShape(ParsePointGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "multipoint": - AddGeoShape(ParseMultiPointGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "polygon": - AddGeoShape(ParsePolygonGeoShape(geometry, serializer), ignoreUnmapped); - break; - case "multipolygon": - AddGeoShape(ParseMultiPolygonGeoShape(geometry, serializer), ignoreUnmapped); - break; - default: - throw new ArgumentException($"cannot parse geo_shape. unknown type '{typeName}'"); - } - } - - return new GeometryCollection { Geometries = geoShapes }; - } - - private MultiPolygonGeoShape ParseMultiPolygonGeoShape(JToken shape, JsonSerializer serializer) => - new MultiPolygonGeoShape - { - Coordinates = GetCoordinates>>>(shape, serializer) - }; - - private PolygonGeoShape ParsePolygonGeoShape(JToken shape, JsonSerializer serializer) => - new PolygonGeoShape {Coordinates = GetCoordinates>>(shape, serializer)}; - - private MultiPointGeoShape ParseMultiPointGeoShape(JToken shape, JsonSerializer serializer) => - new MultiPointGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; - - private PointGeoShape ParsePointGeoShape(JToken shape, JsonSerializer serializer) => - new PointGeoShape {Coordinates = GetCoordinates(shape, serializer)}; - - private MultiLineStringGeoShape ParseMultiLineStringGeoShape(JToken shape, JsonSerializer serializer) => new MultiLineStringGeoShape - { - Coordinates = GetCoordinates>>(shape, serializer) - }; - - private LineStringGeoShape ParseLineStringGeoShape(JToken shape, JsonSerializer serializer) => - new LineStringGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; - - private EnvelopeGeoShape ParseEnvelopeGeoShape(JToken shape, JsonSerializer serializer) => - new EnvelopeGeoShape {Coordinates = GetCoordinates>(shape, serializer)}; - - private CircleGeoShape ParseCircleGeoShape(JToken shape, JsonSerializer serializer, JToken radius) => new CircleGeoShape - { - Coordinates = GetCoordinates(shape, serializer), - Radius = radius?.Value() - }; - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotSupportedException(); private static TShape SetIgnoreUnmapped(TShape shape, bool? ignoreUnmapped) where TShape : IGeoShape { - shape.IgnoreUnmapped = ignoreUnmapped; + if (shape != null) + shape.IgnoreUnmapped = ignoreUnmapped; return shape; } } diff --git a/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs b/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs index 2ac691d9bac..dea38c1c6ee 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs @@ -3,19 +3,32 @@ namespace Nest { + /// + /// A geo shape representing a collection of geometries + /// + [ContractJsonConverter(typeof(GeoShapeConverter))] public interface IGeometryCollection { + /// + /// The type of geo shape + /// [JsonProperty("type")] string Type { get; } + /// + /// A collection of geometries + /// [JsonProperty("geometries")] IEnumerable Geometries { get; set; } } + /// public class GeometryCollection : IGeometryCollection { + /// public string Type => "geometrycollection"; + /// public IEnumerable Geometries { get; set; } } } diff --git a/src/Serializers/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs b/src/Serializers/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs index 41e815e4b5d..bc0d402a7ea 100644 --- a/src/Serializers/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs +++ b/src/Serializers/Nest.JsonNetSerializer/Converters/HandleNestTypesOnSourceJsonConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -40,14 +41,19 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist return _builtInSerializer.Deserialize(objectType, ms); } - private static readonly Type[] NestTypesThatCanAppearInSource = { + private static readonly HashSet NestTypesThatCanAppearInSource = new HashSet + { typeof(JoinField), typeof(QueryContainer), typeof(CompletionField), typeof(Attachment), - typeof(ILazyDocument) + typeof(ILazyDocument), + typeof(GeoCoordinate) }; - public override bool CanConvert(Type objectType) => NestTypesThatCanAppearInSource.Contains(objectType); + public override bool CanConvert(Type objectType) => + NestTypesThatCanAppearInSource.Contains(objectType) || + typeof(IGeoShape).IsAssignableFrom(objectType) || + typeof(IGeometryCollection).IsAssignableFrom(objectType); } } diff --git a/src/Tests/Framework/MockData/Developer.cs b/src/Tests/Framework/MockData/Developer.cs index 82da2ce9d52..aa46309ce67 100644 --- a/src/Tests/Framework/MockData/Developer.cs +++ b/src/Tests/Framework/MockData/Developer.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using Bogus; using Nest; @@ -18,7 +19,7 @@ public class Developer : Person public new static Faker Generator { get; } = new Faker() .UseSeed(TestClient.Configuration.Seed) - .RuleFor(p => p.Id, p => IdState++) + .RuleFor(p => p.Id, p => Interlocked.Increment(ref IdState)) .RuleFor(p => p.FirstName, p => p.Name.FirstName()) .RuleFor(p => p.LastName, p => p.Name.LastName()) .RuleFor(p => p.JobTitle, p => p.Name.JobTitle()) diff --git a/src/Tests/Framework/MockData/Person.cs b/src/Tests/Framework/MockData/Person.cs index 6bbd706aa28..2c2b8cdaac7 100644 --- a/src/Tests/Framework/MockData/Person.cs +++ b/src/Tests/Framework/MockData/Person.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using Bogus; using Nest; @@ -17,7 +18,7 @@ public class Person public static Faker Generator { get; } = new Faker() - .RuleFor(p => p.Id, p => IdState++) + .RuleFor(p => p.Id, p => Interlocked.Increment(ref IdState)) .RuleFor(p => p.FirstName, p => p.Name.FirstName()) .RuleFor(p => p.LastName, p => p.Name.LastName()) .RuleFor(p => p.JobTitle, p => p.Name.JobTitle()) diff --git a/src/Tests/Framework/MockData/Shape.cs b/src/Tests/Framework/MockData/Shape.cs new file mode 100644 index 00000000000..83a1402795c --- /dev/null +++ b/src/Tests/Framework/MockData/Shape.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Bogus; +using Nest; + +namespace Tests.Framework.MockData +{ + public class Shape + { + private static int _idState = 0; + + public int Id { get; set; } + + public IGeometryCollection GeometryCollection { get; set; } + public IEnvelopeGeoShape Envelope { get; set; } + public ICircleGeoShape Circle { get; set; } + + public static Faker Generator { get; } = + new Faker() + .UseSeed(TestClient.Configuration.Seed) + .RuleFor(p => p.Id, p => Interlocked.Increment(ref _idState)) + .RuleFor(p => p.GeometryCollection, p => + new GeometryCollection + { + Geometries = new List + { + GenerateRandomPoint(p), + GenerateRandomMultiPoint(p), + GenerateLineString(p), + GenerateMultiLineString(p), + GeneratePolygon(p), + GenerateMultiPolygon(p) + } + }) + .RuleFor(p => p.Envelope, p => new EnvelopeGeoShape(new [] + { + new GeoCoordinate(0, 0), + new GeoCoordinate(45, 45) + })) + .RuleFor(p => p.Circle, p => new CircleGeoShape(GenerateGeoCoordinate(p)) + { + Radius = $"{p.Random.Int(1, 100)}km" + }) + ; + + public static IList Shapes { get; } = Shape.Generator.Clone().Generate(10); + + private static IPointGeoShape GenerateRandomPoint(Faker p) => + new PointGeoShape { Coordinates = GenerateGeoCoordinate(p) }; + + private static IMultiPointGeoShape GenerateRandomMultiPoint(Faker p) => + new MultiPointGeoShape { Coordinates = GenerateGeoCoordinates(p, p.Random.Int(1, 5)) }; + + private static ILineStringGeoShape GenerateLineString(Faker p) => + new LineStringGeoShape { Coordinates = GenerateGeoCoordinates(p, 3) }; + + private static IMultiLineStringGeoShape GenerateMultiLineString(Faker p) + { + var coordinates = new List>(); + for (var i = 0; i < p.Random.Int(1, 5); i++) + coordinates.Add(GenerateGeoCoordinates(p, 3)); + + return new MultiLineStringGeoShape {Coordinates = coordinates }; + } + + private static IPolygonGeoShape GeneratePolygon(Faker p) + { + return new PolygonGeoShape + { + Coordinates = new List> + { + GeneratePolygonCoordinates(p, GenerateGeoCoordinate(p)) + } + }; + } + + private static IMultiPolygonGeoShape GenerateMultiPolygon(Faker p) + { + return new MultiPolygonGeoShape + { + Coordinates = new List>> + { + new [] { GeneratePolygonCoordinates(p, GenerateGeoCoordinate(p)) } + } + }; + } + + private static GeoCoordinate GenerateGeoCoordinate(Faker p) => + new GeoCoordinate(p.Address.Latitude(), p.Address.Longitude()); + + private static IEnumerable GenerateGeoCoordinates(Faker p, int count) + { + var points = new List(); + + for (var i = 0; i < count; i++) + points.Add(GenerateGeoCoordinate(p)); + + return points; + } + + // adapted from https://gis.stackexchange.com/a/103465/30046 + private static IEnumerable GeneratePolygonCoordinates(Faker p, GeoCoordinate centroid, double maxDistance = 0.0002) + { + const int maxPoints = 20; + var points = new List(maxPoints); + double startingAngle = (int)(p.Random.Double() * (1d / 3) * Math.PI); + var angle = startingAngle; + for (var i = 0; i < maxPoints; i++) + { + var distance = p.Random.Double() * maxDistance; + points.Add(new GeoCoordinate(centroid.Latitude + Math.Sin(angle)*distance, centroid.Longitude + Math.Cos(angle)*distance)); + angle = angle + p.Random.Double() * (2d / 3) * Math.PI; + if (angle > 2 * Math.PI) break; + } + + // close the polygon + points.Add(points[0]); + return points; + } + } +} diff --git a/src/Tests/Framework/SerializationTests/GeoShapeSerializationTests.cs b/src/Tests/Framework/SerializationTests/GeoShapeSerializationTests.cs new file mode 100644 index 00000000000..1b2c0f69225 --- /dev/null +++ b/src/Tests/Framework/SerializationTests/GeoShapeSerializationTests.cs @@ -0,0 +1,14 @@ +using Tests.Framework.MockData; + +namespace Tests.Framework +{ + public class GeoShapeSerializationTests : SerializationTestBase + { + [U] + public void CanSerializeShapes() + { + var shape = Shape.Generator.Generate(); + this.AssertSerializesAndRoundTrips(shape); + } + } +} diff --git a/src/Tests/Framework/TestClient.cs b/src/Tests/Framework/TestClient.cs index 9e3beb46bb9..01956a9b1e1 100644 --- a/src/Tests/Framework/TestClient.cs +++ b/src/Tests/Framework/TestClient.cs @@ -96,6 +96,10 @@ private static ConnectionSettings DefaultSettings(ConnectionSettings settings) = .IndexName("server-metrics") .TypeName("metric") ) + .DefaultMappingFor(map => map + .IndexName("shapes") + .TypeName("doc") + ) .ConnectionLimit(ConnectionLimitDefault) //TODO make this random //.EnableHttpCompression() diff --git a/src/Tests/QueryDsl/Geo/GeoShapeQueryUsageTests.cs b/src/Tests/QueryDsl/Geo/GeoShapeQueryUsageTests.cs new file mode 100644 index 00000000000..d87765e66e1 --- /dev/null +++ b/src/Tests/QueryDsl/Geo/GeoShapeQueryUsageTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Elasticsearch.Net; +using FluentAssertions; +using Nest; +using Tests.Framework; +using Tests.Framework.Integration; +using Tests.Framework.ManagedElasticsearch.Clusters; + +namespace Tests.QueryDsl.Geo +{ + public class GeoShapeQueryUsageTests : + ApiIntegrationTestBase, + ISearchRequest, + SearchDescriptor, + SearchRequest> + { + public GeoShapeQueryUsageTests(IntrusiveOperationCluster cluster, EndpointUsage usage) + : base(cluster, usage) { } + + private const string Index = "shapes"; + + protected override void IntegrationSetup(IElasticClient client, CallUniqueValues values) + { + if (client.IndexExists(Index).Exists) + return; + + var createIndexResponse = client.CreateIndex(Index, c => c + .Settings(s => s + .NumberOfShards(1) + ) + .Mappings(m => m + .Map(mm => mm + .AutoMap() + .Properties(p => p + .GeoShape(g => g + .Name(n => n.GeometryCollection) + ) + .GeoShape(g => g + .Name(n => n.Envelope) + ) + .GeoShape(g => g + .Name(n => n.Circle) + ) + ) + ) + ) + ); + + if (!createIndexResponse.IsValid) + throw new Exception($"Error creating index for integration test: {createIndexResponse.DebugInformation}"); + + var bulkResponse = this.Client.Bulk(b => b + .IndexMany(Framework.MockData.Shape.Shapes) + .Refresh(Refresh.WaitFor) + ); + + if (!bulkResponse.IsValid) + throw new Exception($"Error indexing shapes for integration test: {bulkResponse.DebugInformation}"); + } + + protected override LazyResponses ClientUsage() => Calls( + fluent: (client, f) => client.Search(f), + fluentAsync: (client, f) => client.SearchAsync(f), + request: (client, r) => client.Search(r), + requestAsync: (client, r) => client.SearchAsync(r) + ); + + private readonly IEnumerable _coordinates = + Framework.MockData.Shape.Shapes.First().Envelope.Coordinates; + + protected override object ExpectJson => new + { + query = new + { + geo_shape = new + { + _name="named_query", + boost = 1.1, + envelope = new + { + relation = "intersects", + shape = new + { + type = "envelope", + ignore_unmapped = true, + coordinates = this._coordinates + } + } + } + } + }; + + protected override int ExpectStatusCode => 200; + protected override bool ExpectIsValid => true; + protected override string UrlPath => $"/shapes/doc/_search"; + protected override HttpMethod HttpMethod => HttpMethod.POST; + + protected override SearchRequest Initializer => new SearchRequest + { + Query = new GeoShapeEnvelopeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.Envelope), + Shape = new EnvelopeGeoShape(this._coordinates) { IgnoreUnmapped = true}, + Relation = GeoShapeRelation.Intersects, + } + }; + + protected override Func, ISearchRequest> Fluent => s => s + .Query(q => q + .GeoShapeEnvelope(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.Envelope) + .Coordinates(this._coordinates, ignoreUnmapped: true) + .Relation(GeoShapeRelation.Intersects) + ) + ); + + protected override void ExpectResponse(ISearchResponse response) + { + response.ShouldBeValid(); + response.Documents.Count.Should().Be(10); + } + } +} From d1eee3dc5e45db1767995079dcc59d283c2bcb14 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 26 Apr 2018 11:44:53 +1000 Subject: [PATCH 2/2] GeometryCollection implements IGeoShape This commit changes GeometryCollection to implement IGeoShape, to allow a document to have an IGeoShape member and accept a GeometryCollection. IGeoShape.Type is implemented explicitly so that the existing Type member continues to be the implicit implementation of IGeometryCollection. The ideal scenario would have been for IGeometryCollection to implement IGeoShape however this would break backwards binary compatibility, since both interfaces have a Type member. --- .../Shape/GeometryCollection/GeometryCollection.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs b/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs index dea38c1c6ee..6fd1540a80e 100644 --- a/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs +++ b/src/Nest/QueryDsl/Geo/Shape/GeometryCollection/GeometryCollection.cs @@ -22,12 +22,19 @@ public interface IGeometryCollection IEnumerable Geometries { get; set; } } - /// - public class GeometryCollection : IGeometryCollection + // TODO: IGeometryCollection should implement IGeoShape + /// + public class GeometryCollection : IGeometryCollection, IGeoShape { /// public string Type => "geometrycollection"; + /// + string IGeoShape.Type => this.Type; + + /// + public bool? IgnoreUnmapped { get; set; } + /// public IEnumerable Geometries { get; set; } }