Skip to content

[GEO] Add CRS Support to Geo Field Mappers and QueryProcessors #47250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3ead3d8
reprojection experiment w proj4j dependency
nknize Aug 6, 2019
e0da1be
add CRSHandler interface, default OSS implementation, and spatial xpa…
nknize Sep 7, 2019
fd620d5
add index and query testing for projected coordinates
nknize Sep 8, 2019
1f36ecb
update default CRS String from null to EPSG:4326 for WGS84
nknize Sep 10, 2019
8984966
refactor CRSHandler to resolve to a CRSHandlerFactory
nknize Sep 26, 2019
c16c1ab
pass precommit
nknize Sep 27, 2019
3c710b8
fix default xtor for GeoShapeFieldMapper.Builder
nknize Sep 29, 2019
4d5da5d
Merge branch 'master' into feature/geoProjectionSupport
nknize Sep 30, 2019
e0ccc2c
fix backcompat failures
nknize Sep 30, 2019
4655d9d
switch from GeoPlugin to GeoExtension SPI
nknize Oct 1, 2019
11c0f7a
remove GeoPlugin
nknize Oct 1, 2019
a941976
remove unused import
nknize Oct 1, 2019
9a3d2be
cleanup
nknize Oct 1, 2019
88c678f
move spi directory from test to java
nknize Oct 1, 2019
3b6c990
Merge branch 'master' into feature/geoProjectionSupport
nknize Oct 1, 2019
27da999
add check for null LicenseState
nknize Oct 2, 2019
ad80ef9
update docs
nknize Oct 7, 2019
3a94be6
Merge branch 'master' into feature/geoProjectionSupport
nknize Oct 7, 2019
0865435
remove duplicate CRS docs
nknize Oct 7, 2019
75bfaf2
fix double quote error in docs
nknize Oct 7, 2019
d453a8b
fix double quote error again
nknize Oct 7, 2019
443cfc9
remove references
nknize Oct 7, 2019
f6e5912
Merge branch 'master' into feature/geoProjectionSupport
nknize Mar 3, 2020
85e63ff
incorporate initial PR feedback
nknize Mar 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.Version;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.ParseField;
Expand Down Expand Up @@ -50,6 +51,7 @@
import java.util.Objects;

import static org.elasticsearch.index.mapper.GeoPointFieldMapper.Names.IGNORE_MALFORMED;
import static org.elasticsearch.index.mapper.TypeParsers.parseField;

/**
* Base class for {@link GeoShapeFieldMapper} and {@link LegacyGeoShapeFieldMapper}
Expand All @@ -60,6 +62,7 @@ public abstract class AbstractGeometryFieldMapper<Parsed, Processed> extends Fie
public static class Names {
public static final ParseField ORIENTATION = new ParseField("orientation");
public static final ParseField COERCE = new ParseField("coerce");
public static final ParseField CRS = new ParseField("crs");
}

public static class Defaults {
Expand All @@ -69,7 +72,6 @@ public static class Defaults {
public static final Explicit<Boolean> IGNORE_Z_VALUE = new Explicit<>(true, false);
}


/**
* Interface representing an preprocessor in geo-shape indexing pipeline
*/
Expand Down Expand Up @@ -215,7 +217,7 @@ protected Builder newBuilder(String name, Map<String, Object> params) {
if (params.containsKey(DEPRECATED_PARAMETERS_KEY)) {
return new LegacyGeoShapeFieldMapper.Builder(name, (DeprecatedParameters)params.get(DEPRECATED_PARAMETERS_KEY));
}
return new GeoShapeFieldMapper.Builder(name);
return new GeoShapeFieldMapper.Builder(name, params);
}

@Override
Expand Down Expand Up @@ -245,12 +247,20 @@ public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext
XContentMapValues.nodeBooleanValue(fieldNode,
name + "." + GeoPointFieldMapper.Names.IGNORE_Z_VALUE.getPreferredName()));
iterator.remove();
} else if (Names.CRS.match(fieldName, LoggingDeprecationHandler.INSTANCE)) {
if (fieldNode instanceof Map) {
parseCRS((Map<String, Object>) fieldNode, parserContext, params);
} else {
throw new ElasticsearchParseException("expected object for [{}] field", Names.CRS.getPreferredName());
}
iterator.remove();
}
}
if (parsedDeprecatedParameters == false) {
params.remove(DEPRECATED_PARAMETERS_KEY);
}
Builder builder = newBuilder(name, params);
parseField(builder, name, node, parserContext);

if (params.containsKey(Names.COERCE.getPreferredName())) {
builder.coerce((Boolean)params.get(Names.COERCE.getPreferredName()));
Expand All @@ -270,6 +280,31 @@ public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext

return builder;
}

private static void parseCRS(Map<String, Object> crsNode, ParserContext parserContext, Map<String, Object> params) {
Object typeNode = crsNode.get("type");
if (typeNode != null) {
if (typeNode.toString().equalsIgnoreCase("name") == false) {
throw new ElasticsearchParseException("only type [{}] is supported for [{]}", "name", Names.CRS.getPreferredName());
}
} else {
throw new ElasticsearchParseException("expected [{}] field for [{}]", "type", Names.CRS.getPreferredName());
}

Object propsNode = crsNode.get("properties");
if (propsNode == null) {
throw new ElasticsearchParseException("expected [{}] field for [{}]", "properties", Names.CRS.getPreferredName());
} else if (propsNode instanceof Map == false) {
throw new ElasticsearchParseException("[{}] must be an object", "properties");
}

Map<String, Object> crsProperties = (Map<String, Object>)propsNode;
Object nameNode = crsProperties.get("name");
if (nameNode == null) {
throw new ElasticsearchParseException("expected [{}] field for [{}]", "name", "properties");
}
params.put(Names.CRS.getPreferredName(), nameNode.toString());
}
}

public abstract static class AbstractGeometryFieldType<Parsed, Processed> extends MappedFieldType {
Expand Down Expand Up @@ -385,7 +420,7 @@ protected void parseCreateField(ParseContext context, List<IndexableField> field

@Override
public void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
builder.field("type", contentType());
super.doXContentBody(builder, includeDefaults, params);
AbstractGeometryFieldType ft = (AbstractGeometryFieldType)fieldType();
if (includeDefaults || ft.orientation() != Defaults.ORIENTATION.value()) {
builder.field(Names.ORIENTATION.getPreferredName(), ft.orientation());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@
package org.elasticsearch.index.mapper;

import org.apache.lucene.document.LatLonShape;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.geo.GeometryParser;
import org.elasticsearch.common.geo.builders.ShapeBuilder;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.geometry.Geometry;
import org.elasticsearch.index.query.VectorGeoShapeQueryProcessor;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* FieldMapper for indexing {@link LatLonShape}s.
* <p>
Expand All @@ -49,16 +56,45 @@
public class GeoShapeFieldMapper extends AbstractGeometryFieldMapper<Geometry, Geometry> {
public static final String CONTENT_TYPE = "geo_shape";

public static List<CRSHandlerFactory> CRS_HANDLER_FACTORIES = new ArrayList<>();

public static class Defaults extends AbstractGeometryFieldMapper.Defaults {
public static final Explicit<String> CRS = new Explicit<>("EPSG:4326", false);
}

public static class Builder extends AbstractGeometryFieldMapper.Builder<AbstractGeometryFieldMapper.Builder, GeoShapeFieldMapper> {
CRSHandler crsHandler;
protected String crs;

public Builder(String name) {
super (name, new GeoShapeFieldType(), new GeoShapeFieldType());
this.crs =Defaults.CRS.value();
this.crsHandler = resolveCRSHandler(this.crs().value());
}

public Builder(String name, Map<String, Object> params) {
this(name);
this.crs = params.containsKey("crs") ? (String)params.get("crs") : Defaults.CRS.value();
this.crsHandler = resolveCRSHandler(this.crs().value());
}

@Override
public GeoShapeFieldMapper build(BuilderContext context) {
setupFieldType(context);
return new GeoShapeFieldMapper(name, fieldType, defaultFieldType, ignoreMalformed(context), coerce(context),
ignoreZValue(), context.indexSettings(), multiFieldsBuilder.build(this, context), copyTo);
ignoreZValue(), crs(), context.indexSettings(), multiFieldsBuilder.build(this, context), copyTo);
}

protected Explicit<String> crs() {
if (crs != null) {
return new Explicit<>(crs, true);
}
return Defaults.CRS;
}

public Builder crs(final String crs) {
this.crs = crs;
return this;
}

@Override
Expand All @@ -68,15 +104,17 @@ protected void setupFieldType(BuilderContext context) {
GeoShapeFieldType fieldType = (GeoShapeFieldType)fieldType();
boolean orientation = fieldType.orientation == ShapeBuilder.Orientation.RIGHT;

// @todo the GeometryParser can be static since it doesn't hold state?
GeometryParser geometryParser = new GeometryParser(orientation, coerce(context).value(), ignoreZValue().value());

fieldType.setGeometryIndexer(new GeoShapeIndexer(orientation, fieldType.name()));
fieldType.setGeometryParser( (parser, mapper) -> geometryParser.parse(parser));
fieldType.setGeometryQueryBuilder(new VectorGeoShapeQueryProcessor());

fieldType.setGeometryIndexer(crsHandler.newIndexer(orientation, fieldType.name()));
fieldType.setGeometryQueryBuilder(crsHandler.newQueryProcessor());
}
}

public static final class GeoShapeFieldType extends AbstractGeometryFieldType<Geometry, Geometry> {

public GeoShapeFieldType() {
super();
}
Expand All @@ -96,12 +134,15 @@ public String typeName() {
}
}

protected Explicit<String> crs;

public GeoShapeFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType,
Explicit<Boolean> ignoreMalformed, Explicit<Boolean> coerce,
Explicit<Boolean> ignoreZValue, Settings indexSettings,
Explicit<Boolean> ignoreZValue, Explicit<String> crs, Settings indexSettings,
MultiFields multiFields, CopyTo copyTo) {
super(simpleName, fieldType, defaultFieldType, ignoreMalformed, coerce, ignoreZValue, indexSettings,
multiFields, copyTo);
this.crs = crs;
}

@Override
Expand All @@ -113,4 +154,77 @@ public GeoShapeFieldType fieldType() {
protected String contentType() {
return CONTENT_TYPE;
}

public Explicit<String> crs() {
return crs;
}

@Override
public void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
super.doXContentBody(builder, includeDefaults, params);
if (includeDefaults || crs.explicit()) {
builder.startObject("crs")
.field("type", "name")
.startObject("properties")
.field("name", crs.value())
.endObject()
.endObject();
}
}

public static void registerCRSHandlers(List<CRSHandlerFactory> crsHandlerFactories) {
CRS_HANDLER_FACTORIES.addAll(crsHandlerFactories);
}

public static CRSHandler resolveCRSHandler(String crs) {
CRSHandler crsHandler;
for (CRSHandlerFactory factory : CRS_HANDLER_FACTORIES) {
if ((crsHandler = factory.newCRSHandler(crs)) != null) {
return crsHandler;
}
}
throw new IllegalArgumentException("crs [" + crs + "] not supported");
}

public interface CRSHandlerFactory {
CRSHandler newCRSHandler(String crs);
}

public interface CRSHandler {
Indexer newIndexer(boolean orientation, String fieldName);
QueryProcessor newQueryProcessor();

Object resolveCRS(String crsSpec);
}

public static CRSHandlerFactory DEFAULT_CRS_HANDLER_FACTORY = new CRSHandlerFactory() {
@Override
public CRSHandler newCRSHandler(String crs) {
if (crs.equals(Defaults.CRS.value())) {
return new DefaultCRSHandler();
}
return null;
}
};

protected static class DefaultCRSHandler implements CRSHandler {
@Override
public Indexer newIndexer(boolean orientation, String fieldName) {
return new GeoShapeIndexer(orientation, fieldName);
}

@Override
public QueryProcessor newQueryProcessor() {
return new VectorGeoShapeQueryProcessor();
}

@Override
public Object resolveCRS(String crsSpec) {
throw new ElasticsearchException("resolveCRS not supported for default CRSHandler");
}
};

static {
CRS_HANDLER_FACTORIES.add(DEFAULT_CRS_HANDLER_FACTORY);
}
}
7 changes: 7 additions & 0 deletions server/src/main/java/org/elasticsearch/node/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.analysis.AnalysisRegistry;
import org.elasticsearch.index.engine.EngineFactory;
import org.elasticsearch.index.mapper.GeoShapeFieldMapper;
import org.elasticsearch.indices.IndicesModule;
import org.elasticsearch.indices.IndicesService;
import org.elasticsearch.indices.analysis.AnalysisModule;
Expand All @@ -122,6 +123,7 @@
import org.elasticsearch.plugins.ClusterPlugin;
import org.elasticsearch.plugins.DiscoveryPlugin;
import org.elasticsearch.plugins.EnginePlugin;
import org.elasticsearch.plugins.GeoPlugin;
import org.elasticsearch.plugins.IndexStorePlugin;
import org.elasticsearch.plugins.IngestPlugin;
import org.elasticsearch.plugins.MapperPlugin;
Expand Down Expand Up @@ -378,6 +380,11 @@ protected Node(
IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class));
modules.add(indicesModule);

// register CRS handlers w/ GeoShapeFieldMapper
for (GeoPlugin plugin : pluginsService.filterPlugins(GeoPlugin.class)) {
GeoShapeFieldMapper.registerCRSHandlers((plugin).getCRSHandlerFactories());
}

SearchModule searchModule = new SearchModule(settings, pluginsService.filterPlugins(SearchPlugin.class));
CircuitBreakerService circuitBreakerService = createCircuitBreakerService(settingsModule.getSettings(),
settingsModule.getClusterSettings());
Expand Down
30 changes: 30 additions & 0 deletions server/src/main/java/org/elasticsearch/plugins/GeoPlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.plugins;

import org.elasticsearch.index.mapper.GeoShapeFieldMapper;

import java.util.Collections;
import java.util.List;

public interface GeoPlugin {
default List<GeoShapeFieldMapper.CRSHandlerFactory> getCRSHandlerFactories() {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,45 @@ public void testOrientationParsing() throws IOException {
assertThat(orientation, equalTo(ShapeBuilder.Orientation.CCW));
}

public void testCRSParsing() throws IOException {
// test invalid crs
final String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("location")
.field("type", "geo_shape")
.startObject("crs")
.field("type", "name")
.startObject("properties")
.field("name", "EPSG:4376")
.endObject()
.endObject()
.endObject().endObject()
.endObject().endObject());

IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> createIndex("test").mapperService().documentMapperParser()
.parse("type1", new CompressedXContent(mapping))
);
assertThat(e.getMessage(), containsString("crs [EPSG:4376] not supported"));

// test valid crs (default WGS84)
final String validMapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("location")
.field("type", "geo_shape")
.startObject("crs")
.field("type", "name")
.startObject("properties")
.field("name", "EPSG:4326")
.endObject()
.endObject()
.endObject().endObject()
.endObject().endObject());

DocumentMapper defaultMapper = createIndex("test2").mapperService().documentMapperParser()
.parse("type1", new CompressedXContent(validMapping));
Mapper fieldMapper = defaultMapper.mappers().getMapper("location");
assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
}

/**
* Test that coerce parameter correctly parses
*/
Expand Down
Loading