From 75457faab439071ce092c9caa4aee26c9cfe443a Mon Sep 17 00:00:00 2001 From: Stuart Cam Date: Thu, 17 Oct 2019 19:14:57 +1100 Subject: [PATCH 1/2] Implement shape queries. --- .../geo-shape-query-usage.asciidoc | 2 +- .../shape/shape-query-usage.asciidoc | 890 ++++++++++++++++++ src/Nest/CommonOptions/Shape/ShapeRelation.cs | 18 + .../Abstractions/Container/IQueryContainer.cs | 3 + .../Container/QueryContainer-Assignments.cs | 7 + .../Container/QueryContainerDescriptor.cs | 6 + src/Nest/QueryDsl/Query.cs | 3 + .../QueryDsl/Specialized/Shape/IShapeQuery.cs | 115 +++ .../Specialized/Shape/ShapeQueryFormatter.cs | 186 ++++ .../QueryDsl/Visitor/DslPrettyPrintVisitor.cs | 16 +- src/Nest/QueryDsl/Visitor/QueryVisitor.cs | 4 + src/Nest/QueryDsl/Visitor/QueryWalker.cs | 1 + .../GeoShapeQueryUsageTests.cs | 2 +- .../GeoShapeSerializationTests.cs | 2 +- .../Geo/{Shape => GeoShape}/GeoWKTTests.cs | 2 +- .../Specialized/Shape/ShapeQueryUsageTests.cs | 718 ++++++++++++++ .../Shape/ShapeSerializationTests.cs | 251 +++++ 17 files changed, 2219 insertions(+), 7 deletions(-) rename docs/query-dsl/geo/{shape => geo-shape}/geo-shape-query-usage.asciidoc (99%) create mode 100644 docs/query-dsl/specialized/shape/shape-query-usage.asciidoc create mode 100644 src/Nest/CommonOptions/Shape/ShapeRelation.cs create mode 100644 src/Nest/QueryDsl/Specialized/Shape/IShapeQuery.cs create mode 100644 src/Nest/QueryDsl/Specialized/Shape/ShapeQueryFormatter.cs rename src/Tests/Tests/QueryDsl/Geo/{Shape => GeoShape}/GeoShapeQueryUsageTests.cs (99%) rename src/Tests/Tests/QueryDsl/Geo/{Shape => GeoShape}/GeoShapeSerializationTests.cs (99%) rename src/Tests/Tests/QueryDsl/Geo/{Shape => GeoShape}/GeoWKTTests.cs (99%) create mode 100644 src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeQueryUsageTests.cs create mode 100644 src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeSerializationTests.cs diff --git a/docs/query-dsl/geo/shape/geo-shape-query-usage.asciidoc b/docs/query-dsl/geo/geo-shape/geo-shape-query-usage.asciidoc similarity index 99% rename from docs/query-dsl/geo/shape/geo-shape-query-usage.asciidoc rename to docs/query-dsl/geo/geo-shape/geo-shape-query-usage.asciidoc index 28575146c42..757110a667d 100644 --- a/docs/query-dsl/geo/shape/geo-shape-query-usage.asciidoc +++ b/docs/query-dsl/geo/geo-shape/geo-shape-query-usage.asciidoc @@ -7,7 +7,7 @@ //// IMPORTANT NOTE ============== -This file has been generated from https://github.com/elastic/elasticsearch-net/tree/7.x/src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTests.cs. +This file has been generated from https://github.com/elastic/elasticsearch-net/tree/feature/7.x/shape-queries/src/Tests/Tests/QueryDsl/Geo/GeoShape/GeoShapeQueryUsageTests.cs. If you wish to submit a PR for any spelling mistakes, typos or grammatical errors for this file, please modify the original csharp file found at the link and submit the PR with that change. Thanks! //// diff --git a/docs/query-dsl/specialized/shape/shape-query-usage.asciidoc b/docs/query-dsl/specialized/shape/shape-query-usage.asciidoc new file mode 100644 index 00000000000..45d129ef6c4 --- /dev/null +++ b/docs/query-dsl/specialized/shape/shape-query-usage.asciidoc @@ -0,0 +1,890 @@ +:ref_current: https://www.elastic.co/guide/en/elasticsearch/reference/7.3 + +:github: https://github.com/elastic/elasticsearch-net + +:nuget: https://www.nuget.org/packages + +//// +IMPORTANT NOTE +============== +This file has been generated from https://github.com/elastic/elasticsearch-net/tree/feature/7.x/shape-queries/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeQueryUsageTests.cs. +If you wish to submit a PR for any spelling mistakes, typos or grammatical errors for this file, +please modify the original csharp file found at the link and submit the PR with that change. Thanks! +//// + +[[shape-query-usage]] +=== Shape Query Usage + +Like geo_shape, Elasticsearch supports the ability to index arbitrary two dimension (non Geospatial) geometries making +it possible to map out virtual worlds, sporting venues, theme parks, and CAD diagrams. The shape field type +supports points, lines, polygons, multi-polygons, envelope, etc. + +See the Elasticsearch documentation on {ref_current}/query-dsl-shape-query.html[shape queries] for more detail. + +[[shape-query-point]] +[float] +== Querying with Point + +==== Fluent DSL example + +[source,csharp] +---- +q +.Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .Point(PointCoordinates) + ) + .Relation(ShapeRelation.Intersects) +) +---- + +==== Object Initializer syntax example + +[source,csharp] +---- +new ShapeQuery +{ + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new PointGeoShape(PointCoordinates), + Relation = ShapeRelation.Intersects +} +---- + +[source,javascript] +.Example json output +---- +{ + "shape": { + "_name": "named_query", + "boost": 1.1, + "arbitraryShape": { + "relation": "intersects", + "shape": { + "type": "point", + "coordinates": [ + 38.897676, + -77.03653 + ] + } + } + } +} +---- + +[[shape-query-linestring]] +[float] +== Querying with LineString + +==== Fluent DSL example + +[source,csharp] +---- +q +.Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .LineString(LineStringCoordinates) + ) + .Relation(ShapeRelation.Intersects) +) +---- + +==== Object Initializer syntax example + +[source,csharp] +---- +new ShapeQuery +{ + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new LineStringGeoShape(LineStringCoordinates), + Relation = ShapeRelation.Intersects, +} +---- + +[source,javascript] +.Example json output +---- +{ + "shape": { + "_name": "named_query", + "boost": 1.1, + "arbitraryShape": { + "relation": "intersects", + "shape": { + "type": "linestring", + "coordinates": [ + [ + 38.897676, + -77.03653 + ], + [ + 38.889939, + -77.009051 + ] + ] + } + } + } +} +---- + +[[shape-query-multilinestring]] +[float] +== Querying with MultiLineString + +==== Fluent DSL example + +[source,csharp] +---- +q +.Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .MultiLineString(MultiLineStringCoordinates) + ) + .Relation(ShapeRelation.Intersects) +) +---- + +==== Object Initializer syntax example + +[source,csharp] +---- +new ShapeQuery +{ + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new MultiLineStringGeoShape(MultiLineStringCoordinates), + Relation = ShapeRelation.Intersects, +} +---- + +[source,javascript] +.Example json output +---- +{ + "shape": { + "_name": "named_query", + "boost": 1.1, + "arbitraryShape": { + "relation": "intersects", + "shape": { + "type": "multilinestring", + "coordinates": [ + [ + [ + 2.0, + 12.0 + ], + [ + 2.0, + 13.0 + ], + [ + 3.0, + 13.0 + ], + [ + 3.0, + 12.0 + ] + ], + [ + [ + 0.0, + 10.0 + ], + [ + 0.0, + 11.0 + ], + [ + 1.0, + 11.0 + ], + [ + 1.0, + 10.0 + ] + ], + [ + [ + 0.2, + 10.2 + ], + [ + 0.2, + 10.8 + ], + [ + 0.8, + 10.8 + ], + [ + 0.8, + 12.0 + ] + ] + ] + } + } + } +} +---- + +[[shape-query-polygon]] +[float] +== Querying with Polygon + +==== Fluent DSL example + +[source,csharp] +---- +q +.Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .Polygon(PolygonCoordinates) + ) + .IgnoreUnmapped() + .Relation(ShapeRelation.Intersects) +) +---- + +==== Object Initializer syntax example + +[source,csharp] +---- +new ShapeQuery +{ + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new PolygonGeoShape(PolygonCoordinates), + IgnoreUnmapped = true, + Relation = ShapeRelation.Intersects, +} +---- + +[source,javascript] +.Example json output +---- +{ + "shape": { + "_name": "named_query", + "boost": 1.1, + "ignore_unmapped": true, + "arbitraryShape": { + "relation": "intersects", + "shape": { + "type": "polygon", + "coordinates": [ + [ + [ + 10.0, + -17.0 + ], + [ + 15.0, + 16.0 + ], + [ + 0.0, + 12.0 + ], + [ + -15.0, + 16.0 + ], + [ + -10.0, + -17.0 + ], + [ + 10.0, + -17.0 + ] + ], + [ + [ + 8.2, + 18.2 + ], + [ + 8.2, + -18.8 + ], + [ + -8.8, + -10.8 + ], + [ + 8.8, + 18.2 + ] + ] + ] + } + } + } +} +---- + +[[shape-query-multipolygon]] +[float] +== Querying with MultiPolygon + +==== Fluent DSL example + +[source,csharp] +---- +q +.Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .MultiPolygon(MultiPolygonCoordinates) + ) + .Relation(ShapeRelation.Intersects) +) +---- + +==== Object Initializer syntax example + +[source,csharp] +---- +new ShapeQuery +{ + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new MultiPolygonGeoShape(MultiPolygonCoordinates), + Relation = ShapeRelation.Intersects, +} +---- + +[source,javascript] +.Example json output +---- +{ + "shape": { + "_name": "named_query", + "boost": 1.1, + "arbitraryShape": { + "relation": "intersects", + "shape": { + "type": "multipolygon", + "coordinates": [ + [ + [ + [ + 10.0, + -17.0 + ], + [ + 15.0, + 16.0 + ], + [ + 0.0, + 12.0 + ], + [ + -15.0, + 16.0 + ], + [ + -10.0, + -17.0 + ], + [ + 10.0, + -17.0 + ] + ], + [ + [ + 8.2, + 18.2 + ], + [ + 8.2, + -18.8 + ], + [ + -8.8, + -10.8 + ], + [ + 8.8, + 18.2 + ] + ] + ], + [ + [ + [ + 8.0, + -15.0 + ], + [ + 15.0, + 16.0 + ], + [ + 0.0, + 12.0 + ], + [ + -15.0, + 16.0 + ], + [ + -10.0, + -17.0 + ], + [ + 8.0, + -15.0 + ] + ] + ] + ] + } + } + } +} +---- + +[[shape-query-geometrycollection]] +[float] +== Querying with GeometryCollection + +==== Fluent DSL example + +[source,csharp] +---- +q +.Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .GeometryCollection( + new PointGeoShape(PointCoordinates), + new MultiPointGeoShape(MultiPointCoordinates), + new LineStringGeoShape(LineStringCoordinates), + new MultiLineStringGeoShape(MultiLineStringCoordinates), + new PolygonGeoShape(PolygonCoordinates), + new MultiPolygonGeoShape(MultiPolygonCoordinates) + ) + ) + .Relation(ShapeRelation.Intersects) +) +---- + +==== Object Initializer syntax example + +[source,csharp] +---- +new ShapeQuery +{ + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new GeometryCollection(new IGeoShape[] + { + new PointGeoShape(PointCoordinates), + new MultiPointGeoShape(MultiPointCoordinates), + new LineStringGeoShape(LineStringCoordinates), + new MultiLineStringGeoShape(MultiLineStringCoordinates), + new PolygonGeoShape(PolygonCoordinates), + new MultiPolygonGeoShape(MultiPolygonCoordinates), + }), + Relation = ShapeRelation.Intersects, +} +---- + +[source,javascript] +.Example json output +---- +{ + "shape": { + "_name": "named_query", + "boost": 1.1, + "arbitraryShape": { + "relation": "intersects", + "shape": { + "type": "geometrycollection", + "geometries": [ + { + "type": "point", + "coordinates": [ + 38.897676, + -77.03653 + ] + }, + { + "type": "multipoint", + "coordinates": [ + [ + 38.897676, + -77.03653 + ], + [ + 38.889939, + -77.009051 + ] + ] + }, + { + "type": "linestring", + "coordinates": [ + [ + 38.897676, + -77.03653 + ], + [ + 38.889939, + -77.009051 + ] + ] + }, + { + "type": "multilinestring", + "coordinates": [ + [ + [ + 2.0, + 12.0 + ], + [ + 2.0, + 13.0 + ], + [ + 3.0, + 13.0 + ], + [ + 3.0, + 12.0 + ] + ], + [ + [ + 0.0, + 10.0 + ], + [ + 0.0, + 11.0 + ], + [ + 1.0, + 11.0 + ], + [ + 1.0, + 10.0 + ] + ], + [ + [ + 0.2, + 10.2 + ], + [ + 0.2, + 10.8 + ], + [ + 0.8, + 10.8 + ], + [ + 0.8, + 12.0 + ] + ] + ] + }, + { + "type": "polygon", + "coordinates": [ + [ + [ + 10.0, + -17.0 + ], + [ + 15.0, + 16.0 + ], + [ + 0.0, + 12.0 + ], + [ + -15.0, + 16.0 + ], + [ + -10.0, + -17.0 + ], + [ + 10.0, + -17.0 + ] + ], + [ + [ + 8.2, + 18.2 + ], + [ + 8.2, + -18.8 + ], + [ + -8.8, + -10.8 + ], + [ + 8.8, + 18.2 + ] + ] + ] + }, + { + "type": "multipolygon", + "coordinates": [ + [ + [ + [ + 10.0, + -17.0 + ], + [ + 15.0, + 16.0 + ], + [ + 0.0, + 12.0 + ], + [ + -15.0, + 16.0 + ], + [ + -10.0, + -17.0 + ], + [ + 10.0, + -17.0 + ] + ], + [ + [ + 8.2, + 18.2 + ], + [ + 8.2, + -18.8 + ], + [ + -8.8, + -10.8 + ], + [ + 8.8, + 18.2 + ] + ] + ], + [ + [ + [ + 8.0, + -15.0 + ], + [ + 15.0, + 16.0 + ], + [ + 0.0, + 12.0 + ], + [ + -15.0, + 16.0 + ], + [ + -10.0, + -17.0 + ], + [ + 8.0, + -15.0 + ] + ] + ] + ] + } + ] + } + } + } +} +---- + +[[shape-query-envelope]] +[float] +== Querying with Envelope + +==== Fluent DSL example + +[source,csharp] +---- +q +.Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .Envelope(EnvelopeCoordinates) + ) + .Relation(ShapeRelation.Intersects) +) +---- + +==== Object Initializer syntax example + +[source,csharp] +---- +new ShapeQuery +{ + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new EnvelopeGeoShape(EnvelopeCoordinates), + Relation = ShapeRelation.Intersects, +} +---- + +[source,javascript] +.Example json output +---- +{ + "shape": { + "_name": "named_query", + "boost": 1.1, + "arbitraryShape": { + "relation": "intersects", + "shape": { + "type": "envelope", + "coordinates": [ + [ + -45.0, + 45.0 + ], + [ + 45.0, + -45.0 + ] + ] + } + } + } +} +---- + +[[shape-query-indexedshape]] +[float] +== Querying with an indexed shape + +The GeoShape Query supports using a shape which has already been indexed in another index and/or index type within a geoshape query. +This is particularly useful for when you have a pre-defined list of shapes which are useful to your application and you want to reference this +using a logical name (for example __New Zealand__), rather than having to provide their coordinates within the request each time. + +See the Elasticsearch documentation on {ref_current}/query-dsl-geo-shape-query.html[geoshape queries] for more detail. + +==== Fluent DSL example + +[source,csharp] +---- +q +.Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .IndexedShape(p => p + .Id(Project.Instance.Name) + .Path(pp => pp.ArbitraryShape) + .Routing(Project.Instance.Name) + ) + .Relation(ShapeRelation.Intersects) +) +---- + +==== Object Initializer syntax example + +[source,csharp] +---- +new ShapeQuery +{ + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + IndexedShape = new FieldLookup + { + Id = Project.Instance.Name, + Index = Infer.Index(), + Path = Infer.Field(p => p.ArbitraryShape), + Routing = Project.Instance.Name + }, + Relation = ShapeRelation.Intersects +} +---- + +[source,javascript] +.Example json output +---- +{ + "shape": { + "_name": "named_query", + "boost": 1.1, + "arbitraryShape": { + "indexed_shape": { + "id": "Durgan LLC", + "index": "project", + "path": "arbitraryShape", + "routing": "Durgan LLC" + }, + "relation": "intersects" + } + } +} +---- + diff --git a/src/Nest/CommonOptions/Shape/ShapeRelation.cs b/src/Nest/CommonOptions/Shape/ShapeRelation.cs new file mode 100644 index 00000000000..d1266343467 --- /dev/null +++ b/src/Nest/CommonOptions/Shape/ShapeRelation.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; +using Elasticsearch.Net; + +namespace Nest +{ + [StringEnum] + public enum ShapeRelation + { + [EnumMember(Value = "intersects")] + Intersects, + + [EnumMember(Value = "disjoint")] + Disjoint, + + [EnumMember(Value = "within")] + Within + } +} diff --git a/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs b/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs index 9c93207d60b..85d29e40000 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs @@ -45,6 +45,9 @@ public interface IQueryContainer [DataMember(Name ="geo_shape")] IGeoShapeQuery GeoShape { get; set; } + [DataMember(Name ="shape")] + IShapeQuery Shape { get; set; } + [DataMember(Name ="has_child")] IHasChildQuery HasChild { get; set; } diff --git a/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs b/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs index 6037a140510..bcb1d806c90 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs @@ -21,6 +21,7 @@ public partial class QueryContainer : IQueryContainer, IDescriptor private IGeoDistanceQuery _geoDistance; private IGeoPolygonQuery _geoPolygon; private IGeoShapeQuery _geoShape; + private IShapeQuery _shape; private IHasChildQuery _hasChild; private IHasParentQuery _hasParent; private IIdsQuery _ids; @@ -145,6 +146,12 @@ IGeoShapeQuery IQueryContainer.GeoShape set => _geoShape = Set(value); } + IShapeQuery IQueryContainer.Shape + { + get => _shape; + set => _shape = Set(value); + } + IHasChildQuery IQueryContainer.HasChild { get => _hasChild; diff --git a/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs b/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs index 90360748570..3211673a8fa 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs @@ -164,6 +164,12 @@ public QueryContainer MoreLikeThis(Func, IMoreLik public QueryContainer GeoShape(Func, IGeoShapeQuery> selector) => WrapInContainer(selector, (query, container) => container.GeoShape = query); + /// + /// Finds documents with shapes that either intersect, are within, or do not intersect a specified shape. + /// + public QueryContainer Shape(Func, IShapeQuery> selector) => + WrapInContainer(selector, (query, container) => container.Shape = query); + /// /// Matches documents with a geo_point type field that falls within a polygon of points /// diff --git a/src/Nest/QueryDsl/Query.cs b/src/Nest/QueryDsl/Query.cs index a83fa748fdf..2a44eb2ed39 100644 --- a/src/Nest/QueryDsl/Query.cs +++ b/src/Nest/QueryDsl/Query.cs @@ -52,6 +52,9 @@ public static QueryContainer GeoPolygon(Func, IGeoP public static QueryContainer GeoShape(Func, IGeoShapeQuery> selector) => new QueryContainerDescriptor().GeoShape(selector); + public static QueryContainer Shape(Func, IShapeQuery> selector) => + new QueryContainerDescriptor().Shape(selector); + public static QueryContainer HasChild(Func, IHasChildQuery> selector) where TChild : class => new QueryContainerDescriptor().HasChild(selector); diff --git a/src/Nest/QueryDsl/Specialized/Shape/IShapeQuery.cs b/src/Nest/QueryDsl/Specialized/Shape/IShapeQuery.cs new file mode 100644 index 00000000000..b2168a556aa --- /dev/null +++ b/src/Nest/QueryDsl/Specialized/Shape/IShapeQuery.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Elasticsearch.Net.Utf8Json; + +namespace Nest +{ + [InterfaceDataContract] + [JsonFormatter(typeof(CompositeFormatter))] + public interface IShapeQuery : IFieldNameQuery + { + /// + /// 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. + /// + [DataMember(Name ="ignore_unmapped")] + bool? IgnoreUnmapped { get; set; } + + /// + /// Indexed geo shape to search with + /// + [DataMember(Name ="indexed_shape")] + IFieldLookup IndexedShape { get; set; } + + /// + /// Controls the spatial relation operator to use at search time. + /// + [DataMember(Name ="relation")] + ShapeRelation? Relation { get; set; } + + /// + /// The geo shape to search with + /// + [DataMember(Name ="shape")] + IGeoShape Shape { get; set; } + } + + public class ShapeQuery : FieldNameQueryBase, IShapeQuery + { + /// + public bool? IgnoreUnmapped { get; set; } + + /// + public IFieldLookup IndexedShape { get; set; } + + /// + public ShapeRelation? Relation { get; set; } + + /// + public IGeoShape Shape { get; set; } + + protected override bool Conditionless => IsConditionless(this); + + internal static bool IsConditionless(IShapeQuery q) + { + if (q.Field.IsConditionless()) + return true; + + switch (q.Shape) + { + case ICircleGeoShape circleGeoShape: + return circleGeoShape.Coordinates == null || string.IsNullOrEmpty(circleGeoShape?.Radius); + case IEnvelopeGeoShape envelopeGeoShape: + return envelopeGeoShape.Coordinates == null; + case IGeometryCollection geometryCollection: + return geometryCollection.Geometries == null; + case ILineStringGeoShape lineStringGeoShape: + return lineStringGeoShape.Coordinates == null; + case IMultiLineStringGeoShape multiLineStringGeoShape: + return multiLineStringGeoShape.Coordinates == null; + case IMultiPointGeoShape multiPointGeoShape: + return multiPointGeoShape.Coordinates == null; + case IMultiPolygonGeoShape multiPolygonGeoShape: + return multiPolygonGeoShape.Coordinates == null; + case IPointGeoShape pointGeoShape: + return pointGeoShape.Coordinates == null; + case IPolygonGeoShape polygonGeoShape: + return polygonGeoShape.Coordinates == null; + case null: + return q.IndexedShape.IsConditionless(); + default: + return true; + } + } + + internal override void InternalWrapInContainer(IQueryContainer container) => container.Shape = this; + } + + public class ShapeQueryDescriptor + : FieldNameQueryDescriptorBase, IShapeQuery, T>, IShapeQuery + where T : class + { + protected override bool Conditionless => ShapeQuery.IsConditionless(this); + bool? IShapeQuery.IgnoreUnmapped { get; set; } + IFieldLookup IShapeQuery.IndexedShape { get; set; } + ShapeRelation? IShapeQuery.Relation { get; set; } + IGeoShape IShapeQuery.Shape { get; set; } + + /// + public ShapeQueryDescriptor Relation(ShapeRelation? relation) => + Assign(relation, (a, v) => a.Relation = v); + + /// + public ShapeQueryDescriptor IgnoreUnmapped(bool? ignoreUnmapped = true) => + Assign(ignoreUnmapped, (a, v) => a.IgnoreUnmapped = v); + + /// + public ShapeQueryDescriptor Shape(Func selector) => + Assign(selector, (a, v) => a.Shape = v?.Invoke(new GeoShapeDescriptor())); + + /// + public ShapeQueryDescriptor IndexedShape(Func, IFieldLookup> selector) => + Assign(selector, (a, v) => a.IndexedShape = v?.Invoke(new FieldLookupDescriptor())); + } +} diff --git a/src/Nest/QueryDsl/Specialized/Shape/ShapeQueryFormatter.cs b/src/Nest/QueryDsl/Specialized/Shape/ShapeQueryFormatter.cs new file mode 100644 index 00000000000..00b9318221b --- /dev/null +++ b/src/Nest/QueryDsl/Specialized/Shape/ShapeQueryFormatter.cs @@ -0,0 +1,186 @@ +using System; +using Elasticsearch.Net.Utf8Json; +using Elasticsearch.Net.Utf8Json.Internal; + +namespace Nest +{ + internal class ShapeQueryFieldNameFormatter : IJsonFormatter + { + public IShapeQuery Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) => + throw new NotSupportedException(); + + public void Serialize(ref JsonWriter writer, IShapeQuery value, IJsonFormatterResolver formatterResolver) + { + var fieldName = value.Field; + if (fieldName == null) + { + writer.WriteNull(); + return; + } + + var settings = formatterResolver.GetConnectionSettings(); + var field = settings.Inferrer.Field(fieldName); + + if (field.IsNullOrEmpty()) + { + writer.WriteNull(); + return; + } + + writer.WriteBeginObject(); + var name = value.Name; + var boost = value.Boost; + var ignoreUnmapped = value.IgnoreUnmapped; + + if (!name.IsNullOrEmpty()) + { + writer.WritePropertyName("_name"); + writer.WriteString(name); + writer.WriteValueSeparator(); + } + if (boost != null) + { + writer.WritePropertyName("boost"); + writer.WriteDouble(boost.Value); + writer.WriteValueSeparator(); + } + if (ignoreUnmapped != null) + { + writer.WritePropertyName("ignore_unmapped"); + writer.WriteBoolean(ignoreUnmapped.Value); + writer.WriteValueSeparator(); + } + + writer.WritePropertyName(field); + + writer.WriteBeginObject(); + + var written = false; + + if (value.Shape != null) + { + writer.WritePropertyName("shape"); + var shapeFormatter = formatterResolver.GetFormatter(); + shapeFormatter.Serialize(ref writer, value.Shape, formatterResolver); + written = true; + } + else if (value.IndexedShape != null) + { + writer.WritePropertyName("indexed_shape"); + var fieldLookupFormatter = formatterResolver.GetFormatter(); + fieldLookupFormatter.Serialize(ref writer, value.IndexedShape, formatterResolver); + written = true; + } + + if (value.Relation.HasValue) + { + if (written) + writer.WriteValueSeparator(); + + writer.WritePropertyName("relation"); + formatterResolver.GetFormatter() + .Serialize(ref writer, value.Relation.Value, formatterResolver); + } + + writer.WriteEndObject(); + writer.WriteEndObject(); + } + } + + internal class ShapeQueryFormatter : IJsonFormatter + { + private static readonly AutomataDictionary Fields = new AutomataDictionary + { + { "boost", 0 }, + { "_name", 1 }, + { "ignore_unmapped", 2 } + }; + + private static readonly AutomataDictionary ShapeFields = new AutomataDictionary + { + { "shape", 0 }, + { "indexed_shape", 1 }, + { "relation", 2 } + }; + + public IShapeQuery Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) + { + if (reader.ReadIsNull()) + return null; + + var count = 0; + string field = null; + double? boost = null; + string name = null; + bool? ignoreUnmapped = null; + IShapeQuery query = null; + ShapeRelation? relation = null; + + while (reader.ReadIsInObject(ref count)) + { + var propertyName = reader.ReadPropertyNameSegmentRaw(); + if (Fields.TryGetValue(propertyName, out var value)) + { + switch (value) + { + case 0: + boost = reader.ReadDouble(); + break; + case 1: + name = reader.ReadString(); + break; + case 2: + ignoreUnmapped = reader.ReadBoolean(); + break; + } + } + else + { + field = propertyName.Utf8String(); + var shapeCount = 0; + while (reader.ReadIsInObject(ref shapeCount)) + { + var shapeProperty = reader.ReadPropertyNameSegmentRaw(); + if (ShapeFields.TryGetValue(shapeProperty, out var shapeValue)) + { + switch (shapeValue) + { + case 0: + var shapeFormatter = formatterResolver.GetFormatter(); + query = new ShapeQuery + { + Shape = shapeFormatter.Deserialize(ref reader, formatterResolver) + }; + break; + case 1: + var fieldLookupFormatter = formatterResolver.GetFormatter(); + query = new ShapeQuery + { + IndexedShape = fieldLookupFormatter.Deserialize(ref reader, formatterResolver) + }; + break; + case 2: + relation = formatterResolver.GetFormatter() + .Deserialize(ref reader, formatterResolver); + break; + } + } + } + } + } + + if (query == null) + return null; + + query.Boost = boost; + query.Name = name; + query.Field = field; + query.Relation = relation; + query.IgnoreUnmapped = ignoreUnmapped; + return query; + } + + public void Serialize(ref JsonWriter writer, IShapeQuery value, IJsonFormatterResolver formatterResolver) => + throw new NotSupportedException(); + } +} diff --git a/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs b/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs index 16b113f21db..26bbb0dad45 100644 --- a/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs +++ b/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs @@ -82,11 +82,21 @@ public virtual void Visit(IQuery query) { } public virtual void Visit(IFuzzyStringQuery query) => Write("fuzzy_string", query.Field); public virtual void Visit(IGeoShapeQuery query) + { + WriteShape(query.Shape, query.IndexedShape, query.Field, "geo_shape"); + } + + public virtual void Visit(IShapeQuery query) + { + WriteShape(query.Shape, query.IndexedShape, query.Field, "shape"); + } + + private void WriteShape(IGeoShape shape, IFieldLookup indexedField, Field field, string queryType) { // ReSharper disable UnusedVariable - switch (query.Shape) + switch (shape) { - case null when query.IndexedShape != null: + case null when indexedField != null: Write("geo_indexed_shape"); break; case ICircleGeoShape circleGeoShape: @@ -118,7 +128,7 @@ public virtual void Visit(IGeoShapeQuery query) break; // ReSharper restore UnusedVariable default: - Write("geo_shape", query.Field); + Write(queryType, field); break; } } diff --git a/src/Nest/QueryDsl/Visitor/QueryVisitor.cs b/src/Nest/QueryDsl/Visitor/QueryVisitor.cs index 673efcd7c3d..57f407162b2 100644 --- a/src/Nest/QueryDsl/Visitor/QueryVisitor.cs +++ b/src/Nest/QueryDsl/Visitor/QueryVisitor.cs @@ -134,6 +134,8 @@ public interface IQueryVisitor void Visit(IGeoShapeQuery query); + void Visit(IShapeQuery query); + void Visit(IRawQuery query); void Visit(IPercolateQuery query); @@ -191,6 +193,8 @@ public virtual void Visit(IFuzzyDateQuery query) { } public virtual void Visit(IGeoShapeQuery query) { } + public virtual void Visit(IShapeQuery query) { } + public virtual void Visit(IHasChildQuery query) { } public virtual void Visit(IHasParentQuery query) { } diff --git a/src/Nest/QueryDsl/Visitor/QueryWalker.cs b/src/Nest/QueryDsl/Visitor/QueryWalker.cs index 4632b245531..03f609c94c2 100644 --- a/src/Nest/QueryDsl/Visitor/QueryWalker.cs +++ b/src/Nest/QueryDsl/Visitor/QueryWalker.cs @@ -30,6 +30,7 @@ public void Walk(IQueryContainer qd, IQueryVisitor visitor) VisitQuery(d as ITermRangeQuery, visitor, (vv, dd) => v.Visit(dd)); }); VisitQuery(qd.GeoShape, visitor, (v, d) => v.Visit(d)); + VisitQuery(qd.Shape, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.Ids, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.Intervals, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.Prefix, visitor, (v, d) => v.Visit(d)); diff --git a/src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Geo/GeoShape/GeoShapeQueryUsageTests.cs similarity index 99% rename from src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTests.cs rename to src/Tests/Tests/QueryDsl/Geo/GeoShape/GeoShapeQueryUsageTests.cs index 92194b229f8..3665d48ba3a 100644 --- a/src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/GeoShape/GeoShapeQueryUsageTests.cs @@ -5,7 +5,7 @@ using Tests.Domain; using Tests.Framework.EndpointTests.TestState; -namespace Tests.QueryDsl.Geo.Shape +namespace Tests.QueryDsl.Geo.GeoShape { /** * The GeoShape Query uses the same grid square representation as the geo_shape mapping diff --git a/src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeSerializationTests.cs b/src/Tests/Tests/QueryDsl/Geo/GeoShape/GeoShapeSerializationTests.cs similarity index 99% rename from src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeSerializationTests.cs rename to src/Tests/Tests/QueryDsl/Geo/GeoShape/GeoShapeSerializationTests.cs index 21fcd1c62e1..e035c588f7c 100644 --- a/src/Tests/Tests/QueryDsl/Geo/Shape/GeoShapeSerializationTests.cs +++ b/src/Tests/Tests/QueryDsl/Geo/GeoShape/GeoShapeSerializationTests.cs @@ -12,7 +12,7 @@ using Tests.Framework.EndpointTests; using Tests.Framework.EndpointTests.TestState; -namespace Tests.QueryDsl.Geo.Shape +namespace Tests.QueryDsl.Geo.GeoShape { public abstract class GeoShapeSerializationTestsBase : ApiIntegrationTestBase EnvelopeCoordinates = new GeoCoordinate[] + { + new[] { 45.0, -45.0, }, + new[] { -45.0, 45.0 } + }; + + protected static readonly IEnumerable LineStringCoordinates = new GeoCoordinate[] + { + new[] { -77.03653, 38.897676 }, + new[] { -77.009051, 38.889939 } + }; + + protected static readonly IEnumerable> MultiLineStringCoordinates = new[] + { + new GeoCoordinate[] { new[] { 12.0, 2.0 }, new[] { 13.0, 2.0 }, new[] { 13.0, 3.0 }, new[] { 12.0, 3.0 } }, + new GeoCoordinate[] { new[] { 10.0, 0.0 }, new[] { 11.0, 0.0 }, new[] { 11.0, 1.0 }, new[] { 10.0, 1.0 } }, + new GeoCoordinate[] { new[] { 10.2, 0.2 }, new[] { 10.8, 0.2 }, new[] { 10.8, 0.8 }, new[] { 12.0, 0.8 } }, + }; + + protected static readonly IEnumerable MultiPointCoordinates = new GeoCoordinate[] + { + new[] { -77.03653, 38.897676 }, + new[] { -77.009051, 38.889939 } + }; + + protected static readonly IEnumerable>> MultiPolygonCoordinates = new[] + { + new[] + { + new GeoCoordinate[] + { + new[] { -17.0, 10.0 }, + new[] { 16.0, 15.0 }, + new[] { 12.0, 0.0 }, + new[] { 16.0, -15.0 }, + new[] { -17.0, -10.0 }, + new[] { -17.0, 10.0 } + }, + new GeoCoordinate[] + { + new[] { 18.2, 8.2 }, + new[] { -18.8, 8.2 }, + new[] { -10.8, -8.8 }, + new[] { 18.2, 8.2 } + } + }, + new[] + { + new GeoCoordinate[] + { + new[] { -15.0, 8.0 }, + new[] { 16.0, 15.0 }, + new[] { 12.0, 0.0 }, + new[] { 16.0, -15.0 }, + new[] { -17.0, -10.0 }, + new[] { -15.0, 8.0 } + } + } + }; + + protected static readonly GeoCoordinate PointCoordinates = new[] { -77.03653, 38.897676 }; + + protected static readonly IEnumerable> PolygonCoordinates = new[] + { + new GeoCoordinate[] + { + new[] { -17.0, 10.0 }, new[] { 16.0, 15.0 }, new[] { 12.0, 0.0 }, new[] { 16.0, -15.0 }, new[] { -17.0, -10.0 }, new[] { -17.0, 10.0 } + }, + new GeoCoordinate[] + { + new[] { 18.2, 8.2 }, new[] { -18.8, 8.2 }, new[] { -10.8, -8.8 }, new[] { 18.2, 8.2 } + } + }; + + + protected ShapeQueryUsageTestsBase(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + } + + /** + * [float] + * [[shape-query-point]] + * == Querying with Point + * + */ + public class ShapePointQueryUsageTests : ShapeQueryUsageTestsBase + { + public ShapePointQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.Shape = null, + q => ((IPointGeoShape)q.Shape).Coordinates = null, + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new PointGeoShape(PointCoordinates), + Relation = ShapeRelation.Intersects + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + arbitraryShape = new + { + relation = "intersects", + shape = new + { + type = "point", + coordinates = PointCoordinates + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .Point(PointCoordinates) + ) + .Relation(ShapeRelation.Intersects) + ); + } + + // hide + [SkipVersion(">=7.0.0", "multipoint queries are not supported")] + public class ShapeMultiPointQueryUsageTests : ShapeQueryUsageTestsBase + { + public ShapeMultiPointQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.Shape = null, + q => ((IMultiPointGeoShape)q.Shape).Coordinates = null, + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new MultiPointGeoShape(MultiPointCoordinates), + Relation = ShapeRelation.Intersects, + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + arbitraryShape = new + { + relation = "intersects", + shape = new + { + type = "multipoint", + coordinates = MultiPointCoordinates + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .MultiPoint(MultiPointCoordinates) + ) + .Relation(ShapeRelation.Intersects) + ); + } + + /** + * [float] + * [[shape-query-linestring]] + * == Querying with LineString + * + */ + public class ShapeLineStringQueryUsageTests : ShapeQueryUsageTestsBase + { + public ShapeLineStringQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.Shape = null, + q => ((ILineStringGeoShape)q.Shape).Coordinates = null, + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new LineStringGeoShape(LineStringCoordinates), + Relation = ShapeRelation.Intersects, + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + arbitraryShape = new + { + relation = "intersects", + shape = new + { + type = "linestring", + coordinates = LineStringCoordinates + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .LineString(LineStringCoordinates) + ) + .Relation(ShapeRelation.Intersects) + ); + } + + /** + * [float] + * [[shape-query-multilinestring]] + * == Querying with MultiLineString + * + */ + public class ShapeMultiLineStringQueryUsageTests : ShapeQueryUsageTestsBase + { + public ShapeMultiLineStringQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.Shape = null, + q => ((IMultiLineStringGeoShape)q.Shape).Coordinates = null, + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new MultiLineStringGeoShape(MultiLineStringCoordinates), + Relation = ShapeRelation.Intersects, + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + arbitraryShape = new + { + relation = "intersects", + shape = new + { + type = "multilinestring", + coordinates = MultiLineStringCoordinates + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .MultiLineString(MultiLineStringCoordinates) + ) + .Relation(ShapeRelation.Intersects) + ); + } + + /** + * [float] + * [[shape-query-polygon]] + * == Querying with Polygon + * + */ + public class ShapePolygonQueryUsageTests : ShapeQueryUsageTestsBase + { + public ShapePolygonQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.Shape = null, + q => ((IPolygonGeoShape)q.Shape).Coordinates = null + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new PolygonGeoShape(PolygonCoordinates), + IgnoreUnmapped = true, + Relation = ShapeRelation.Intersects, + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + ignore_unmapped = true, + arbitraryShape = new + { + relation = "intersects", + shape = new + { + type = "polygon", + coordinates = PolygonCoordinates + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .Polygon(PolygonCoordinates) + ) + .IgnoreUnmapped() + .Relation(ShapeRelation.Intersects) + ); + } + + /** + * [float] + * [[shape-query-multipolygon]] + * == Querying with MultiPolygon + * + */ + public class ShapeMultiPolygonQueryUsageTests : ShapeQueryUsageTestsBase + { + public ShapeMultiPolygonQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.Shape = null, + q => ((IMultiPolygonGeoShape)q.Shape).Coordinates = null + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new MultiPolygonGeoShape(MultiPolygonCoordinates), + Relation = ShapeRelation.Intersects, + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + arbitraryShape = new + { + relation = "intersects", + shape = new + { + type = "multipolygon", + coordinates = MultiPolygonCoordinates + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .MultiPolygon(MultiPolygonCoordinates) + ) + .Relation(ShapeRelation.Intersects) + ); + } + + /** + * [float] + * [[shape-query-geometrycollection]] + * == Querying with GeometryCollection + * + */ + public class ShapeGeometryCollectionQueryUsageTests : ShapeQueryUsageTestsBase + { + public ShapeGeometryCollectionQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.Shape = null, + q => ((IGeometryCollection)q.Shape).Geometries = null, + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new GeometryCollection(new IGeoShape[] + { + new PointGeoShape(PointCoordinates), + new MultiPointGeoShape(MultiPointCoordinates), + new LineStringGeoShape(LineStringCoordinates), + new MultiLineStringGeoShape(MultiLineStringCoordinates), + new PolygonGeoShape(PolygonCoordinates), + new MultiPolygonGeoShape(MultiPolygonCoordinates), + }), + Relation = ShapeRelation.Intersects, + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + arbitraryShape = new + { + relation = "intersects", + shape = new + { + type = "geometrycollection", + geometries = new object[] + { + new + { + type = "point", + coordinates = PointCoordinates + }, + new + { + type = "multipoint", + coordinates = MultiPointCoordinates + }, + new + { + type = "linestring", + coordinates = LineStringCoordinates + }, + new + { + type = "multilinestring", + coordinates = MultiLineStringCoordinates + }, + new + { + type = "polygon", + coordinates = PolygonCoordinates + }, + new + { + type = "multipolygon", + coordinates = MultiPolygonCoordinates + } + } + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .GeometryCollection( + new PointGeoShape(PointCoordinates), + new MultiPointGeoShape(MultiPointCoordinates), + new LineStringGeoShape(LineStringCoordinates), + new MultiLineStringGeoShape(MultiLineStringCoordinates), + new PolygonGeoShape(PolygonCoordinates), + new MultiPolygonGeoShape(MultiPolygonCoordinates) + ) + ) + .Relation(ShapeRelation.Intersects) + ); + } + + /** + * [float] + * [[shape-query-envelope]] + * == Querying with Envelope + * + */ + public class ShapeEnvelopeQueryUsageTests : ShapeQueryUsageTestsBase + { + public ShapeEnvelopeQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.Shape = null, + q => ((IEnvelopeGeoShape)q.Shape).Coordinates = null, + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new EnvelopeGeoShape(EnvelopeCoordinates), + Relation = ShapeRelation.Intersects, + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + arbitraryShape = new + { + relation = "intersects", + shape = new + { + type = "envelope", + coordinates = EnvelopeCoordinates + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .Envelope(EnvelopeCoordinates) + ) + .Relation(ShapeRelation.Intersects) + ); + } + + // hide + [SkipVersion(">=7.0.0", "CIRCLE geometry is not supported. See https://github.com/elastic/elasticsearch/issues/39237")] + public class ShapeCircleQueryUsageTests : ShapeQueryUsageTestsBase + { + public ShapeCircleQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.Shape = null, + q => ((ICircleGeoShape)q.Shape).Coordinates = null, + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + Shape = new CircleGeoShape(CircleCoordinates, "100m"), + Relation = ShapeRelation.Intersects, + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + arbitraryShape = new + { + relation = "intersects", + shape = new + { + type = "circle", + radius = "100m", + coordinates = CircleCoordinates + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .Shape(s => s + .Circle(CircleCoordinates, "100m") + ) + .Relation(ShapeRelation.Intersects) + ); + } + + /** + * [float] + * [[shape-query-indexedshape]] + * == Querying with an indexed shape + * + * The GeoShape Query supports using a shape which has already been indexed in another index and/or index type within a geoshape query. + * This is particularly useful for when you have a pre-defined list of shapes which are useful to your application and you want to reference this + * using a logical name (for example __New Zealand__), rather than having to provide their coordinates within the request each time. + * + * See the Elasticsearch documentation on {ref_current}/query-dsl-geo-shape-query.html[geoshape queries] for more detail. + */ + public class ShapeIndexedShapeQueryUsageTests : QueryDslUsageTestsBase + { + public ShapeIndexedShapeQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Shape) + { + q => q.Field = null, + q => q.IndexedShape = null, + q => q.IndexedShape.Id = null, + q => q.IndexedShape.Index = null, + q => q.IndexedShape.Path = null, + }; + + protected override QueryContainer QueryInitializer => new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.ArbitraryShape), + IndexedShape = new FieldLookup + { + Id = Project.Instance.Name, + Index = Infer.Index(), + Path = Infer.Field(p => p.ArbitraryShape), + Routing = Project.Instance.Name + }, + Relation = ShapeRelation.Intersects + }; + + protected override object QueryJson => new + { + shape = new + { + _name = "named_query", + boost = 1.1, + arbitraryShape = new + { + indexed_shape = new + { + id = Project.Instance.Name, + index = "project", + path = "arbitraryShape", + routing = Project.Instance.Name + }, + relation = "intersects" + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.ArbitraryShape) + .IndexedShape(p => p + .Id(Project.Instance.Name) + .Path(pp => pp.ArbitraryShape) + .Routing(Project.Instance.Name) + ) + .Relation(ShapeRelation.Intersects) + ); + } +} diff --git a/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeSerializationTests.cs b/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeSerializationTests.cs new file mode 100644 index 00000000000..93f4916d863 --- /dev/null +++ b/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeSerializationTests.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Elastic.Xunit.XunitPlumbing; +using Elasticsearch.Net; +using FluentAssertions; +using Nest; +using Newtonsoft.Json.Linq; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Framework.EndpointTests; +using Tests.Framework.EndpointTests.TestState; + +namespace Tests.QueryDsl.Geo.Shape +{ + public abstract class ShapeSerializationTestsBase + : ApiIntegrationTestBase, + ISearchRequest, + SearchDescriptor, + SearchRequest> + { + private readonly IEnumerable _coordinates = + Domain.Shape.Shapes.First().Envelope.Coordinates; + + protected ShapeSerializationTestsBase(IntrusiveOperationCluster cluster, EndpointUsage usage) + : base(cluster, usage) { } + + protected override bool ExpectIsValid => true; + + protected override object ExpectJson => new + { + query = new + { + shape = new + { + _name = "named_query", + boost = 1.1, + ignore_unmapped = true, + envelope = new + { + relation = "intersects", + shape = new + { + type = "envelope", + coordinates = _coordinates + } + } + } + } + }; + + protected override int ExpectStatusCode => 200; + + protected override Func, ISearchRequest> Fluent => s => s + .Index(Index) + .Query(q => q + .Shape(c => c + .Name("named_query") + .Boost(1.1) + .Field(p => p.Envelope) + .Shape(sh => sh + .Envelope(_coordinates) + ) + .Relation(ShapeRelation.Intersects) + .IgnoreUnmapped() + ) + ); + + protected override HttpMethod HttpMethod => HttpMethod.POST; + + protected abstract string Index { get; } + + protected override SearchRequest Initializer => new SearchRequest(Index) + { + Query = new ShapeQuery + { + Name = "named_query", + Boost = 1.1, + Field = Infer.Field(p => p.Envelope), + Shape = new EnvelopeGeoShape(_coordinates), + Relation = ShapeRelation.Intersects, + IgnoreUnmapped = true, + } + }; + + protected override string UrlPath => $"/{Index}/_search"; + + protected override LazyResponses ClientUsage() => Calls( + (client, f) => client.Search(f), + (client, f) => client.SearchAsync(f), + (client, r) => client.Search(r), + (client, r) => client.SearchAsync(r) + ); + + protected override void ExpectResponse(ISearchResponse response) + { + response.IsValid.Should().BeTrue(); + response.Documents.Count.Should().Be(10); + } + } + + public class ShapeSerializationTests : ShapeSerializationTestsBase + { + public ShapeSerializationTests(IntrusiveOperationCluster cluster, EndpointUsage usage) + : base(cluster, usage) { } + + protected override string Index => "geoshapes"; + + protected override void IntegrationSetup(IElasticClient client, CallUniqueValues values) + { + if (client.Indices.Exists(Index).Exists) + return; + + var createIndexResponse = client.Indices.Create(Index, c => c + .Settings(s => s + .NumberOfShards(1) + .NumberOfReplicas(0) + ) + .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) + .Strategy(GeoStrategy.Recursive) + ) + ) + ) + ); + + if (!createIndexResponse.IsValid) + throw new Exception($"Error creating index for integration test: {createIndexResponse.DebugInformation}"); + + var bulkResponse = Client.Bulk(b => b + .Index(Index) + .IndexMany(Domain.Shape.Shapes) + .Refresh(Refresh.WaitFor) + ); + + if (!bulkResponse.IsValid) + throw new Exception($"Error indexing shapes for integration test: {bulkResponse.DebugInformation}"); + } + } + + [SkipVersion("<6.2.0", "Support for WKT in Elasticsearch 6.2.0+")] + public class ShapeGeoWKTSerializationTests : ShapeSerializationTestsBase + { + public ShapeGeoWKTSerializationTests(IntrusiveOperationCluster cluster, EndpointUsage usage) + : base(cluster, usage) { } + + protected override string Index => "wkt-geoshapes"; + + protected override void IntegrationSetup(IElasticClient client, CallUniqueValues values) + { + if (client.Indices.Exists(Index).Exists) + return; + + var createIndexResponse = client.Indices.Create(Index, c => c + .Settings(s => s + .NumberOfShards(1) + .NumberOfReplicas(0) + ) + .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) + .Strategy(GeoStrategy.Recursive) + ) + ) + ) + ); + + if (!createIndexResponse.IsValid) + throw new Exception($"Error creating index for integration test: {createIndexResponse.DebugInformation}"); + + var bulk = new List(); + + foreach (var shape in Domain.Shape.Shapes) + { + bulk.Add(new { index = new { _index = Index, _id = shape.Id } }); + bulk.Add(new + { + id = shape.Id, + geometryCollection = GeoWKTWriter.Write(shape.GeometryCollection), + envelope = GeoWKTWriter.Write(shape.Envelope), + circle = shape.Circle + }); + } + + var bulkResponse = Client.LowLevel.Bulk( + PostData.MultiJson(bulk), + new BulkRequestParameters { Refresh = Refresh.WaitFor } + ); + + if (!bulkResponse.IsValid) + throw new Exception($"Error indexing shapes for integration test: {bulkResponse.DebugInformation}"); + } + + protected override void ExpectResponse(ISearchResponse response) + { + base.ExpectResponse(response); + + // index shapes again + var bulkResponse = Client.Bulk(b => b + .Index(Index) + .IndexMany(response.Documents) + .Refresh(Refresh.WaitFor) + .RequestConfiguration(r => r + .DisableDirectStreaming() + ) + ); + + bulkResponse.IsValid.Should().BeTrue(); + + // ensure they were indexed as WKT + var request = Encoding.UTF8.GetString(bulkResponse.ApiCall.RequestBodyInBytes); + using (var reader = new StringReader(request)) + { + string line; + var i = 0; + while ((line = reader.ReadLine()) != null) + { + i++; + if (i % 2 != 0) + continue; + + var jObject = JObject.Parse(line); + var jValue = (JValue)jObject["geometryCollection"]; + jValue.Value.Should().BeOfType(); + jValue = (JValue)jObject["envelope"]; + jValue.Value.Should().BeOfType(); + jObject["circle"].Should().BeOfType(); + } + } + } + } +} From 7939e3c0ea83518b95e10e14c75fbf7b404a30e7 Mon Sep 17 00:00:00 2001 From: Stuart Cam Date: Fri, 18 Oct 2019 14:31:10 +1100 Subject: [PATCH 2/2] Address PR comments; Skipversions and docs check --- .../Specialized/Shape/ShapeQueryUsageTests.cs | 20 +++++++++++++------ .../Shape/ShapeSerializationTests.cs | 3 ++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeQueryUsageTests.cs index 96fb07ad6e8..3bdebc05d85 100644 --- a/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeQueryUsageTests.cs +++ b/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeQueryUsageTests.cs @@ -102,6 +102,7 @@ protected ShapeQueryUsageTestsBase(ReadOnlyCluster i, EndpointUsage usage) : bas * == Querying with Point * */ + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapePointQueryUsageTests : ShapeQueryUsageTestsBase { public ShapePointQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } @@ -153,7 +154,7 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor } // hide - [SkipVersion(">=7.0.0", "multipoint queries are not supported")] + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeMultiPointQueryUsageTests : ShapeQueryUsageTestsBase { public ShapeMultiPointQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } @@ -210,6 +211,7 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor * == Querying with LineString * */ + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeLineStringQueryUsageTests : ShapeQueryUsageTestsBase { public ShapeLineStringQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } @@ -266,6 +268,7 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor * == Querying with MultiLineString * */ + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeMultiLineStringQueryUsageTests : ShapeQueryUsageTestsBase { public ShapeMultiLineStringQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } @@ -322,6 +325,7 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor * == Querying with Polygon * */ + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapePolygonQueryUsageTests : ShapeQueryUsageTestsBase { public ShapePolygonQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } @@ -381,6 +385,7 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor * == Querying with MultiPolygon * */ + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeMultiPolygonQueryUsageTests : ShapeQueryUsageTestsBase { public ShapeMultiPolygonQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } @@ -437,6 +442,7 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor * == Querying with GeometryCollection * */ + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeGeometryCollectionQueryUsageTests : ShapeQueryUsageTestsBase { public ShapeGeometryCollectionQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } @@ -540,6 +546,7 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor * == Querying with Envelope * */ + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeEnvelopeQueryUsageTests : ShapeQueryUsageTestsBase { public ShapeEnvelopeQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } @@ -591,7 +598,7 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor } // hide - [SkipVersion(">=7.0.0", "CIRCLE geometry is not supported. See https://github.com/elastic/elasticsearch/issues/39237")] + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeCircleQueryUsageTests : ShapeQueryUsageTestsBase { public ShapeCircleQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } @@ -648,12 +655,13 @@ protected override QueryContainer QueryFluent(QueryContainerDescriptor * [[shape-query-indexedshape]] * == Querying with an indexed shape * - * The GeoShape Query supports using a shape which has already been indexed in another index and/or index type within a geoshape query. - * This is particularly useful for when you have a pre-defined list of shapes which are useful to your application and you want to reference this - * using a logical name (for example __New Zealand__), rather than having to provide their coordinates within the request each time. + * The Query also supports using a shape which has already been indexed in another index. This is particularly useful for when you have + * a pre-defined list of shapes which are useful to your application and you want to reference this using a logical name (for example New Zealand) + * rather than having to provide their coordinates each time. In this situation it is only necessary to provide: * - * See the Elasticsearch documentation on {ref_current}/query-dsl-geo-shape-query.html[geoshape queries] for more detail. + * See the Elasticsearch documentation on {ref_current}/query-dsl-shape-query.html for more detail. */ + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeIndexedShapeQueryUsageTests : QueryDslUsageTestsBase { public ShapeIndexedShapeQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } diff --git a/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeSerializationTests.cs b/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeSerializationTests.cs index 93f4916d863..8ea69b6fc60 100644 --- a/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeSerializationTests.cs +++ b/src/Tests/Tests/QueryDsl/Specialized/Shape/ShapeSerializationTests.cs @@ -101,6 +101,7 @@ protected override void ExpectResponse(ISearchResponse response) } } + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeSerializationTests : ShapeSerializationTestsBase { public ShapeSerializationTests(IntrusiveOperationCluster cluster, EndpointUsage usage) @@ -149,7 +150,7 @@ protected override void IntegrationSetup(IElasticClient client, CallUniqueValues } } - [SkipVersion("<6.2.0", "Support for WKT in Elasticsearch 6.2.0+")] + [SkipVersion("<7.4.0", "Shape queries introduced in 7.4.0+")] public class ShapeGeoWKTSerializationTests : ShapeSerializationTestsBase { public ShapeGeoWKTSerializationTests(IntrusiveOperationCluster cluster, EndpointUsage usage)