This commit adds a new point field that is able to index arbitrary pair of values (x/y) in the cartesian space. It only supports filtering using shape queries at the moment.
This commit is contained in:
parent
4d36917e52
commit
076c199484
|
@ -0,0 +1,99 @@
|
||||||
|
[[point]]
|
||||||
|
[role="xpack"]
|
||||||
|
[testenv="basic"]
|
||||||
|
=== Point datatype
|
||||||
|
++++
|
||||||
|
<titleabbrev>Point</titleabbrev>
|
||||||
|
++++
|
||||||
|
|
||||||
|
The `point` datatype facilitates the indexing of and searching
|
||||||
|
arbitrary `x, y` pairs that fall in a 2-dimensional planar
|
||||||
|
coordinate system.
|
||||||
|
|
||||||
|
You can query documents using this type using
|
||||||
|
<<query-dsl-shape-query,shape Query>>.
|
||||||
|
|
||||||
|
There are four ways that a point may be specified, as demonstrated below:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
--------------------------------------------------
|
||||||
|
PUT my_index
|
||||||
|
{
|
||||||
|
"mappings": {
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"type": "point"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PUT my_index/_doc/1
|
||||||
|
{
|
||||||
|
"text": "Point as an object",
|
||||||
|
"location": { <1>
|
||||||
|
"x": 41.12,
|
||||||
|
"y": -71.34
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PUT my_index/_doc/2
|
||||||
|
{
|
||||||
|
"text": "Point as a string",
|
||||||
|
"location": "41.12,-71.34" <2>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PUT my_index/_doc/4
|
||||||
|
{
|
||||||
|
"text": "Point as an array",
|
||||||
|
"location": [41.12, -71.34] <3>
|
||||||
|
}
|
||||||
|
|
||||||
|
PUT my_index/_doc/5
|
||||||
|
{
|
||||||
|
"text": "Point as a WKT POINT primitive",
|
||||||
|
"location" : "POINT (41.12 -71.34)" <4>
|
||||||
|
}
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
<1> Point expressed as an object, with `x` and `y` keys.
|
||||||
|
<2> Point expressed as a string with the format: `"x,y"`.
|
||||||
|
<4> Point expressed as an array with the format: [ `x`, `y`]
|
||||||
|
<5> Point expressed as a http://docs.opengeospatial.org/is/12-063r5/12-063r5.html[Well-Known Text]
|
||||||
|
POINT with the format: `"POINT(x y)"`
|
||||||
|
|
||||||
|
The coordinates provided to the indexer are single precision floating point values so
|
||||||
|
the field guarantees the same accuracy provided by the java virtual machine (typically
|
||||||
|
`1E-38`).
|
||||||
|
|
||||||
|
[[geo-point-params]]
|
||||||
|
==== Parameters for `geo_point` fields
|
||||||
|
|
||||||
|
The following parameters are accepted by `point` fields:
|
||||||
|
|
||||||
|
[horizontal]
|
||||||
|
|
||||||
|
<<ignore-malformed,`ignore_malformed`>>::
|
||||||
|
|
||||||
|
If `true`, malformed points are ignored. If `false` (default),
|
||||||
|
malformed points throw an exception and reject the whole document.
|
||||||
|
|
||||||
|
`ignore_z_value`::
|
||||||
|
|
||||||
|
If `true` (default) three dimension points will be accepted (stored in source)
|
||||||
|
but only x and y values will be indexed; the third dimension is
|
||||||
|
ignored. If `false`, points containing any more than x and y
|
||||||
|
(two dimensions) values throw an exception and reject the whole document.
|
||||||
|
|
||||||
|
<<null-value,`null_value`>>::
|
||||||
|
|
||||||
|
Accepts an point value which is substituted for any explicit `null` values.
|
||||||
|
Defaults to `null`, which means the field is treated as missing.
|
||||||
|
|
||||||
|
==== Sorting and Retrieving index Shapes
|
||||||
|
|
||||||
|
It is currently not possible to sort shapes or retrieve their fields
|
||||||
|
directly. The `point` value is only retrievable through the `_source`
|
||||||
|
field.
|
|
@ -3,16 +3,21 @@
|
||||||
[testenv="basic"]
|
[testenv="basic"]
|
||||||
== Shape queries
|
== Shape queries
|
||||||
|
|
||||||
|
|
||||||
Like <<geo-shape,`geo_shape`>> Elasticsearch supports the ability to index
|
Like <<geo-shape,`geo_shape`>> Elasticsearch supports the ability to index
|
||||||
arbitrary two dimension (non Geospatial) geometries making it possible to
|
arbitrary two dimension (non Geospatial) geometries making it possible to
|
||||||
map out virtual worlds, sporting venues, theme parks, and CAD diagrams. The
|
map out virtual worlds, sporting venues, theme parks, and CAD diagrams.
|
||||||
<<shape,`shape`>> field type supports points, lines, polygons, multi-polygons,
|
|
||||||
envelope, etc.
|
Elasticsearch supports two types of cartesian data:
|
||||||
|
<<point,`point`>> fields which support x/y pairs, and
|
||||||
|
<<shape,`shape`>> fields, which support points, lines, circles, polygons, multi-polygons, etc.
|
||||||
|
|
||||||
The queries in this group are:
|
The queries in this group are:
|
||||||
|
|
||||||
<<query-dsl-shape-query,`shape`>> query::
|
<<query-dsl-shape-query,`shape`>> query::
|
||||||
Finds documents with shapes that either intersect, are within, or do not
|
Finds documents with:
|
||||||
intersect a specified shape.
|
* `shapes` which either intersect, are contained by, are within or do not intersect
|
||||||
|
with the specified shape
|
||||||
|
* `points` which intersect the specified shape
|
||||||
|
|
||||||
include::shape-query.asciidoc[]
|
include::shape-query.asciidoc[]
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.elasticsearch.plugins.MapperPlugin;
|
||||||
import org.elasticsearch.plugins.Plugin;
|
import org.elasticsearch.plugins.Plugin;
|
||||||
import org.elasticsearch.xpack.core.XPackPlugin;
|
import org.elasticsearch.xpack.core.XPackPlugin;
|
||||||
import org.elasticsearch.plugins.SearchPlugin;
|
import org.elasticsearch.plugins.SearchPlugin;
|
||||||
|
import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper;
|
||||||
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
|
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
|
||||||
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
|
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
|
||||||
import org.elasticsearch.xpack.spatial.ingest.CircleProcessor;
|
import org.elasticsearch.xpack.spatial.ingest.CircleProcessor;
|
||||||
|
@ -41,6 +42,7 @@ public class SpatialPlugin extends Plugin implements MapperPlugin, SearchPlugin,
|
||||||
public Map<String, Mapper.TypeParser> getMappers() {
|
public Map<String, Mapper.TypeParser> getMappers() {
|
||||||
Map<String, Mapper.TypeParser> mappers = new LinkedHashMap<>();
|
Map<String, Mapper.TypeParser> mappers = new LinkedHashMap<>();
|
||||||
mappers.put(ShapeFieldMapper.CONTENT_TYPE, new ShapeFieldMapper.TypeParser());
|
mappers.put(ShapeFieldMapper.CONTENT_TYPE, new ShapeFieldMapper.TypeParser());
|
||||||
|
mappers.put(PointFieldMapper.CONTENT_TYPE, new PointFieldMapper.TypeParser());
|
||||||
return Collections.unmodifiableMap(mappers);
|
return Collections.unmodifiableMap(mappers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,293 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.elasticsearch.xpack.spatial.index.mapper;
|
||||||
|
|
||||||
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
|
import org.elasticsearch.common.ParseField;
|
||||||
|
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
|
||||||
|
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
||||||
|
import org.elasticsearch.common.xcontent.ToXContentFragment;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentSubParser;
|
||||||
|
import org.elasticsearch.common.xcontent.support.MapXContentParser;
|
||||||
|
import org.elasticsearch.geometry.Geometry;
|
||||||
|
import org.elasticsearch.geometry.ShapeType;
|
||||||
|
import org.elasticsearch.geometry.utils.StandardValidator;
|
||||||
|
import org.elasticsearch.geometry.utils.WellKnownText;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a point in the cartesian space.
|
||||||
|
*/
|
||||||
|
public final class CartesianPoint implements ToXContentFragment {
|
||||||
|
|
||||||
|
private static final ParseField X_FIELD = new ParseField("x");
|
||||||
|
private static final ParseField Y_FIELD = new ParseField("y");
|
||||||
|
private static final ParseField Z_FIELD = new ParseField("z");
|
||||||
|
|
||||||
|
private float x;
|
||||||
|
private float y;
|
||||||
|
|
||||||
|
public CartesianPoint() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public CartesianPoint(float x, float y) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CartesianPoint reset(float x, float y) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CartesianPoint resetFromString(String value, final boolean ignoreZValue) {
|
||||||
|
if (value.toLowerCase(Locale.ROOT).contains("point")) {
|
||||||
|
return resetFromWKT(value, ignoreZValue);
|
||||||
|
} else {
|
||||||
|
return resetFromCoordinates(value, ignoreZValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public CartesianPoint resetFromCoordinates(String value, final boolean ignoreZValue) {
|
||||||
|
String[] vals = value.split(",");
|
||||||
|
if (vals.length > 3 || vals.length < 2) {
|
||||||
|
throw new ElasticsearchParseException("failed to parse [{}], expected 2 or 3 coordinates "
|
||||||
|
+ "but found: [{}]", vals, vals.length);
|
||||||
|
}
|
||||||
|
final float x;
|
||||||
|
final float y;
|
||||||
|
try {
|
||||||
|
x = Float.parseFloat(vals[0].trim());
|
||||||
|
if (Float.isFinite(x) == false) {
|
||||||
|
throw new ElasticsearchParseException("invalid [{}] value [{}]; " +
|
||||||
|
"must be between -3.4028234663852886E38 and 3.4028234663852886E38",
|
||||||
|
X_FIELD.getPreferredName(),
|
||||||
|
x);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
throw new ElasticsearchParseException("[{}]] must be a number", X_FIELD.getPreferredName());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
y = Float.parseFloat(vals[1].trim());
|
||||||
|
if (Float.isFinite(y) == false) {
|
||||||
|
throw new ElasticsearchParseException("invalid [{}] value [{}]; " +
|
||||||
|
"must be between -3.4028234663852886E38 and 3.4028234663852886E38",
|
||||||
|
Y_FIELD.getPreferredName(),
|
||||||
|
y);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
throw new ElasticsearchParseException("[{}]] must be a number", Y_FIELD.getPreferredName());
|
||||||
|
}
|
||||||
|
if (vals.length > 2) {
|
||||||
|
try {
|
||||||
|
CartesianPoint.assertZValue(ignoreZValue, Float.parseFloat(vals[2].trim()));
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
throw new ElasticsearchParseException("[{}]] must be a number", Y_FIELD.getPreferredName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reset(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CartesianPoint resetFromWKT(String value, boolean ignoreZValue) {
|
||||||
|
Geometry geometry;
|
||||||
|
try {
|
||||||
|
geometry = new WellKnownText(false, new StandardValidator(ignoreZValue))
|
||||||
|
.fromWKT(value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ElasticsearchParseException("Invalid WKT format", e);
|
||||||
|
}
|
||||||
|
if (geometry.type() != ShapeType.POINT) {
|
||||||
|
throw new ElasticsearchParseException("[{}] supports only POINT among WKT primitives, " +
|
||||||
|
"but found {}", PointFieldMapper.CONTENT_TYPE, geometry.type());
|
||||||
|
}
|
||||||
|
org.elasticsearch.geometry.Point point = (org.elasticsearch.geometry.Point) geometry;
|
||||||
|
return reset((float) point.getX(), (float) point.getY());
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getX() {
|
||||||
|
return this.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getY() {
|
||||||
|
return this.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
CartesianPoint point = (CartesianPoint) o;
|
||||||
|
|
||||||
|
if (Float.compare(point.x, x) != 0) return false;
|
||||||
|
if (Float.compare(point.y, y) != 0) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result;
|
||||||
|
int temp;
|
||||||
|
temp = x != +0.0f ? Float.floatToIntBits(x) : 0;
|
||||||
|
result = Integer.hashCode(temp);
|
||||||
|
temp = y != +0.0f ? Float.floatToIntBits(y) : 0;
|
||||||
|
result = 31 * result + Integer.hashCode(temp);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return x + ", " + y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
return builder.startObject().field(X_FIELD.getPreferredName(), x).field(Y_FIELD.getPreferredName(), y).endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CartesianPoint parsePoint(XContentParser parser, CartesianPoint point, boolean ignoreZvalue)
|
||||||
|
throws IOException, ElasticsearchParseException {
|
||||||
|
float x = Float.NaN;
|
||||||
|
float y = Float.NaN;
|
||||||
|
NumberFormatException numberFormatException = null;
|
||||||
|
|
||||||
|
if(parser.currentToken() == XContentParser.Token.START_OBJECT) {
|
||||||
|
try (XContentSubParser subParser = new XContentSubParser(parser)) {
|
||||||
|
while (subParser.nextToken() != XContentParser.Token.END_OBJECT) {
|
||||||
|
if (subParser.currentToken() == XContentParser.Token.FIELD_NAME) {
|
||||||
|
String field = subParser.currentName();
|
||||||
|
if (field.equals(X_FIELD.getPreferredName())) {
|
||||||
|
subParser.nextToken();
|
||||||
|
switch (subParser.currentToken()) {
|
||||||
|
case VALUE_NUMBER:
|
||||||
|
case VALUE_STRING:
|
||||||
|
try {
|
||||||
|
x = subParser.floatValue(true);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
numberFormatException = e;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ElasticsearchParseException("[{}] must be a number",
|
||||||
|
X_FIELD.getPreferredName());
|
||||||
|
}
|
||||||
|
} else if (field.equals(Y_FIELD.getPreferredName())) {
|
||||||
|
subParser.nextToken();
|
||||||
|
switch (subParser.currentToken()) {
|
||||||
|
case VALUE_NUMBER:
|
||||||
|
case VALUE_STRING:
|
||||||
|
try {
|
||||||
|
y = subParser.floatValue(true);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
numberFormatException = e;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ElasticsearchParseException("[{}] must be a number",
|
||||||
|
Y_FIELD.getPreferredName());
|
||||||
|
}
|
||||||
|
} else if (field.equals(Z_FIELD.getPreferredName())) {
|
||||||
|
subParser.nextToken();
|
||||||
|
switch (subParser.currentToken()) {
|
||||||
|
case VALUE_NUMBER:
|
||||||
|
case VALUE_STRING:
|
||||||
|
try {
|
||||||
|
CartesianPoint.assertZValue(ignoreZvalue, subParser.floatValue(true));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
numberFormatException = e;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ElasticsearchParseException("[{}] must be a number",
|
||||||
|
Z_FIELD.getPreferredName());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("field must be either [{}] or [{}]",
|
||||||
|
X_FIELD.getPreferredName(),
|
||||||
|
Y_FIELD.getPreferredName());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("token [{}] not allowed", subParser.currentToken());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (numberFormatException != null) {
|
||||||
|
throw new ElasticsearchParseException("[{}] and [{}] must be valid float values", numberFormatException,
|
||||||
|
X_FIELD.getPreferredName(),
|
||||||
|
Y_FIELD.getPreferredName());
|
||||||
|
} else if (Float.isNaN(x)) {
|
||||||
|
throw new ElasticsearchParseException("field [{}] missing", X_FIELD.getPreferredName());
|
||||||
|
} else if (Float.isNaN(y)) {
|
||||||
|
throw new ElasticsearchParseException("field [{}] missing", Y_FIELD.getPreferredName());
|
||||||
|
} else {
|
||||||
|
return point.reset(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if(parser.currentToken() == XContentParser.Token.START_ARRAY) {
|
||||||
|
try (XContentSubParser subParser = new XContentSubParser(parser)) {
|
||||||
|
int element = 0;
|
||||||
|
while (subParser.nextToken() != XContentParser.Token.END_ARRAY) {
|
||||||
|
if (subParser.currentToken() == XContentParser.Token.VALUE_NUMBER) {
|
||||||
|
element++;
|
||||||
|
if (element == 1) {
|
||||||
|
x = subParser.floatValue();
|
||||||
|
} else if (element == 2) {
|
||||||
|
y = subParser.floatValue();
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("[{}}] field type does not accept > 2 dimensions",
|
||||||
|
PointFieldMapper.CONTENT_TYPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("numeric value expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return point.reset(x, y);
|
||||||
|
} else if(parser.currentToken() == XContentParser.Token.VALUE_STRING) {
|
||||||
|
String val = parser.text();
|
||||||
|
return point.resetFromString(val, ignoreZvalue);
|
||||||
|
} else {
|
||||||
|
throw new ElasticsearchParseException("point expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CartesianPoint parsePoint(Object value, boolean ignoreZValue) throws ElasticsearchParseException {
|
||||||
|
try (XContentParser parser = new MapXContentParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE,
|
||||||
|
Collections.singletonMap("null_value", value), null)) {
|
||||||
|
parser.nextToken(); // start object
|
||||||
|
parser.nextToken(); // field name
|
||||||
|
parser.nextToken(); // field value
|
||||||
|
return parsePoint(parser, new CartesianPoint(), ignoreZValue);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new ElasticsearchParseException("error parsing point", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double assertZValue(final boolean ignoreZValue, float zValue) {
|
||||||
|
if (ignoreZValue == false) {
|
||||||
|
throw new ElasticsearchParseException("Exception parsing coordinates: found Z value [{}] but [{}] "
|
||||||
|
+ "parameter is [{}]", zValue, IGNORE_Z_VALUE, ignoreZValue);
|
||||||
|
}
|
||||||
|
if (Float.isFinite(zValue) == false) {
|
||||||
|
throw new ElasticsearchParseException("invalid [{}] value [{}]; " +
|
||||||
|
"must be between -3.4028234663852886E38 and 3.4028234663852886E38",
|
||||||
|
Z_FIELD.getPreferredName(),
|
||||||
|
zValue);
|
||||||
|
}
|
||||||
|
return zValue;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,352 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.spatial.index.mapper;
|
||||||
|
|
||||||
|
import org.apache.lucene.document.StoredField;
|
||||||
|
import org.apache.lucene.document.XYDocValuesField;
|
||||||
|
import org.apache.lucene.document.XYPointField;
|
||||||
|
import org.apache.lucene.index.IndexOptions;
|
||||||
|
import org.apache.lucene.index.IndexableField;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.apache.lucene.search.DocValuesFieldExistsQuery;
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.apache.lucene.search.TermQuery;
|
||||||
|
import org.elasticsearch.ElasticsearchParseException;
|
||||||
|
import org.elasticsearch.common.Explicit;
|
||||||
|
import org.elasticsearch.common.ParseField;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.common.xcontent.support.XContentMapValues;
|
||||||
|
import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType;
|
||||||
|
import org.elasticsearch.index.mapper.ArrayValueMapperParser;
|
||||||
|
import org.elasticsearch.index.mapper.FieldMapper;
|
||||||
|
import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
|
||||||
|
import org.elasticsearch.index.mapper.GeoPointFieldMapper;
|
||||||
|
import org.elasticsearch.index.mapper.MappedFieldType;
|
||||||
|
import org.elasticsearch.index.mapper.Mapper;
|
||||||
|
import org.elasticsearch.index.mapper.MapperParsingException;
|
||||||
|
import org.elasticsearch.index.mapper.ParseContext;
|
||||||
|
import org.elasticsearch.index.query.QueryShardContext;
|
||||||
|
import org.elasticsearch.index.query.QueryShardException;
|
||||||
|
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryPointProcessor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.elasticsearch.index.mapper.TypeParsers.parseField;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field Mapper for point type.
|
||||||
|
*
|
||||||
|
* Uses lucene 8 XYPoint encoding
|
||||||
|
*/
|
||||||
|
public class PointFieldMapper extends FieldMapper implements ArrayValueMapperParser {
|
||||||
|
public static final String CONTENT_TYPE = "point";
|
||||||
|
|
||||||
|
public static class Names {
|
||||||
|
public static final ParseField IGNORE_MALFORMED = new ParseField("ignore_malformed");
|
||||||
|
public static final ParseField IGNORE_Z_VALUE = new ParseField("ignore_z_value");
|
||||||
|
public static final ParseField NULL_VALUE = new ParseField("null_value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Defaults {
|
||||||
|
public static final Explicit<Boolean> IGNORE_MALFORMED = new Explicit<>(false, false);
|
||||||
|
public static final PointFieldType FIELD_TYPE = new PointFieldType();
|
||||||
|
public static final Explicit<Boolean> IGNORE_Z_VALUE = new Explicit<>(true, false);
|
||||||
|
|
||||||
|
static {
|
||||||
|
FIELD_TYPE.setTokenized(false);
|
||||||
|
FIELD_TYPE.setHasDocValues(true);
|
||||||
|
FIELD_TYPE.setDimensions(2, Integer.BYTES);
|
||||||
|
FIELD_TYPE.freeze();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder extends FieldMapper.Builder<Builder, PointFieldMapper> {
|
||||||
|
protected Boolean ignoreMalformed;
|
||||||
|
private Boolean ignoreZValue;
|
||||||
|
|
||||||
|
public Builder(String name) {
|
||||||
|
super(name, Defaults.FIELD_TYPE, Defaults.FIELD_TYPE);
|
||||||
|
builder = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder ignoreMalformed(boolean ignoreMalformed) {
|
||||||
|
this.ignoreMalformed = ignoreMalformed;
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Explicit<Boolean> ignoreMalformed(BuilderContext context) {
|
||||||
|
if (ignoreMalformed != null) {
|
||||||
|
return new Explicit<>(ignoreMalformed, true);
|
||||||
|
}
|
||||||
|
if (context.indexSettings() != null) {
|
||||||
|
return new Explicit<>(IGNORE_MALFORMED_SETTING.get(context.indexSettings()), false);
|
||||||
|
}
|
||||||
|
return PointFieldMapper.Defaults.IGNORE_MALFORMED;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Explicit<Boolean> ignoreZValue(BuilderContext context) {
|
||||||
|
if (ignoreZValue != null) {
|
||||||
|
return new Explicit<>(ignoreZValue, true);
|
||||||
|
}
|
||||||
|
return PointFieldMapper.Defaults.IGNORE_Z_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PointFieldMapper.Builder ignoreZValue(final boolean ignoreZValue) {
|
||||||
|
this.ignoreZValue = ignoreZValue;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public PointFieldMapper build(BuilderContext context, String simpleName, MappedFieldType fieldType,
|
||||||
|
MappedFieldType defaultFieldType, Settings indexSettings,
|
||||||
|
MultiFields multiFields, Explicit<Boolean> ignoreMalformed,
|
||||||
|
CopyTo copyTo) {
|
||||||
|
setupFieldType(context);
|
||||||
|
return new PointFieldMapper(simpleName, fieldType, defaultFieldType, indexSettings, multiFields,
|
||||||
|
ignoreMalformed, ignoreZValue(context), copyTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PointFieldType fieldType() {
|
||||||
|
return (PointFieldType)fieldType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PointFieldMapper build(BuilderContext context) {
|
||||||
|
return build(context, name, fieldType, defaultFieldType, context.indexSettings(),
|
||||||
|
multiFieldsBuilder.build(this, context), ignoreMalformed(context), copyTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupFieldType(BuilderContext context) {
|
||||||
|
super.setupFieldType(context);
|
||||||
|
|
||||||
|
fieldType().setGeometryQueryBuilder(new ShapeQueryPointProcessor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TypeParser implements Mapper.TypeParser {
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext)
|
||||||
|
throws MapperParsingException {
|
||||||
|
Builder builder = new PointFieldMapper.Builder(name);
|
||||||
|
parseField(builder, name, node, parserContext);
|
||||||
|
Object nullValue = null;
|
||||||
|
for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
|
||||||
|
Map.Entry<String, Object> entry = iterator.next();
|
||||||
|
String propName = entry.getKey();
|
||||||
|
Object propNode = entry.getValue();
|
||||||
|
|
||||||
|
if (propName.equals(Names.IGNORE_MALFORMED.getPreferredName())) {
|
||||||
|
builder.ignoreMalformed(XContentMapValues.nodeBooleanValue(propNode, name + "." + Names.IGNORE_MALFORMED));
|
||||||
|
iterator.remove();
|
||||||
|
} else if (propName.equals(PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName())) {
|
||||||
|
builder.ignoreZValue(XContentMapValues.nodeBooleanValue(propNode,
|
||||||
|
name + "." + PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName()));
|
||||||
|
iterator.remove();
|
||||||
|
} else if (propName.equals(Names.NULL_VALUE.getPreferredName())) {
|
||||||
|
if (propNode == null) {
|
||||||
|
throw new MapperParsingException("Property [null_value] cannot be null.");
|
||||||
|
}
|
||||||
|
nullValue = propNode;
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nullValue != null) {
|
||||||
|
boolean ignoreMalformed = builder.ignoreMalformed == null ?
|
||||||
|
Defaults.IGNORE_MALFORMED.value() : builder.ignoreMalformed;
|
||||||
|
boolean ignoreZValue = builder.ignoreZValue == null ?
|
||||||
|
Defaults.IGNORE_Z_VALUE.value() : builder.ignoreZValue;
|
||||||
|
CartesianPoint point = CartesianPoint.parsePoint(nullValue, ignoreZValue);
|
||||||
|
if (ignoreMalformed == false) {
|
||||||
|
if (Float.isFinite(point.getX()) == false) {
|
||||||
|
throw new IllegalArgumentException("illegal x value [" + point.getX() + "]");
|
||||||
|
}
|
||||||
|
if (Float.isFinite(point.getY()) == false) {
|
||||||
|
throw new IllegalArgumentException("illegal y value [" + point.getY() + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.nullValue(point);
|
||||||
|
}
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Explicit<Boolean> ignoreMalformed;
|
||||||
|
protected Explicit<Boolean> ignoreZValue;
|
||||||
|
|
||||||
|
public PointFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType,
|
||||||
|
Settings indexSettings, MultiFields multiFields, Explicit<Boolean> ignoreMalformed,
|
||||||
|
Explicit<Boolean> ignoreZValue, CopyTo copyTo) {
|
||||||
|
super(simpleName, fieldType, defaultFieldType, indexSettings, multiFields, copyTo);
|
||||||
|
this.ignoreMalformed = ignoreMalformed;
|
||||||
|
this.ignoreZValue = ignoreZValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doMerge(Mapper mergeWith) {
|
||||||
|
super.doMerge(mergeWith);
|
||||||
|
PointFieldMapper gpfmMergeWith = (PointFieldMapper) mergeWith;
|
||||||
|
if (gpfmMergeWith.ignoreMalformed.explicit()) {
|
||||||
|
this.ignoreMalformed = gpfmMergeWith.ignoreMalformed;
|
||||||
|
}
|
||||||
|
if (gpfmMergeWith.ignoreZValue.explicit()) {
|
||||||
|
this.ignoreZValue = gpfmMergeWith.ignoreZValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String contentType() {
|
||||||
|
return CONTENT_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void parseCreateField(ParseContext context, List<IndexableField> fields) throws IOException {
|
||||||
|
throw new UnsupportedOperationException("Parsing is implemented in parse(), this method should NEVER be called");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PointFieldType extends AbstractSearchableGeometryFieldType {
|
||||||
|
public PointFieldType() {
|
||||||
|
}
|
||||||
|
|
||||||
|
PointFieldType(PointFieldType ref) {
|
||||||
|
super(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String typeName() {
|
||||||
|
return CONTENT_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MappedFieldType clone() {
|
||||||
|
return new PointFieldType(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query existsQuery(QueryShardContext context) {
|
||||||
|
if (hasDocValues()) {
|
||||||
|
return new DocValuesFieldExistsQuery(name());
|
||||||
|
} else {
|
||||||
|
return new TermQuery(new Term(FieldNamesFieldMapper.NAME, name()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query termQuery(Object value, QueryShardContext context) {
|
||||||
|
throw new QueryShardException(context, "Spatial fields do not support exact searching, " +
|
||||||
|
"use dedicated spatial queries instead: [" + name() + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void parse(ParseContext context, CartesianPoint point) throws IOException {
|
||||||
|
|
||||||
|
if (fieldType().indexOptions() != IndexOptions.NONE) {
|
||||||
|
context.doc().add(new XYPointField(fieldType().name(), point.getX(), point.getY()));
|
||||||
|
}
|
||||||
|
if (fieldType().stored()) {
|
||||||
|
context.doc().add(new StoredField(fieldType().name(), point.toString()));
|
||||||
|
}
|
||||||
|
if (fieldType.hasDocValues()) {
|
||||||
|
context.doc().add(new XYDocValuesField(fieldType().name(), point.getX(), point.getY()));
|
||||||
|
} else if (fieldType().stored() || fieldType().indexOptions() != IndexOptions.NONE) {
|
||||||
|
List<IndexableField> fields = new ArrayList<>(1);
|
||||||
|
createFieldNamesField(context, fields);
|
||||||
|
for (IndexableField field : fields) {
|
||||||
|
context.doc().add(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if the mapping contains multi-fields then throw an error?
|
||||||
|
if (multiFields.iterator().hasNext()) {
|
||||||
|
throw new ElasticsearchParseException("[{}] field type does not accept multi-fields", CONTENT_TYPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parse(ParseContext context) throws IOException {
|
||||||
|
context.path().add(simpleName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
CartesianPoint sparse = context.parseExternalValue(CartesianPoint.class);
|
||||||
|
|
||||||
|
if (sparse != null) {
|
||||||
|
parse(context, sparse);
|
||||||
|
} else {
|
||||||
|
sparse = new CartesianPoint();
|
||||||
|
XContentParser.Token token = context.parser().currentToken();
|
||||||
|
if (token == XContentParser.Token.START_ARRAY) {
|
||||||
|
token = context.parser().nextToken();
|
||||||
|
if (token == XContentParser.Token.VALUE_NUMBER) {
|
||||||
|
float x = context.parser().floatValue();
|
||||||
|
context.parser().nextToken();
|
||||||
|
float y = context.parser().floatValue();
|
||||||
|
token = context.parser().nextToken();
|
||||||
|
if (token == XContentParser.Token.VALUE_NUMBER) {
|
||||||
|
CartesianPoint.assertZValue(ignoreZValue.value(), context.parser().floatValue());
|
||||||
|
} else if (token != XContentParser.Token.END_ARRAY) {
|
||||||
|
throw new ElasticsearchParseException("[{}] field type does not accept > 3 dimensions", CONTENT_TYPE);
|
||||||
|
}
|
||||||
|
parse(context, sparse.reset(x, y));
|
||||||
|
} else {
|
||||||
|
while (token != XContentParser.Token.END_ARRAY) {
|
||||||
|
parsePointIgnoringMalformed(context, sparse);
|
||||||
|
token = context.parser().nextToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (token == XContentParser.Token.VALUE_NULL) {
|
||||||
|
if (fieldType.nullValue() != null) {
|
||||||
|
parse(context, (CartesianPoint) fieldType.nullValue());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsePointIgnoringMalformed(context, sparse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new MapperParsingException("failed to parse field [{}] of type [{}]", ex, fieldType().name(), fieldType().typeName());
|
||||||
|
}
|
||||||
|
|
||||||
|
context.path().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses point represented as an object or an array, ignores malformed points if needed
|
||||||
|
*/
|
||||||
|
private void parsePointIgnoringMalformed(ParseContext context, CartesianPoint sparse) throws IOException {
|
||||||
|
try {
|
||||||
|
parse(context, CartesianPoint.parsePoint(context.parser(), sparse, ignoreZValue().value()));
|
||||||
|
} catch (ElasticsearchParseException e) {
|
||||||
|
if (ignoreMalformed.value() == false) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
context.addIgnoredField(fieldType.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
|
||||||
|
super.doXContentBody(builder, includeDefaults, params);
|
||||||
|
if (includeDefaults || ignoreMalformed.explicit()) {
|
||||||
|
builder.field(Names.IGNORE_MALFORMED.getPreferredName(), ignoreMalformed.value());
|
||||||
|
}
|
||||||
|
if (includeDefaults || ignoreZValue.explicit()) {
|
||||||
|
builder.field(GeoPointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue.value());
|
||||||
|
}
|
||||||
|
if (includeDefaults || fieldType().nullValue() != null) {
|
||||||
|
builder.field(Names.NULL_VALUE.getPreferredName(), fieldType().nullValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Explicit<Boolean> ignoreZValue() {
|
||||||
|
return ignoreZValue;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,13 +17,14 @@ import org.elasticsearch.common.logging.DeprecationLogger;
|
||||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
import org.elasticsearch.common.xcontent.XContentParser;
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
import org.elasticsearch.geometry.Geometry;
|
import org.elasticsearch.geometry.Geometry;
|
||||||
import org.elasticsearch.index.mapper.AbstractGeometryFieldMapper;
|
import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType;
|
||||||
import org.elasticsearch.index.mapper.MappedFieldType;
|
import org.elasticsearch.index.mapper.MappedFieldType;
|
||||||
import org.elasticsearch.index.query.AbstractGeometryQueryBuilder;
|
import org.elasticsearch.index.query.AbstractGeometryQueryBuilder;
|
||||||
import org.elasticsearch.index.query.GeoShapeQueryBuilder;
|
import org.elasticsearch.index.query.GeoShapeQueryBuilder;
|
||||||
import org.elasticsearch.index.query.QueryRewriteContext;
|
import org.elasticsearch.index.query.QueryRewriteContext;
|
||||||
import org.elasticsearch.index.query.QueryShardContext;
|
import org.elasticsearch.index.query.QueryShardContext;
|
||||||
import org.elasticsearch.index.query.QueryShardException;
|
import org.elasticsearch.index.query.QueryShardException;
|
||||||
|
import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper;
|
||||||
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
|
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -48,7 +49,7 @@ public class ShapeQueryBuilder extends AbstractGeometryQueryBuilder<ShapeQueryBu
|
||||||
"The type should no longer be specified in the [indexed_shape] section.";
|
"The type should no longer be specified in the [indexed_shape] section.";
|
||||||
|
|
||||||
protected static final List<String> validContentTypes =
|
protected static final List<String> validContentTypes =
|
||||||
Collections.unmodifiableList(Arrays.asList(ShapeFieldMapper.CONTENT_TYPE));
|
Collections.unmodifiableList(Arrays.asList(ShapeFieldMapper.CONTENT_TYPE, PointFieldMapper.CONTENT_TYPE));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new GeoShapeQueryBuilder whose Query will be against the given
|
* Creates a new GeoShapeQueryBuilder whose Query will be against the given
|
||||||
|
@ -138,7 +139,7 @@ public class ShapeQueryBuilder extends AbstractGeometryQueryBuilder<ShapeQueryBu
|
||||||
+ "] but of type [" + fieldType.typeName() + "]");
|
+ "] but of type [" + fieldType.typeName() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
final AbstractGeometryFieldMapper.AbstractGeometryFieldType ft = (AbstractGeometryFieldMapper.AbstractGeometryFieldType) fieldType;
|
final AbstractSearchableGeometryFieldType ft = (AbstractSearchableGeometryFieldType) fieldType;
|
||||||
return new ConstantScoreQuery(ft.geometryQueryBuilder().process(shape, ft.name(), relation, context));
|
return new ConstantScoreQuery(ft.geometryQueryBuilder().process(shape, ft.name(), relation, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.spatial.index.query;
|
||||||
|
|
||||||
|
import org.apache.lucene.document.XYDocValuesField;
|
||||||
|
import org.apache.lucene.document.XYPointField;
|
||||||
|
import org.apache.lucene.geo.XYCircle;
|
||||||
|
import org.apache.lucene.geo.XYRectangle;
|
||||||
|
import org.apache.lucene.search.BooleanClause;
|
||||||
|
import org.apache.lucene.search.BooleanQuery;
|
||||||
|
import org.apache.lucene.search.IndexOrDocValuesQuery;
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.elasticsearch.common.geo.ShapeRelation;
|
||||||
|
import org.elasticsearch.geometry.Circle;
|
||||||
|
import org.elasticsearch.geometry.Geometry;
|
||||||
|
import org.elasticsearch.geometry.GeometryCollection;
|
||||||
|
import org.elasticsearch.geometry.GeometryVisitor;
|
||||||
|
import org.elasticsearch.geometry.LinearRing;
|
||||||
|
import org.elasticsearch.geometry.MultiLine;
|
||||||
|
import org.elasticsearch.geometry.MultiPoint;
|
||||||
|
import org.elasticsearch.geometry.MultiPolygon;
|
||||||
|
import org.elasticsearch.geometry.Point;
|
||||||
|
import org.elasticsearch.geometry.Polygon;
|
||||||
|
import org.elasticsearch.geometry.Rectangle;
|
||||||
|
import org.elasticsearch.geometry.ShapeType;
|
||||||
|
import org.elasticsearch.index.mapper.AbstractSearchableGeometryFieldType;
|
||||||
|
import org.elasticsearch.index.mapper.MappedFieldType;
|
||||||
|
import org.elasticsearch.index.query.QueryShardContext;
|
||||||
|
import org.elasticsearch.index.query.QueryShardException;
|
||||||
|
import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper;
|
||||||
|
import org.elasticsearch.xpack.spatial.index.mapper.ShapeUtils;
|
||||||
|
|
||||||
|
|
||||||
|
public class ShapeQueryPointProcessor implements AbstractSearchableGeometryFieldType.QueryProcessor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query process(Geometry shape, String fieldName, ShapeRelation relation, QueryShardContext context) {
|
||||||
|
validateIsPointFieldType(fieldName, context);
|
||||||
|
// only the intersects relation is supported for indexed cartesian point types
|
||||||
|
if (relation != ShapeRelation.INTERSECTS) {
|
||||||
|
throw new QueryShardException(context,
|
||||||
|
relation+ " query relation not supported for Field [" + fieldName + "].");
|
||||||
|
}
|
||||||
|
// wrap XYPoint query as a ConstantScoreQuery
|
||||||
|
return getVectorQueryFromShape(shape, fieldName, relation, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateIsPointFieldType(String fieldName, QueryShardContext context) {
|
||||||
|
MappedFieldType fieldType = context.fieldMapper(fieldName);
|
||||||
|
if (fieldType instanceof PointFieldMapper.PointFieldType == false) {
|
||||||
|
throw new QueryShardException(context, "Expected " + PointFieldMapper.CONTENT_TYPE
|
||||||
|
+ " field type for Field [" + fieldName + "] but found " + fieldType.typeName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Query getVectorQueryFromShape(
|
||||||
|
Geometry queryShape, String fieldName, ShapeRelation relation, QueryShardContext context) {
|
||||||
|
ShapeVisitor shapeVisitor = new ShapeVisitor(context, fieldName, relation);
|
||||||
|
return queryShape.visit(shapeVisitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ShapeVisitor implements GeometryVisitor<Query, RuntimeException> {
|
||||||
|
QueryShardContext context;
|
||||||
|
MappedFieldType fieldType;
|
||||||
|
String fieldName;
|
||||||
|
ShapeRelation relation;
|
||||||
|
|
||||||
|
ShapeVisitor(QueryShardContext context, String fieldName, ShapeRelation relation) {
|
||||||
|
this.context = context;
|
||||||
|
this.fieldType = context.fieldMapper(fieldName);
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
this.relation = relation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query visit(Circle circle) {
|
||||||
|
XYCircle xyCircle = ShapeUtils.toLuceneXYCircle(circle);
|
||||||
|
Query query = XYPointField.newDistanceQuery(fieldName, xyCircle.getX(), xyCircle.getY(), xyCircle.getRadius());
|
||||||
|
if (fieldType.hasDocValues()) {
|
||||||
|
Query dvQuery = XYDocValuesField.newSlowDistanceQuery(fieldName,
|
||||||
|
xyCircle.getX(), xyCircle.getY(), xyCircle.getRadius());
|
||||||
|
query = new IndexOrDocValuesQuery(query, dvQuery);
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query visit(GeometryCollection<?> collection) {
|
||||||
|
BooleanQuery.Builder bqb = new BooleanQuery.Builder();
|
||||||
|
visit(bqb, collection);
|
||||||
|
return bqb.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visit(BooleanQuery.Builder bqb, GeometryCollection<?> collection) {
|
||||||
|
BooleanClause.Occur occur = BooleanClause.Occur.FILTER;
|
||||||
|
for (Geometry shape : collection) {
|
||||||
|
bqb.add(shape.visit(this), occur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query visit(org.elasticsearch.geometry.Line line) {
|
||||||
|
throw new QueryShardException(context, "Field [" + fieldName + "] does not support "
|
||||||
|
+ ShapeType.LINESTRING + " queries");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
// don't think this is called directly
|
||||||
|
public Query visit(LinearRing ring) {
|
||||||
|
throw new QueryShardException(context, "Field [" + fieldName + "] does not support "
|
||||||
|
+ ShapeType.LINEARRING + " queries");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query visit(MultiLine multiLine) {
|
||||||
|
throw new QueryShardException(context, "Field [" + fieldName + "] does not support "
|
||||||
|
+ ShapeType.MULTILINESTRING + " queries");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query visit(MultiPoint multiPoint) {
|
||||||
|
throw new QueryShardException(context, "Field [" + fieldName + "] does not support "
|
||||||
|
+ ShapeType.MULTIPOINT + " queries");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query visit(MultiPolygon multiPolygon) {
|
||||||
|
org.apache.lucene.geo.XYPolygon[] lucenePolygons =
|
||||||
|
new org.apache.lucene.geo.XYPolygon[multiPolygon.size()];
|
||||||
|
for (int i = 0; i < multiPolygon.size(); i++) {
|
||||||
|
lucenePolygons[i] = ShapeUtils.toLuceneXYPolygon(multiPolygon.get(i));
|
||||||
|
}
|
||||||
|
Query query = XYPointField.newPolygonQuery(fieldName, lucenePolygons);
|
||||||
|
if (fieldType.hasDocValues()) {
|
||||||
|
Query dvQuery = XYDocValuesField.newSlowPolygonQuery(fieldName, lucenePolygons);
|
||||||
|
query = new IndexOrDocValuesQuery(query, dvQuery);
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query visit(Point point) {
|
||||||
|
// not currently supported
|
||||||
|
throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.POINT +
|
||||||
|
" queries");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query visit(Polygon polygon) {
|
||||||
|
org.apache.lucene.geo.XYPolygon lucenePolygon = ShapeUtils.toLuceneXYPolygon(polygon);
|
||||||
|
Query query = XYPointField.newPolygonQuery(fieldName, lucenePolygon);
|
||||||
|
if (fieldType.hasDocValues()) {
|
||||||
|
Query dvQuery = XYDocValuesField.newSlowPolygonQuery(fieldName, lucenePolygon);
|
||||||
|
query = new IndexOrDocValuesQuery(query, dvQuery);
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query visit(Rectangle r) {
|
||||||
|
XYRectangle xyRectangle = ShapeUtils.toLuceneXYRectangle(r);
|
||||||
|
Query query = XYPointField.newBoxQuery(fieldName, xyRectangle.minX, xyRectangle.maxX, xyRectangle.minY, xyRectangle.maxY);
|
||||||
|
if (fieldType.hasDocValues()) {
|
||||||
|
Query dvQuery = XYDocValuesField.newSlowBoxQuery(
|
||||||
|
fieldName, xyRectangle.minX, xyRectangle.maxX, xyRectangle.minY, xyRectangle.maxY);
|
||||||
|
query = new IndexOrDocValuesQuery(query, dvQuery);
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.elasticsearch.xpack.spatial.index.mapper;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
|
import org.elasticsearch.common.compress.CompressedXContent;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
|
import org.elasticsearch.index.mapper.DocumentMapper;
|
||||||
|
import org.elasticsearch.index.mapper.DocumentMapperParser;
|
||||||
|
import org.elasticsearch.index.mapper.MapperParsingException;
|
||||||
|
import org.elasticsearch.index.mapper.ParsedDocument;
|
||||||
|
import org.elasticsearch.index.mapper.SourceToParse;
|
||||||
|
import org.elasticsearch.plugins.Plugin;
|
||||||
|
import org.elasticsearch.test.ESSingleNodeTestCase;
|
||||||
|
import org.elasticsearch.test.InternalSettingsPlugin;
|
||||||
|
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
|
||||||
|
import org.elasticsearch.xpack.spatial.SpatialPlugin;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
|
/** Base class for testing cartesian field mappers */
|
||||||
|
public abstract class CartesianFieldMapperTests extends ESSingleNodeTestCase {
|
||||||
|
|
||||||
|
private static final String FIELD_NAME = "location";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Collection<Class<? extends Plugin>> getPlugins() {
|
||||||
|
return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract XContentBuilder createDefaultMapping(String fieldName,
|
||||||
|
boolean ignored_malformed,
|
||||||
|
boolean ignoreZValue) throws IOException;
|
||||||
|
|
||||||
|
|
||||||
|
public void testWKT() throws IOException {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, randomBoolean(), randomBoolean()));
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field(FIELD_NAME, "POINT (2000.1 305.6)")
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
assertThat(doc.rootDoc().getField(FIELD_NAME), notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testEmptyName() throws IOException {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping("", randomBoolean(), randomBoolean()));
|
||||||
|
|
||||||
|
DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser();
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
|
||||||
|
() -> parser.parse("type", new CompressedXContent(mapping))
|
||||||
|
);
|
||||||
|
assertThat(e.getMessage(), containsString("name cannot be empty string"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testInvalidPointValuesIgnored() throws IOException {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, true, randomBoolean()));
|
||||||
|
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field(FIELD_NAME, "1234.333").endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field("lat", "-").field("x", 1.3).endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field("lat", 1.3).field("y", "-").endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field(FIELD_NAME, "-,1.3").endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field(FIELD_NAME, "1.3,-").endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field("x", "NaN").field("y", "NaN").endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field("lat", 12).field("y", "NaN").endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field("x", "NaN").field("y", 10).endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field(FIELD_NAME, "NaN,NaN").endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field(FIELD_NAME, "10,NaN").endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field(FIELD_NAME, "NaN,12").endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().startObject(FIELD_NAME).nullField("y").field("x", 1).endObject().endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
|
||||||
|
assertThat(defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject().startObject(FIELD_NAME).nullField("x").nullField("y").endObject().endObject()
|
||||||
|
), XContentType.JSON)).rootDoc().getField(FIELD_NAME), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testZValue() throws IOException {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping(FIELD_NAME, false, true));
|
||||||
|
DocumentMapper defaultMapper = createIndex("test1").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test1","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field(FIELD_NAME, "POINT (2000.1 305.6 34567.33)")
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
assertThat(doc.rootDoc().getField(FIELD_NAME), notNullValue());
|
||||||
|
|
||||||
|
mapping = Strings.toString(createDefaultMapping(FIELD_NAME, false, false));
|
||||||
|
DocumentMapper defaultMapper2 = createIndex("test2").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
MapperParsingException e = expectThrows(MapperParsingException.class,
|
||||||
|
() -> defaultMapper2.parse(new SourceToParse("test2","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field(FIELD_NAME, "POINT (2000.1 305.6 34567.33)")
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON))
|
||||||
|
);
|
||||||
|
assertThat(e.getMessage(), containsString("failed to parse field [" + FIELD_NAME + "] of type"));
|
||||||
|
assertThat(e.getRootCause().getMessage(),
|
||||||
|
containsString("found Z value [34567.33] but [ignore_z_value] parameter is [false]"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,297 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.spatial.index.mapper;
|
||||||
|
|
||||||
|
import org.apache.lucene.util.BytesRef;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
|
import org.elasticsearch.common.compress.CompressedXContent;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
|
import org.elasticsearch.index.mapper.DocumentMapper;
|
||||||
|
import org.elasticsearch.index.mapper.Mapper;
|
||||||
|
import org.elasticsearch.index.mapper.ParsedDocument;
|
||||||
|
import org.elasticsearch.index.mapper.SourceToParse;
|
||||||
|
import org.hamcrest.CoreMatchers;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
|
||||||
|
import static org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper.Names.NULL_VALUE;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
|
||||||
|
public class PointFieldMapperTests extends CartesianFieldMapperTests {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected XContentBuilder createDefaultMapping(String fieldName,
|
||||||
|
boolean ignored_malformed,
|
||||||
|
boolean ignoreZValue) throws IOException {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject(fieldName).field("type", "point");
|
||||||
|
if (ignored_malformed || randomBoolean()) {
|
||||||
|
xContentBuilder.field(PointFieldMapper.Names.IGNORE_MALFORMED.getPreferredName(), ignored_malformed);
|
||||||
|
}
|
||||||
|
if (ignoreZValue == false || randomBoolean()) {
|
||||||
|
xContentBuilder.field(PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue);
|
||||||
|
}
|
||||||
|
return xContentBuilder.endObject().endObject().endObject().endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testValuesStored() throws Exception {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject("point").field("type", "point");
|
||||||
|
String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject());
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.startObject("point").field("x", 2000.1).field("y", 305.6).endObject()
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
assertThat(doc.rootDoc().getField("point"), notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testArrayValues() throws Exception {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject("point").field("type", "point").field("doc_values", false);
|
||||||
|
String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject());
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.startArray("point")
|
||||||
|
.startObject().field("x", 1.2).field("y", 1.3).endObject()
|
||||||
|
.startObject().field("x", 1.4).field("y", 1.5).endObject()
|
||||||
|
.endArray()
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
// doc values are enabled by default, but in this test we disable them; we should only have 2 points
|
||||||
|
assertThat(doc.rootDoc().getFields("point"), notNullValue());
|
||||||
|
assertThat(doc.rootDoc().getFields("point").length, equalTo(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testLatLonInOneValue() throws Exception {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject("point").field("type", "point");
|
||||||
|
String mapping = Strings.toString(xContentBuilder.endObject().endObject().endObject().endObject());
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type","1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("point", "1.2,1.3")
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
assertThat(doc.rootDoc().getField("point"), notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testInOneValueStored() throws Exception {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject("point").field("type", "point");
|
||||||
|
String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject());
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("point", "1.2,1.3")
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
assertThat(doc.rootDoc().getField("point"), notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testLatLonInOneValueArray() throws Exception {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject("point").field("type", "point").field("doc_values", false);
|
||||||
|
String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject());
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.startArray("point")
|
||||||
|
.value("1.2,1.3")
|
||||||
|
.value("1.4,1.5")
|
||||||
|
.endArray()
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
// doc values are enabled by default, but in this test we disable them; we should only have 2 points
|
||||||
|
assertThat(doc.rootDoc().getFields("point"), notNullValue());
|
||||||
|
assertThat(doc.rootDoc().getFields("point").length, equalTo(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testArray() throws Exception {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject("point").field("type", "point");
|
||||||
|
String mapping = Strings.toString(xContentBuilder.endObject().endObject().endObject().endObject());
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.startArray("point").value(1.3).value(1.2).endArray()
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
assertThat(doc.rootDoc().getField("point"), notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testArrayDynamic() throws Exception {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startArray("dynamic_templates").startObject().startObject("point").field("match", "point*")
|
||||||
|
.startObject("mapping").field("type", "point");
|
||||||
|
String mapping = Strings.toString(xContentBuilder.endObject().endObject().endObject().endArray().endObject().endObject());
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.startArray("point").value(1.3).value(1.2).endArray()
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
assertThat(doc.rootDoc().getField("point"), notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testArrayStored() throws Exception {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject("point").field("type", "point");
|
||||||
|
String mapping = Strings.toString(xContentBuilder.field("store", true).endObject().endObject().endObject().endObject());
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test", "type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.startArray("point").value(1.3).value(1.2).endArray()
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
assertThat(doc.rootDoc().getField("point"), notNullValue());
|
||||||
|
assertThat(doc.rootDoc().getFields("point").length, equalTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testArrayArrayStored() throws Exception {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject("point").field("type", "point");
|
||||||
|
String mapping = Strings.toString(xContentBuilder.field("store", true)
|
||||||
|
.field("doc_values", false).endObject().endObject()
|
||||||
|
.endObject().endObject());
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.startArray("point")
|
||||||
|
.startArray().value(1.3).value(1.2).endArray()
|
||||||
|
.startArray().value(1.5).value(1.4).endArray()
|
||||||
|
.endArray()
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
assertThat(doc.rootDoc().getFields("point"), notNullValue());
|
||||||
|
assertThat(doc.rootDoc().getFields("point").length, CoreMatchers.equalTo(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNullValue() throws Exception {
|
||||||
|
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject("location")
|
||||||
|
.field("type", "point")
|
||||||
|
.field(NULL_VALUE.getPreferredName(), "1,2")
|
||||||
|
.endObject().endObject()
|
||||||
|
.endObject().endObject());
|
||||||
|
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type", new CompressedXContent(mapping));
|
||||||
|
Mapper fieldMapper = defaultMapper.mappers().getMapper("location");
|
||||||
|
assertThat(fieldMapper, instanceOf(PointFieldMapper.class));
|
||||||
|
|
||||||
|
Object nullValue = ((PointFieldMapper) fieldMapper).fieldType().nullValue();
|
||||||
|
assertThat(nullValue, equalTo(new CartesianPoint(1, 2)));
|
||||||
|
|
||||||
|
ParsedDocument doc = defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.nullField("location")
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
|
||||||
|
assertThat(doc.rootDoc().getField("location"), notNullValue());
|
||||||
|
BytesRef defaultValue = doc.rootDoc().getField("location").binaryValue();
|
||||||
|
|
||||||
|
doc = defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("location", "1, 2")
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
// Shouldn't matter if we specify the value explicitly or use null value
|
||||||
|
assertThat(defaultValue, equalTo(doc.rootDoc().getField("location").binaryValue()));
|
||||||
|
|
||||||
|
doc = defaultMapper.parse(new SourceToParse("test","type", "1",
|
||||||
|
BytesReference.bytes(XContentFactory.jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("location", "3, 4")
|
||||||
|
.endObject()),
|
||||||
|
XContentType.JSON));
|
||||||
|
// Shouldn't matter if we specify the value explicitly or use null value
|
||||||
|
assertThat(defaultValue, not(equalTo(doc.rootDoc().getField("location").binaryValue())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that accept_z_value parameter correctly parses
|
||||||
|
*/
|
||||||
|
public void testIgnoreZValue() throws IOException {
|
||||||
|
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1")
|
||||||
|
.startObject("properties").startObject("location")
|
||||||
|
.field("type", "point")
|
||||||
|
.field(IGNORE_Z_VALUE.getPreferredName(), "true")
|
||||||
|
.endObject().endObject()
|
||||||
|
.endObject().endObject());
|
||||||
|
|
||||||
|
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
|
||||||
|
.parse("type1", new CompressedXContent(mapping));
|
||||||
|
Mapper fieldMapper = defaultMapper.mappers().getMapper("location");
|
||||||
|
assertThat(fieldMapper, instanceOf(PointFieldMapper.class));
|
||||||
|
|
||||||
|
boolean ignoreZValue = ((PointFieldMapper)fieldMapper).ignoreZValue().value();
|
||||||
|
assertThat(ignoreZValue, equalTo(true));
|
||||||
|
|
||||||
|
// explicit false accept_z_value test
|
||||||
|
mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1")
|
||||||
|
.startObject("properties").startObject("location")
|
||||||
|
.field("type", "point")
|
||||||
|
.field(IGNORE_Z_VALUE.getPreferredName(), "false")
|
||||||
|
.endObject().endObject()
|
||||||
|
.endObject().endObject());
|
||||||
|
|
||||||
|
defaultMapper = createIndex("test2").mapperService().documentMapperParser()
|
||||||
|
.parse("type1", new CompressedXContent(mapping));
|
||||||
|
fieldMapper = defaultMapper.mappers().getMapper("location");
|
||||||
|
assertThat(fieldMapper, instanceOf(PointFieldMapper.class));
|
||||||
|
|
||||||
|
ignoreZValue = ((PointFieldMapper)fieldMapper).ignoreZValue().value();
|
||||||
|
assertThat(ignoreZValue, equalTo(false));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.spatial.index.mapper;
|
||||||
|
|
||||||
|
import org.elasticsearch.index.mapper.FieldTypeTestCase;
|
||||||
|
import org.elasticsearch.index.mapper.MappedFieldType;
|
||||||
|
|
||||||
|
public class PointFieldTypeTests extends FieldTypeTestCase {
|
||||||
|
@Override
|
||||||
|
protected MappedFieldType createDefaultFieldType() {
|
||||||
|
return new PointFieldMapper.PointFieldType();
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ import org.elasticsearch.index.mapper.DocumentMapperParser;
|
||||||
import org.elasticsearch.index.mapper.Mapper;
|
import org.elasticsearch.index.mapper.Mapper;
|
||||||
import org.elasticsearch.index.mapper.MapperService;
|
import org.elasticsearch.index.mapper.MapperService;
|
||||||
import org.elasticsearch.plugins.Plugin;
|
import org.elasticsearch.plugins.Plugin;
|
||||||
import org.elasticsearch.test.ESSingleNodeTestCase;
|
|
||||||
import org.elasticsearch.test.InternalSettingsPlugin;
|
import org.elasticsearch.test.InternalSettingsPlugin;
|
||||||
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
|
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
|
||||||
import org.elasticsearch.xpack.spatial.SpatialPlugin;
|
import org.elasticsearch.xpack.spatial.SpatialPlugin;
|
||||||
|
@ -28,17 +27,31 @@ import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
|
import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_Z_VALUE;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.instanceOf;
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
|
|
||||||
/** testing for {@link org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper} */
|
/** testing for {@link org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper} */
|
||||||
public class ShapeFieldMapperTests extends ESSingleNodeTestCase {
|
public class ShapeFieldMapperTests extends CartesianFieldMapperTests {
|
||||||
@Override
|
@Override
|
||||||
protected Collection<Class<? extends Plugin>> getPlugins() {
|
protected Collection<Class<? extends Plugin>> getPlugins() {
|
||||||
return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class);
|
return pluginList(InternalSettingsPlugin.class, SpatialPlugin.class, LocalStateCompositeXPackPlugin.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected XContentBuilder createDefaultMapping(String fieldName,
|
||||||
|
boolean ignored_malformed,
|
||||||
|
boolean ignoreZValue) throws IOException {
|
||||||
|
XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type")
|
||||||
|
.startObject("properties").startObject(fieldName).field("type", "shape");
|
||||||
|
if (ignored_malformed || randomBoolean()) {
|
||||||
|
xContentBuilder.field("ignore_malformed", ignored_malformed);
|
||||||
|
}
|
||||||
|
if (ignoreZValue == false || randomBoolean()) {
|
||||||
|
xContentBuilder.field(PointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName(), ignoreZValue);
|
||||||
|
}
|
||||||
|
return xContentBuilder.endObject().endObject().endObject().endObject();
|
||||||
|
}
|
||||||
|
|
||||||
public void testDefaultConfiguration() throws IOException {
|
public void testDefaultConfiguration() throws IOException {
|
||||||
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1")
|
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1")
|
||||||
.startObject("properties").startObject("location")
|
.startObject("properties").startObject("location")
|
||||||
|
@ -249,21 +262,6 @@ public class ShapeFieldMapperTests extends ESSingleNodeTestCase {
|
||||||
assertThat(shapeFieldMapper.fieldType().orientation(), equalTo(ShapeBuilder.Orientation.CW));
|
assertThat(shapeFieldMapper.fieldType().orientation(), equalTo(ShapeBuilder.Orientation.CW));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testEmptyName() throws Exception {
|
|
||||||
// after 5.x
|
|
||||||
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1")
|
|
||||||
.startObject("properties").startObject("")
|
|
||||||
.field("type", "shape")
|
|
||||||
.endObject().endObject()
|
|
||||||
.endObject().endObject());
|
|
||||||
DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser();
|
|
||||||
|
|
||||||
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
|
|
||||||
() -> parser.parse("type1", new CompressedXContent(mapping))
|
|
||||||
);
|
|
||||||
assertThat(e.getMessage(), containsString("name cannot be empty string"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testSerializeDefaults() throws Exception {
|
public void testSerializeDefaults() throws Exception {
|
||||||
DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser();
|
DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser();
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.spatial.index.query;
|
||||||
|
|
||||||
|
|
||||||
|
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.common.compress.CompressedXContent;
|
||||||
|
import org.elasticsearch.common.geo.ShapeRelation;
|
||||||
|
import org.elasticsearch.geometry.Geometry;
|
||||||
|
import org.elasticsearch.geometry.ShapeType;
|
||||||
|
import org.elasticsearch.index.mapper.MapperService;
|
||||||
|
import org.elasticsearch.xpack.spatial.util.ShapeTestUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
|
public class ShapeQueryBuilderOverPointTests extends ShapeQueryBuilderTests {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initializeAdditionalMappings(MapperService mapperService) throws IOException {
|
||||||
|
mapperService.merge(docType, new CompressedXContent(Strings.toString(PutMappingRequest.buildFromSimplifiedDef(docType,
|
||||||
|
fieldName(), "type=point"))), MapperService.MergeReason.MAPPING_UPDATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShapeRelation getShapeRelation(ShapeType type) {
|
||||||
|
return ShapeRelation.INTERSECTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Geometry getGeometry() {
|
||||||
|
if (randomBoolean()) {
|
||||||
|
if (randomBoolean()) {
|
||||||
|
return ShapeTestUtils.randomMultiPolygon(false);
|
||||||
|
} else {
|
||||||
|
return ShapeTestUtils.randomPolygon(false);
|
||||||
|
}
|
||||||
|
} else if (randomBoolean()) {
|
||||||
|
// it should be a circle
|
||||||
|
return ShapeTestUtils.randomPolygon(false);
|
||||||
|
} else {
|
||||||
|
return ShapeTestUtils.randomRectangle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.spatial.index.query;
|
||||||
|
|
||||||
|
import org.elasticsearch.Version;
|
||||||
|
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.common.compress.CompressedXContent;
|
||||||
|
import org.elasticsearch.common.geo.ShapeRelation;
|
||||||
|
import org.elasticsearch.geometry.Geometry;
|
||||||
|
import org.elasticsearch.geometry.ShapeType;
|
||||||
|
import org.elasticsearch.index.mapper.MapperService;
|
||||||
|
import org.elasticsearch.index.query.QueryShardContext;
|
||||||
|
import org.elasticsearch.xpack.spatial.util.ShapeTestUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ShapeQueryBuilderOverShapeTests extends ShapeQueryBuilderTests {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initializeAdditionalMappings(MapperService mapperService) throws IOException {
|
||||||
|
mapperService.merge(docType, new CompressedXContent(Strings.toString(PutMappingRequest.buildFromSimplifiedDef(docType,
|
||||||
|
fieldName(), "type=shape"))), MapperService.MergeReason.MAPPING_UPDATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShapeRelation getShapeRelation(ShapeType type) {
|
||||||
|
QueryShardContext context = createShardContext();
|
||||||
|
if (context.indexVersionCreated().onOrAfter(Version.V_7_5_0)) { // CONTAINS is only supported from version 7.5
|
||||||
|
if (type == ShapeType.LINESTRING || type == ShapeType.MULTILINESTRING) {
|
||||||
|
return randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.CONTAINS);
|
||||||
|
} else {
|
||||||
|
return randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS,
|
||||||
|
ShapeRelation.WITHIN, ShapeRelation.CONTAINS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (type == ShapeType.LINESTRING || type == ShapeType.MULTILINESTRING) {
|
||||||
|
return randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS);
|
||||||
|
} else {
|
||||||
|
return randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.WITHIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Geometry getGeometry() {
|
||||||
|
return ShapeTestUtils.randomGeometry(false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,13 +10,10 @@ import org.apache.lucene.search.ConstantScoreQuery;
|
||||||
import org.apache.lucene.search.MatchNoDocsQuery;
|
import org.apache.lucene.search.MatchNoDocsQuery;
|
||||||
import org.apache.lucene.search.Query;
|
import org.apache.lucene.search.Query;
|
||||||
import org.elasticsearch.ElasticsearchException;
|
import org.elasticsearch.ElasticsearchException;
|
||||||
import org.elasticsearch.Version;
|
|
||||||
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
|
|
||||||
import org.elasticsearch.action.get.GetRequest;
|
import org.elasticsearch.action.get.GetRequest;
|
||||||
import org.elasticsearch.action.get.GetResponse;
|
import org.elasticsearch.action.get.GetResponse;
|
||||||
import org.elasticsearch.common.Strings;
|
import org.elasticsearch.common.Strings;
|
||||||
import org.elasticsearch.common.bytes.BytesArray;
|
import org.elasticsearch.common.bytes.BytesArray;
|
||||||
import org.elasticsearch.common.compress.CompressedXContent;
|
|
||||||
import org.elasticsearch.common.geo.GeoJson;
|
import org.elasticsearch.common.geo.GeoJson;
|
||||||
import org.elasticsearch.common.geo.ShapeRelation;
|
import org.elasticsearch.common.geo.ShapeRelation;
|
||||||
import org.elasticsearch.common.io.stream.BytesStreamOutput;
|
import org.elasticsearch.common.io.stream.BytesStreamOutput;
|
||||||
|
@ -36,7 +33,6 @@ import org.elasticsearch.index.query.Rewriteable;
|
||||||
import org.elasticsearch.plugins.Plugin;
|
import org.elasticsearch.plugins.Plugin;
|
||||||
import org.elasticsearch.test.AbstractQueryTestCase;
|
import org.elasticsearch.test.AbstractQueryTestCase;
|
||||||
import org.elasticsearch.xpack.spatial.SpatialPlugin;
|
import org.elasticsearch.xpack.spatial.SpatialPlugin;
|
||||||
import org.elasticsearch.xpack.spatial.util.ShapeTestUtils;
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -49,11 +45,11 @@ import static org.hamcrest.CoreMatchers.notNullValue;
|
||||||
import static org.hamcrest.Matchers.anyOf;
|
import static org.hamcrest.Matchers.anyOf;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
|
||||||
public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuilder> {
|
public abstract class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuilder> {
|
||||||
|
|
||||||
protected static final String SHAPE_FIELD_NAME = "mapped_shape";
|
protected static final String SHAPE_FIELD_NAME = "mapped_shape";
|
||||||
|
|
||||||
private static String docType = "_doc";
|
protected static String docType = "_doc";
|
||||||
|
|
||||||
protected static String indexedShapeId;
|
protected static String indexedShapeId;
|
||||||
protected static String indexedShapeType;
|
protected static String indexedShapeType;
|
||||||
|
@ -62,17 +58,15 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
|
||||||
protected static String indexedShapeRouting;
|
protected static String indexedShapeRouting;
|
||||||
protected static Geometry indexedShapeToReturn;
|
protected static Geometry indexedShapeToReturn;
|
||||||
|
|
||||||
|
protected abstract ShapeRelation getShapeRelation(ShapeType type);
|
||||||
|
|
||||||
|
protected abstract Geometry getGeometry();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Collection<Class<? extends Plugin>> getPlugins() {
|
protected Collection<Class<? extends Plugin>> getPlugins() {
|
||||||
return Collections.singleton(SpatialPlugin.class);
|
return Collections.singleton(SpatialPlugin.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initializeAdditionalMappings(MapperService mapperService) throws IOException {
|
|
||||||
mapperService.merge(docType, new CompressedXContent(Strings.toString(PutMappingRequest.buildFromSimplifiedDef(docType,
|
|
||||||
fieldName(), "type=shape"))), MapperService.MergeReason.MAPPING_UPDATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String fieldName() {
|
protected String fieldName() {
|
||||||
return SHAPE_FIELD_NAME;
|
return SHAPE_FIELD_NAME;
|
||||||
}
|
}
|
||||||
|
@ -83,7 +77,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) {
|
protected ShapeQueryBuilder doCreateTestQueryBuilder(boolean indexedShape) {
|
||||||
Geometry shape = ShapeTestUtils.randomGeometry(false);
|
Geometry shape = getGeometry();
|
||||||
|
|
||||||
ShapeQueryBuilder builder;
|
ShapeQueryBuilder builder;
|
||||||
clearShapeFields();
|
clearShapeFields();
|
||||||
|
@ -109,21 +103,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
|
||||||
}
|
}
|
||||||
|
|
||||||
if (randomBoolean()) {
|
if (randomBoolean()) {
|
||||||
QueryShardContext context = createShardContext();
|
builder.relation(getShapeRelation(shape.type()));
|
||||||
if (context.indexVersionCreated().onOrAfter(Version.V_7_5_0)) { // CONTAINS is only supported from version 7.5
|
|
||||||
if (shape.type() == ShapeType.LINESTRING || shape.type() == ShapeType.MULTILINESTRING) {
|
|
||||||
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.CONTAINS));
|
|
||||||
} else {
|
|
||||||
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS,
|
|
||||||
ShapeRelation.WITHIN, ShapeRelation.CONTAINS));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (shape.type() == ShapeType.LINESTRING || shape.type() == ShapeType.MULTILINESTRING) {
|
|
||||||
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS));
|
|
||||||
} else {
|
|
||||||
builder.relation(randomFrom(ShapeRelation.DISJOINT, ShapeRelation.INTERSECTS, ShapeRelation.WITHIN));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (randomBoolean()) {
|
if (randomBoolean()) {
|
||||||
|
@ -152,7 +132,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testNoFieldName() {
|
public void testNoFieldName() {
|
||||||
Geometry shape = ShapeTestUtils.randomGeometry(false);
|
Geometry shape = getGeometry();
|
||||||
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new ShapeQueryBuilder(null, shape));
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new ShapeQueryBuilder(null, shape));
|
||||||
assertEquals("fieldName is required", e.getMessage());
|
assertEquals("fieldName is required", e.getMessage());
|
||||||
}
|
}
|
||||||
|
@ -168,7 +148,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testNoRelation() {
|
public void testNoRelation() {
|
||||||
Geometry shape = ShapeTestUtils.randomGeometry(false);
|
Geometry shape = getGeometry();
|
||||||
ShapeQueryBuilder builder = new ShapeQueryBuilder(fieldName(), shape);
|
ShapeQueryBuilder builder = new ShapeQueryBuilder(fieldName(), shape);
|
||||||
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> builder.relation(null));
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> builder.relation(null));
|
||||||
assertEquals("No Shape Relation defined", e.getMessage());
|
assertEquals("No Shape Relation defined", e.getMessage());
|
||||||
|
@ -223,7 +203,7 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testIgnoreUnmapped() throws IOException {
|
public void testIgnoreUnmapped() throws IOException {
|
||||||
Geometry shape = ShapeTestUtils.randomGeometry(false);
|
Geometry shape = getGeometry();
|
||||||
final ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder("unmapped", shape);
|
final ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder("unmapped", shape);
|
||||||
queryBuilder.ignoreUnmapped(true);
|
queryBuilder.ignoreUnmapped(true);
|
||||||
Query query = queryBuilder.toQuery(createShardContext());
|
Query query = queryBuilder.toQuery(createShardContext());
|
||||||
|
@ -233,14 +213,14 @@ public class ShapeQueryBuilderTests extends AbstractQueryTestCase<ShapeQueryBuil
|
||||||
final ShapeQueryBuilder failingQueryBuilder = new ShapeQueryBuilder("unmapped", shape);
|
final ShapeQueryBuilder failingQueryBuilder = new ShapeQueryBuilder("unmapped", shape);
|
||||||
failingQueryBuilder.ignoreUnmapped(false);
|
failingQueryBuilder.ignoreUnmapped(false);
|
||||||
QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(createShardContext()));
|
QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(createShardContext()));
|
||||||
assertThat(e.getMessage(), containsString("failed to find shape field [unmapped]"));
|
assertThat(e.getMessage(), containsString("failed to find shape or point field [unmapped]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testWrongFieldType() {
|
public void testWrongFieldType() {
|
||||||
Geometry shape = ShapeTestUtils.randomGeometry(false);
|
Geometry shape = getGeometry();
|
||||||
final ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder(TEXT_FIELD_NAME, shape);
|
final ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder(TEXT_FIELD_NAME, shape);
|
||||||
QueryShardException e = expectThrows(QueryShardException.class, () -> queryBuilder.toQuery(createShardContext()));
|
QueryShardException e = expectThrows(QueryShardException.class, () -> queryBuilder.toQuery(createShardContext()));
|
||||||
assertThat(e.getMessage(), containsString("Field [mapped_string] is not of type [shape] but of type [text]"));
|
assertThat(e.getMessage(), containsString("Field [mapped_string] is not of type [shape or point] but of type [text]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSerializationFailsUnlessFetched() throws IOException {
|
public void testSerializationFailsUnlessFetched() throws IOException {
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.spatial.search;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.search.SearchAction;
|
||||||
|
import org.elasticsearch.action.search.SearchPhaseExecutionException;
|
||||||
|
import org.elasticsearch.action.search.SearchRequestBuilder;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.common.geo.ShapeRelation;
|
||||||
|
import org.elasticsearch.common.geo.builders.CoordinatesBuilder;
|
||||||
|
import org.elasticsearch.common.geo.builders.LineStringBuilder;
|
||||||
|
import org.elasticsearch.common.geo.builders.MultiLineStringBuilder;
|
||||||
|
import org.elasticsearch.common.geo.builders.MultiPointBuilder;
|
||||||
|
import org.elasticsearch.common.geo.builders.PointBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
|
import org.elasticsearch.geometry.Line;
|
||||||
|
import org.elasticsearch.geometry.LinearRing;
|
||||||
|
import org.elasticsearch.geometry.MultiLine;
|
||||||
|
import org.elasticsearch.geometry.MultiPoint;
|
||||||
|
import org.elasticsearch.geometry.Point;
|
||||||
|
import org.elasticsearch.geometry.Rectangle;
|
||||||
|
import org.elasticsearch.geometry.ShapeType;
|
||||||
|
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
|
||||||
|
import org.hamcrest.CoreMatchers;
|
||||||
|
|
||||||
|
public class ShapeQueryOverPointTests extends ShapeQueryTests {
|
||||||
|
@Override
|
||||||
|
protected XContentBuilder createDefaultMapping() throws Exception {
|
||||||
|
XContentBuilder xcb = XContentFactory.jsonBuilder().startObject()
|
||||||
|
.startObject("properties").startObject(defaultFieldName)
|
||||||
|
.field("type", "point")
|
||||||
|
.endObject().endObject().endObject();
|
||||||
|
|
||||||
|
return xcb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testProcessRelationSupport() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
Rectangle rectangle = new Rectangle(-35, -25, -25, -35);
|
||||||
|
|
||||||
|
for (ShapeRelation shapeRelation : ShapeRelation.values()) {
|
||||||
|
if (!shapeRelation.equals(ShapeRelation.INTERSECTS)) {
|
||||||
|
SearchPhaseExecutionException e = expectThrows(SearchPhaseExecutionException.class, () ->
|
||||||
|
client().prepareSearch("test")
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, rectangle)
|
||||||
|
.relation(shapeRelation))
|
||||||
|
.get());
|
||||||
|
assertThat(e.getCause().getMessage(),
|
||||||
|
CoreMatchers.containsString(shapeRelation
|
||||||
|
+ " query relation not supported for Field [" + defaultFieldName + "]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testQueryLine() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
Line line = new Line(new double[]{-25, -25}, new double[]{-35, -35});
|
||||||
|
|
||||||
|
try {
|
||||||
|
client().prepareSearch("test")
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, line)).get();
|
||||||
|
} catch (
|
||||||
|
SearchPhaseExecutionException e) {
|
||||||
|
assertThat(e.getCause().getMessage(),
|
||||||
|
CoreMatchers.containsString("does not support " + ShapeType.LINESTRING + " queries"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testQueryLinearRing() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
LinearRing linearRing = new LinearRing(new double[]{-25,-35,-25}, new double[]{-25,-35,-25});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// LinearRing extends Line implements Geometry: expose the build process
|
||||||
|
ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder(defaultFieldName, linearRing);
|
||||||
|
SearchRequestBuilder searchRequestBuilder = new SearchRequestBuilder(client(), SearchAction.INSTANCE);
|
||||||
|
searchRequestBuilder.setQuery(queryBuilder);
|
||||||
|
searchRequestBuilder.setIndices("test");
|
||||||
|
searchRequestBuilder.get();
|
||||||
|
} catch (
|
||||||
|
SearchPhaseExecutionException e) {
|
||||||
|
assertThat(e.getCause().getMessage(),
|
||||||
|
CoreMatchers.containsString("Field [" + defaultFieldName + "] does not support LINEARRING queries"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testQueryMultiLine() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
CoordinatesBuilder coords1 = new CoordinatesBuilder()
|
||||||
|
.coordinate(-35,-35)
|
||||||
|
.coordinate(-25,-25);
|
||||||
|
CoordinatesBuilder coords2 = new CoordinatesBuilder()
|
||||||
|
.coordinate(-15,-15)
|
||||||
|
.coordinate(-5,-5);
|
||||||
|
LineStringBuilder lsb1 = new LineStringBuilder(coords1);
|
||||||
|
LineStringBuilder lsb2 = new LineStringBuilder(coords2);
|
||||||
|
MultiLineStringBuilder mlb = new MultiLineStringBuilder().linestring(lsb1).linestring(lsb2);
|
||||||
|
MultiLine multiline = (MultiLine) mlb.buildGeometry();
|
||||||
|
|
||||||
|
try {
|
||||||
|
client().prepareSearch("test")
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, multiline)).get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertThat(e.getCause().getMessage(),
|
||||||
|
CoreMatchers.containsString("does not support " + ShapeType.MULTILINESTRING + " queries"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testQueryMultiPoint() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
MultiPointBuilder mpb = new MultiPointBuilder().coordinate(-35,-25).coordinate(-15,-5);
|
||||||
|
MultiPoint multiPoint = mpb.buildGeometry();
|
||||||
|
|
||||||
|
try {
|
||||||
|
client().prepareSearch("test")
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, multiPoint)).get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertThat(e.getCause().getMessage(),
|
||||||
|
CoreMatchers.containsString("does not support " + ShapeType.MULTIPOINT + " queries"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testQueryPoint() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate("test").addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
PointBuilder pb = new PointBuilder().coordinate(-35, -25);
|
||||||
|
Point point = pb.buildGeometry();
|
||||||
|
|
||||||
|
try {
|
||||||
|
client().prepareSearch("test")
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, point)).get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertThat(e.getCause().getMessage(),
|
||||||
|
CoreMatchers.containsString("does not support " + ShapeType.POINT + " queries"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,349 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.spatial.search;
|
||||||
|
|
||||||
|
import org.elasticsearch.action.get.GetResponse;
|
||||||
|
import org.elasticsearch.action.index.IndexRequest;
|
||||||
|
import org.elasticsearch.action.search.SearchResponse;
|
||||||
|
import org.elasticsearch.common.geo.GeoJson;
|
||||||
|
import org.elasticsearch.common.geo.ShapeRelation;
|
||||||
|
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
|
||||||
|
import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder;
|
||||||
|
import org.elasticsearch.common.geo.builders.MultiPointBuilder;
|
||||||
|
import org.elasticsearch.common.geo.builders.PointBuilder;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
|
import org.elasticsearch.geometry.Geometry;
|
||||||
|
import org.elasticsearch.geometry.ShapeType;
|
||||||
|
import org.elasticsearch.index.query.ExistsQueryBuilder;
|
||||||
|
import org.elasticsearch.index.query.QueryBuilders;
|
||||||
|
import org.elasticsearch.plugins.Plugin;
|
||||||
|
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
|
||||||
|
import org.elasticsearch.xpack.spatial.SpatialPlugin;
|
||||||
|
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
|
||||||
|
import org.elasticsearch.xpack.spatial.util.ShapeTestUtils;
|
||||||
|
import org.locationtech.jts.geom.Coordinate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
|
||||||
|
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||||
|
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
|
||||||
|
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
|
||||||
|
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
|
||||||
|
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
|
public class ShapeQueryOverShapeTests extends ShapeQueryTests {
|
||||||
|
|
||||||
|
private static String INDEX = "test";
|
||||||
|
private static String IGNORE_MALFORMED_INDEX = INDEX + "_ignore_malformed";
|
||||||
|
private static String FIELD = "shape";
|
||||||
|
private static Geometry queryGeometry = null;
|
||||||
|
|
||||||
|
private int numDocs;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected XContentBuilder createDefaultMapping() throws Exception {
|
||||||
|
XContentBuilder xcb = XContentFactory.jsonBuilder().startObject()
|
||||||
|
.startObject("properties").startObject(defaultFieldName)
|
||||||
|
.field("type", "shape")
|
||||||
|
.endObject().endObject().endObject();
|
||||||
|
|
||||||
|
return xcb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
super.setUp();
|
||||||
|
|
||||||
|
// create test index
|
||||||
|
assertAcked(client().admin().indices().prepareCreate(INDEX)
|
||||||
|
.addMapping(defaultFieldType, FIELD, "type=shape", "alias", "type=alias,path=" + FIELD).get());
|
||||||
|
// create index that ignores malformed geometry
|
||||||
|
assertAcked(client().admin().indices().prepareCreate(IGNORE_MALFORMED_INDEX)
|
||||||
|
.addMapping(defaultFieldType,FIELD, "type=shape,ignore_malformed=true", "_source", "enabled=false").get());
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
// index random shapes
|
||||||
|
numDocs = randomIntBetween(25, 50);
|
||||||
|
// reset query geometry to make sure we pick one from the indexed shapes
|
||||||
|
queryGeometry = null;
|
||||||
|
Geometry geometry;
|
||||||
|
for (int i = 0; i < numDocs; ++i) {
|
||||||
|
geometry = ShapeTestUtils.randomGeometry(false);
|
||||||
|
if (geometry.type() == ShapeType.CIRCLE) continue;
|
||||||
|
if (queryGeometry == null && geometry.type() != ShapeType.MULTIPOINT) {
|
||||||
|
queryGeometry = geometry;
|
||||||
|
}
|
||||||
|
XContentBuilder geoJson = GeoJson.toXContent(geometry, XContentFactory.jsonBuilder()
|
||||||
|
.startObject().field(FIELD), null).endObject();
|
||||||
|
|
||||||
|
try {
|
||||||
|
client().prepareIndex(INDEX, defaultFieldType).setSource(geoJson).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
client().prepareIndex(IGNORE_MALFORMED_INDEX, defaultFieldType).setRefreshPolicy(IMMEDIATE).setSource(geoJson).get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// sometimes GeoTestUtil will create invalid geometry; catch and continue:
|
||||||
|
if (queryGeometry == geometry) {
|
||||||
|
// reset query geometry as it didn't get indexed
|
||||||
|
queryGeometry = null;
|
||||||
|
}
|
||||||
|
--i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIndexedShapeReferenceSourceDisabled() throws Exception {
|
||||||
|
EnvelopeBuilder shape = new EnvelopeBuilder(new Coordinate(-45, 45), new Coordinate(45, -45));
|
||||||
|
|
||||||
|
client().prepareIndex(IGNORE_MALFORMED_INDEX, defaultFieldType).setId("Big_Rectangle").setSource(jsonBuilder().startObject()
|
||||||
|
.field(FIELD, shape).endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> client().prepareSearch(IGNORE_MALFORMED_INDEX)
|
||||||
|
.setQuery(new ShapeQueryBuilder(FIELD, "Big_Rectangle").indexedShapeIndex(IGNORE_MALFORMED_INDEX)).get());
|
||||||
|
assertThat(e.getMessage(), containsString("source disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testShapeFetchingPath() throws Exception {
|
||||||
|
String indexName = "shapes_index";
|
||||||
|
String searchIndex = "search_index";
|
||||||
|
createIndex(indexName);
|
||||||
|
client().admin().indices().prepareCreate(searchIndex).addMapping(defaultFieldType,"location", "type=shape").get();
|
||||||
|
|
||||||
|
String location = "\"location\" : {\"type\":\"polygon\", \"coordinates\":[[[-10,-10],[10,-10],[10,10],[-10,10],[-10,-10]]]}";
|
||||||
|
|
||||||
|
client().prepareIndex(indexName, defaultFieldType).setId("1")
|
||||||
|
.setSource(
|
||||||
|
String.format(
|
||||||
|
Locale.ROOT, "{ %s, \"1\" : { %s, \"2\" : { %s, \"3\" : { %s } }} }", location, location, location, location
|
||||||
|
), XContentType.JSON)
|
||||||
|
.setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
client().prepareIndex(searchIndex, defaultFieldType).setId("1")
|
||||||
|
.setSource(jsonBuilder().startObject().startObject("location")
|
||||||
|
.field("type", "polygon")
|
||||||
|
.startArray("coordinates").startArray()
|
||||||
|
.startArray().value(-20).value(-20).endArray()
|
||||||
|
.startArray().value(20).value(-20).endArray()
|
||||||
|
.startArray().value(20).value(20).endArray()
|
||||||
|
.startArray().value(-20).value(20).endArray()
|
||||||
|
.startArray().value(-20).value(-20).endArray()
|
||||||
|
.endArray().endArray()
|
||||||
|
.endObject().endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
ShapeQueryBuilder filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS)
|
||||||
|
.indexedShapeIndex(indexName)
|
||||||
|
.indexedShapePath("location");
|
||||||
|
SearchResponse result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery())
|
||||||
|
.setPostFilter(filter).get();
|
||||||
|
assertSearchResponse(result);
|
||||||
|
assertHitCount(result, 1);
|
||||||
|
filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS)
|
||||||
|
.indexedShapeIndex(indexName)
|
||||||
|
.indexedShapePath("1.location");
|
||||||
|
result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery())
|
||||||
|
.setPostFilter(filter).get();
|
||||||
|
assertSearchResponse(result);
|
||||||
|
assertHitCount(result, 1);
|
||||||
|
filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS)
|
||||||
|
.indexedShapeIndex(indexName)
|
||||||
|
.indexedShapePath("1.2.location");
|
||||||
|
result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery())
|
||||||
|
.setPostFilter(filter).get();
|
||||||
|
assertSearchResponse(result);
|
||||||
|
assertHitCount(result, 1);
|
||||||
|
filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS)
|
||||||
|
.indexedShapeIndex(indexName)
|
||||||
|
.indexedShapePath("1.2.3.location");
|
||||||
|
result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery())
|
||||||
|
.setPostFilter(filter).get();
|
||||||
|
assertSearchResponse(result);
|
||||||
|
assertHitCount(result, 1);
|
||||||
|
|
||||||
|
// now test the query variant
|
||||||
|
ShapeQueryBuilder query = new ShapeQueryBuilder("location", "1")
|
||||||
|
.indexedShapeIndex(indexName)
|
||||||
|
.indexedShapePath("location");
|
||||||
|
result = client().prepareSearch(searchIndex).setQuery(query).get();
|
||||||
|
assertSearchResponse(result);
|
||||||
|
assertHitCount(result, 1);
|
||||||
|
query = new ShapeQueryBuilder("location", "1")
|
||||||
|
.indexedShapeIndex(indexName)
|
||||||
|
.indexedShapePath("1.location");
|
||||||
|
result = client().prepareSearch(searchIndex).setQuery(query).get();
|
||||||
|
assertSearchResponse(result);
|
||||||
|
assertHitCount(result, 1);
|
||||||
|
query = new ShapeQueryBuilder("location", "1")
|
||||||
|
.indexedShapeIndex(indexName)
|
||||||
|
.indexedShapePath("1.2.location");
|
||||||
|
result = client().prepareSearch(searchIndex).setQuery(query).get();
|
||||||
|
assertSearchResponse(result);
|
||||||
|
assertHitCount(result, 1);
|
||||||
|
query = new ShapeQueryBuilder("location", "1")
|
||||||
|
.indexedShapeIndex(indexName)
|
||||||
|
.indexedShapePath("1.2.3.location");
|
||||||
|
result = client().prepareSearch(searchIndex).setQuery(query).get();
|
||||||
|
assertSearchResponse(result);
|
||||||
|
assertHitCount(result, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Collection<Class<? extends Plugin>> getPlugins() {
|
||||||
|
return pluginList(SpatialPlugin.class, LocalStateCompositeXPackPlugin.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that ignore_malformed on GeoShapeFieldMapper does not fail the entire document
|
||||||
|
*/
|
||||||
|
public void testIgnoreMalformed() {
|
||||||
|
assertHitCount(client().prepareSearch(IGNORE_MALFORMED_INDEX).setQuery(matchAllQuery()).get(), numDocs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that the indexed shape routing can be provided if it is required
|
||||||
|
*/
|
||||||
|
public void testIndexShapeRouting() {
|
||||||
|
String source = "{\n" +
|
||||||
|
" \"shape\" : {\n" +
|
||||||
|
" \"type\" : \"bbox\",\n" +
|
||||||
|
" \"coordinates\" : [[" + -Float.MAX_VALUE + "," + Float.MAX_VALUE + "], [" + Float.MAX_VALUE + ", " + -Float.MAX_VALUE
|
||||||
|
+ "]]\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
client().prepareIndex(INDEX, defaultFieldType).setId("0").setSource(source, XContentType.JSON).setRouting("ABC").get();
|
||||||
|
client().admin().indices().prepareRefresh(INDEX).get();
|
||||||
|
|
||||||
|
SearchResponse searchResponse = client().prepareSearch(INDEX).setQuery(
|
||||||
|
new ShapeQueryBuilder(FIELD, "0").indexedShapeIndex(INDEX).indexedShapeRouting("ABC")
|
||||||
|
).get();
|
||||||
|
|
||||||
|
assertThat(searchResponse.getHits().getTotalHits().value, equalTo((long)numDocs+1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNullShape() {
|
||||||
|
// index a null shape
|
||||||
|
client().prepareIndex(INDEX, defaultFieldType).setId("aNullshape").setSource("{\"" + FIELD + "\": null}", XContentType.JSON)
|
||||||
|
.setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
client().prepareIndex(IGNORE_MALFORMED_INDEX, defaultFieldType).setId("aNullshape").setSource("{\"" + FIELD + "\": null}",
|
||||||
|
XContentType.JSON).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
GetResponse result = client().prepareGet(INDEX, defaultFieldType, "aNullshape").get();
|
||||||
|
assertThat(result.getField(FIELD), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testExistsQuery() {
|
||||||
|
ExistsQueryBuilder eqb = QueryBuilders.existsQuery(FIELD);
|
||||||
|
SearchResponse result = client().prepareSearch(INDEX).setQuery(eqb).get();
|
||||||
|
assertSearchResponse(result);
|
||||||
|
assertHitCount(result, numDocs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testFieldAlias() {
|
||||||
|
SearchResponse response = client().prepareSearch(INDEX)
|
||||||
|
.setQuery(new ShapeQueryBuilder("alias", queryGeometry).relation(ShapeRelation.INTERSECTS))
|
||||||
|
.get();
|
||||||
|
assertTrue(response.getHits().getTotalHits().value > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testContainsShapeQuery() {
|
||||||
|
|
||||||
|
client().admin().indices().prepareCreate("test_contains").addMapping(defaultFieldType,"location", "type=shape")
|
||||||
|
.execute().actionGet();
|
||||||
|
|
||||||
|
String doc = "{\"location\" : {\"type\":\"envelope\", \"coordinates\":[ [-100.0, 100.0], [100.0, -100.0]]}}";
|
||||||
|
client().prepareIndex("test_contains", defaultFieldType).setId("1")
|
||||||
|
.setSource(doc, XContentType.JSON).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
// index the mbr of the collection
|
||||||
|
EnvelopeBuilder queryShape = new EnvelopeBuilder(new Coordinate(-50, 50), new Coordinate(50, -50));
|
||||||
|
ShapeQueryBuilder queryBuilder =
|
||||||
|
new ShapeQueryBuilder("location", queryShape.buildGeometry()).relation(ShapeRelation.CONTAINS);
|
||||||
|
SearchResponse response = client().prepareSearch("test_contains").setQuery(queryBuilder).get();
|
||||||
|
assertSearchResponse(response);
|
||||||
|
|
||||||
|
assertThat(response.getHits().getTotalHits().value, equalTo(1L));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGeometryCollectionRelations() throws IOException {
|
||||||
|
XContentBuilder mapping = XContentFactory.jsonBuilder().startObject()
|
||||||
|
.startObject("properties")
|
||||||
|
.startObject("geometry").field("type", "shape").endObject()
|
||||||
|
.endObject()
|
||||||
|
.endObject();
|
||||||
|
|
||||||
|
createIndex("test_collections", Settings.builder().put("index.number_of_shards", 1).build()
|
||||||
|
, defaultFieldType, mapping);
|
||||||
|
|
||||||
|
EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10));
|
||||||
|
|
||||||
|
client().index(new IndexRequest("test_collections")
|
||||||
|
.source(jsonBuilder().startObject().field("geometry", envelopeBuilder).endObject())
|
||||||
|
.setRefreshPolicy(IMMEDIATE)).actionGet();
|
||||||
|
|
||||||
|
{
|
||||||
|
// A geometry collection that is fully within the indexed shape
|
||||||
|
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
|
||||||
|
builder.shape(new PointBuilder(1, 2));
|
||||||
|
builder.shape(new PointBuilder(-2, -1));
|
||||||
|
SearchResponse response = client().prepareSearch("test_collections")
|
||||||
|
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
|
||||||
|
.get();
|
||||||
|
assertEquals(1, response.getHits().getTotalHits().value);
|
||||||
|
response = client().prepareSearch("test_collections")
|
||||||
|
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
|
||||||
|
.get();
|
||||||
|
assertEquals(1, response.getHits().getTotalHits().value);
|
||||||
|
response = client().prepareSearch("test_collections")
|
||||||
|
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
|
||||||
|
.get();
|
||||||
|
assertEquals(0, response.getHits().getTotalHits().value);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// A geometry collection (as multi point) that is partially within the indexed shape
|
||||||
|
MultiPointBuilder builder = new MultiPointBuilder();
|
||||||
|
builder.coordinate(1, 2);
|
||||||
|
builder.coordinate(20, 30);
|
||||||
|
SearchResponse response = client().prepareSearch("test_collections")
|
||||||
|
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
|
||||||
|
.get();
|
||||||
|
assertEquals(0, response.getHits().getTotalHits().value);
|
||||||
|
response = client().prepareSearch("test_collections")
|
||||||
|
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
|
||||||
|
.get();
|
||||||
|
assertEquals(1, response.getHits().getTotalHits().value);
|
||||||
|
response = client().prepareSearch("test_collections")
|
||||||
|
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
|
||||||
|
.get();
|
||||||
|
assertEquals(0, response.getHits().getTotalHits().value);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// A geometry collection that is disjoint with the indexed shape
|
||||||
|
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
|
||||||
|
MultiPointBuilder innerBuilder = new MultiPointBuilder();
|
||||||
|
innerBuilder.coordinate(-20, -30);
|
||||||
|
innerBuilder.coordinate(20, 30);
|
||||||
|
builder.shape(innerBuilder);
|
||||||
|
SearchResponse response = client().prepareSearch("test_collections")
|
||||||
|
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
|
||||||
|
.get();
|
||||||
|
assertEquals(0, response.getHits().getTotalHits().value);
|
||||||
|
response = client().prepareSearch("test_collections")
|
||||||
|
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
|
||||||
|
.get();
|
||||||
|
assertEquals(0, response.getHits().getTotalHits().value);
|
||||||
|
response = client().prepareSearch("test_collections")
|
||||||
|
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
|
||||||
|
.get();
|
||||||
|
assertEquals(1, response.getHits().getTotalHits().value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,336 +8,322 @@ package org.elasticsearch.xpack.spatial.search;
|
||||||
import org.elasticsearch.action.get.GetResponse;
|
import org.elasticsearch.action.get.GetResponse;
|
||||||
import org.elasticsearch.action.index.IndexRequest;
|
import org.elasticsearch.action.index.IndexRequest;
|
||||||
import org.elasticsearch.action.search.SearchResponse;
|
import org.elasticsearch.action.search.SearchResponse;
|
||||||
import org.elasticsearch.common.geo.GeoJson;
|
import org.elasticsearch.common.Strings;
|
||||||
import org.elasticsearch.common.geo.ShapeRelation;
|
import org.elasticsearch.common.geo.ShapeRelation;
|
||||||
import org.elasticsearch.common.geo.builders.CircleBuilder;
|
import org.elasticsearch.common.geo.builders.CircleBuilder;
|
||||||
|
import org.elasticsearch.common.geo.builders.CoordinatesBuilder;
|
||||||
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
|
import org.elasticsearch.common.geo.builders.EnvelopeBuilder;
|
||||||
import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder;
|
import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder;
|
||||||
import org.elasticsearch.common.geo.builders.MultiPointBuilder;
|
import org.elasticsearch.common.geo.builders.MultiPolygonBuilder;
|
||||||
import org.elasticsearch.common.geo.builders.PointBuilder;
|
import org.elasticsearch.common.geo.builders.PointBuilder;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.geo.builders.PolygonBuilder;
|
||||||
import org.elasticsearch.common.unit.DistanceUnit;
|
import org.elasticsearch.common.unit.DistanceUnit;
|
||||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
import org.elasticsearch.common.xcontent.XContentFactory;
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
import org.elasticsearch.common.xcontent.XContentType;
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
import org.elasticsearch.geometry.Geometry;
|
import org.elasticsearch.geometry.Geometry;
|
||||||
import org.elasticsearch.geometry.ShapeType;
|
import org.elasticsearch.geometry.Rectangle;
|
||||||
import org.elasticsearch.index.query.ExistsQueryBuilder;
|
|
||||||
import org.elasticsearch.index.query.QueryBuilders;
|
|
||||||
import org.elasticsearch.plugins.Plugin;
|
import org.elasticsearch.plugins.Plugin;
|
||||||
|
import org.elasticsearch.search.SearchHits;
|
||||||
import org.elasticsearch.test.ESSingleNodeTestCase;
|
import org.elasticsearch.test.ESSingleNodeTestCase;
|
||||||
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
|
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
|
||||||
import org.elasticsearch.xpack.spatial.SpatialPlugin;
|
import org.elasticsearch.xpack.spatial.SpatialPlugin;
|
||||||
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
|
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
|
||||||
import org.elasticsearch.xpack.spatial.util.ShapeTestUtils;
|
|
||||||
import org.locationtech.jts.geom.Coordinate;
|
import org.locationtech.jts.geom.Coordinate;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
|
import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
|
||||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||||
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
|
|
||||||
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
|
|
||||||
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
|
|
||||||
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
|
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
public class ShapeQueryTests extends ESSingleNodeTestCase {
|
public abstract class ShapeQueryTests extends ESSingleNodeTestCase {
|
||||||
|
|
||||||
private static String INDEX = "test";
|
|
||||||
private static String IGNORE_MALFORMED_INDEX = INDEX + "_ignore_malformed";
|
|
||||||
private static String FIELD_TYPE = "geometry";
|
|
||||||
private static String FIELD = "shape";
|
|
||||||
private static Geometry queryGeometry = null;
|
|
||||||
|
|
||||||
private int numDocs;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp() throws Exception {
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
// create test index
|
|
||||||
assertAcked(client().admin().indices().prepareCreate(INDEX)
|
|
||||||
.addMapping(FIELD_TYPE, FIELD, "type=shape", "alias", "type=alias,path=" + FIELD).get());
|
|
||||||
// create index that ignores malformed geometry
|
|
||||||
assertAcked(client().admin().indices().prepareCreate(IGNORE_MALFORMED_INDEX)
|
|
||||||
.addMapping(FIELD_TYPE, FIELD, "type=shape,ignore_malformed=true", "_source", "enabled=false").get());
|
|
||||||
ensureGreen();
|
|
||||||
|
|
||||||
// index random shapes
|
|
||||||
numDocs = randomIntBetween(25, 50);
|
|
||||||
// reset query geometry to make sure we pick one from the indexed shapes
|
|
||||||
queryGeometry = null;
|
|
||||||
Geometry geometry;
|
|
||||||
for (int i = 0; i < numDocs; ++i) {
|
|
||||||
geometry = ShapeTestUtils.randomGeometry(false);
|
|
||||||
if (geometry.type() == ShapeType.CIRCLE) continue;
|
|
||||||
if (queryGeometry == null && geometry.type() != ShapeType.MULTIPOINT) {
|
|
||||||
queryGeometry = geometry;
|
|
||||||
}
|
|
||||||
XContentBuilder geoJson = GeoJson.toXContent(geometry, XContentFactory.jsonBuilder()
|
|
||||||
.startObject().field(FIELD), null).endObject();
|
|
||||||
|
|
||||||
try {
|
|
||||||
client().prepareIndex(INDEX, FIELD_TYPE).setSource(geoJson).setRefreshPolicy(IMMEDIATE).get();
|
|
||||||
client().prepareIndex(IGNORE_MALFORMED_INDEX, FIELD_TYPE).setRefreshPolicy(IMMEDIATE).setSource(geoJson).get();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// sometimes GeoTestUtil will create invalid geometry; catch and continue:
|
|
||||||
if (queryGeometry == geometry) {
|
|
||||||
// reset query geometry as it didn't get indexed
|
|
||||||
queryGeometry = null;
|
|
||||||
}
|
|
||||||
--i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testIndexedShapeReferenceSourceDisabled() throws Exception {
|
|
||||||
EnvelopeBuilder shape = new EnvelopeBuilder(new Coordinate(-45, 45), new Coordinate(45, -45));
|
|
||||||
|
|
||||||
client().prepareIndex(IGNORE_MALFORMED_INDEX, FIELD_TYPE, "Big_Rectangle").setSource(jsonBuilder().startObject()
|
|
||||||
.field(FIELD, shape).endObject()).setRefreshPolicy(IMMEDIATE).get();
|
|
||||||
|
|
||||||
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> client().prepareSearch(IGNORE_MALFORMED_INDEX)
|
|
||||||
.setQuery(new ShapeQueryBuilder(FIELD, "Big_Rectangle").indexedShapeIndex(IGNORE_MALFORMED_INDEX)).get());
|
|
||||||
assertThat(e.getMessage(), containsString("source disabled"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testShapeFetchingPath() throws Exception {
|
|
||||||
String indexName = "shapes_index";
|
|
||||||
String searchIndex = "search_index";
|
|
||||||
createIndex(indexName);
|
|
||||||
client().admin().indices().prepareCreate(searchIndex).addMapping("type", "location", "type=shape").get();
|
|
||||||
|
|
||||||
String location = "\"location\" : {\"type\":\"polygon\", \"coordinates\":[[[-10,-10],[10,-10],[10,10],[-10,10],[-10,-10]]]}";
|
|
||||||
|
|
||||||
client().prepareIndex(indexName, "type", "1")
|
|
||||||
.setSource(
|
|
||||||
String.format(
|
|
||||||
Locale.ROOT, "{ %s, \"1\" : { %s, \"2\" : { %s, \"3\" : { %s } }} }", location, location, location, location
|
|
||||||
), XContentType.JSON)
|
|
||||||
.setRefreshPolicy(IMMEDIATE).get();
|
|
||||||
client().prepareIndex(searchIndex, "type", "1")
|
|
||||||
.setSource(jsonBuilder().startObject().startObject("location")
|
|
||||||
.field("type", "polygon")
|
|
||||||
.startArray("coordinates").startArray()
|
|
||||||
.startArray().value(-20).value(-20).endArray()
|
|
||||||
.startArray().value(20).value(-20).endArray()
|
|
||||||
.startArray().value(20).value(20).endArray()
|
|
||||||
.startArray().value(-20).value(20).endArray()
|
|
||||||
.startArray().value(-20).value(-20).endArray()
|
|
||||||
.endArray().endArray()
|
|
||||||
.endObject().endObject()).setRefreshPolicy(IMMEDIATE).get();
|
|
||||||
|
|
||||||
ShapeQueryBuilder filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS)
|
|
||||||
.indexedShapeIndex(indexName)
|
|
||||||
.indexedShapePath("location");
|
|
||||||
SearchResponse result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery())
|
|
||||||
.setPostFilter(filter).get();
|
|
||||||
assertSearchResponse(result);
|
|
||||||
assertHitCount(result, 1);
|
|
||||||
filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS)
|
|
||||||
.indexedShapeIndex(indexName)
|
|
||||||
.indexedShapePath("1.location");
|
|
||||||
result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery())
|
|
||||||
.setPostFilter(filter).get();
|
|
||||||
assertSearchResponse(result);
|
|
||||||
assertHitCount(result, 1);
|
|
||||||
filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS)
|
|
||||||
.indexedShapeIndex(indexName)
|
|
||||||
.indexedShapePath("1.2.location");
|
|
||||||
result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery())
|
|
||||||
.setPostFilter(filter).get();
|
|
||||||
assertSearchResponse(result);
|
|
||||||
assertHitCount(result, 1);
|
|
||||||
filter = new ShapeQueryBuilder("location", "1").relation(ShapeRelation.INTERSECTS)
|
|
||||||
.indexedShapeIndex(indexName)
|
|
||||||
.indexedShapePath("1.2.3.location");
|
|
||||||
result = client().prepareSearch(searchIndex).setQuery(QueryBuilders.matchAllQuery())
|
|
||||||
.setPostFilter(filter).get();
|
|
||||||
assertSearchResponse(result);
|
|
||||||
assertHitCount(result, 1);
|
|
||||||
|
|
||||||
// now test the query variant
|
|
||||||
ShapeQueryBuilder query = new ShapeQueryBuilder("location", "1")
|
|
||||||
.indexedShapeIndex(indexName)
|
|
||||||
.indexedShapePath("location");
|
|
||||||
result = client().prepareSearch(searchIndex).setQuery(query).get();
|
|
||||||
assertSearchResponse(result);
|
|
||||||
assertHitCount(result, 1);
|
|
||||||
query = new ShapeQueryBuilder("location", "1")
|
|
||||||
.indexedShapeIndex(indexName)
|
|
||||||
.indexedShapePath("1.location");
|
|
||||||
result = client().prepareSearch(searchIndex).setQuery(query).get();
|
|
||||||
assertSearchResponse(result);
|
|
||||||
assertHitCount(result, 1);
|
|
||||||
query = new ShapeQueryBuilder("location", "1")
|
|
||||||
.indexedShapeIndex(indexName)
|
|
||||||
.indexedShapePath("1.2.location");
|
|
||||||
result = client().prepareSearch(searchIndex).setQuery(query).get();
|
|
||||||
assertSearchResponse(result);
|
|
||||||
assertHitCount(result, 1);
|
|
||||||
query = new ShapeQueryBuilder("location", "1")
|
|
||||||
.indexedShapeIndex(indexName)
|
|
||||||
.indexedShapePath("1.2.3.location");
|
|
||||||
result = client().prepareSearch(searchIndex).setQuery(query).get();
|
|
||||||
assertSearchResponse(result);
|
|
||||||
assertHitCount(result, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Collection<Class<? extends Plugin>> getPlugins() {
|
protected Collection<Class<? extends Plugin>> getPlugins() {
|
||||||
return pluginList(SpatialPlugin.class, LocalStateCompositeXPackPlugin.class);
|
return pluginList(SpatialPlugin.class, LocalStateCompositeXPackPlugin.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected abstract XContentBuilder createDefaultMapping() throws Exception;
|
||||||
* Test that ignore_malformed on GeoShapeFieldMapper does not fail the entire document
|
|
||||||
*/
|
|
||||||
public void testIgnoreMalformed() {
|
|
||||||
assertHitCount(client().prepareSearch(IGNORE_MALFORMED_INDEX).setQuery(matchAllQuery()).get(), numDocs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
static String defaultFieldName = "xy";
|
||||||
* Test that the indexed shape routing can be provided if it is required
|
static String defaultIndexName = "test-points";
|
||||||
*/
|
static String defaultFieldType = "doc";
|
||||||
public void testIndexShapeRouting() {
|
|
||||||
String source = "{\n" +
|
|
||||||
" \"shape\" : {\n" +
|
|
||||||
" \"type\" : \"bbox\",\n" +
|
|
||||||
" \"coordinates\" : [[" + -Float.MAX_VALUE + "," + Float.MAX_VALUE + "], [" + Float.MAX_VALUE + ", " + -Float.MAX_VALUE
|
|
||||||
+ "]]\n" +
|
|
||||||
" }\n" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
client().prepareIndex(INDEX, FIELD_TYPE, "0").setSource(source, XContentType.JSON).setRouting("ABC").get();
|
public void testNullShape() throws Exception {
|
||||||
client().admin().indices().prepareRefresh(INDEX).get();
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate(defaultIndexName)
|
||||||
|
.addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
SearchResponse searchResponse = client().prepareSearch(INDEX).setQuery(
|
client().prepareIndex(defaultIndexName, defaultFieldType, "aNullshape")
|
||||||
new ShapeQueryBuilder(FIELD, "0").indexedShapeIndex(INDEX).indexedShapeRouting("ABC")
|
.setSource("{\"geo\": null}", XContentType.JSON)
|
||||||
).get();
|
|
||||||
|
|
||||||
assertThat(searchResponse.getHits().getTotalHits().value, equalTo((long)numDocs+1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testNullShape() {
|
|
||||||
// index a null shape
|
|
||||||
client().prepareIndex(INDEX, FIELD_TYPE, "aNullshape").setSource("{\"" + FIELD + "\": null}", XContentType.JSON)
|
|
||||||
.setRefreshPolicy(IMMEDIATE).get();
|
.setRefreshPolicy(IMMEDIATE).get();
|
||||||
client().prepareIndex(IGNORE_MALFORMED_INDEX, FIELD_TYPE, "aNullshape").setSource("{\"" + FIELD + "\": null}",
|
GetResponse result = client().prepareGet(defaultIndexName, defaultFieldType, "aNullshape").get();
|
||||||
XContentType.JSON).setRefreshPolicy(IMMEDIATE).get();
|
assertThat(result.getField("location"), nullValue());
|
||||||
GetResponse result = client().prepareGet(INDEX, FIELD_TYPE, "aNullshape").get();
|
};
|
||||||
assertThat(result.getField(FIELD), nullValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testExistsQuery() {
|
public void testIndexPointsFilterRectangle() throws Exception {
|
||||||
ExistsQueryBuilder eqb = QueryBuilders.existsQuery(FIELD);
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
SearchResponse result = client().prepareSearch(INDEX).setQuery(eqb).get();
|
client().admin().indices().prepareCreate(defaultIndexName)
|
||||||
assertSearchResponse(result);
|
.addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
assertHitCount(result, numDocs);
|
ensureGreen();
|
||||||
}
|
|
||||||
|
|
||||||
public void testFieldAlias() {
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder()
|
||||||
SearchResponse response = client().prepareSearch(INDEX)
|
.startObject()
|
||||||
.setQuery(new ShapeQueryBuilder("alias", queryGeometry).relation(ShapeRelation.INTERSECTS))
|
.field("name", "Document 1")
|
||||||
|
.field(defaultFieldName, "POINT(-30 -30)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("name", "Document 2")
|
||||||
|
.field(defaultFieldName, "POINT(-45 -50)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
EnvelopeBuilder shape = new EnvelopeBuilder(new Coordinate(-45, 45), new Coordinate(45, -45));
|
||||||
|
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(shape);
|
||||||
|
Geometry geometry = builder.buildGeometry().get(0);
|
||||||
|
SearchResponse searchResponse = client().prepareSearch(defaultIndexName)
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, geometry)
|
||||||
|
.relation(ShapeRelation.INTERSECTS))
|
||||||
.get();
|
.get();
|
||||||
assertTrue(response.getHits().getTotalHits().value > 0);
|
|
||||||
|
assertSearchResponse(searchResponse);
|
||||||
|
assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L));
|
||||||
|
assertThat(searchResponse.getHits().getHits().length, equalTo(1));
|
||||||
|
assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1"));
|
||||||
|
|
||||||
|
// default query, without specifying relation (expect intersects)
|
||||||
|
searchResponse = client().prepareSearch(defaultIndexName)
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, geometry))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
assertSearchResponse(searchResponse);
|
||||||
|
assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L));
|
||||||
|
assertThat(searchResponse.getHits().getHits().length, equalTo(1));
|
||||||
|
assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testContainsShapeQuery() {
|
public void testIndexPointsCircle() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate(defaultIndexName)
|
||||||
|
.addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
client().admin().indices().prepareCreate("test_contains").addMapping("type", "location", "type=shape")
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder()
|
||||||
.execute().actionGet();
|
.startObject()
|
||||||
|
.field("name", "Document 1")
|
||||||
|
.field(defaultFieldName, "POINT(-30 -30)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
String doc = "{\"location\" : {\"type\":\"envelope\", \"coordinates\":[ [-100.0, 100.0], [100.0, -100.0]]}}";
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder()
|
||||||
client().prepareIndex("test_contains", "type").setId("1").setSource(doc, XContentType.JSON).setRefreshPolicy(IMMEDIATE).get();
|
.startObject()
|
||||||
|
.field("name", "Document 2")
|
||||||
|
.field(defaultFieldName, "POINT(-45 -50)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
// index the mbr of the collection
|
CircleBuilder shape = new CircleBuilder().center(new Coordinate(-30, -30)).radius("1");
|
||||||
EnvelopeBuilder queryShape = new EnvelopeBuilder(new Coordinate(-50, 50), new Coordinate(50, -50));
|
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(shape);
|
||||||
ShapeQueryBuilder queryBuilder = new ShapeQueryBuilder("location", queryShape.buildGeometry()).relation(ShapeRelation.CONTAINS);
|
Geometry geometry = builder.buildGeometry().get(0);
|
||||||
SearchResponse response = client().prepareSearch("test_contains").setQuery(queryBuilder).get();
|
|
||||||
assertSearchResponse(response);
|
|
||||||
|
|
||||||
assertThat(response.getHits().getTotalHits().value, equalTo(1L));
|
SearchResponse searchResponse = client().prepareSearch(defaultIndexName)
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, geometry)
|
||||||
|
.relation(ShapeRelation.INTERSECTS))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
assertSearchResponse(searchResponse);
|
||||||
|
assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L));
|
||||||
|
assertThat(searchResponse.getHits().getHits().length, equalTo(1));
|
||||||
|
assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testGeometryCollectionRelations() throws IOException {
|
public void testIndexPointsPolygon() throws Exception {
|
||||||
XContentBuilder mapping = XContentFactory.jsonBuilder().startObject()
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
.startObject("doc")
|
client().admin().indices().prepareCreate(defaultIndexName)
|
||||||
.startObject("properties")
|
.addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
.startObject("geometry").field("type", "shape").endObject()
|
ensureGreen();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field(defaultFieldName, "POINT(-30 -30)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field(defaultFieldName, "POINT(-45 -50)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
CoordinatesBuilder cb = new CoordinatesBuilder();
|
||||||
|
cb.coordinate(new Coordinate(-35, -35))
|
||||||
|
.coordinate(new Coordinate(-35, -25))
|
||||||
|
.coordinate(new Coordinate(-25, -25))
|
||||||
|
.coordinate(new Coordinate(-25, -35))
|
||||||
|
.coordinate(new Coordinate(-35, -35));
|
||||||
|
PolygonBuilder shape = new PolygonBuilder(cb);
|
||||||
|
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(shape);
|
||||||
|
Geometry geometry = builder.buildGeometry();
|
||||||
|
SearchResponse searchResponse = client().prepareSearch(defaultIndexName)
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, geometry)
|
||||||
|
.relation(ShapeRelation.INTERSECTS))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
assertSearchResponse(searchResponse);
|
||||||
|
SearchHits searchHits = searchResponse.getHits();
|
||||||
|
assertThat(searchHits.getTotalHits().value, equalTo(1L));
|
||||||
|
assertThat(searchHits.getAt(0).getId(), equalTo("1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIndexPointsMultiPolygon() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate(defaultIndexName)
|
||||||
|
.addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("name", "Document 1")
|
||||||
|
.field(defaultFieldName, "POINT(-30 -30)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("name", "Document 2")
|
||||||
|
.field(defaultFieldName, "POINT(-40 -40)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("3").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("name", "Document 3")
|
||||||
|
.field(defaultFieldName, "POINT(-50 -50)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
CoordinatesBuilder encloseDocument1Cb = new CoordinatesBuilder();
|
||||||
|
encloseDocument1Cb.coordinate(new Coordinate(-35, -35))
|
||||||
|
.coordinate(new Coordinate(-35, -25))
|
||||||
|
.coordinate(new Coordinate(-25, -25))
|
||||||
|
.coordinate(new Coordinate(-25, -35))
|
||||||
|
.coordinate(new Coordinate(-35, -35));
|
||||||
|
PolygonBuilder encloseDocument1Shape = new PolygonBuilder(encloseDocument1Cb);
|
||||||
|
|
||||||
|
CoordinatesBuilder encloseDocument2Cb = new CoordinatesBuilder();
|
||||||
|
encloseDocument2Cb.coordinate(new Coordinate(-55, -55))
|
||||||
|
.coordinate(new Coordinate(-55, -45))
|
||||||
|
.coordinate(new Coordinate(-45, -45))
|
||||||
|
.coordinate(new Coordinate(-45, -55))
|
||||||
|
.coordinate(new Coordinate(-55, -55));
|
||||||
|
PolygonBuilder encloseDocument2Shape = new PolygonBuilder(encloseDocument2Cb);
|
||||||
|
|
||||||
|
MultiPolygonBuilder mp = new MultiPolygonBuilder();
|
||||||
|
mp.polygon(encloseDocument1Shape).polygon(encloseDocument2Shape);
|
||||||
|
|
||||||
|
GeometryCollectionBuilder builder = new GeometryCollectionBuilder().shape(mp);
|
||||||
|
Geometry geometry = builder.buildGeometry();
|
||||||
|
SearchResponse searchResponse = client().prepareSearch(defaultIndexName)
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, geometry)
|
||||||
|
.relation(ShapeRelation.INTERSECTS))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
assertSearchResponse(searchResponse);
|
||||||
|
assertThat(searchResponse.getHits().getTotalHits().value, equalTo(2L));
|
||||||
|
assertThat(searchResponse.getHits().getHits().length, equalTo(2));
|
||||||
|
assertThat(searchResponse.getHits().getAt(0).getId(), not(equalTo("2")));
|
||||||
|
assertThat(searchResponse.getHits().getAt(1).getId(), not(equalTo("2")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIndexPointsRectangle() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate(defaultIndexName)
|
||||||
|
.addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("1").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("name", "Document 1")
|
||||||
|
.field(defaultFieldName, "POINT(-30 -30)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("2").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field("name", "Document 2")
|
||||||
|
.field(defaultFieldName, "POINT(-45 -50)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
Rectangle rectangle = new Rectangle(-50, -40, -45, -55);
|
||||||
|
|
||||||
|
SearchResponse searchResponse = client().prepareSearch(defaultIndexName)
|
||||||
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, rectangle)
|
||||||
|
.relation(ShapeRelation.INTERSECTS))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
assertSearchResponse(searchResponse);
|
||||||
|
assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L));
|
||||||
|
assertThat(searchResponse.getHits().getHits().length, equalTo(1));
|
||||||
|
assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIndexPointsIndexedRectangle() throws Exception {
|
||||||
|
String mapping = Strings.toString(createDefaultMapping());
|
||||||
|
client().admin().indices().prepareCreate(defaultIndexName)
|
||||||
|
.addMapping(defaultFieldType, mapping, XContentType.JSON).get();
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("point1").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field(defaultFieldName, "POINT(-30 -30)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
client().prepareIndex(defaultIndexName, defaultFieldType).setId("point2").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field(defaultFieldName, "POINT(-45 -50)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
|
String indexedShapeIndex = "indexed_query_shapes";
|
||||||
|
String indexedShapePath = "shape";
|
||||||
|
String queryShapesMapping = Strings.toString(XContentFactory.jsonBuilder().startObject()
|
||||||
|
.startObject("properties").startObject(indexedShapePath)
|
||||||
|
.field("type", "shape")
|
||||||
.endObject()
|
.endObject()
|
||||||
.endObject()
|
.endObject()
|
||||||
.endObject();
|
.endObject());
|
||||||
|
client().admin().indices().prepareCreate(indexedShapeIndex)
|
||||||
|
.addMapping(defaultFieldType, queryShapesMapping, XContentType.JSON);
|
||||||
|
ensureGreen();
|
||||||
|
|
||||||
createIndex("test_collections", Settings.builder().put("index.number_of_shards", 1).build(), "doc", mapping);
|
client().prepareIndex(indexedShapeIndex, defaultFieldType).setId("shape1").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field(indexedShapePath, "BBOX(-50, -40, -45, -55)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
EnvelopeBuilder envelopeBuilder = new EnvelopeBuilder(new Coordinate(-10, 10), new Coordinate(10, -10));
|
client().prepareIndex(indexedShapeIndex, defaultFieldType).setId("shape2").setSource(jsonBuilder()
|
||||||
|
.startObject()
|
||||||
|
.field(indexedShapePath, "BBOX(-60, -50, -50, -60)")
|
||||||
|
.endObject()).setRefreshPolicy(IMMEDIATE).get();
|
||||||
|
|
||||||
client().index(new IndexRequest("test_collections")
|
SearchResponse searchResponse = client().prepareSearch(defaultIndexName)
|
||||||
.source(jsonBuilder().startObject().field("geometry", envelopeBuilder).endObject())
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, "shape1")
|
||||||
.setRefreshPolicy(IMMEDIATE)).actionGet();
|
.relation(ShapeRelation.INTERSECTS)
|
||||||
|
.indexedShapeIndex(indexedShapeIndex)
|
||||||
|
.indexedShapePath(indexedShapePath))
|
||||||
|
.get();
|
||||||
|
|
||||||
{
|
assertSearchResponse(searchResponse);
|
||||||
// A geometry collection that is fully within the indexed shape
|
assertThat(searchResponse.getHits().getTotalHits().value, equalTo(1L));
|
||||||
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
|
assertThat(searchResponse.getHits().getHits().length, equalTo(1));
|
||||||
builder.shape(new PointBuilder(1, 2));
|
assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("point2"));
|
||||||
builder.shape(new PointBuilder(-2, -1));
|
|
||||||
SearchResponse response = client().prepareSearch("test_collections")
|
searchResponse = client().prepareSearch(defaultIndexName)
|
||||||
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
|
.setQuery(new ShapeQueryBuilder(defaultFieldName, "shape2")
|
||||||
.get();
|
.relation(ShapeRelation.INTERSECTS)
|
||||||
assertEquals(1, response.getHits().getTotalHits().value);
|
.indexedShapeIndex(indexedShapeIndex)
|
||||||
response = client().prepareSearch("test_collections")
|
.indexedShapePath(indexedShapePath))
|
||||||
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
|
.get();
|
||||||
.get();
|
assertSearchResponse(searchResponse);
|
||||||
assertEquals(1, response.getHits().getTotalHits().value);
|
assertThat(searchResponse.getHits().getTotalHits().value, equalTo(0L));
|
||||||
response = client().prepareSearch("test_collections")
|
|
||||||
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
|
|
||||||
.get();
|
|
||||||
assertEquals(0, response.getHits().getTotalHits().value);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// A geometry collection (as multi point) that is partially within the indexed shape
|
|
||||||
MultiPointBuilder builder = new MultiPointBuilder();
|
|
||||||
builder.coordinate(1, 2);
|
|
||||||
builder.coordinate(20, 30);
|
|
||||||
SearchResponse response = client().prepareSearch("test_collections")
|
|
||||||
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
|
|
||||||
.get();
|
|
||||||
assertEquals(0, response.getHits().getTotalHits().value);
|
|
||||||
response = client().prepareSearch("test_collections")
|
|
||||||
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
|
|
||||||
.get();
|
|
||||||
assertEquals(1, response.getHits().getTotalHits().value);
|
|
||||||
response = client().prepareSearch("test_collections")
|
|
||||||
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
|
|
||||||
.get();
|
|
||||||
assertEquals(0, response.getHits().getTotalHits().value);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// A geometry collection that is disjoint with the indexed shape
|
|
||||||
GeometryCollectionBuilder builder = new GeometryCollectionBuilder();
|
|
||||||
MultiPointBuilder innerBuilder = new MultiPointBuilder();
|
|
||||||
innerBuilder.coordinate(-20, -30);
|
|
||||||
innerBuilder.coordinate(20, 30);
|
|
||||||
builder.shape(innerBuilder);
|
|
||||||
SearchResponse response = client().prepareSearch("test_collections")
|
|
||||||
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.CONTAINS))
|
|
||||||
.get();
|
|
||||||
assertEquals(0, response.getHits().getTotalHits().value);
|
|
||||||
response = client().prepareSearch("test_collections")
|
|
||||||
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.INTERSECTS))
|
|
||||||
.get();
|
|
||||||
assertEquals(0, response.getHits().getTotalHits().value);
|
|
||||||
response = client().prepareSearch("test_collections")
|
|
||||||
.setQuery(new ShapeQueryBuilder("geometry", builder.buildGeometry()).relation(ShapeRelation.DISJOINT))
|
|
||||||
.get();
|
|
||||||
assertEquals(1, response.getHits().getTotalHits().value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testDistanceQuery() throws Exception {
|
public void testDistanceQuery() throws Exception {
|
||||||
|
|
Loading…
Reference in New Issue